cadence-skill-installer 0.2.5 → 0.2.6

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.5",
3
+ "version": "0.2.6",
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
@@ -9,6 +9,28 @@ description: Structured project operating system for end-to-end greenfield or br
9
9
  1. Keep this root skill as an orchestrator.
10
10
  2. Delegate concrete execution to focused subskills.
11
11
 
12
+ ## Response Hygiene
13
+ 1. Keep user-facing replies outcome-focused and concise.
14
+ 2. Do not expose internal execution details unless the user explicitly asks for them:
15
+ - skill routing chains
16
+ - gate-by-gate status narration
17
+ - raw commands, terminal traces, or timing metadata
18
+
19
+ ## Repo Status Gate
20
+ 1. At the start of every Cadence turn, run `python3 scripts/check-project-repo-status.py` (resolve this relative path from this skill directory).
21
+ 2. Read `repo_enabled` from script output and treat it as the authoritative push mode.
22
+ 3. If `repo_enabled` is false, continue with local commits only until a GitHub remote is configured.
23
+
24
+ ## Git Checkpoints
25
+ 1. Enforce Cadence commit convention from `config/commit-conventions.json`.
26
+ 2. At the end of each successful subskill conversation, run `scripts/finalize-skill-checkpoint.py` with configured `--scope` and `--checkpoint` values.
27
+ 3. `finalize-skill-checkpoint.py` must check git diff first, split changes into atomic semantic batches, and create multiple small checkpoint commits when needed before push.
28
+ 4. `finalize-skill-checkpoint.py` must use repo status output so commits are local-only when `repo_enabled` is false.
29
+ 5. Use `--paths .` unless a narrower path is explicitly required, so commits include any files changed by the skill.
30
+ 6. Treat checkpoint or push failures as blocking; surface the exact script error to the user.
31
+ 7. If finalization returns `status=no_changes`, continue without failure.
32
+ 8. Do not gate checkpoint commits on test results yet; add that gate only when explicitly introduced later.
33
+
12
34
  ## Scaffold Gate (Mandatory On First Turn)
13
35
  1. Check for `.cadence` in the project root.
14
36
  2. If `.cadence` is missing, invoke `skills/scaffold/SKILL.md`.
@@ -26,7 +48,7 @@ description: Structured project operating system for end-to-end greenfield or br
26
48
  ## Ideation Flow
27
49
  1. Do not switch to `skills/ideator/SKILL.md` inside this conversation.
28
50
  2. After scaffold and prerequisite gates pass for a net-new project, hand off to a fresh chat so context resets cleanly.
29
- 3. Tell the user exactly: `now make a new chat and say "help me define my project" or provide a project brief.`
51
+ 3. Tell the user: `Start a new chat and either say "help me define my project" or share your project brief.`
30
52
  4. Stop here and wait for the user to continue in the new chat.
31
53
 
32
54
  ## Ideation Update Flow
@@ -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). 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. For net-new project starts, after scaffold and prerequisite gates pass, do not switch skills in-thread; tell the user: now make a new chat and say \"help me define my project\" or provide a project brief."
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. 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. For net-new project starts, after scaffold and prerequisite gates pass, do not switch skills in-thread; tell the user: Start a new chat and either say \"help me define my project\" or share your project brief."
@@ -36,6 +36,10 @@
36
36
  - Zero context switching required from the user
37
37
  - Go fix failing CI tests without being told how
38
38
 
39
+ ### 7. User-Facing Hygiene
40
+ - Keep user-facing messages outcome-focused
41
+ - Do not expose internal routing, command traces, terminal transcripts, or timing metadata unless the user explicitly asks
42
+
39
43
  ## Task Management
40
44
 
41
45
  1. **Plan First**: Write plan to `.cadence/tasks/todo.md` with checkable items
@@ -2,7 +2,8 @@
2
2
  "prerequisites-pass": false,
3
3
  "state": {
4
4
  "ideation-completed": false,
5
- "cadence-scripts-dir": ""
5
+ "cadence-scripts-dir": "",
6
+ "repo-enabled": false
6
7
  },
7
8
  "workflow": {
8
9
  "schema_version": 2,
@@ -0,0 +1,120 @@
1
+ {
2
+ "schema_version": 1,
3
+ "commit_type": "cadence",
4
+ "subject_max_length": 72,
5
+ "default_remote": "origin",
6
+ "atomic": {
7
+ "max_files_per_commit": 4,
8
+ "group_order": [
9
+ "cadence-state",
10
+ "skill-instructions",
11
+ "docs",
12
+ "scripts",
13
+ "tests",
14
+ "config",
15
+ "source"
16
+ ],
17
+ "groups": {
18
+ "cadence-state": {
19
+ "tag": "state",
20
+ "label": "cadence state",
21
+ "patterns": [
22
+ ".cadence/**"
23
+ ]
24
+ },
25
+ "skill-instructions": {
26
+ "tag": "skills",
27
+ "label": "skill instructions",
28
+ "patterns": [
29
+ "**/SKILL.md",
30
+ "**/AGENTS.md"
31
+ ]
32
+ },
33
+ "docs": {
34
+ "tag": "docs",
35
+ "label": "documentation",
36
+ "patterns": [
37
+ "docs/**",
38
+ "**/*.md"
39
+ ]
40
+ },
41
+ "scripts": {
42
+ "tag": "scripts",
43
+ "label": "automation scripts",
44
+ "patterns": [
45
+ "scripts/**",
46
+ "**/*.py",
47
+ "**/*.sh"
48
+ ]
49
+ },
50
+ "tests": {
51
+ "tag": "tests",
52
+ "label": "test updates",
53
+ "patterns": [
54
+ "tests/**",
55
+ "**/*test*",
56
+ "**/*.spec.*"
57
+ ]
58
+ },
59
+ "config": {
60
+ "tag": "config",
61
+ "label": "configuration",
62
+ "patterns": [
63
+ ".gitignore",
64
+ "**/.gitignore",
65
+ "package.json",
66
+ "package-lock.json",
67
+ "pnpm-lock.yaml",
68
+ "yarn.lock",
69
+ "**/*.json",
70
+ "**/*.yaml",
71
+ "**/*.yml",
72
+ "**/*.toml",
73
+ "**/*.ini"
74
+ ]
75
+ },
76
+ "source": {
77
+ "tag": "src",
78
+ "label": "source changes",
79
+ "patterns": [
80
+ "src/**",
81
+ "app/**",
82
+ "lib/**"
83
+ ]
84
+ }
85
+ }
86
+ },
87
+ "scopes": {
88
+ "scaffold": {
89
+ "description": "Scaffold and repository visibility policy",
90
+ "checkpoints": {
91
+ "cadence-tracked": "track .cadence and persist scaffold state",
92
+ "cadence-ignored": "ignore .cadence and persist scaffold state"
93
+ }
94
+ },
95
+ "prerequisite-gate": {
96
+ "description": "Prerequisite gate state transitions",
97
+ "checkpoints": {
98
+ "prerequisites-passed": "persist prerequisite gate pass state"
99
+ }
100
+ },
101
+ "ideator": {
102
+ "description": "Initial ideation persistence",
103
+ "checkpoints": {
104
+ "ideation-completed": "persist finalized ideation"
105
+ }
106
+ },
107
+ "ideation-updater": {
108
+ "description": "Iterative ideation updates",
109
+ "checkpoints": {
110
+ "ideation-updated": "persist ideation updates"
111
+ }
112
+ },
113
+ "project-progress": {
114
+ "description": "Project progress routing checks",
115
+ "checkpoints": {
116
+ "progress-checked": "check progress and route next action"
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ """Check GitHub repo status and persist state.repo-enabled when .cadence exists."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import copy
8
+ import json
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from workflow_state import default_data, reconcile_workflow_state
15
+
16
+
17
+ CADENCE_DIR = Path(".cadence")
18
+ CADENCE_JSON_PATH = CADENCE_DIR / "cadence.json"
19
+
20
+
21
+ def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
22
+ return subprocess.run(
23
+ command,
24
+ cwd=str(cwd),
25
+ capture_output=True,
26
+ text=True,
27
+ check=False,
28
+ )
29
+
30
+
31
+ def parse_args() -> argparse.Namespace:
32
+ parser = argparse.ArgumentParser(
33
+ description="Check project repo readiness and persist Cadence repo-enabled state.",
34
+ )
35
+ parser.add_argument(
36
+ "--project-root",
37
+ default=".",
38
+ help="Project root where git and .cadence state should be evaluated.",
39
+ )
40
+ parser.add_argument(
41
+ "--set-local-only",
42
+ action="store_true",
43
+ help="Persist local-only mode (repo-enabled=false) when no GitHub remote is configured.",
44
+ )
45
+ return parser.parse_args()
46
+
47
+
48
+ def load_cadence_data(project_root: Path) -> dict[str, Any] | None:
49
+ cadence_path = project_root / CADENCE_JSON_PATH
50
+ cadence_dir = project_root / CADENCE_DIR
51
+ if not cadence_path.exists():
52
+ return None
53
+
54
+ try:
55
+ with cadence_path.open("r", encoding="utf-8") as file:
56
+ raw = json.load(file)
57
+ except json.JSONDecodeError as exc:
58
+ print(f"INVALID_CADENCE_JSON: {exc}", file=sys.stderr)
59
+ raise SystemExit(1)
60
+
61
+ return reconcile_workflow_state(raw, cadence_dir_exists=cadence_dir.exists())
62
+
63
+
64
+ def save_cadence_data(project_root: Path, data: dict[str, Any]) -> None:
65
+ cadence_path = project_root / CADENCE_JSON_PATH
66
+ cadence_path.parent.mkdir(parents=True, exist_ok=True)
67
+ with cadence_path.open("w", encoding="utf-8") as file:
68
+ json.dump(data, file, indent=4)
69
+ file.write("\n")
70
+
71
+
72
+ def parse_remotes(remote_text: str) -> list[dict[str, str]]:
73
+ remotes: list[dict[str, str]] = []
74
+ seen: set[tuple[str, str]] = set()
75
+
76
+ for line in remote_text.splitlines():
77
+ parts = line.split()
78
+ if len(parts) < 2:
79
+ continue
80
+ name = parts[0].strip()
81
+ url = parts[1].strip()
82
+ key = (name, url)
83
+ if not name or not url or key in seen:
84
+ continue
85
+ seen.add(key)
86
+ remotes.append({"name": name, "url": url})
87
+
88
+ return remotes
89
+
90
+
91
+ def detect_git_repo(project_root: Path) -> dict[str, Any]:
92
+ inside_result = run_command(["git", "rev-parse", "--is-inside-work-tree"], project_root)
93
+ git_initialized = inside_result.returncode == 0 and inside_result.stdout.strip() == "true"
94
+
95
+ repo_root = ""
96
+ if git_initialized:
97
+ root_result = run_command(["git", "rev-parse", "--show-toplevel"], project_root)
98
+ if root_result.returncode == 0:
99
+ repo_root = root_result.stdout.strip()
100
+
101
+ remotes: list[dict[str, str]] = []
102
+ github_remote_name = ""
103
+ github_remote_url = ""
104
+ if git_initialized:
105
+ remote_result = run_command(["git", "remote", "-v"], project_root)
106
+ if remote_result.returncode == 0:
107
+ remotes = parse_remotes(remote_result.stdout)
108
+
109
+ for remote in remotes:
110
+ url = remote.get("url", "")
111
+ if "github.com" in url.lower() or "github." in url.lower():
112
+ github_remote_name = remote.get("name", "")
113
+ github_remote_url = url
114
+ break
115
+
116
+ github_remote_configured = bool(github_remote_name and github_remote_url)
117
+ repo_enabled_detected = bool(git_initialized and github_remote_configured)
118
+
119
+ return {
120
+ "git_initialized": git_initialized,
121
+ "repo_root": repo_root,
122
+ "remotes": remotes,
123
+ "github_remote_configured": github_remote_configured,
124
+ "github_remote_name": github_remote_name,
125
+ "github_remote_url": github_remote_url,
126
+ "repo_enabled_detected": repo_enabled_detected,
127
+ }
128
+
129
+
130
+ def ensure_default_state(data: dict[str, Any]) -> dict[str, Any]:
131
+ reconciled = reconcile_workflow_state(data, cadence_dir_exists=True)
132
+ state = reconciled.setdefault("state", {})
133
+ if not isinstance(state, dict):
134
+ state = {}
135
+ reconciled["state"] = state
136
+ state["repo-enabled"] = bool(state.get("repo-enabled", False))
137
+ return reconciled
138
+
139
+
140
+ def main() -> int:
141
+ args = parse_args()
142
+ project_root = Path(args.project_root).resolve()
143
+
144
+ repo_status = detect_git_repo(project_root)
145
+ cadence_exists = (project_root / CADENCE_JSON_PATH).exists()
146
+ state_updated = False
147
+
148
+ data = load_cadence_data(project_root)
149
+ if data is None and cadence_exists:
150
+ # Safety fallback for race conditions between exists check and file read.
151
+ data = default_data()
152
+
153
+ repo_enabled_state = bool(repo_status["repo_enabled_detected"])
154
+ if data is not None:
155
+ original = copy.deepcopy(data)
156
+ data = ensure_default_state(data)
157
+ state = data["state"]
158
+
159
+ if repo_status["repo_enabled_detected"]:
160
+ state["repo-enabled"] = True
161
+ elif args.set_local_only:
162
+ state["repo-enabled"] = False
163
+ else:
164
+ state["repo-enabled"] = False
165
+
166
+ repo_enabled_state = bool(state.get("repo-enabled", False))
167
+ if data != original:
168
+ save_cadence_data(project_root, data)
169
+ state_updated = True
170
+
171
+ response = {
172
+ "status": "ok",
173
+ "project_root": str(project_root),
174
+ "cadence_state_path": str(project_root / CADENCE_JSON_PATH),
175
+ "cadence_state_available": data is not None,
176
+ "state_updated": state_updated,
177
+ "repo_enabled": repo_enabled_state,
178
+ "repo_enabled_detected": bool(repo_status["repo_enabled_detected"]),
179
+ "git_initialized": bool(repo_status["git_initialized"]),
180
+ "github_remote_configured": bool(repo_status["github_remote_configured"]),
181
+ "github_remote_name": repo_status.get("github_remote_name", ""),
182
+ "github_remote_url": repo_status.get("github_remote_url", ""),
183
+ "set_local_only": bool(args.set_local_only),
184
+ }
185
+ print(json.dumps(response))
186
+ return 0
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """Update project .gitignore policy for .cadence visibility."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ TARGET_PATTERNS = {".cadence", ".cadence/"}
12
+
13
+
14
+ def parse_args() -> argparse.Namespace:
15
+ parser = argparse.ArgumentParser(
16
+ description="Configure whether .cadence should be tracked or ignored by git.",
17
+ )
18
+ parser.add_argument(
19
+ "--mode",
20
+ choices=["track", "ignore"],
21
+ required=True,
22
+ help="track removes .cadence ignore entries; ignore adds .cadence/ entry",
23
+ )
24
+ parser.add_argument(
25
+ "--gitignore-path",
26
+ default=".gitignore",
27
+ help="Path to .gitignore file",
28
+ )
29
+ return parser.parse_args()
30
+
31
+
32
+ def normalize_lines(text: str) -> list[str]:
33
+ return text.splitlines()
34
+
35
+
36
+ def apply_mode(lines: list[str], mode: str) -> list[str]:
37
+ filtered = [line for line in lines if line.strip() not in TARGET_PATTERNS]
38
+ if mode == "ignore":
39
+ filtered.append(".cadence/")
40
+ return filtered
41
+
42
+
43
+ def render_lines(lines: list[str]) -> str:
44
+ if not lines:
45
+ return ""
46
+ return "\n".join(lines) + "\n"
47
+
48
+
49
+ def main() -> int:
50
+ args = parse_args()
51
+ gitignore_path = Path(args.gitignore_path)
52
+
53
+ original_text = ""
54
+ if gitignore_path.exists():
55
+ original_text = gitignore_path.read_text(encoding="utf-8")
56
+
57
+ original_lines = normalize_lines(original_text)
58
+ updated_lines = apply_mode(original_lines, args.mode)
59
+ updated_text = render_lines(updated_lines)
60
+ changed = updated_text != original_text
61
+
62
+ if changed:
63
+ gitignore_path.write_text(updated_text, encoding="utf-8")
64
+
65
+ print(
66
+ json.dumps(
67
+ {
68
+ "status": "ok",
69
+ "mode": args.mode,
70
+ "path": str(gitignore_path),
71
+ "changed": changed,
72
+ }
73
+ )
74
+ )
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(main())