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 +1 -1
- package/skill/SKILL.md +21 -4
- package/skill/agents/openai.yaml +1 -1
- package/skill/assets/AGENTS.md +7 -6
- package/skill/assets/cadence.json +29 -3
- package/skill/config/commit-conventions.json +12 -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-documentation.py +531 -0
- package/skill/scripts/run-brownfield-intake.py +445 -0
- package/skill/scripts/scaffold-project.sh +32 -5
- package/skill/scripts/workflow_state.py +241 -2
- package/skill/skills/brownfield-documenter/SKILL.md +45 -0
- package/skill/skills/brownfield-documenter/agents/openai.yaml +4 -0
- package/skill/skills/brownfield-intake/SKILL.md +27 -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_documentation.py +304 -0
- package/skill/tests/test_run_brownfield_intake.py +99 -0
- package/skill/tests/test_run_research_pass.py +8 -1
- package/skill/tests/test_workflow_state.py +54 -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,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 `
|
|
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
|
|
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.
|
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 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."
|
package/skill/assets/AGENTS.md
CHANGED
|
@@ -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:
|
|
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**:
|
|
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**:
|
|
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**:
|
|
50
|
-
6. **Capture Lessons**:
|
|
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
|
-
- **
|
|
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":
|
|
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
|
-
|
|
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
|