Pipeline Path Resolver — Scope-Aware Layout
Data Root & Git Integration
Generated pipelines (v2.x+) are data under the single tier-independent data root
(RESOLVE_DATA_ROOT); the per-tier scope roots below are legacy/materialization only.
Per-Tier Scope Roots (legacy back-compat reads + native materialization target)
These per-tier roots no longer locate generated pipeline data (see Data Root above). They are
consumed only by (a) ENUMERATE_ALL_SCOPE_ROOTS read-only legacy back-compat and (b) sk-platform-dispatch
MATERIALIZE, which writes the ephemeral native agent file into the host's own dir.
RESOLVE_DATA_ROOT(scope): // Data-only pipelines (v2.x+): the single artifact root for ALL generated // pipeline DATA. Tier-INDEPENDENT — NOT the tier-1 scope root, a sibling of it. // Only dispatch + model resolution remain tier-specific (design spec §4). if scope in {project, local}: return RESOLVE_HOST_WORKSPACE() + "/.superpipelines" if scope == user: return expand("~/.superpipelines")
RESOLVE_HOST_WORKSPACE():
// The main-worktree root, NOT a linked worktree's cwd. Artifacts and state
// MUST anchor here so they survive worktree teardown (issue #31).
common = git rev-parse --path-format=absolute --git-common-dir // → /.git
if command succeeds: return dirname(strip_trailing("/.git", common))
else (not a git repo): return cwd
// INVARIANT: the superpipelines/temp/{P}/{runId}/ artifact + state tree ALWAYS // resolves under RESOLVE_HOST_WORKSPACE(), never a linked worktree path. Code // edits stay isolated in the worktree; coordination artifacts land on the host.
// LEGACY back-compat only. Data-only pipelines store paths relative to DATA_ROOT // (tier-independent), so PORTABILITY_REWRITE is a no-op for them and retires for paths // (design spec §4). It still rewrites old-root (pre-v2) cross-tier paths. PORTABILITY_REWRITE(artifact_path, source_tier, target_tier): if source_tier == target_tier: return artifact_path if source_tier == "data" OR target_tier == "data": return artifact_path // data paths are tier-independent source_root = per-tier table[source_tier][workspace_or_user] target_root = per-tier table[target_tier][workspace_or_user] return artifact_path.replace(source_root, target_root, count=1)
// Discovery & resume: DATA-ROOT FIRST, legacy multi-root as a self-gating back-compat
// tail. The DEFAULT cost is two REGISTRY reads (workspace + user .superpipelines/);
// the §4 "no per-tier loop" win is about registry enumeration — registries are read
// (in running-a-pipeline Phase 0) ONLY for the roots returned here. The legacy tail is
// a profile-driven dir_exists sweep that parses NO registries and returns [] on an
// all-data workspace, so a workspace with only data-only pipelines reads exactly two
// registries. Legacy back-compat (design spec §8) is preserved for any workspace that
// still holds a pre-v2 old-root dir, with the per-tier dir names sourced from the
// profiles (DEPENDENCY_INVERSION: PROFILE_DRIVEN) — never duplicated in this body.
ENUMERATE_PIPELINE_ROOTS(workspace) → [{tier, scope, root, layout}, ...]:
roots = []
// (1) Data-only roots — canonical, tier-independent. Two dir checks, no per-tier loop.
IF dir_exists(RESOLVE_DATA_ROOT(project)): roots.append({tier: "data", scope: "workspace", root: RESOLVE_DATA_ROOT(project), layout: "data"})
IF dir_exists(RESOLVE_DATA_ROOT(user)): roots.append({tier: "data", scope: "user", root: RESOLVE_DATA_ROOT(user), layout: "data"})
// (2) Legacy per-tier roots — read-only back-compat tail; ENUMERATE_ALL_SCOPE_ROOTS
// returns [] when no legacy root dir exists (the common all-data case), so this
// adds no registry I/O and is its own gate.
roots += ENUMERATE_ALL_SCOPE_ROOTS(workspace) // each annotated layout: "legacy"; [] on all-data workspace
return roots
// Legacy multi-root enumeration — RETAINED as the self-gating back-compat tail above.
// Old-root pipelines (pre-v2) still list/resume via these 5 per-tier scope roots. Reads
// the per-tier scope-root dir names from the profile JSONs (single source of truth) and
// does dir_exists only — it parses NO registries, so on an all-data workspace it
// returns [] cheaply (small profile reads + dir stats, no per-pipeline I/O).
ENUMERATE_ALL_SCOPE_ROOTS(workspace) → [{tier, scope, root, layout}, ...]:
roots = []
FOR tier IN [tier_1, tier_1b, tier_1c, tier_1d, tier_2]:
profile = READ(skills/sk-platform-dispatch/profiles/{tier}.json)
workspace_root = workspace + "/" + profile.scope_root.workspace
user_root = expand(profile.scope_root.user)
// A legacy old-root pipeline lives under the scope root's superpipelines/ subdir
// (path templates below). tier_2's scope root IS the data root, so its legacy
// old-root pipelines are the ones whose registry sits at <root>/superpipelines/...,
// distinct from the data-only <root>/registry.json — no double-count.
IF dir_exists(workspace_root + "/superpipelines"): roots.append({tier, scope: "workspace", root: workspace_root, layout: "legacy"})
IF dir_exists(user_root + "/superpipelines"): roots.append({tier, scope: "user", root: user_root, layout: "legacy"})
return roots
Path Templates
Data-Only Path Templates (v2.x — relative to DATA_ROOT)
The canonical templates for new pipelines. DATA_ROOT = RESOLVE_DATA_ROOT(scope) (the
.superpipelines/ root). Everything a pipeline needs — including the entry orchestration body
and inline CAD operational protocols — is DATA here; nothing is written to a tool dir as source.
Materialized CC Agent (ephemeral cache — Option A dispatch):
RESOLVE_SCOPE_ROOT(scope, tier_1) + "/agents/superpipelines/{P}/{agent-name}.md". Written by
sk-platform-dispatch MATERIALIZE just before native dispatch, translated from the CAD;
regenerated every run; never read as source; cleaned at Phase 4 completion. This is the
one place generated content lands in a tool dir, and it is disposable cache, not source.
Legacy Path Templates (READ-ONLY back-compat — old-root pipelines only)
Pipelines scaffolded before v2.x persist under per-tier scope roots ({ROOT} =
RESOLVE_SCOPE_ROOT(scope, tier)). These templates are a read-only back-compat path for
discovery and resume of existing pipelines — they are never a write target for new
scaffolds (design spec §8: old-root writes removed immediately, old-root reads kept one major).
Pipeline Name Constraints
Collision Semantics
When discovery (ENUMERATE_PIPELINE_ROOTS, which merges the data roots with the gated legacy ENUMERATE_ALL_SCOPE_ROOTS tail) returns multiple registry entries with the same pipeline name across different scopes or tiers, the resolution contract is:
Precedence rule (highest wins):
workspace/project>workspace/local>user/global- Within the same scope-bucket,
runtime_tier> other tiers.
Disambiguation prompt: When two or more entries tie after applying precedence (e.g., same-name pipeline in both workspace/project tier_1 and workspace/project tier_2), the runner MUST present a disambiguated list and require explicit selection:
Multiple pipelines named `deploy-feature` found:
[1] project scope / tier_1 (.claude/) scaffolded 2026-04-10
[2] project scope / tier_2 (.superpipelines/) scaffolded 2026-05-12
Pick one: [1/2]
No silent first-wins. The slash-command form (/superpipelines:{P}, OC-only) follows the same precedence rule; OC's command resolver MUST honor it.
Registry Entry Schema
Each registry entry carries an explicit scope field to disambiguate project-vs-local entries that share the same physical .claude/ directory:
{
"name": "deploy-feature",
"scope": "project | local | user",
"source_tier": "tier_1 | tier_1b | tier_1c | tier_1d | tier_2",
"plugin_version": "2.0.0",
"...": "..."
}
The merge logic treats (name, scope, source_tier) as the composite identity key.
Reference Files
sk-pipeline-state/SKILL.md— State persistence schema.sk-claude-code-conventions/SKILL.md— Frontmatter and directory rules.