cadence-skill-installer 0.2.26 → 0.2.27
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 +1 -1
- package/skill/SKILL.md +9 -3
- package/skill/agents/openai.yaml +1 -1
- package/skill/assets/cadence.json +18 -3
- package/skill/config/commit-conventions.json +6 -0
- package/skill/scripts/assert-workflow-route.py +3 -1
- package/skill/scripts/check-project-repo-status.py +28 -5
- package/skill/scripts/finalize-skill-checkpoint.py +87 -12
- package/skill/scripts/read-workflow-state.py +3 -1
- package/skill/scripts/run-brownfield-intake.py +426 -0
- package/skill/scripts/scaffold-project.sh +21 -5
- package/skill/scripts/workflow_state.py +114 -1
- package/skill/skills/brownfield-intake/SKILL.md +26 -0
- package/skill/skills/brownfield-intake/agents/openai.yaml +4 -0
- package/skill/skills/scaffold/SKILL.md +4 -2
- package/skill/skills/scaffold/agents/openai.yaml +1 -1
- package/skill/tests/test_check_project_repo_status.py +71 -0
- package/skill/tests/test_finalize_checkpoint_batches.py +25 -0
- package/skill/tests/test_route_assertion.py +23 -0
- package/skill/tests/test_run_brownfield_intake.py +98 -0
- package/skill/tests/test_run_research_pass.py +1 -1
- package/skill/tests/test_workflow_state.py +23 -1
package/package.json
CHANGED
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,
|
|
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,12 @@ 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
|
|
63
|
+
2. If `route.skill_name` is not `prerequisite-gate` (for example `brownfield-intake`, `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.
|
|
63
69
|
|
|
64
70
|
## Progress / Resume Flow
|
|
65
71
|
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,7 +79,7 @@ description: Structured project operating system for end-to-end greenfield or br
|
|
|
73
79
|
5. Do not execute state-changing subskill steps when assertion fails.
|
|
74
80
|
|
|
75
81
|
## Ideation Flow
|
|
76
|
-
1. When scaffold and
|
|
82
|
+
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.`
|
|
77
83
|
2. In subsequent conversations, if the workflow route is `ideator`, do not rerun prerequisite gate.
|
|
78
84
|
3. If the user asks to define the project or provides a brief while route is `ideator`, invoke `skills/ideator/SKILL.md`.
|
|
79
85
|
4. If route is `ideator` and the user has not provided ideation input yet, ask one kickoff ideation question in-thread and continue.
|
package/skill/agents/openai.yaml
CHANGED
|
@@ -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
|
|
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 brownfield baseline capture happen before ideation. If scaffold, prerequisite, and brownfield-intake 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 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. 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,10 +4,12 @@
|
|
|
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
|
|
8
10
|
},
|
|
9
11
|
"workflow": {
|
|
10
|
-
"schema_version":
|
|
12
|
+
"schema_version": 4,
|
|
11
13
|
"plan": [
|
|
12
14
|
{
|
|
13
15
|
"id": "milestone-foundation",
|
|
@@ -44,6 +46,16 @@
|
|
|
44
46
|
"reason": "Prerequisite gate has not passed yet."
|
|
45
47
|
}
|
|
46
48
|
},
|
|
49
|
+
{
|
|
50
|
+
"id": "task-brownfield-intake",
|
|
51
|
+
"kind": "task",
|
|
52
|
+
"title": "Capture project mode and baseline",
|
|
53
|
+
"route": {
|
|
54
|
+
"skill_name": "brownfield-intake",
|
|
55
|
+
"skill_path": "skills/brownfield-intake/SKILL.md",
|
|
56
|
+
"reason": "Project mode and existing codebase baseline have not been captured yet."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
47
59
|
{
|
|
48
60
|
"id": "task-ideation",
|
|
49
61
|
"kind": "task",
|
|
@@ -72,7 +84,10 @@
|
|
|
72
84
|
}
|
|
73
85
|
]
|
|
74
86
|
},
|
|
75
|
-
"project-details": {
|
|
87
|
+
"project-details": {
|
|
88
|
+
"mode": "unknown",
|
|
89
|
+
"brownfield_baseline": {}
|
|
90
|
+
},
|
|
76
91
|
"ideation": {
|
|
77
92
|
"research_agenda": {
|
|
78
93
|
"version": 1,
|
|
@@ -104,6 +104,12 @@
|
|
|
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
|
+
},
|
|
107
113
|
"ideation-updater": {
|
|
108
114
|
"description": "Iterative ideation updates",
|
|
109
115
|
"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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|