cc-dev-template 0.1.100 → 0.1.102

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": "cc-dev-template",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
4
4
  "description": "Structured AI-assisted development framework for Claude Code",
5
5
  "bin": {
6
6
  "cc-dev-template": "./bin/install.js"
@@ -7,15 +7,15 @@
7
7
  * of (phase, agent_type, tool_name, target_path) rules. No-op when
8
8
  * ship is not active for the current session.
9
9
  *
10
- * Shared mailbox: {cwd}/.claude/ship-hook-state.json
11
- * Written by orchestrator (doesn't know its session_id).
10
+ * Per-session state: {cwd}/.claude/ship-sessions/{session_id}.json
11
+ * Created/updated by this hook when it intercepts `ship-state.js`
12
+ * Bash calls. The agent doesn't know its session_id — only the hook does.
12
13
  *
13
- * Per-session: {cwd}/.claude/ship-sessions/{session_id}.json
14
- * Created by the hook when it intercepts a Write to the mailbox.
15
- * This is what the hook reads for enforcement.
14
+ * Feature state: {spec_dir}/state.yaml
15
+ * Created/updated by ship-state.js (the CLI script the agent runs).
16
16
  */
17
17
 
18
- const { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync } = require('fs');
18
+ const { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, realpathSync } = require('fs');
19
19
  const { join, resolve, relative } = require('path');
20
20
 
21
21
  // Tools that bypass all policy checks
@@ -61,16 +61,6 @@ function relPath(tool, input, cwd) {
61
61
  return relative(cwd, abs);
62
62
  }
63
63
 
64
- function realResolve(p) {
65
- const abs = resolve(p);
66
- try { return realpathSync(abs); } catch {}
67
- // File may not exist yet — resolve parent
68
- const dir = resolve(abs, '..');
69
- const base = require('path').basename(abs);
70
- try { return join(realpathSync(dir), base); } catch {}
71
- return abs;
72
- }
73
-
74
64
  function under(p, prefix) {
75
65
  const dir = prefix.replace(/\/$/, '');
76
66
  return p === dir || p.startsWith(dir + '/');
@@ -101,8 +91,8 @@ function orchestratorPolicy(tool, input, cwd, specDir, phase) {
101
91
  const p = relPath(tool, input, cwd);
102
92
  if (p === null || p === undefined) return allow();
103
93
 
104
- // State/mailbox files always writable
105
- if (p === `${specDir}/state.yaml` || p === '.claude/ship-hook-state.json') return allow();
94
+ // State file always writable
95
+ if (p === `${specDir}/state.yaml`) return allow();
106
96
 
107
97
  // Per-phase permissions
108
98
  const permFn = ORCHESTRATOR_WRITES[phase];
@@ -202,21 +192,44 @@ function main() {
202
192
  const input = JSON.parse(readFileSync(0, 'utf-8'));
203
193
  let cwd;
204
194
  try { cwd = realpathSync(process.cwd()); } catch { cwd = process.cwd(); }
205
- const stateFile = join(cwd, '.claude', 'ship-hook-state.json');
206
195
  const sessionsDir = join(cwd, '.claude', 'ship-sessions');
207
196
  const sessionFile = join(sessionsDir, `${input.session_id}.json`);
208
197
 
209
- // Detect writes to the shared mailbox copy content to per-session file
210
- if ((input.tool_name === 'Write' || input.tool_name === 'Edit') && input.tool_input) {
211
- const targetPath = input.tool_input.file_path;
212
- if (targetPath && realResolve(targetPath) === stateFile) {
213
- if (input.tool_name === 'Write') {
214
- try {
198
+ // Detect ship-state.js Bash callsmanage per-session state
199
+ if (input.tool_name === 'Bash' && input.tool_input) {
200
+ const cmd = input.tool_input.command || '';
201
+ const match = cmd.match(/ship-state\.js\s+(set|release)/);
202
+ if (match) {
203
+ const action = match[1];
204
+ if (action === 'set') {
205
+ const phaseMatch = cmd.match(/--phase\s+(\S+)/);
206
+ const featureMatch = cmd.match(/--feature\s+(\S+)/);
207
+ const specDirMatch = cmd.match(/--spec-dir\s+(\S+)/);
208
+ const subPhaseMatch = cmd.match(/--sub-phase\s+(?:"([^"]+)"|(\S+))/);
209
+ const subPhaseValue = subPhaseMatch ? (subPhaseMatch[1] || subPhaseMatch[2]) : null;
210
+
211
+ if (existsSync(sessionFile)) {
212
+ let state;
213
+ try { state = JSON.parse(readFileSync(sessionFile, 'utf-8')); } catch { state = {}; }
214
+ if (phaseMatch) { state.phase = phaseMatch[1]; state.sub_phase = null; }
215
+ if (featureMatch) state.feature = featureMatch[1];
216
+ if (specDirMatch) state.spec_dir = specDirMatch[1];
217
+ if (subPhaseValue) state.sub_phase = subPhaseValue;
218
+ writeFileSync(sessionFile, JSON.stringify(state, null, 2));
219
+ } else {
215
220
  mkdirSync(sessionsDir, { recursive: true });
216
- writeFileSync(sessionFile, input.tool_input.content);
217
- } catch {}
221
+ const state = {
222
+ phase: phaseMatch ? phaseMatch[1] : null,
223
+ feature: featureMatch ? featureMatch[1] : null,
224
+ spec_dir: specDirMatch ? specDirMatch[1] : null,
225
+ sub_phase: subPhaseValue,
226
+ };
227
+ writeFileSync(sessionFile, JSON.stringify(state, null, 2));
228
+ }
229
+ } else if (action === 'release') {
230
+ try { unlinkSync(sessionFile); } catch {}
218
231
  }
219
- process.exit(0); // Allow the write (Edit is allowed too, per-session file stays slightly stale)
232
+ process.exit(0);
220
233
  }
221
234
  }
222
235
 
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ship State CLI — manages feature state for the ship skill.
5
+ *
6
+ * Commands:
7
+ * set --phase <p> --feature <f> --spec-dir <d> Initial claim or resume
8
+ * set --phase <p> --spec-dir <d> Phase transition
9
+ * set --sub-phase <s> Review loop tracking (no-op here, hook handles it)
10
+ * release Cleanup sessions directory
11
+ *
12
+ * The PreToolUse hook (ship-policy.js) intercepts these Bash calls to manage
13
+ * per-session state files. This script handles feature-level state (state.yaml).
14
+ */
15
+
16
+ const { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } = require('fs');
17
+ const { join } = require('path');
18
+
19
+ function parseArgs(args) {
20
+ const flags = {};
21
+ for (let i = 0; i < args.length; i++) {
22
+ if (args[i].startsWith('--')) {
23
+ const key = args[i].slice(2);
24
+ const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
25
+ flags[key] = val;
26
+ }
27
+ }
28
+ return flags;
29
+ }
30
+
31
+ const [command, ...rest] = process.argv.slice(2);
32
+
33
+ if (command === 'set') {
34
+ const flags = parseArgs(rest);
35
+
36
+ // sub-phase only — hook handles per-session state, script is a no-op
37
+ if (flags['sub-phase'] && !flags.phase && !flags['spec-dir']) {
38
+ process.exit(0);
39
+ }
40
+
41
+ if (flags['spec-dir'] && flags.phase) {
42
+ const specDir = flags['spec-dir'];
43
+ mkdirSync(specDir, { recursive: true });
44
+
45
+ const stateFile = join(specDir, 'state.yaml');
46
+ const existed = existsSync(stateFile);
47
+ let yaml;
48
+
49
+ if (existed) {
50
+ yaml = readFileSync(stateFile, 'utf-8');
51
+ yaml = yaml.replace(/^phase:.*$/m, `phase: ${flags.phase}`);
52
+ if (flags.feature) {
53
+ yaml = yaml.replace(/^feature:.*$/m, `feature: ${flags.feature}`);
54
+ }
55
+ } else {
56
+ yaml = `feature: ${flags.feature || 'unknown'}\nphase: ${flags.phase}\ndir: ${specDir}\n`;
57
+ }
58
+
59
+ writeFileSync(stateFile, yaml);
60
+ }
61
+ } else if (command === 'release') {
62
+ rmSync('.claude/ship-sessions/', { recursive: true, force: true });
63
+ } else {
64
+ console.error(`Unknown command: ${command}`);
65
+ process.exit(1);
66
+ }
@@ -20,8 +20,8 @@ const { homedir } = require('os');
20
20
 
21
21
  // Usage API cache
22
22
  const USAGE_CACHE_PATH = join(homedir(), '.claude', '.usage-cache.json');
23
- const USAGE_CACHE_TTL = 45000; // 45 seconds
24
- const USAGE_HISTORY_MAX = 20; // ~15 min of readings at 45s intervals
23
+ const USAGE_CACHE_TTL = 120000; // 2 minutes
24
+ const USAGE_HISTORY_MAX = 20; // ~40 min of readings at 2min intervals
25
25
 
26
26
  // Background refresh mode: fetch usage data and write cache, then exit
27
27
  if (process.argv.includes('--refresh')) {
@@ -373,6 +373,13 @@ function getUsageData() {
373
373
 
374
374
  // Trigger background refresh if cache is stale
375
375
  if (cacheAge > USAGE_CACHE_TTL) {
376
+ // Optimistic lock: claim the timestamp so other sessions see fresh cache
377
+ try {
378
+ const raw = readFileSync(USAGE_CACHE_PATH, 'utf-8');
379
+ const cache = JSON.parse(raw);
380
+ cache.timestamp = Date.now();
381
+ writeFileSync(USAGE_CACHE_PATH, JSON.stringify(cache));
382
+ } catch {}
376
383
  try {
377
384
  const child = spawn(process.execPath, [__filename, '--refresh'], {
378
385
  detached: true,
@@ -44,27 +44,16 @@ 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`).
47
+ Also run: `node ~/.claude/scripts/ship-state.js set --phase {phase} --feature {feature} --spec-dir {dir}` (using values from state.yaml).
48
48
 
49
49
  Read the step file for the current phase and follow its instructions.
50
50
 
51
- **If state.yaml does not exist**, this is a new feature. Create the spec directory and an initial state.yaml:
51
+ **If state.yaml does not exist**, this is a new feature. Run:
52
52
 
53
- ```yaml
54
- feature: {feature-name}
55
- phase: intent
56
- dir: docs/specs/{feature-name}
57
53
  ```
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
- }
54
+ node ~/.claude/scripts/ship-state.js set --phase intent --feature {feature-name} --spec-dir docs/specs/{feature-name}
68
55
  ```
69
56
 
57
+ This creates the spec directory, initializes state.yaml, and enables policy enforcement for this session.
58
+
70
59
  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`. Update `.claude/ship-hook-state.json` set `phase` to `"questions"`, `sub_phase` to `null`.
48
+ Run: `node ~/.claude/scripts/ship-state.js set --phase questions --spec-dir {spec_dir}`
49
49
 
50
50
  Use the Read tool on `references/step-2-questions.md` to generate research questions from the intent.
@@ -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`. Update `.claude/ship-hook-state.json` set `phase` to `"research"`, `sub_phase` to `null`.
39
+ Run: `node ~/.claude/scripts/ship-state.js set --phase research --spec-dir {spec_dir}`
40
40
 
41
41
  Use the Read tool on `references/step-3-research.md` to begin objective codebase research.
@@ -58,6 +58,6 @@ If the research is thin or missing critical areas, spawn the objective-researche
58
58
 
59
59
  ## Task 3: Proceed
60
60
 
61
- Update `{spec_dir}/state.yaml` — set `phase: design`. Update `.claude/ship-hook-state.json` set `phase` to `"design"`, `sub_phase` to `null`.
61
+ Run: `node ~/.claude/scripts/ship-state.js set --phase design --spec-dir {spec_dir}`
62
62
 
63
63
  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`. Update `.claude/ship-hook-state.json` set `phase` to `"spec"`, `sub_phase` to `null`.
66
+ Run: `node ~/.claude/scripts/ship-state.js set --phase spec --spec-dir {spec_dir}`
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. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1).
41
+ Spawn a FRESH instance of spec-writer in review mode. At least one review is mandatory. Before each review cycle, run: `node ~/.claude/scripts/ship-state.js set --sub-phase "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`. Update `.claude/ship-hook-state.json` set `phase` to `"verify"`, `sub_phase` to `null`.
74
+ Run: `node ~/.claude/scripts/ship-state.js set --phase verify --spec-dir {spec_dir}`
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. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1).
30
+ Spawn a FRESH instance of test-planner in review mode. At least one review is mandatory. Before each review cycle, run: `node ~/.claude/scripts/ship-state.js set --sub-phase "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`. Update `.claude/ship-hook-state.json` set `phase` to `"tasks"`, `sub_phase` to `null`.
62
+ Run: `node ~/.claude/scripts/ship-state.js set --phase tasks --spec-dir {spec_dir}`
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. Before each review cycle, update `.claude/ship-hook-state.json` `sub_phase` to `"review-cycle-N"` (N = cycle number, starting at 1):
28
+ Spawn a FRESH instance of task-breakdown in review mode. Before each review cycle, run: `node ~/.claude/scripts/ship-state.js set --sub-phase "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`. Update `.claude/ship-hook-state.json` set `phase` to `"implement"`, `sub_phase` to `null`.
58
+ Run: `node ~/.claude/scripts/ship-state.js set --phase implement --spec-dir {spec_dir}`
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`. Update `.claude/ship-hook-state.json` set `phase` to `"complete"`, `sub_phase` to `null`.
46
+ Run: `node ~/.claude/scripts/ship-state.js set --phase complete --spec-dir {spec_dir}`
47
47
 
48
48
  Present a summary to the user:
49
49
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Cleanup
4
4
 
5
- Delete `.claude/ship-hook-state.json` and the `.claude/ship-sessions/` directory policy enforcement is no longer needed for this feature.
5
+ Run: `node ~/.claude/scripts/ship-state.js release` — this cleans up per-session state and the sessions directory.
6
6
 
7
7
  ## Self-Assessment
8
8