cadence-skill-installer 0.2.8 → 0.2.10
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 +17 -10
- package/skill/agents/openai.yaml +1 -1
- package/skill/scripts/assert-workflow-route.py +53 -39
- package/skill/scripts/check-project-repo-status.py +3 -0
- package/skill/scripts/handle-prerequisite-state.py +54 -23
- package/skill/scripts/init-cadence-scripts-dir.py +50 -14
- package/skill/scripts/project_root.py +93 -0
- package/skill/scripts/read-workflow-state.py +51 -14
- package/skill/scripts/resolve-project-root.py +73 -0
- package/skill/scripts/resolve-project-scripts-dir.py +49 -13
- package/skill/scripts/run-prerequisite-gate.py +72 -12
- package/skill/scripts/run-scaffold-gate.py +66 -14
- package/skill/skills/ideation-updater/SKILL.md +20 -19
- package/skill/skills/ideation-updater/agents/openai.yaml +1 -1
- package/skill/skills/ideator/SKILL.md +23 -22
- package/skill/skills/ideator/agents/openai.yaml +1 -1
- package/skill/skills/prerequisite-gate/SKILL.md +11 -9
- package/skill/skills/prerequisite-gate/agents/openai.yaml +1 -1
- package/skill/skills/project-progress/SKILL.md +10 -8
- package/skill/skills/project-progress/agents/openai.yaml +1 -1
- package/skill/skills/scaffold/SKILL.md +24 -23
- package/skill/skills/scaffold/agents/openai.yaml +1 -1
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -37,24 +37,31 @@ description: Structured project operating system for end-to-end greenfield or br
|
|
|
37
37
|
3. Scaffold initializes and persists `state.cadence-scripts-dir` for later subskill commands.
|
|
38
38
|
4. If `.cadence` exists, skip scaffold.
|
|
39
39
|
|
|
40
|
-
##
|
|
41
|
-
1.
|
|
42
|
-
2.
|
|
40
|
+
## Workflow Route Gate (Mandatory Every Turn)
|
|
41
|
+
1. After scaffold handling, run `python3 scripts/read-workflow-state.py` and parse the JSON response.
|
|
42
|
+
2. Treat `next_item` and `route.skill_name` from that response as the authoritative workflow route.
|
|
43
|
+
3. Do not invoke a state-changing subskill unless it matches `route.skill_name`.
|
|
44
|
+
|
|
45
|
+
## Prerequisite Gate (Conditional)
|
|
46
|
+
1. Invoke `skills/prerequisite-gate/SKILL.md` only when `route.skill_name` is `prerequisite-gate`.
|
|
47
|
+
2. If `route.skill_name` is not `prerequisite-gate` (for example `ideator`), skip prerequisite gate and follow the active route instead.
|
|
43
48
|
|
|
44
49
|
## Progress / Resume Flow
|
|
45
50
|
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?").
|
|
46
51
|
2. Use that skill's state-based routing result to continue from the correct next phase.
|
|
47
52
|
|
|
48
53
|
## Manual Subskill Safety Gate
|
|
49
|
-
1. If the user manually requests a Cadence subskill,
|
|
50
|
-
2.
|
|
51
|
-
3.
|
|
54
|
+
1. If the user manually requests a Cadence subskill, first resolve `PROJECT_ROOT` with `python3 scripts/resolve-project-root.py`.
|
|
55
|
+
2. Run `python3 scripts/assert-workflow-route.py --skill-name <subskill> --project-root "$PROJECT_ROOT"` before executing that subskill.
|
|
56
|
+
3. If route assertion fails, stop and surface the exact script error.
|
|
57
|
+
4. Do not execute state-changing subskill steps when assertion fails.
|
|
52
58
|
|
|
53
59
|
## Ideation Flow
|
|
54
|
-
1.
|
|
55
|
-
2.
|
|
56
|
-
3.
|
|
57
|
-
4.
|
|
60
|
+
1. When scaffold and prerequisite both complete in this same conversation for a net-new project, hand off to a fresh chat so context resets cleanly.
|
|
61
|
+
2. Use this exact handoff line: `Start a new chat and either say "help me define my project" or share your project brief.`
|
|
62
|
+
3. In subsequent conversations, if the workflow route is `ideator`, do not rerun prerequisite gate.
|
|
63
|
+
4. If the user asks to define the project or provides a brief while route is `ideator`, invoke `skills/ideator/SKILL.md`.
|
|
64
|
+
5. If route is `ideator` and the user has not provided ideation input yet, prompt with the same handoff line and wait.
|
|
58
65
|
|
|
59
66
|
## Ideation Update Flow
|
|
60
67
|
1. If the user wants to modify or discuss existing ideation, invoke `skills/ideation-updater/SKILL.md`.
|
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.
|
|
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. At the start of each Cadence turn, run scripts/check-project-repo-status.py, run scaffold only when .cadence is missing, then run scripts/read-workflow-state.py and treat route.skill_name as authoritative for the next state-changing skill. Invoke skills/prerequisite-gate/SKILL.md only when route.skill_name is prerequisite-gate. If route.skill_name is ideator, do not run prerequisite again. For net-new project starts where scaffold and prerequisite just completed in-thread, hand off 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 and the user asks to define the project or provides a brief, invoke skills/ideator/SKILL.md. 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 and then run scripts/assert-workflow-route.py --skill-name <subskill> --project-root \"$PROJECT_ROOT\" before any state-changing actions."
|
|
@@ -3,70 +3,81 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import argparse
|
|
6
7
|
import copy
|
|
7
8
|
import json
|
|
8
9
|
import sys
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
12
|
+
from project_root import resolve_project_root, write_project_root_hint
|
|
11
13
|
from workflow_state import default_data, reconcile_workflow_state
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
CADENCE_JSON_PATH = CADENCE_DIR / "cadence.json"
|
|
16
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def load_state() -> dict:
|
|
19
|
-
|
|
19
|
+
def load_state(project_root: Path, requested_skill: str) -> dict:
|
|
20
|
+
cadence_dir = project_root / ".cadence"
|
|
21
|
+
cadence_json_path = cadence_dir / "cadence.json"
|
|
22
|
+
cadence_exists = cadence_dir.exists()
|
|
20
23
|
|
|
21
|
-
if not
|
|
24
|
+
if not cadence_json_path.exists():
|
|
25
|
+
if requested_skill != "scaffold":
|
|
26
|
+
print(
|
|
27
|
+
f"MISSING_CADENCE_STATE: project_root={project_root}",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
raise SystemExit(2)
|
|
22
31
|
data = default_data()
|
|
23
32
|
return reconcile_workflow_state(data, cadence_dir_exists=cadence_exists)
|
|
24
33
|
|
|
25
34
|
try:
|
|
26
|
-
with
|
|
35
|
+
with cadence_json_path.open("r", encoding="utf-8") as file:
|
|
27
36
|
original_data = json.load(file)
|
|
28
37
|
except json.JSONDecodeError as exc:
|
|
29
|
-
print(f"INVALID_CADENCE_JSON: {exc}", file=sys.stderr)
|
|
38
|
+
print(f"INVALID_CADENCE_JSON: {exc} path={cadence_json_path}", file=sys.stderr)
|
|
30
39
|
raise SystemExit(1)
|
|
31
40
|
|
|
32
41
|
return reconcile_workflow_state(copy.deepcopy(original_data), cadence_dir_exists=cadence_exists)
|
|
33
42
|
|
|
34
43
|
|
|
35
|
-
def parse_args(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
continue
|
|
52
|
-
if token == "--allow-complete":
|
|
53
|
-
allow_complete = True
|
|
54
|
-
idx += 1
|
|
55
|
-
continue
|
|
56
|
-
|
|
57
|
-
print(f"UNKNOWN_ARGUMENT: {token}", file=sys.stderr)
|
|
58
|
-
raise SystemExit(2)
|
|
44
|
+
def parse_args() -> argparse.Namespace:
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
description="Assert that a requested Cadence skill matches the workflow route.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument("--skill-name", required=True, help="Requested subskill name.")
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--allow-complete",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Allow success when workflow is already complete.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--project-root",
|
|
56
|
+
default="",
|
|
57
|
+
help="Explicit project root path override.",
|
|
58
|
+
)
|
|
59
|
+
return parser.parse_args()
|
|
59
60
|
|
|
60
|
-
if not skill_name:
|
|
61
|
-
print("MISSING_SKILL_NAME", file=sys.stderr)
|
|
62
|
-
raise SystemExit(2)
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
def main() -> int:
|
|
63
|
+
args = parse_args()
|
|
64
|
+
requested_skill = str(args.skill_name).strip()
|
|
65
|
+
allow_complete = bool(args.allow_complete)
|
|
66
|
+
explicit_project_root = args.project_root.strip() or None
|
|
65
67
|
|
|
68
|
+
try:
|
|
69
|
+
project_root, root_source = resolve_project_root(
|
|
70
|
+
script_dir=SCRIPT_DIR,
|
|
71
|
+
explicit_project_root=explicit_project_root,
|
|
72
|
+
require_cadence=False,
|
|
73
|
+
allow_hint=True,
|
|
74
|
+
)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
print(str(exc), file=sys.stderr)
|
|
77
|
+
return 2
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data = load_state()
|
|
79
|
+
write_project_root_hint(SCRIPT_DIR, project_root)
|
|
80
|
+
data = load_state(project_root, requested_skill)
|
|
70
81
|
|
|
71
82
|
workflow = data.get("workflow", {})
|
|
72
83
|
next_item = workflow.get("next_item", {})
|
|
@@ -87,7 +98,8 @@ def main() -> int:
|
|
|
87
98
|
"WORKFLOW_ROUTE_MISMATCH: "
|
|
88
99
|
f"expected={expected} "
|
|
89
100
|
f"requested={requested_skill} "
|
|
90
|
-
f"next_item={next_item_id}"
|
|
101
|
+
f"next_item={next_item_id} "
|
|
102
|
+
f"project_root={project_root}"
|
|
91
103
|
),
|
|
92
104
|
file=sys.stderr,
|
|
93
105
|
)
|
|
@@ -102,6 +114,8 @@ def main() -> int:
|
|
|
102
114
|
"next_item_id": next_item_id,
|
|
103
115
|
"next_item_title": next_item_title,
|
|
104
116
|
"workflow_complete": next_item_id == "complete",
|
|
117
|
+
"project_root": str(project_root),
|
|
118
|
+
"project_root_source": root_source,
|
|
105
119
|
}
|
|
106
120
|
)
|
|
107
121
|
)
|
|
@@ -11,11 +11,13 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from project_root import write_project_root_hint
|
|
14
15
|
from workflow_state import default_data, reconcile_workflow_state
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
CADENCE_DIR = Path(".cadence")
|
|
18
19
|
CADENCE_JSON_PATH = CADENCE_DIR / "cadence.json"
|
|
20
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
@@ -140,6 +142,7 @@ def ensure_default_state(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
140
142
|
def main() -> int:
|
|
141
143
|
args = parse_args()
|
|
142
144
|
project_root = Path(args.project_root).resolve()
|
|
145
|
+
write_project_root_hint(SCRIPT_DIR, project_root)
|
|
143
146
|
|
|
144
147
|
repo_status = detect_git_repo(project_root)
|
|
145
148
|
cadence_exists = (project_root / CADENCE_JSON_PATH).exists()
|
|
@@ -1,49 +1,80 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Read or update prerequisite pass state in .cadence/cadence.json."""
|
|
3
3
|
|
|
4
|
+
import argparse
|
|
4
5
|
import json
|
|
5
|
-
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from project_root import resolve_project_root, write_project_root_hint
|
|
8
9
|
from workflow_state import default_data, reconcile_workflow_state
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
|
|
15
|
+
def parse_args() -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Read or update prerequisites-pass in cadence.json.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"pass_state",
|
|
21
|
+
nargs="?",
|
|
22
|
+
choices=("0", "1"),
|
|
23
|
+
help="Set prerequisites-pass to 0 or 1. Omit to read current value.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--project-root",
|
|
27
|
+
default="",
|
|
28
|
+
help="Explicit project root path override.",
|
|
29
|
+
)
|
|
30
|
+
return parser.parse_args()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cadence_json_path(project_root: Path) -> Path:
|
|
34
|
+
return project_root / ".cadence" / "cadence.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_data(project_root: Path):
|
|
38
|
+
state_path = cadence_json_path(project_root)
|
|
39
|
+
if not state_path.exists():
|
|
16
40
|
return default_data()
|
|
17
|
-
with
|
|
41
|
+
with state_path.open("r", encoding="utf-8") as file:
|
|
18
42
|
data = json.load(file)
|
|
19
|
-
return reconcile_workflow_state(data, cadence_dir_exists=
|
|
43
|
+
return reconcile_workflow_state(data, cadence_dir_exists=state_path.parent.exists())
|
|
20
44
|
|
|
21
45
|
|
|
22
|
-
def save_data(data):
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
def save_data(project_root: Path, data):
|
|
47
|
+
state_path = cadence_json_path(project_root)
|
|
48
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
with state_path.open("w", encoding="utf-8") as file:
|
|
25
50
|
json.dump(data, file, indent=4)
|
|
26
51
|
file.write("\n")
|
|
27
52
|
|
|
28
53
|
|
|
29
54
|
def main():
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
args = parse_args()
|
|
56
|
+
explicit_project_root = args.project_root.strip() or None
|
|
57
|
+
try:
|
|
58
|
+
project_root, _ = resolve_project_root(
|
|
59
|
+
script_dir=SCRIPT_DIR,
|
|
60
|
+
explicit_project_root=explicit_project_root,
|
|
61
|
+
require_cadence=False,
|
|
62
|
+
allow_hint=True,
|
|
63
|
+
)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
print(str(exc))
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
write_project_root_hint(SCRIPT_DIR, project_root)
|
|
69
|
+
data = load_data(project_root)
|
|
70
|
+
|
|
71
|
+
if args.pass_state is None:
|
|
41
72
|
print("true" if bool(data.get("prerequisites-pass", False)) else "false")
|
|
42
73
|
return 0
|
|
43
74
|
|
|
44
|
-
data["prerequisites-pass"] =
|
|
45
|
-
data = reconcile_workflow_state(data, cadence_dir_exists=
|
|
46
|
-
save_data(data)
|
|
75
|
+
data["prerequisites-pass"] = args.pass_state == "1"
|
|
76
|
+
data = reconcile_workflow_state(data, cadence_dir_exists=cadence_json_path(project_root).parent.exists())
|
|
77
|
+
save_data(project_root, data)
|
|
47
78
|
print("true" if data["prerequisites-pass"] else "false")
|
|
48
79
|
return 0
|
|
49
80
|
|
|
@@ -1,43 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Persist Cadence helper scripts directory in .cadence/cadence.json."""
|
|
3
3
|
|
|
4
|
+
import argparse
|
|
4
5
|
import json
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
8
|
+
from project_root import resolve_project_root, write_project_root_hint
|
|
7
9
|
from workflow_state import default_data, reconcile_workflow_state
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
CADENCE_JSON_PATH = CADENCE_DIR / "cadence.json"
|
|
12
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
|
|
15
|
+
def parse_args() -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Persist cadence-scripts-dir in a project cadence.json.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--project-root",
|
|
21
|
+
default="",
|
|
22
|
+
help="Explicit project root path override.",
|
|
23
|
+
)
|
|
24
|
+
return parser.parse_args()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cadence_paths(project_root: Path) -> tuple[Path, Path]:
|
|
28
|
+
cadence_dir = project_root / ".cadence"
|
|
29
|
+
cadence_json_path = cadence_dir / "cadence.json"
|
|
30
|
+
return cadence_dir, cadence_json_path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_data(project_root: Path):
|
|
34
|
+
cadence_dir, cadence_json_path = cadence_paths(project_root)
|
|
35
|
+
if not cadence_json_path.exists():
|
|
16
36
|
return default_data()
|
|
17
|
-
with
|
|
37
|
+
with cadence_json_path.open("r", encoding="utf-8") as file:
|
|
18
38
|
data = json.load(file)
|
|
19
|
-
return reconcile_workflow_state(data, cadence_dir_exists=
|
|
39
|
+
return reconcile_workflow_state(data, cadence_dir_exists=cadence_dir.exists())
|
|
20
40
|
|
|
21
41
|
|
|
22
|
-
def save_data(data):
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
def save_data(project_root: Path, data):
|
|
43
|
+
cadence_dir, cadence_json_path = cadence_paths(project_root)
|
|
44
|
+
cadence_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
with cadence_json_path.open("w", encoding="utf-8") as file:
|
|
25
46
|
json.dump(data, file, indent=4)
|
|
26
47
|
file.write("\n")
|
|
27
48
|
|
|
28
49
|
|
|
29
50
|
def main():
|
|
30
|
-
|
|
51
|
+
args = parse_args()
|
|
52
|
+
explicit_project_root = args.project_root.strip() or None
|
|
53
|
+
try:
|
|
54
|
+
project_root, _ = resolve_project_root(
|
|
55
|
+
script_dir=SCRIPT_DIR,
|
|
56
|
+
explicit_project_root=explicit_project_root,
|
|
57
|
+
require_cadence=False,
|
|
58
|
+
allow_hint=True,
|
|
59
|
+
)
|
|
60
|
+
except ValueError as exc:
|
|
61
|
+
print(str(exc))
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
cadence_dir, _ = cadence_paths(project_root)
|
|
65
|
+
if not cadence_dir.exists():
|
|
31
66
|
print("MISSING_CADENCE_DIR")
|
|
32
67
|
return 1
|
|
33
68
|
|
|
34
|
-
scripts_dir = str(
|
|
69
|
+
scripts_dir = str(SCRIPT_DIR)
|
|
35
70
|
|
|
36
|
-
data = load_data()
|
|
71
|
+
data = load_data(project_root)
|
|
37
72
|
state = data.setdefault("state", {})
|
|
38
73
|
state["cadence-scripts-dir"] = scripts_dir
|
|
39
|
-
data = reconcile_workflow_state(data, cadence_dir_exists=
|
|
40
|
-
save_data(data)
|
|
74
|
+
data = reconcile_workflow_state(data, cadence_dir_exists=cadence_dir.exists())
|
|
75
|
+
save_data(project_root, data)
|
|
76
|
+
write_project_root_hint(SCRIPT_DIR, project_root)
|
|
41
77
|
|
|
42
78
|
print(json.dumps({"status": "ok", "cadence_scripts_dir": scripts_dir}))
|
|
43
79
|
return 0
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Project-root resolution and persistence helpers for Cadence scripts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PROJECT_ROOT_HINT_FILE = ".last-project-root"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def hint_file_path(script_dir: Path) -> Path:
|
|
13
|
+
return script_dir / PROJECT_ROOT_HINT_FILE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_project_root_hint(script_dir: Path, project_root: Path) -> None:
|
|
17
|
+
"""Best-effort write of the most recent Cadence project root."""
|
|
18
|
+
try:
|
|
19
|
+
hint_path = hint_file_path(script_dir)
|
|
20
|
+
hint_path.write_text(f"{project_root.resolve()}\n", encoding="utf-8")
|
|
21
|
+
except OSError:
|
|
22
|
+
# Hint persistence is convenience only; never fail gate scripts for this.
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_project_root_hint(script_dir: Path) -> Path | None:
|
|
27
|
+
hint_path = hint_file_path(script_dir)
|
|
28
|
+
try:
|
|
29
|
+
raw = hint_path.read_text(encoding="utf-8").strip()
|
|
30
|
+
except OSError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
if not raw:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
candidate = Path(raw).expanduser().resolve()
|
|
37
|
+
if (candidate / ".cadence").is_dir():
|
|
38
|
+
return candidate
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_cadence_project_root(start: Path) -> Path | None:
|
|
43
|
+
current = start.resolve()
|
|
44
|
+
for candidate in [current, *current.parents]:
|
|
45
|
+
if (candidate / ".cadence").is_dir():
|
|
46
|
+
return candidate
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_project_root(
|
|
51
|
+
*,
|
|
52
|
+
script_dir: Path,
|
|
53
|
+
explicit_project_root: str | None = None,
|
|
54
|
+
require_cadence: bool = False,
|
|
55
|
+
allow_hint: bool = True,
|
|
56
|
+
) -> tuple[Path, str]:
|
|
57
|
+
"""Resolve the active project root.
|
|
58
|
+
|
|
59
|
+
Resolution order:
|
|
60
|
+
1) Explicit `--project-root`
|
|
61
|
+
2) Nearest ancestor of cwd containing `.cadence`
|
|
62
|
+
3) Most recent persisted hint (if enabled)
|
|
63
|
+
4) cwd fallback
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
source = "cwd"
|
|
67
|
+
if explicit_project_root:
|
|
68
|
+
project_root = Path(explicit_project_root).expanduser().resolve()
|
|
69
|
+
source = "explicit"
|
|
70
|
+
else:
|
|
71
|
+
project_root = find_cadence_project_root(Path.cwd()) or Path.cwd().resolve()
|
|
72
|
+
if (project_root / ".cadence").is_dir():
|
|
73
|
+
source = "cwd"
|
|
74
|
+
elif allow_hint:
|
|
75
|
+
hinted_root = read_project_root_hint(script_dir)
|
|
76
|
+
if hinted_root is not None:
|
|
77
|
+
project_root = hinted_root
|
|
78
|
+
source = "hint"
|
|
79
|
+
else:
|
|
80
|
+
source = "cwd-fallback"
|
|
81
|
+
else:
|
|
82
|
+
source = "cwd-fallback"
|
|
83
|
+
|
|
84
|
+
if require_cadence and not (project_root / ".cadence").is_dir():
|
|
85
|
+
raise ValueError(f"MISSING_CADENCE_DIR: {project_root}")
|
|
86
|
+
|
|
87
|
+
if not project_root.exists():
|
|
88
|
+
raise ValueError(f"PROJECT_ROOT_NOT_FOUND: {project_root}")
|
|
89
|
+
|
|
90
|
+
if not project_root.is_dir():
|
|
91
|
+
raise ValueError(f"PROJECT_ROOT_NOT_DIRECTORY: {project_root}")
|
|
92
|
+
|
|
93
|
+
return project_root, source
|
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Read and normalize Cadence workflow state from .cadence/cadence.json."""
|
|
3
3
|
|
|
4
|
+
import argparse
|
|
4
5
|
import copy
|
|
5
6
|
import json
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
10
|
+
from project_root import resolve_project_root, write_project_root_hint
|
|
9
11
|
from workflow_state import default_data, reconcile_workflow_state
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
CADENCE_JSON_PATH = CADENCE_DIR / "cadence.json"
|
|
14
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
-
|
|
17
|
+
def parse_args() -> argparse.Namespace:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Read and normalize Cadence workflow state.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--project-root",
|
|
23
|
+
default="",
|
|
24
|
+
help="Explicit project root path override.",
|
|
25
|
+
)
|
|
26
|
+
return parser.parse_args()
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
|
|
29
|
+
def cadence_paths(project_root: Path) -> tuple[Path, Path]:
|
|
30
|
+
cadence_dir = project_root / ".cadence"
|
|
31
|
+
cadence_json_path = cadence_dir / "cadence.json"
|
|
32
|
+
return cadence_dir, cadence_json_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_state(project_root: Path):
|
|
36
|
+
cadence_dir, cadence_json_path = cadence_paths(project_root)
|
|
37
|
+
cadence_exists = cadence_dir.exists()
|
|
38
|
+
|
|
39
|
+
if not cadence_json_path.exists():
|
|
20
40
|
data = default_data()
|
|
21
41
|
data = reconcile_workflow_state(data, cadence_dir_exists=cadence_exists)
|
|
22
42
|
if cadence_exists:
|
|
23
|
-
save_state(data)
|
|
43
|
+
save_state(project_root, data)
|
|
24
44
|
return data
|
|
25
45
|
|
|
26
46
|
try:
|
|
27
|
-
with
|
|
47
|
+
with cadence_json_path.open("r", encoding="utf-8") as file:
|
|
28
48
|
original_data = json.load(file)
|
|
29
49
|
except json.JSONDecodeError as exc:
|
|
30
50
|
print(f"INVALID_CADENCE_JSON: {exc}", file=sys.stderr)
|
|
@@ -32,18 +52,19 @@ def load_state():
|
|
|
32
52
|
|
|
33
53
|
data = reconcile_workflow_state(copy.deepcopy(original_data), cadence_dir_exists=cadence_exists)
|
|
34
54
|
if data != original_data:
|
|
35
|
-
save_state(data)
|
|
55
|
+
save_state(project_root, data)
|
|
36
56
|
return data
|
|
37
57
|
|
|
38
58
|
|
|
39
|
-
def save_state(data):
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
def save_state(project_root: Path, data):
|
|
60
|
+
cadence_dir, cadence_json_path = cadence_paths(project_root)
|
|
61
|
+
cadence_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
with cadence_json_path.open("w", encoding="utf-8") as file:
|
|
42
63
|
json.dump(data, file, indent=4)
|
|
43
64
|
file.write("\n")
|
|
44
65
|
|
|
45
66
|
|
|
46
|
-
def build_response(data):
|
|
67
|
+
def build_response(data, project_root: Path, project_root_source: str):
|
|
47
68
|
workflow = data.get("workflow", {})
|
|
48
69
|
next_item = workflow.get("next_item", {})
|
|
49
70
|
route = workflow.get("next_route", {"skill_name": "", "skill_path": "", "reason": ""})
|
|
@@ -69,12 +90,28 @@ def build_response(data):
|
|
|
69
90
|
"next_phase": str(workflow.get("next_phase", next_item_id)),
|
|
70
91
|
"route": route,
|
|
71
92
|
"message": message,
|
|
93
|
+
"project_root": str(project_root),
|
|
94
|
+
"project_root_source": project_root_source,
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
|
|
75
98
|
def main():
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
args = parse_args()
|
|
100
|
+
explicit_project_root = args.project_root.strip() or None
|
|
101
|
+
try:
|
|
102
|
+
project_root, project_root_source = resolve_project_root(
|
|
103
|
+
script_dir=SCRIPT_DIR,
|
|
104
|
+
explicit_project_root=explicit_project_root,
|
|
105
|
+
require_cadence=False,
|
|
106
|
+
allow_hint=True,
|
|
107
|
+
)
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
print(str(exc), file=sys.stderr)
|
|
110
|
+
raise SystemExit(1)
|
|
111
|
+
|
|
112
|
+
write_project_root_hint(SCRIPT_DIR, project_root)
|
|
113
|
+
data = load_state(project_root)
|
|
114
|
+
response = build_response(data, project_root, project_root_source)
|
|
78
115
|
print(json.dumps(response))
|
|
79
116
|
|
|
80
117
|
|