cc-dev-template 0.1.97 → 0.1.98

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/bin/install.js CHANGED
@@ -254,6 +254,7 @@ if (fs.existsSync(mergeSettingsPath)) {
254
254
  const configs = [
255
255
  { file: 'task-output-guard-hook.json', name: 'TaskOutput context guard' },
256
256
  { file: 'statusline-config.json', name: 'Custom status line' },
257
+ { file: 'ship-policy-hook.json', name: 'Ship policy enforcement' },
257
258
  // Spinner verbs - choose one (Star Trek or Factorio)
258
259
  { file: 'spinner-verbs-startrek.json', name: 'Star Trek spinner verbs' }
259
260
  // { file: 'spinner-verbs-factorio.json', name: 'Factorio spinner verbs' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-dev-template",
3
- "version": "0.1.97",
3
+ "version": "0.1.98",
4
4
  "description": "Structured AI-assisted development framework for Claude Code",
5
5
  "bin": {
6
6
  "cc-dev-template": "./bin/install.js"
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: question-generator
3
3
  description: Generates research questions from a feature intent document. Cannot explore the codebase — produces questions only.
4
- tools: Write
4
+ tools: Read, Write
5
5
  permissionMode: bypassPermissions
6
6
  ---
7
7
 
8
- You are a question generator. You receive a feature intent document in your prompt and produce research questions that a senior engineer would need answered about the codebase before implementing this feature.
8
+ You are a question generator. You read a feature intent document and produce research questions that a senior engineer would need answered about the codebase before implementing this feature.
9
9
 
10
- You generate questions only. You have no ability to read files or explore the codebase. Your only tool is Write to write the questions file.
10
+ You generate questions only. You can read the intent document and write the questions file the ship policy hook restricts you to exactly those two paths.
11
11
 
12
12
  ## Process
13
13
 
14
- 1. Analyze the intent document provided below in your prompt
14
+ 1. Read the intent document at the path provided in your prompt
15
15
  2. Think deeply about what you'd need to know to actually build this — not just what the system looks like, but how you'd hook into it
16
16
  3. Write organized, specific questions to the output path provided in your prompt
17
17
 
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node ~/.claude/scripts/ship-policy.js"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ship Policy Hook — Phase-aware enforcement for the ship skill.
5
+ *
6
+ * PreToolUse hook that checks every tool call against a policy matrix
7
+ * of (phase, agent_type, tool_name, target_path) rules. No-op when
8
+ * ship is not active (state file missing or session mismatch).
9
+ *
10
+ * State: {cwd}/.claude/ship-hook-state.json
11
+ */
12
+
13
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
14
+ const { join, resolve, relative } = require('path');
15
+
16
+ // Tools that bypass all policy checks
17
+ const BYPASS_TOOLS = new Set([
18
+ 'AskUserQuestion', 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet',
19
+ 'TaskOutput', 'TaskStop', 'ToolSearch', 'SendMessage', 'Agent',
20
+ 'TeamCreate', 'TeamDelete',
21
+ ]);
22
+
23
+ // Per-phase orchestrator write permissions (beyond state files)
24
+ const ORCHESTRATOR_WRITES = {
25
+ intent: (d) => [`${d}/intent.md`],
26
+ questions: () => [],
27
+ research: (d) => [`${d}/research.md`],
28
+ design: (d) => [`${d}/design.md`],
29
+ spec: () => [],
30
+ verify: () => [],
31
+ tasks: () => [],
32
+ implement: () => [],
33
+ complete: () => [],
34
+ };
35
+
36
+ // Spec artifacts that spec-implementer cannot overwrite
37
+ const PROTECTED_ARTIFACTS = [
38
+ 'spec.md', 'test-plan.md', 'design.md', 'intent.md',
39
+ 'questions.md', 'research.md',
40
+ ];
41
+
42
+ function block(reason) { return { blocked: true, reason }; }
43
+ function allow() { return { blocked: false }; }
44
+
45
+ /**
46
+ * Get the target path relative to cwd.
47
+ * Returns: string (relative path), null (outside project), or undefined (no path in input).
48
+ */
49
+ function relPath(tool, input, cwd) {
50
+ let raw;
51
+ if (tool === 'Read' || tool === 'Write' || tool === 'Edit') raw = input.file_path;
52
+ else if (tool === 'Grep' || tool === 'Glob') raw = input.path;
53
+ if (!raw) return undefined;
54
+ const abs = resolve(raw);
55
+ if (abs !== cwd && !abs.startsWith(cwd + '/')) return null;
56
+ return relative(cwd, abs);
57
+ }
58
+
59
+ function under(p, prefix) {
60
+ const dir = prefix.replace(/\/$/, '');
61
+ return p === dir || p.startsWith(dir + '/');
62
+ }
63
+
64
+ // ── Orchestrator Policy ──
65
+
66
+ function orchestratorPolicy(tool, input, cwd, specDir, phase) {
67
+ if (tool === 'Bash') return block('Orchestrator cannot run Bash — delegate to a sub-agent');
68
+
69
+ if (tool === 'Read' || tool === 'Grep' || tool === 'Glob') {
70
+ const p = relPath(tool, input, cwd);
71
+ if (p === null) return allow(); // Outside project (skill files, etc.)
72
+
73
+ // Grep/Glob with no path or project root — check pattern prefix
74
+ if ((tool === 'Grep' || tool === 'Glob') && (p === undefined || p === '')) {
75
+ const pat = input.pattern || '';
76
+ if (/^(docs|references|\.claude)(\/|$)/.test(pat)) return allow();
77
+ return block('Orchestrator cannot search the full project — use a path under docs/, references/, or .claude/');
78
+ }
79
+
80
+ if (p === undefined) return allow();
81
+ if (under(p, 'docs') || under(p, 'references') || under(p, '.claude')) return allow();
82
+ return block(`Orchestrator cannot read source code (${p}) — delegate to a research agent`);
83
+ }
84
+
85
+ if (tool === 'Write' || tool === 'Edit') {
86
+ const p = relPath(tool, input, cwd);
87
+ if (p === null || p === undefined) return allow();
88
+
89
+ // State files always writable
90
+ if (p === `${specDir}/state.yaml` || p === '.claude/ship-hook-state.json') return allow();
91
+
92
+ // Per-phase permissions
93
+ const permFn = ORCHESTRATOR_WRITES[phase];
94
+ if (permFn && permFn(specDir).includes(p)) return allow();
95
+
96
+ return block(`Orchestrator cannot write ${p} in ${phase} phase — delegate to a sub-agent`);
97
+ }
98
+
99
+ return allow();
100
+ }
101
+
102
+ // ── Sub-Agent Policies ──
103
+
104
+ function questionGeneratorPolicy(tool, input, cwd, specDir) {
105
+ const p = relPath(tool, input, cwd);
106
+
107
+ if (tool === 'Read') {
108
+ if (p === null) return allow();
109
+ if (p === `${specDir}/intent.md`) return allow();
110
+ return block(`question-generator can only read ${specDir}/intent.md`);
111
+ }
112
+ if (tool === 'Write') {
113
+ if (p === null) return allow();
114
+ if (p === `${specDir}/questions.md`) return allow();
115
+ return block(`question-generator can only write ${specDir}/questions.md`);
116
+ }
117
+ return block(`question-generator cannot use ${tool}`);
118
+ }
119
+
120
+ function objectiveResearcherPolicy(tool, input, cwd, specDir) {
121
+ const p = relPath(tool, input, cwd);
122
+
123
+ if (tool === 'Read' || tool === 'Grep' || tool === 'Glob') {
124
+ if (p === null) return allow();
125
+ if (p === undefined) return allow(); // Grep/Glob with no path — project-wide search
126
+ if (p.startsWith(`${specDir}/research`) && p.endsWith('.md')) return allow(); // Own output
127
+ if (under(p, 'docs')) return block('objective-researcher cannot read docs/ — research must stay objective');
128
+ return allow();
129
+ }
130
+
131
+ if (tool === 'Write') {
132
+ if (p === null) return allow();
133
+ if (p !== undefined && p.startsWith(`${specDir}/research`) && p.endsWith('.md')) return allow();
134
+ return block(`objective-researcher can only write to ${specDir}/research-*.md`);
135
+ }
136
+
137
+ if (tool === 'Bash') return allow();
138
+ return block(`objective-researcher cannot use ${tool}`);
139
+ }
140
+
141
+ function specDirWriterPolicy(tool, input, cwd, specDir) {
142
+ if (tool === 'Read' || tool === 'Grep' || tool === 'Glob') return allow();
143
+ if (tool === 'Write' || tool === 'Edit') {
144
+ const p = relPath(tool, input, cwd);
145
+ if (p === null || p === undefined) return allow();
146
+ if (under(p, specDir)) return allow();
147
+ return block(`Cannot write outside ${specDir}`);
148
+ }
149
+ if (tool === 'Bash') return block('This agent cannot run Bash commands');
150
+ return allow();
151
+ }
152
+
153
+ function specImplementerPolicy(tool, input, cwd, specDir) {
154
+ if (tool === 'Write' || tool === 'Edit') {
155
+ const p = relPath(tool, input, cwd);
156
+ if (p === null || p === undefined) return allow();
157
+ for (const artifact of PROTECTED_ARTIFACTS) {
158
+ if (p === `${specDir}/${artifact}`) return block(`spec-implementer cannot modify ${artifact}`);
159
+ }
160
+ }
161
+ return allow();
162
+ }
163
+
164
+ function specValidatorPolicy(tool, input, cwd, specDir) {
165
+ if (tool === 'Write' || tool === 'Edit') {
166
+ const p = relPath(tool, input, cwd);
167
+ if (p === null || p === undefined) return allow();
168
+ if (under(p, `${specDir}/tasks`)) return allow(); // Review Notes in task files
169
+ return block('spec-validator cannot write outside task files');
170
+ }
171
+ return allow();
172
+ }
173
+
174
+ // ── Main ──
175
+
176
+ const AGENT_POLICIES = {
177
+ 'question-generator': questionGeneratorPolicy,
178
+ 'objective-researcher': objectiveResearcherPolicy,
179
+ 'spec-writer': specDirWriterPolicy,
180
+ 'test-planner': specDirWriterPolicy,
181
+ 'task-breakdown': specDirWriterPolicy,
182
+ 'spec-implementer': specImplementerPolicy,
183
+ 'spec-validator': specValidatorPolicy,
184
+ };
185
+
186
+ function main() {
187
+ const input = JSON.parse(readFileSync(0, 'utf-8'));
188
+ const cwd = process.cwd();
189
+ const stateFile = join(cwd, '.claude', 'ship-hook-state.json');
190
+
191
+ if (!existsSync(stateFile)) process.exit(0);
192
+
193
+ let state;
194
+ try { state = JSON.parse(readFileSync(stateFile, 'utf-8')); }
195
+ catch { process.exit(0); }
196
+
197
+ // First-touch: inject session_id if missing
198
+ if (!state.session_id) {
199
+ state.session_id = input.session_id;
200
+ try { writeFileSync(stateFile, JSON.stringify(state, null, 2)); } catch {}
201
+ }
202
+
203
+ if (state.session_id !== input.session_id) process.exit(0);
204
+ if (BYPASS_TOOLS.has(input.tool_name)) process.exit(0);
205
+
206
+ const caller = input.agent_type || 'orchestrator';
207
+ const tool = input.tool_name;
208
+ const toolInput = input.tool_input || {};
209
+
210
+ let result;
211
+ if (caller === 'orchestrator') {
212
+ result = orchestratorPolicy(tool, toolInput, cwd, state.spec_dir, state.phase);
213
+ } else {
214
+ const policyFn = AGENT_POLICIES[caller];
215
+ if (!policyFn) process.exit(0); // Unknown agent — not a ship agent
216
+ result = policyFn(tool, toolInput, cwd, state.spec_dir);
217
+ }
218
+
219
+ if (result.blocked) {
220
+ console.log(JSON.stringify({ decision: 'block', reason: result.reason }));
221
+ }
222
+ }
223
+
224
+ main();
@@ -560,11 +560,25 @@ function main() {
560
560
  usageLines.push(makeBoxLine(usageDisplay));
561
561
  }
562
562
 
563
+ // Ship phase line (if active)
564
+ const shipLines = [];
565
+ try {
566
+ const shipStatePath = join(data.workspace.project_dir, '.claude', 'ship-hook-state.json');
567
+ const shipStat = statSync(shipStatePath);
568
+ if (Date.now() - shipStat.mtimeMs < 300000) { // < 5 min old
569
+ const shipState = JSON.parse(readFileSync(shipStatePath, 'utf-8'));
570
+ const phase = (shipState.phase || '').toUpperCase();
571
+ const feature = shipState.feature || '';
572
+ const subPhase = shipState.sub_phase ? ` ${shipState.sub_phase}` : '';
573
+ shipLines.push(makeBoxLine(`SHIP: ${phase} [${feature}]${subPhase}`));
574
+ }
575
+ } catch {}
576
+
563
577
  // Bottom border (add 2 to match content line width)
564
578
  const bottomBorder = `${DIM_GREY}╚${'═'.repeat(width + 2)}╝${RESET}`;
565
579
 
566
580
  // Combine all lines
567
- const allLines = [topBorder, line0, ...branchLines, ctxLine, ...usageLines, bottomBorder];
581
+ const allLines = [topBorder, line0, ...shipLines, ...branchLines, ctxLine, ...usageLines, bottomBorder];
568
582
  console.log(allLines.join('\n'));
569
583
  } catch (error) {
570
584
  // Log error for debugging (goes to stderr, not visible in status line)
@@ -44,6 +44,8 @@ Look for `docs/specs/{feature-name}/state.yaml`.
44
44
  | tasks | `references/step-7-tasks.md` |
45
45
  | implement | `references/step-8-implement.md` |
46
46
 
47
+ Also write/update `.claude/ship-hook-state.json` with `phase`, `feature`, and `spec_dir` from state.yaml (set `sub_phase` to `null`).
48
+
47
49
  Read the step file for the current phase and follow its instructions.
48
50
 
49
51
  **If state.yaml does not exist**, this is a new feature. Create the spec directory and an initial state.yaml:
@@ -54,4 +56,15 @@ phase: intent
54
56
  dir: docs/specs/{feature-name}
55
57
  ```
56
58
 
59
+ Also write `.claude/ship-hook-state.json` (enables policy enforcement for this session):
60
+
61
+ ```json
62
+ {
63
+ "phase": "intent",
64
+ "feature": "{feature-name}",
65
+ "spec_dir": "docs/specs/{feature-name}",
66
+ "sub_phase": null
67
+ }
68
+ ```
69
+
57
70
  Then read `references/step-1-intent.md` to begin.
@@ -45,6 +45,6 @@ Present the intent document to the user for confirmation. Adjust if they have co
45
45
 
46
46
  ## Task 3: Proceed
47
47
 
48
- Update `{spec_dir}/state.yaml` — set `phase: questions`.
48
+ Update `{spec_dir}/state.yaml` — set `phase: questions`. Update `.claude/ship-hook-state.json` — set `phase` to `"questions"`, `sub_phase` to `null`.
49
49
 
50
50
  Use the Read tool on `references/step-2-questions.md` to generate research questions from the intent.
@@ -14,15 +14,15 @@ Create these tasks and work through them in order:
14
14
 
15
15
  ## Task 1: Generate Questions
16
16
 
17
- Read `{spec_dir}/intent.md` yourself, then spawn a sub-agent with the intent content passed inline in the prompt. The agent has `tools: Write` only — it cannot read any files.
17
+ Spawn the question-generator sub-agent. Tell it the spec directory — it will read the intent document directly.
18
18
 
19
19
  ```
20
20
  Agent tool:
21
21
  subagent_type: "question-generator"
22
- prompt: "Write research questions to {spec_dir}/questions.md based on this intent document:\n\n{paste the full intent.md content here}"
22
+ prompt: "Read the intent document at {spec_dir}/intent.md and write research questions to {spec_dir}/questions.md."
23
23
  ```
24
24
 
25
- The question-generator has zero read access. The intent content comes via the prompt, and its only tool is Write. It cannot explore the codebase.
25
+ The question-generator has `tools: Read, Write` with hook-enforced path restrictions it can only read `{spec_dir}/intent.md` and write to `{spec_dir}/questions.md`.
26
26
 
27
27
  ## Task 2: Review Questions
28
28
 
@@ -36,6 +36,6 @@ Update `questions.md` based on user feedback. The user may add questions about p
36
36
 
37
37
  ## Task 3: Proceed
38
38
 
39
- Update `{spec_dir}/state.yaml` — set `phase: research`.
39
+ Update `{spec_dir}/state.yaml` — set `phase: research`. Update `.claude/ship-hook-state.json` — set `phase` to `"research"`, `sub_phase` to `null`.
40
40
 
41
41
  Use the Read tool on `references/step-3-research.md` to begin objective codebase research.
@@ -51,6 +51,6 @@ If the research is thin or missing critical areas, spawn the objective-researche
51
51
 
52
52
  ## Task 4: Proceed
53
53
 
54
- Update `{spec_dir}/state.yaml` — set `phase: design`.
54
+ Update `{spec_dir}/state.yaml` — set `phase: design`. Update `.claude/ship-hook-state.json` — set `phase` to `"design"`, `sub_phase` to `null`.
55
55
 
56
56
  Use the Read tool on `references/step-4-design.md` to begin the design discussion with the user.
@@ -63,6 +63,6 @@ Present to the user for confirmation.
63
63
 
64
64
  ## Task 4: Proceed
65
65
 
66
- Update `{spec_dir}/state.yaml` — set `phase: spec`.
66
+ Update `{spec_dir}/state.yaml` — set `phase: spec`. Update `.claude/ship-hook-state.json` — set `phase` to `"spec"`, `sub_phase` to `null`.
67
67
 
68
68
  Use the Read tool on `references/step-5-spec.md` to generate the implementation specification.
@@ -38,7 +38,7 @@ Agent tool:
38
38
 
39
39
  ## Task 3: Review Loop
40
40
 
41
- Spawn a FRESH instance of spec-writer in review mode. At least one review is mandatory.
41
+ Spawn a FRESH instance of spec-writer in review mode. At least one review is mandatory. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1).
42
42
 
43
43
  ```
44
44
  Agent tool:
@@ -71,6 +71,6 @@ Revise based on user feedback. If changes are substantial, re-run the review loo
71
71
 
72
72
  ## Task 5: Proceed
73
73
 
74
- Update `{spec_dir}/state.yaml` — set `phase: verify`.
74
+ Update `{spec_dir}/state.yaml` — set `phase: verify`. Update `.claude/ship-hook-state.json` — set `phase` to `"verify"`, `sub_phase` to `null`.
75
75
 
76
76
  Use the Read tool on `references/step-6-verify.md` to plan verification for the spec.
@@ -27,7 +27,7 @@ Agent tool:
27
27
 
28
28
  ## Task 2: Review Loop
29
29
 
30
- Spawn a FRESH instance of test-planner in review mode. At least one review is mandatory.
30
+ Spawn a FRESH instance of test-planner in review mode. At least one review is mandatory. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1).
31
31
 
32
32
  ```
33
33
  Agent tool:
@@ -59,6 +59,6 @@ Ask the user if the verification strategy is complete. Revise based on feedback.
59
59
 
60
60
  ## Task 4: Proceed
61
61
 
62
- Update `{spec_dir}/state.yaml` — set `phase: tasks`.
62
+ Update `{spec_dir}/state.yaml` — set `phase: tasks`. Update `.claude/ship-hook-state.json` — set `phase` to `"tasks"`, `sub_phase` to `null`.
63
63
 
64
64
  Use the Read tool on `references/step-7-tasks.md` to break the spec into implementation tasks.
@@ -25,7 +25,7 @@ Agent tool:
25
25
 
26
26
  ## Task 2: Review Loop
27
27
 
28
- Spawn a FRESH instance of task-breakdown in review mode:
28
+ Spawn a FRESH instance of task-breakdown in review mode. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1):
29
29
 
30
30
  ```
31
31
  Agent tool:
@@ -55,6 +55,6 @@ Revise based on user feedback. If changes are substantial, re-run the review loo
55
55
 
56
56
  ## Task 4: Proceed
57
57
 
58
- Update `{spec_dir}/state.yaml` — set `phase: implement`.
58
+ Update `{spec_dir}/state.yaml` — set `phase: implement`. Update `.claude/ship-hook-state.json` — set `phase` to `"implement"`, `sub_phase` to `null`.
59
59
 
60
60
  Use the Read tool on `references/step-8-implement.md` to begin implementation.
@@ -43,7 +43,7 @@ Run independent tasks (no dependency between them) in parallel when possible. Al
43
43
 
44
44
  ## Step 3: Finalize
45
45
 
46
- Update `{spec_dir}/state.yaml` — set `phase: complete`.
46
+ Update `{spec_dir}/state.yaml` — set `phase: complete`. Update `.claude/ship-hook-state.json` — set `phase` to `"complete"`, `sub_phase` to `null`.
47
47
 
48
48
  Present a summary to the user:
49
49
 
@@ -1,9 +1,13 @@
1
1
  # Reflect
2
2
 
3
- Review how this workflow performed and identify improvements.
3
+ ## Cleanup
4
+
5
+ Delete `.claude/ship-hook-state.json` — policy enforcement is no longer needed for this feature.
4
6
 
5
7
  ## Self-Assessment
6
8
 
9
+ Review how this workflow performed and identify improvements.
10
+
7
11
  Consider each phase:
8
12
 
9
13
  1. **Intent capture**: Did the intent document accurately capture what the user wanted? Did the spec drift from the original intent?