cadence-skill-installer 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadence-skill-installer",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Install the Cadence skill into supported AI tool skill directories.",
5
5
  "repository": "https://github.com/snowdamiz/cadence",
6
6
  "private": false,
package/skill/SKILL.md CHANGED
@@ -45,7 +45,8 @@ description: Structured project operating system for end-to-end greenfield or br
45
45
  1. Check for `.cadence` in the project root.
46
46
  2. If `.cadence` is missing, invoke `skills/scaffold/SKILL.md`.
47
47
  3. Scaffold initializes and persists `state.cadence-scripts-dir` for later subskill commands.
48
- 4. If `.cadence` exists, skip scaffold.
48
+ 4. If `.cadence` exists but `.cadence/cadence.json` is missing, invoke `skills/scaffold/SKILL.md` for recovery.
49
+ 5. If both `.cadence` and `.cadence/cadence.json` exist, skip scaffold.
49
50
 
50
51
  ## Workflow Route Gate (Mandatory At Entry And Transitions)
51
52
  1. After scaffold handling on Cadence entry, run `python3 scripts/read-workflow-state.py --project-root "$PROJECT_ROOT"` and parse the JSON response.
@@ -59,7 +60,17 @@ description: Structured project operating system for end-to-end greenfield or br
59
60
 
60
61
  ## Prerequisite Gate (Conditional)
61
62
  1. Invoke `skills/prerequisite-gate/SKILL.md` only when `route.skill_name` is `prerequisite-gate`.
62
- 2. If `route.skill_name` is not `prerequisite-gate` (for example `ideator` or `researcher`), skip prerequisite gate and follow the active route instead.
63
+ 2. If `route.skill_name` is not `prerequisite-gate` (for example `brownfield-intake`, `brownfield-documenter`, `ideator`, or `researcher`), skip prerequisite gate and follow the active route instead.
64
+
65
+ ## Project Mode Intake Gate (Conditional)
66
+ 1. Invoke `skills/brownfield-intake/SKILL.md` only when `route.skill_name` is `brownfield-intake`.
67
+ 2. Use this gate to classify `greenfield` vs `brownfield` execution mode and capture baseline inventory for existing codebases.
68
+ 3. If `route.skill_name` is not `brownfield-intake`, skip this gate and follow the active route instead.
69
+
70
+ ## Brownfield Documentation Gate (Conditional)
71
+ 1. Invoke `skills/brownfield-documenter/SKILL.md` only when `route.skill_name` is `brownfield-documenter`.
72
+ 2. Use this gate to document the existing project into the canonical ideation and research agenda structures before researcher routing.
73
+ 3. If `route.skill_name` is not `brownfield-documenter`, skip this gate and follow the active route instead.
63
74
 
64
75
  ## Progress / Resume Flow
65
76
  1. Invoke `skills/project-progress/SKILL.md` when the user asks to continue/resume or requests progress status (for example: "continue the project", "how far along are we?", "where did we leave off?").
@@ -73,12 +84,18 @@ description: Structured project operating system for end-to-end greenfield or br
73
84
  5. Do not execute state-changing subskill steps when assertion fails.
74
85
 
75
86
  ## Ideation Flow
76
- 1. When scaffold and prerequisite both complete in this same conversation for a net-new project and route advances to `ideator`, force a subskill handoff and end with this exact line: `Start a new chat and either say "help me define my project" or share your project brief.`
77
- 2. In subsequent conversations, if the workflow route is `ideator`, do not rerun prerequisite gate.
87
+ 1. When scaffold, prerequisite, and project mode intake complete in this same conversation for a net-new project and route advances to `ideator`, force a subskill handoff and end with this exact line: `Start a new chat and either say "help me define my project" or share your project brief.`
88
+ 2. In subsequent conversations, if the workflow route is `ideator`, do not rerun prerequisite gate or project mode intake.
78
89
  3. If the user asks to define the project or provides a brief while route is `ideator`, invoke `skills/ideator/SKILL.md`.
79
90
  4. If route is `ideator` and the user has not provided ideation input yet, ask one kickoff ideation question in-thread and continue.
80
91
  5. When route advances from `ideator` to `researcher`, force a handoff and end with this exact line: `Start a new chat with a new agent and say "plan my project".`
81
92
 
93
+ ## Brownfield Flow
94
+ 1. When scaffold, prerequisite, and project mode intake complete in this same conversation for a brownfield project and route advances to `brownfield-documenter`, force a subskill handoff and end with this exact line: `Start a new chat and say "document my existing project".`
95
+ 2. In subsequent conversations, if workflow route is `brownfield-documenter`, invoke `skills/brownfield-documenter/SKILL.md`.
96
+ 3. Do not route brownfield projects to `skills/ideator/SKILL.md` unless the user explicitly asks to run net-new ideation discovery.
97
+ 4. When route advances from `brownfield-documenter` to `researcher`, force a handoff and end with this exact line: `Start a new chat with a new agent and say "plan my project".`
98
+
82
99
  ## Research Flow
83
100
  1. If the workflow route is `researcher`, invoke `skills/researcher/SKILL.md`.
84
101
  2. Enforce one research pass per conversation so context stays bounded.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Cadence"
3
3
  short_description: "Lifecycle + delivery system for structured project execution"
4
- default_prompt: "Use Cadence to guide this project from lifecycle setup through phased execution, traceability, audit, and milestone completion. Always read and apply the active SOUL persona from .cadence/SOUL.json (fallback: SOUL.json). Keep user-facing responses concise and outcome-focused, and never expose internal skill-routing or command-execution traces unless the user explicitly asks. Do not announce successful internal checks; only surface them when a check fails and blocks progress. At Cadence entry (first assistant response in a conversation), resolve PROJECT_ROOT with scripts/resolve-project-root.py --project-root \"$PWD\". If \"$PROJECT_ROOT/.cadence\" exists, run scripts/check-project-repo-status.py --project-root \"$PROJECT_ROOT\" and treat repo_enabled as the authoritative push mode (if false, keep commits local-only). Never run scripts/check-project-repo-status.py without --project-root. If \"$PROJECT_ROOT/.cadence\" is missing, run scaffold first and let scaffold establish repo mode. After scaffold handling, run scripts/read-workflow-state.py --project-root \"$PROJECT_ROOT\" and treat route.skill_name as authoritative for the next state-changing skill. During normal multi-turn subskill conversation flow, do not rerun repo/route gates between each user reply; rerun them only when checkpointing into a new subskill, handling explicit resume/status/reroute requests, or recovering from assertion/gate failures. Invoke skills/prerequisite-gate/SKILL.md only when route.skill_name is prerequisite-gate. If scaffold and prerequisite complete in-thread and route advances to ideator, force subskill handoff with: Start a new chat and either say \"help me define my project\" or share your project brief. In later chats, if route.skill_name is ideator, do not rerun prerequisite; invoke skills/ideator/SKILL.md in the same chat, and if the user has not provided ideation input yet, ask one kickoff ideation question in-thread instead of handing off again. When route advances from ideator to researcher, force a handoff with: Start a new chat with a new agent and say \"plan my project\". If route.skill_name is researcher, invoke skills/researcher/SKILL.md and enforce one pass per conversation; when more passes remain, end with: Start a new chat and say \"continue research\". If user intent indicates resuming/continuing work or asking progress, invoke skills/project-progress/SKILL.md first, report current phase, then route to the next step. If the user manually requests a Cadence subskill, resolve PROJECT_ROOT with scripts/resolve-project-root.py --project-root \"$PWD\" and then run scripts/assert-workflow-route.py --skill-name <subskill> --project-root \"$PROJECT_ROOT\" before any state-changing actions. Ensure direct subskill execution follows the same Git Checkpoints policy from this main skill: run scripts/finalize-skill-checkpoint.py with each subskill's configured --scope/--checkpoint and --paths ., allow status=no_changes without failure, and treat checkpoint or push failures as blocking errors surfaced verbatim."
4
+ default_prompt: "Use Cadence to guide this project from lifecycle setup through phased execution, traceability, audit, and milestone completion. Always read and apply the active SOUL persona from .cadence/SOUL.json (fallback: SOUL.json). Keep user-facing responses concise and outcome-focused, and never expose internal skill-routing or command-execution traces unless the user explicitly asks. Do not announce successful internal checks; only surface them when a check fails and blocks progress. At Cadence entry (first assistant response in a conversation), resolve PROJECT_ROOT with scripts/resolve-project-root.py --project-root \"$PWD\". If \"$PROJECT_ROOT/.cadence\" exists, run scripts/check-project-repo-status.py --project-root \"$PROJECT_ROOT\" and treat repo_enabled as the authoritative push mode (if false, keep commits local-only). Never run scripts/check-project-repo-status.py without --project-root. If \"$PROJECT_ROOT/.cadence\" is missing, run scaffold first and let scaffold establish repo mode. If `.cadence` exists but `.cadence/cadence.json` is missing, run scaffold in recovery mode before routing. After scaffold handling, run scripts/read-workflow-state.py --project-root \"$PROJECT_ROOT\" and treat route.skill_name as authoritative for the next state-changing skill. During normal multi-turn subskill conversation flow, do not rerun repo/route gates between each user reply; rerun them only when checkpointing into a new subskill, handling explicit resume/status/reroute requests, or recovering from assertion/gate failures. Invoke skills/prerequisite-gate/SKILL.md only when route.skill_name is prerequisite-gate. Invoke skills/brownfield-intake/SKILL.md only when route.skill_name is brownfield-intake so project mode and baseline capture happen before downstream ideation routing. Invoke skills/brownfield-documenter/SKILL.md only when route.skill_name is brownfield-documenter so existing-project context is documented into canonical ideation structures. If scaffold, prerequisite, and brownfield-intake complete in-thread and route advances to ideator for a greenfield project, force subskill handoff with: Start a new chat and either say \"help me define my project\" or share your project brief. If scaffold, prerequisite, and brownfield-intake complete in-thread and route advances to brownfield-documenter for a brownfield project, force subskill handoff with: Start a new chat and say \"document my existing project\". In later chats, if route.skill_name is ideator, do not rerun prerequisite or brownfield-intake; invoke skills/ideator/SKILL.md in the same chat, and if the user has not provided ideation input yet, ask one kickoff ideation question in-thread instead of handing off again. In later chats, if route.skill_name is brownfield-documenter, invoke skills/brownfield-documenter/SKILL.md and do not route to ideator unless the user explicitly requests net-new ideation discovery. When route advances from ideator or brownfield-documenter to researcher, force a handoff with: Start a new chat with a new agent and say \"plan my project\". If route.skill_name is researcher, invoke skills/researcher/SKILL.md and enforce one pass per conversation; when more passes remain, end with: Start a new chat and say \"continue research\". If user intent indicates resuming/continuing work or asking progress, invoke skills/project-progress/SKILL.md first, report current phase, then route to the next step. If the user manually requests a Cadence subskill, resolve PROJECT_ROOT with scripts/resolve-project-root.py --project-root \"$PWD\" and then run scripts/assert-workflow-route.py --skill-name <subskill> --project-root \"$PROJECT_ROOT\" before any state-changing actions. Ensure direct subskill execution follows the same Git Checkpoints policy from this main skill: run scripts/finalize-skill-checkpoint.py with each subskill's configured --scope/--checkpoint and --paths ., allow status=no_changes without failure, and treat checkpoint or push failures as blocking errors surfaced verbatim."
@@ -13,10 +13,11 @@
13
13
  - One tack per subagent for focused execution
14
14
 
15
15
  ### 3. Self-Improvement Loop
16
- - After ANY correction from the user: update `.cadence/tasks/lessons.md` with the pattern
16
+ - After ANY correction from the user: record the lesson inside `.cadence/cadence.json` (for example `project-details.lessons`)
17
17
  - Write rules for yourself that prevent the same mistake
18
18
  - Ruthlessly iterate on these lessons until mistake rate drops
19
19
  - Review lessons at session start for relevant project
20
+ - Do not create ad-hoc tracking files like `.cadence/tasks/*`
20
21
 
21
22
  ### 4. Verification Before Done
22
23
  - Never mark a task complete without proving it works
@@ -42,15 +43,15 @@
42
43
 
43
44
  ## Task Management
44
45
 
45
- 1. **Plan First**: Write plan to `.cadence/tasks/todo.md` with checkable items
46
+ 1. **Plan First**: track plan state in `.cadence/cadence.json` workflow items
46
47
  2. **Verify Plan**: Check in before starting implementation
47
- 3. **Track Progress**: Mark items complete as you go
48
+ 3. **Track Progress**: update workflow item status as work advances
48
49
  4. **Explain Changes**: High-level summary at each step
49
- 5. **Document Results**: Add review section to `.cadence/tasks/todo.md`
50
- 6. **Capture Lessons**: Update `.cadence/tasks/lessons.md` after corrections
50
+ 5. **Document Results**: persist structured results in `.cadence/cadence.json`
51
+ 6. **Capture Lessons**: update `.cadence/cadence.json` only
51
52
 
52
53
  ## Core Principles
53
54
 
54
55
  - **Simplicity First**: Make every change as simple as possible. Impact minimal code.
55
56
  - **No Laziness**: Find root causes. No temporary fixes. Senior developer standards.
56
- - **Minimat Impact**: Changes should only touch what's necessary. Avoid introducing bugs.
57
+ - **Minimal Impact**: Changes should only touch what's necessary. Avoid introducing bugs.
@@ -4,10 +4,13 @@
4
4
  "ideation-completed": false,
5
5
  "research-completed": false,
6
6
  "cadence-scripts-dir": "",
7
- "repo-enabled": false
7
+ "repo-enabled": false,
8
+ "project-mode": "unknown",
9
+ "brownfield-intake-completed": false,
10
+ "brownfield-documentation-completed": false
8
11
  },
9
12
  "workflow": {
10
- "schema_version": 3,
13
+ "schema_version": 5,
11
14
  "plan": [
12
15
  {
13
16
  "id": "milestone-foundation",
@@ -44,6 +47,26 @@
44
47
  "reason": "Prerequisite gate has not passed yet."
45
48
  }
46
49
  },
50
+ {
51
+ "id": "task-brownfield-intake",
52
+ "kind": "task",
53
+ "title": "Capture project mode and baseline",
54
+ "route": {
55
+ "skill_name": "brownfield-intake",
56
+ "skill_path": "skills/brownfield-intake/SKILL.md",
57
+ "reason": "Project mode and existing codebase baseline have not been captured yet."
58
+ }
59
+ },
60
+ {
61
+ "id": "task-brownfield-documentation",
62
+ "kind": "task",
63
+ "title": "Document existing project context",
64
+ "route": {
65
+ "skill_name": "brownfield-documenter",
66
+ "skill_path": "skills/brownfield-documenter/SKILL.md",
67
+ "reason": "Brownfield project context has not been documented into ideation yet."
68
+ }
69
+ },
47
70
  {
48
71
  "id": "task-ideation",
49
72
  "kind": "task",
@@ -72,7 +95,10 @@
72
95
  }
73
96
  ]
74
97
  },
75
- "project-details": {},
98
+ "project-details": {
99
+ "mode": "unknown",
100
+ "brownfield_baseline": {}
101
+ },
76
102
  "ideation": {
77
103
  "research_agenda": {
78
104
  "version": 1,
@@ -104,6 +104,18 @@
104
104
  "ideation-completed": "persist finalized ideation"
105
105
  }
106
106
  },
107
+ "brownfield-intake": {
108
+ "description": "Project mode classification and brownfield baseline capture",
109
+ "checkpoints": {
110
+ "baseline-captured": "capture project mode and baseline inventory"
111
+ }
112
+ },
113
+ "brownfield-documenter": {
114
+ "description": "Brownfield project documentation persistence",
115
+ "checkpoints": {
116
+ "documentation-captured": "persist brownfield ideation documentation from existing codebase"
117
+ }
118
+ },
107
119
  "ideation-updater": {
108
120
  "description": "Iterative ideation updates",
109
121
  "checkpoints": {
@@ -29,7 +29,9 @@ def load_state(project_root: Path, requested_skill: str) -> dict:
29
29
  )
30
30
  raise SystemExit(2)
31
31
  data = default_data()
32
- return reconcile_workflow_state(data, cadence_dir_exists=cadence_exists)
32
+ # When `.cadence` exists but cadence.json is missing, recover through the
33
+ # scaffold route instead of treating scaffold as already complete.
34
+ return reconcile_workflow_state(data, cadence_dir_exists=False)
33
35
 
34
36
  try:
35
37
  with cadence_json_path.open("r", encoding="utf-8") as file:
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Check GitHub repo status and persist state.repo-enabled when .cadence exists."""
2
+ """Check repository remote status and persist state.repo-enabled when .cadence exists."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
@@ -42,7 +42,13 @@ def parse_args() -> argparse.Namespace:
42
42
  parser.add_argument(
43
43
  "--set-local-only",
44
44
  action="store_true",
45
- help="Persist local-only mode (repo-enabled=false) when no GitHub remote is configured.",
45
+ help="Persist local-only mode (repo-enabled=false) when remote policy is not satisfied.",
46
+ )
47
+ parser.add_argument(
48
+ "--remote-policy",
49
+ choices=("any", "github"),
50
+ default="any",
51
+ help="Remote policy for repo-enabled detection.",
46
52
  )
47
53
  return parser.parse_args()
48
54
 
@@ -90,7 +96,7 @@ def parse_remotes(remote_text: str) -> list[dict[str, str]]:
90
96
  return remotes
91
97
 
92
98
 
93
- def detect_git_repo(project_root: Path) -> dict[str, Any]:
99
+ def detect_git_repo(project_root: Path, *, remote_policy: str) -> dict[str, Any]:
94
100
  inside_result = run_command(["git", "rev-parse", "--is-inside-work-tree"], project_root)
95
101
  git_initialized = inside_result.returncode == 0 and inside_result.stdout.strip() == "true"
96
102
 
@@ -101,12 +107,17 @@ def detect_git_repo(project_root: Path) -> dict[str, Any]:
101
107
  repo_root = root_result.stdout.strip()
102
108
 
103
109
  remotes: list[dict[str, str]] = []
110
+ primary_remote_name = ""
111
+ primary_remote_url = ""
104
112
  github_remote_name = ""
105
113
  github_remote_url = ""
106
114
  if git_initialized:
107
115
  remote_result = run_command(["git", "remote", "-v"], project_root)
108
116
  if remote_result.returncode == 0:
109
117
  remotes = parse_remotes(remote_result.stdout)
118
+ if remotes:
119
+ primary_remote_name = remotes[0].get("name", "")
120
+ primary_remote_url = remotes[0].get("url", "")
110
121
 
111
122
  for remote in remotes:
112
123
  url = remote.get("url", "")
@@ -115,13 +126,21 @@ def detect_git_repo(project_root: Path) -> dict[str, Any]:
115
126
  github_remote_url = url
116
127
  break
117
128
 
129
+ remote_configured = bool(primary_remote_name and primary_remote_url)
118
130
  github_remote_configured = bool(github_remote_name and github_remote_url)
119
- repo_enabled_detected = bool(git_initialized and github_remote_configured)
131
+ if remote_policy == "github":
132
+ repo_enabled_detected = bool(git_initialized and github_remote_configured)
133
+ else:
134
+ repo_enabled_detected = bool(git_initialized and remote_configured)
120
135
 
121
136
  return {
122
137
  "git_initialized": git_initialized,
123
138
  "repo_root": repo_root,
124
139
  "remotes": remotes,
140
+ "remote_policy": remote_policy,
141
+ "remote_configured": remote_configured,
142
+ "primary_remote_name": primary_remote_name,
143
+ "primary_remote_url": primary_remote_url,
125
144
  "github_remote_configured": github_remote_configured,
126
145
  "github_remote_name": github_remote_name,
127
146
  "github_remote_url": github_remote_url,
@@ -166,7 +185,7 @@ def main() -> int:
166
185
 
167
186
  write_project_root_hint(SCRIPT_DIR, project_root)
168
187
 
169
- repo_status = detect_git_repo(project_root)
188
+ repo_status = detect_git_repo(project_root, remote_policy=args.remote_policy)
170
189
  cadence_exists = (project_root / CADENCE_JSON_PATH).exists()
171
190
  state_updated = False
172
191
 
@@ -202,6 +221,10 @@ def main() -> int:
202
221
  "state_updated": state_updated,
203
222
  "repo_enabled": repo_enabled_state,
204
223
  "repo_enabled_detected": bool(repo_status["repo_enabled_detected"]),
224
+ "remote_policy": str(repo_status.get("remote_policy", args.remote_policy)),
225
+ "remote_configured": bool(repo_status.get("remote_configured", False)),
226
+ "primary_remote_name": repo_status.get("primary_remote_name", ""),
227
+ "primary_remote_url": repo_status.get("primary_remote_url", ""),
205
228
  "git_initialized": bool(repo_status["git_initialized"]),
206
229
  "github_remote_configured": bool(repo_status["github_remote_configured"]),
207
230
  "github_remote_name": repo_status.get("github_remote_name", ""),
@@ -32,6 +32,14 @@ def run_cmd(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
32
32
  )
33
33
 
34
34
 
35
+ def resolve_repo_root(project_root: Path) -> Path:
36
+ result = run_cmd(["git", "rev-parse", "--show-toplevel"], project_root)
37
+ if result.returncode != 0:
38
+ detail = result.stderr.strip() or result.stdout.strip() or "NOT_A_GIT_REPOSITORY"
39
+ raise FinalizeError(detail)
40
+ return Path(result.stdout.strip()).resolve()
41
+
42
+
35
43
  def parse_args() -> argparse.Namespace:
36
44
  parser = argparse.ArgumentParser(
37
45
  description="Create atomic checkpoint commits for changed files at skill finalization.",
@@ -129,6 +137,61 @@ def sanitize_tag(tag: str) -> str:
129
137
  return compact[:10]
130
138
 
131
139
 
140
+ def project_relative_root(repo_root: Path, project_root: Path) -> str:
141
+ try:
142
+ relative = project_root.resolve().relative_to(repo_root.resolve())
143
+ except ValueError as exc:
144
+ raise FinalizeError("PROJECT_ROOT_OUTSIDE_REPOSITORY") from exc
145
+ text = normalize_path(relative.as_posix())
146
+ return "." if text in {"", "."} else text
147
+
148
+
149
+ def normalize_requested_pathspecs(
150
+ *,
151
+ requested_pathspecs: list[str],
152
+ project_root: Path,
153
+ repo_root: Path,
154
+ ) -> list[str]:
155
+ project_rel = project_relative_root(repo_root, project_root)
156
+ normalized_specs: list[str] = []
157
+
158
+ for raw in requested_pathspecs:
159
+ text = str(raw).strip()
160
+ if not text or text == ".":
161
+ normalized = project_rel
162
+ else:
163
+ candidate = Path(text)
164
+ if candidate.is_absolute():
165
+ try:
166
+ relative = candidate.resolve().relative_to(repo_root.resolve())
167
+ except ValueError as exc:
168
+ raise FinalizeError(f"PATHSPEC_OUTSIDE_REPOSITORY: {text}") from exc
169
+ normalized = normalize_path(relative.as_posix())
170
+ else:
171
+ parts = candidate.parts
172
+ if any(part == ".." for part in parts):
173
+ raise FinalizeError(f"PATHSPEC_OUTSIDE_PROJECT_ROOT: {text}")
174
+ rel_text = normalize_path(text)
175
+ if project_rel == ".":
176
+ normalized = rel_text
177
+ else:
178
+ normalized = normalize_path(f"{project_rel}/{rel_text}")
179
+
180
+ if project_rel != ".":
181
+ project_prefix = project_rel.rstrip("/")
182
+ if normalized != project_prefix and not normalized.startswith(f"{project_prefix}/"):
183
+ raise FinalizeError(f"PATHSPEC_OUTSIDE_PROJECT_ROOT: {text}")
184
+
185
+ if not normalized:
186
+ normalized = "."
187
+ if normalized not in normalized_specs:
188
+ normalized_specs.append(normalized)
189
+
190
+ if not normalized_specs:
191
+ return [project_rel]
192
+ return normalized_specs
193
+
194
+
132
195
  def classify_path(
133
196
  path: str,
134
197
  group_order: list[str],
@@ -324,6 +387,17 @@ def main() -> int:
324
387
  print("LOCAL_GIT_REPOSITORY_NOT_INITIALIZED", file=sys.stderr)
325
388
  return 2
326
389
 
390
+ try:
391
+ repo_root = resolve_repo_root(project_root)
392
+ scoped_pathspecs = normalize_requested_pathspecs(
393
+ requested_pathspecs=[str(path) for path in args.paths],
394
+ project_root=project_root,
395
+ repo_root=repo_root,
396
+ )
397
+ except FinalizeError as exc:
398
+ print(str(exc), file=sys.stderr)
399
+ return 2
400
+
327
401
  push_enabled = bool(repo_status.get("repo_enabled", False))
328
402
 
329
403
  status_result = run_cmd(
@@ -356,7 +430,7 @@ def main() -> int:
356
430
  )
357
431
  return 0
358
432
 
359
- eligible_files = filter_paths(changed_files, args.paths)
433
+ eligible_files = filter_paths(changed_files, scoped_pathspecs)
360
434
  if not eligible_files:
361
435
  print(
362
436
  json.dumps(
@@ -399,20 +473,21 @@ def main() -> int:
399
473
 
400
474
  print(
401
475
  json.dumps(
402
- {
403
- "status": "ok",
404
- "scope": args.scope,
405
- "checkpoint": args.checkpoint,
406
- "atomic": True,
476
+ {
477
+ "status": "ok",
478
+ "scope": args.scope,
479
+ "checkpoint": args.checkpoint,
480
+ "atomic": True,
407
481
  "changed_file_count": len(eligible_files),
408
482
  "batch_count": len(batches),
409
- "commit_count": len(commits),
410
- "push_enabled": push_enabled,
411
- "repo_status": repo_status,
412
- "commits": commits,
413
- }
483
+ "commit_count": len(commits),
484
+ "push_enabled": push_enabled,
485
+ "scoped_pathspecs": scoped_pathspecs,
486
+ "repo_status": repo_status,
487
+ "commits": commits,
488
+ }
489
+ )
414
490
  )
415
- )
416
491
  return 0
417
492
 
418
493
 
@@ -38,7 +38,9 @@ def load_state(project_root: Path):
38
38
 
39
39
  if not cadence_json_path.exists():
40
40
  data = default_data()
41
- data = reconcile_workflow_state(data, cadence_dir_exists=cadence_exists)
41
+ # When `.cadence` exists without cadence.json, initialize recovery state
42
+ # with scaffold pending so route guards can re-enter scaffold safely.
43
+ data = reconcile_workflow_state(data, cadence_dir_exists=False)
42
44
  if cadence_exists:
43
45
  save_state(project_root, data)
44
46
  return data