@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.8

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.
Files changed (87) hide show
  1. package/README.md +21 -58
  2. package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
  3. package/bin/__tests__/codex-hooks.test.js +382 -0
  4. package/bin/__tests__/codex-native.test.js +579 -80
  5. package/bin/__tests__/develop-contract.test.js +155 -76
  6. package/bin/__tests__/installer-safety.test.js +19 -154
  7. package/bin/__tests__/package-inventory.test.js +494 -26
  8. package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
  9. package/bin/__tests__/validator-grounding.test.js +11 -11
  10. package/bin/install.js +2 -7
  11. package/bin/lib/context.js +0 -45
  12. package/bin/lib/copy-utils.js +1 -1
  13. package/bin/lib/i18n.js +0 -3
  14. package/bin/lib/manifest.js +1 -1
  15. package/bin/phases/claude-runtime.js +1 -3
  16. package/bin/phases/codex-runtime.js +4 -2
  17. package/bin/phases/copy-payload.js +3 -15
  18. package/bin/phases/post-install.js +5 -17
  19. package/bin/phases/root-config.js +2 -3
  20. package/bin/phases/select-platform.js +2 -5
  21. package/bin/phases/summary.js +0 -1
  22. package/package.json +2 -5
  23. package/src/claude/CLAUDE.md +19 -6
  24. package/src/claude/agents/project-manager.md +41 -9
  25. package/src/claude/agents/spec-maker.md +82 -155
  26. package/src/claude/gitignore +1 -1
  27. package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
  28. package/src/claude/hooks/__tests__/state.test.js +249 -3
  29. package/src/claude/hooks/lib/context.cjs +1 -1
  30. package/src/claude/hooks/lib/detect.cjs +1 -1
  31. package/src/claude/hooks/spec-gate.cjs +44 -9
  32. package/src/claude/hooks/spec-state.cjs +51 -13
  33. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  34. package/src/claude/migration-manifest.json +17 -3
  35. package/src/claude/rules/manage-docs.md +43 -24
  36. package/src/claude/rules/state-sync.md +57 -62
  37. package/src/claude/rules/workflow.md +30 -13
  38. package/src/claude/scripts/change-firewall.cjs +2 -2
  39. package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
  40. package/src/claude/scripts/spec-receipt.cjs +196 -1
  41. package/src/claude/scripts/spec-resolver.cjs +428 -0
  42. package/src/claude/skills/develop/SKILL.md +89 -125
  43. package/src/claude/skills/develop/references/parallel-waves.md +75 -162
  44. package/src/claude/skills/develop/references/quality-gate.md +61 -121
  45. package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
  46. package/src/claude/skills/question/SKILL.md +1 -1
  47. package/src/claude/skills/specs/SKILL.md +152 -204
  48. package/src/claude/skills/specs/references/review.md +124 -243
  49. package/src/claude/skills/specs/references/templates.md +202 -0
  50. package/src/claude/skills/sync/SKILL.md +66 -90
  51. package/src/claude/skills/sync/references/sync-protocols.md +62 -83
  52. package/src/codex/AGENTS.md +20 -7
  53. package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
  54. package/src/codex/hooks/lib/spec-utils.cjs +25 -0
  55. package/src/codex/hooks/spec-gate.cjs +46 -16
  56. package/src/codex/hooks/spec-state.cjs +54 -16
  57. package/src/codex/rules/state-sync.md +57 -61
  58. package/src/common/AGENTS.md +8 -1
  59. package/bin/__tests__/opencode-spec-gate.test.js +0 -799
  60. package/bin/lib/opencode-install.js +0 -899
  61. package/bin/phases/opencode-runtime.js +0 -38
  62. package/src/claude/skills/specs/references/archive-workflow.md +0 -74
  63. package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
  64. package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
  65. package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
  66. package/src/claude/skills/specs/references/research-strategy.md +0 -107
  67. package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
  68. package/src/claude/skills/specs/references/translation-mirror.md +0 -83
  69. package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
  70. package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
  71. package/src/claude/skills/specs/rules/design-principles.md +0 -138
  72. package/src/claude/skills/specs/rules/design-review.md +0 -55
  73. package/src/claude/skills/specs/rules/ears-format.md +0 -42
  74. package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
  75. package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
  76. package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
  77. package/src/opencode/AGENTS.md +0 -41
  78. package/src/opencode/plugins/docs-sync.ts +0 -175
  79. package/src/opencode/plugins/inspect-block.ts +0 -164
  80. package/src/opencode/plugins/privacy-block.ts +0 -182
  81. package/src/opencode/plugins/rules.ts +0 -155
  82. package/src/opencode/plugins/session.ts +0 -179
  83. package/src/opencode/plugins/spec-gate.ts +0 -457
  84. package/src/opencode/plugins/spec-state.ts +0 -311
  85. package/src/opencode/plugins/state.ts +0 -253
  86. package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
  87. package/src/opencode/plugins/usage.ts +0 -178
@@ -7,7 +7,253 @@ const fs = require('node:fs');
7
7
  const os = require('node:os');
8
8
  const path = require('node:path');
9
9
 
10
- const HOOK = path.join(__dirname, '..', 'state.cjs');
10
+ const CLAUDE_ROOT = path.join(__dirname, '..', '..');
11
+ const STATE_HOOK = path.join(CLAUDE_ROOT, 'hooks', 'state.cjs');
12
+ const POLICY = require(path.join(CLAUDE_ROOT, 'scripts', 'workflow-policy.cjs'));
13
+
14
+ function processTaskContent(title, status, dependency = 'none') {
15
+ return [
16
+ `# ${title}`, '', `Status: ${status}`, '',
17
+ '## Dependencies', '', `- ${dependency}`, '',
18
+ '## Verification Plan', '', '- Command: node --test', '',
19
+ ].join('\n');
20
+ }
21
+
22
+ function canonicalProcessTask(root, title, dependency = 'none') {
23
+ const context = POLICY.deriveRuntimeContext({
24
+ projectRoot: root,
25
+ specsRoot: path.join(root, 'specs'),
26
+ specFile: path.join(root, 'specs', 'auth', 'plan.md'),
27
+ featureName: 'auth',
28
+ runtimeSession: 'session-auth-state',
29
+ });
30
+ return [
31
+ processTaskContent(title, 'done', dependency),
32
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
33
+ `Base: ${context.base}`, `Head: ${context.head}`,
34
+ '```text', 'node --test', '1 test passed', '```', '',
35
+ ].join('\n');
36
+ }
37
+
38
+ function inInstalledProcessSpecStateFixture(run) {
39
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-state-'));
40
+ try {
41
+ const hooks = path.join(dir, '.claude', 'hooks');
42
+ const scripts = path.join(dir, '.claude', 'scripts');
43
+ fs.mkdirSync(hooks, { recursive: true });
44
+ fs.mkdirSync(scripts, { recursive: true });
45
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'spec-state.cjs'), path.join(hooks, 'spec-state.cjs'));
46
+ for (const file of [
47
+ 'provenance.cjs',
48
+ 'spec-receipt.cjs',
49
+ 'spec-resolver.cjs',
50
+ 'workflow-policy.cjs',
51
+ ]) {
52
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'scripts', file), path.join(scripts, file));
53
+ }
54
+
55
+ const feature = path.join(dir, 'specs', 'auth');
56
+ fs.mkdirSync(feature, { recursive: true });
57
+ fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
58
+
59
+ for (const args of [
60
+ ['init', '-q'],
61
+ ['config', 'user.email', 'cafekit@example.invalid'],
62
+ ['config', 'user.name', 'CafeKit Test'],
63
+ ['commit', '--allow-empty', '-qm', 'fixture'],
64
+ ]) {
65
+ const git = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
66
+ assert.strictEqual(git.status, 0, git.stderr);
67
+ }
68
+
69
+ const writeTask = (basename, title, status, dependency = 'none') => {
70
+ fs.writeFileSync(
71
+ path.join(feature, basename),
72
+ processTaskContent(title, status, dependency),
73
+ );
74
+ };
75
+ const runState = () => spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
76
+ cwd: dir,
77
+ env: { ...process.env, PROJECT_ROOT: dir },
78
+ input: JSON.stringify({
79
+ cwd: dir,
80
+ featureName: 'auth',
81
+ session_id: 'session-auth-state',
82
+ hook_event_name: 'UserPromptSubmit',
83
+ prompt: 'Continue',
84
+ }),
85
+ encoding: 'utf8',
86
+ });
87
+ return run({
88
+ cacheFile: path.join(hooks, '.logs', 'tollgate-last.txt'),
89
+ dir,
90
+ feature,
91
+ hooks,
92
+ runState,
93
+ writePlan: (content) => fs.writeFileSync(path.join(feature, 'plan.md'), content),
94
+ writeTask,
95
+ });
96
+ } finally {
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ }
99
+ }
100
+
101
+ test('Claude installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
102
+ inInstalledProcessSpecStateFixture(({ cacheFile, hooks, runState, writeTask }) => {
103
+ writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
104
+
105
+ const blocked = runState();
106
+ assert.strictEqual(blocked.status, 0, blocked.stderr);
107
+ assert.match(blocked.stdout, /0 pending, 1 blocked/);
108
+ assert.doesNotMatch(blocked.stdout, /Next unblocked/);
109
+ assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
110
+ assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
111
+ const blockedCache = fs.readFileSync(cacheFile, 'utf8');
112
+
113
+ writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
114
+ const pending = runState();
115
+ assert.strictEqual(pending.status, 0, pending.stderr);
116
+ assert.match(pending.stdout, /Spec state changed: `auth`/);
117
+ assert.match(pending.stdout, /1 pending, 0 blocked/);
118
+ assert.match(pending.stdout, /Next unblocked: `task-01-auth\.md`/);
119
+ assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), blockedCache, 'state cache must change');
120
+ });
121
+ });
122
+
123
+ test('Claude installed spec-state requires current canonical dependency proof and keys receipt mutations', () => {
124
+ inInstalledProcessSpecStateFixture(({ cacheFile, dir, feature, hooks, runState, writeTask }) => {
125
+ const predecessor = 'task-01-semantic-review.md';
126
+ const dependent = 'task-02-implementation.md';
127
+ writeTask(predecessor, 'Task 01: semantic review', 'blocked');
128
+ writeTask(dependent, 'Task 02: implementation', 'pending', predecessor);
129
+ const dependentPath = path.join(feature, dependent);
130
+ const unchangedDependent = fs.readFileSync(dependentPath, 'utf8');
131
+
132
+ const blocked = runState();
133
+ assert.strictEqual(blocked.status, 0, blocked.stderr);
134
+ assert.match(blocked.stdout, /0 done \/ 2 total \(0 in_progress, 1 pending, 1 blocked\)/);
135
+ assert.doesNotMatch(blocked.stdout, /Next unblocked/);
136
+ assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
137
+ assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
138
+ const blockedCache = fs.readFileSync(cacheFile, 'utf8');
139
+
140
+ writeTask(predecessor, 'Task 01: semantic review', 'done');
141
+ const missing = runState();
142
+ assert.strictEqual(missing.status, 0, missing.stderr);
143
+ assert.doesNotMatch(missing.stdout, /Next unblocked/);
144
+ const missingCache = fs.readFileSync(cacheFile, 'utf8');
145
+ assert.notStrictEqual(missingCache, blockedCache, 'done without proof must change cached proof state');
146
+
147
+ fs.appendFileSync(path.join(feature, predecessor), '\n## Receipt\n\nVerification: PASS\n');
148
+ const malformed = runState();
149
+ assert.strictEqual(malformed.status, 0, malformed.stderr);
150
+ assert.doesNotMatch(malformed.stdout, /Next unblocked/);
151
+
152
+ fs.writeFileSync(path.join(feature, predecessor), canonicalProcessTask(dir, 'Task 01: semantic review'));
153
+ const ready = runState();
154
+ assert.strictEqual(ready.status, 0, ready.stderr);
155
+ assert.match(ready.stdout, /Next unblocked: `task-02-implementation\.md`/);
156
+ const readyCache = fs.readFileSync(cacheFile, 'utf8');
157
+ assert.notStrictEqual(readyCache, missingCache, 'canonical proof must change cached proof state');
158
+
159
+ fs.writeFileSync(
160
+ path.join(feature, predecessor),
161
+ fs.readFileSync(path.join(feature, predecessor), 'utf8').replace('Verification: PASS', 'Verification: FAIL'),
162
+ );
163
+ const mutated = runState();
164
+ assert.strictEqual(mutated.status, 0, mutated.stderr);
165
+ assert.doesNotMatch(mutated.stdout, /Next unblocked/);
166
+ assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), readyCache, 'receipt-only mutation must change cache');
167
+ assert.strictEqual(fs.readFileSync(dependentPath, 'utf8'), unchangedDependent, 'dependent must remain pending');
168
+ });
169
+ });
170
+
171
+ test('Claude installed spec-state keeps unversioned pending packets out of Next with migration guidance', () => {
172
+ inInstalledProcessSpecStateFixture(({ feature, runState, writePlan, writeTask }) => {
173
+ const fencedMarkerPlan = '# Auth plan\n\n```markdown\nSpecs-Contract: process-first-ready-v1\n```\n';
174
+ writePlan(fencedMarkerPlan);
175
+ writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
176
+ const taskBytes = fs.readFileSync(path.join(feature, 'task-01-auth.md'));
177
+ const result = runState();
178
+ assert.strictEqual(result.status, 0, result.stderr);
179
+ assert.doesNotMatch(result.stdout, /Next unblocked/);
180
+ assert.match(result.stdout, /Migration required: add `Specs-Contract: process-first-ready-v1`/);
181
+ const cached = runState();
182
+ assert.strictEqual(cached.status, 0, cached.stderr);
183
+ assert.doesNotMatch(cached.stdout, /Next unblocked/);
184
+ assert.match(cached.stdout, /Add `Specs-Contract: process-first-ready-v1`/);
185
+
186
+ writePlan(fencedMarkerPlan.replace('# Auth plan\n', '# Auth plan\nSpecs-Contract: process-first-ready-v1\n'));
187
+ const marked = runState();
188
+ assert.strictEqual(marked.status, 0, marked.stderr);
189
+ assert.match(marked.stdout, /Spec state changed: `auth`/);
190
+ assert.match(marked.stdout, /Next unblocked: `task-01-auth\.md`/);
191
+ assert.deepStrictEqual(fs.readFileSync(path.join(feature, 'task-01-auth.md')), taskBytes, 'marker-only transition must not change task bytes');
192
+ });
193
+ });
194
+
195
+ test('Claude resolver ignores fenced workflow annotations and rejects fenced-only Status', () => {
196
+ inInstalledProcessSpecStateFixture(({ dir, feature, runState, writePlan, writeTask }) => {
197
+ const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
198
+ const trailingFence = resolver.annotatedMarkdownLines([
199
+ '```markdown', '``` trailing', 'Specs-Contract: process-first-ready-v1',
200
+ 'Status: done', '## Receipt', '```',
201
+ ].join('\n'));
202
+ assert.deepStrictEqual(trailingFence.slice(2, 5).map(({ outsideFence }) => outsideFence), [false, false, false]);
203
+ const fourSpaceFence = resolver.annotatedMarkdownLines(' ```markdown\nStatus: pending');
204
+ assert.strictEqual(fourSpaceFence[1].outsideFence, true);
205
+ writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
206
+ fs.writeFileSync(path.join(feature, 'task-01-fenced-only.md'), [
207
+ '# Task 01', '', '```markdown', 'Status: done', '```', '',
208
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '',
209
+ ].join('\n'));
210
+ const malformed = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
211
+ assert.strictEqual(malformed.error, 'explicit_malformed');
212
+ assert.match(malformed.reason, /must contain exactly one supported Status field/);
213
+
214
+ fs.rmSync(path.join(feature, 'task-01-fenced-only.md'));
215
+ writeTask('task-01-dependent.md', 'Task 01: dependent', 'pending', 'task-02-predecessor.md');
216
+ fs.writeFileSync(path.join(feature, 'task-02-predecessor.md'), [
217
+ '# Task 02', '', 'Status: pending', '', '## Dependencies', '', '- none', '',
218
+ '## Verification Plan', '', '- Command: node --test', '',
219
+ '~~~~markdown', 'Status: done', '## Dependencies', '- task-01-dependent.md', '## Receipt',
220
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0', '```text', 'fake pass', '```', '~~~~', '',
221
+ ].join('\n'));
222
+ const result = runState();
223
+ assert.strictEqual(result.status, 0, result.stderr);
224
+ assert.match(result.stdout, /Next unblocked: `task-02-predecessor\.md`/);
225
+ assert.doesNotMatch(result.stdout, /Next unblocked: `task-01-dependent\.md`/);
226
+ });
227
+ });
228
+
229
+ test('Claude resolver accepts unique legacy task numbers, rejects ambiguity, and rejects exact cycles', () => {
230
+ inInstalledProcessSpecStateFixture(({ dir, feature, writePlan, writeTask }) => {
231
+ const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
232
+ writePlan('# Auth plan\n');
233
+ writeTask('task-01-a.md', 'Task 01: a', 'pending');
234
+ writeTask('task-02-b.md', 'Task 02: b', 'pending', 'Task 01');
235
+ fs.writeFileSync(path.join(feature, 'task-03-c.md'), '# Task 03: c\n\nStatus: pending\n');
236
+ const unique = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
237
+ assert.deepStrictEqual(unique.taskRegistry['task-02-b.md'].dependencies, ['task-01-a.md']);
238
+ assert.deepStrictEqual(unique.taskRegistry['task-03-c.md'].dependencies, []);
239
+ assert.strictEqual(unique.workflowContract, null);
240
+ assert.strictEqual(unique.queueReady, false);
241
+
242
+ writeTask('task-01-second.md', 'Task 01: second', 'pending');
243
+ const ambiguous = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
244
+ assert.strictEqual(ambiguous.error, 'explicit_malformed');
245
+ assert.match(ambiguous.reason, /legacy dependency Task 01 maps to 2 task basenames/);
246
+
247
+ fs.rmSync(path.join(feature, 'task-01-second.md'));
248
+ fs.rmSync(path.join(feature, 'task-03-c.md'));
249
+ writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
250
+ writeTask('task-01-a.md', 'Task 01: a', 'pending', 'task-02-b.md');
251
+ writeTask('task-02-b.md', 'Task 02: b', 'pending', 'task-01-a.md');
252
+ const cycle = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
253
+ assert.strictEqual(cycle.error, 'explicit_malformed');
254
+ assert.match(cycle.reason, /dependency cycle detected: task-01-a\.md -> task-02-b\.md -> task-01-a\.md/);
255
+ });
256
+ });
11
257
 
12
258
  test('Stop snapshot includes tracked modifications and untracked files', () => {
13
259
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-'));
@@ -22,7 +268,7 @@ test('Stop snapshot includes tracked modifications and untracked files', () => {
22
268
 
23
269
  fs.writeFileSync(path.join(dir, 'tracked.txt'), 'after\n');
24
270
  fs.writeFileSync(path.join(dir, 'new-untracked.json'), '{}\n');
25
- const result = spawnSync(process.execPath, [HOOK], {
271
+ const result = spawnSync(process.execPath, [STATE_HOOK], {
26
272
  cwd: dir,
27
273
  input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
28
274
  encoding: 'utf8',
@@ -47,7 +293,7 @@ test('Stop snapshot includes untracked files before the first commit', () => {
47
293
  spawnSync('git', ['init', '-q'], { cwd: dir });
48
294
  fs.writeFileSync(path.join(dir, 'first-untracked.txt'), 'new\n');
49
295
 
50
- const result = spawnSync(process.execPath, [HOOK], {
296
+ const result = spawnSync(process.execPath, [STATE_HOOK], {
51
297
  cwd: dir,
52
298
  input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
53
299
  encoding: 'utf8',
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * context.cjs - Context/reminder building for session injection
4
4
  *
5
- * Extracted from hooks for reuse in both Claude hooks and OpenCode plugins.
5
+ * Extracted from hooks for reuse across runtime adapters.
6
6
  * Builds session context, rules, paths, and plan information.
7
7
  *
8
8
  * @module context
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * detect.cjs - Project and environment detection logic
4
4
  *
5
- * Extracted from session.cjs for reuse in both Claude hooks and OpenCode plugins.
5
+ * Extracted from session.cjs for reuse across runtime adapters.
6
6
  * Detects project type, package manager, framework, and runtime versions.
7
7
  *
8
8
  * @module detect
@@ -93,6 +93,7 @@ try {
93
93
  if (typeof POLICY.validateCanonicalReceipt !== 'function'
94
94
  || typeof POLICY.completionDecisionForSpec !== 'function'
95
95
  || typeof RECEIPT.checkTaskReceipt !== 'function'
96
+ || typeof RECEIPT.checkWorkflowReceiptSet !== 'function'
96
97
  || typeof FINAL_STATE.evaluateCloseout !== 'function') {
97
98
  throw new Error('shared workflow policy lacks completion authority functions');
98
99
  }
@@ -112,8 +113,37 @@ try {
112
113
  } catch { /* gate stays on */ }
113
114
  // Active-spec discovery via shared resolver — explicit target if present, else fail on ambiguity.
114
115
  const baseDir = cwd;
115
- const resolved = FINAL_STATE.resolveCandidate({ resolver: RESOLVER, projectRoot: baseDir, runtime, payload });
116
+ const target = typeof RESOLVER.extractExplicitTarget === 'function'
117
+ ? RESOLVER.extractExplicitTarget(payload)
118
+ : null;
119
+ let resolved = typeof RESOLVER.resolveWorkflowCandidate === 'function'
120
+ ? RESOLVER.resolveWorkflowCandidate({ projectRoot: baseDir, runtime, target, includeCompleted: true })
121
+ : FINAL_STATE.resolveCandidate({ resolver: RESOLVER, projectRoot: baseDir, runtime, payload });
122
+ if (typeof RESOLVER.refineWorkflowGateResolution === 'function') {
123
+ resolved = RESOLVER.refineWorkflowGateResolution(resolved);
124
+ }
116
125
  if (!resolved) process.exit(0);
126
+ if (resolved.layoutKind === 'process-v3-completed-set') {
127
+ const configuredGate = runtime.spec?.completion_gate;
128
+ if (configuredGate !== undefined && configuredGate !== true) {
129
+ emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
130
+ process.exit(0);
131
+ }
132
+ const checked = RECEIPT.checkWorkflowReceiptSet(
133
+ resolved.candidates,
134
+ baseDir,
135
+ sessionIdentity(payload),
136
+ POLICY,
137
+ );
138
+ if (checked.failures.length === 0) process.exit(0);
139
+ const lines = [`⚠️ Completion gate: ${checked.failures.length} done task(s) lack a verification receipt.`];
140
+ for (const failure of checked.failures) {
141
+ lines.push(`- \`${failure.featureName}/${failure.taskPath}\`: failed check(s) ${failure.failures.join(', ')}`);
142
+ lines.push(` Fix: write a runtime-bound \`## Receipt\` with the planned command in \`specs/${failure.featureName}/${failure.taskPath}\`.`);
143
+ }
144
+ emitBlock(lines.slice(0, 8).join('\n'));
145
+ process.exit(0);
146
+ }
117
147
  if (resolved.error === 'multiple_active' || resolved.error === 'multiple_persisted') {
118
148
  process.stdout.write(JSON.stringify({
119
149
  decision: 'block',
@@ -137,13 +167,14 @@ try {
137
167
  }
138
168
  if (resolved.error) process.exit(0);
139
169
 
140
- const activeSpec = resolved.spec;
170
+ const activeSpec = resolved.spec || {};
171
+ const processWorkflow = resolved.layoutKind === 'process-v3';
141
172
  const featureName = resolved.featureName;
142
173
  const specsPath = resolved.specsDir;
143
174
  const lifecyclePhase = activeSpec.current_phase || activeSpec.phase;
144
175
  const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
145
176
  || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
146
- if (activeSpec.schema_version === '2.1') {
177
+ if (!processWorkflow && activeSpec.schema_version === '2.1') {
147
178
  const finalState = FINAL_STATE.evaluateCloseout({
148
179
  resolver: RESOLVER,
149
180
  policy: POLICY,
@@ -160,7 +191,7 @@ try {
160
191
  const runtimeContext = POLICY.deriveRuntimeContext({
161
192
  projectRoot: baseDir,
162
193
  specsRoot: specsPath,
163
- specFile: resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
194
+ specFile: resolved.stateFile || resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
164
195
  featureName,
165
196
  runtimeSession: sessionIdentity(payload),
166
197
  });
@@ -170,7 +201,7 @@ try {
170
201
  emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
171
202
  process.exit(0);
172
203
  }
173
- const taskRegistry = activeSpec.task_registry || {};
204
+ const taskRegistry = resolved.taskRegistry || activeSpec.task_registry || {};
174
205
  const cacheFile = path.join(__dirname, '.logs', 'spec-gate-last.json');
175
206
  const cacheExists = fs.existsSync(cacheFile);
176
207
  let cache = {};
@@ -183,7 +214,7 @@ try {
183
214
  currentStatuses[tp] = task?.status || 'pending';
184
215
  }
185
216
 
186
- const staleFlashTasks = Object.entries(taskRegistry)
217
+ const staleFlashTasks = processWorkflow ? [] : Object.entries(taskRegistry)
187
218
  .filter(([, task]) => POLICY.isStaleFlashDone(task))
188
219
  .map(([taskPath]) => taskPath);
189
220
  if (staleFlashTasks.length > 0) {
@@ -206,11 +237,13 @@ try {
206
237
  const failures = allDoneTasks
207
238
  .map((taskPath) => ({
208
239
  taskPath,
209
- fails: RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY).failures,
240
+ fails: processWorkflow
241
+ ? RECEIPT.checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, POLICY).failures
242
+ : RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY).failures,
210
243
  }))
211
244
  .filter((f) => f.fails.length > 0);
212
245
 
213
- const featureCloseoutRequired = explicitCloseout && allDoneTasks.length > 0;
246
+ const featureCloseoutRequired = !processWorkflow && explicitCloseout && allDoneTasks.length > 0;
214
247
  const featureReceipt = featureCloseoutRequired
215
248
  ? RECEIPT.checkFeatureReceipt(featureDir, runtimeContext, POLICY)
216
249
  : null;
@@ -253,7 +286,9 @@ try {
253
286
  lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(', ')}`);
254
287
  lines.push(taskPath === 'feature-receipt.md'
255
288
  ? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
256
- : ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
289
+ : processWorkflow
290
+ ? ` Fix: write a runtime-bound \`## Receipt\` with command output in \`specs/${featureName}/${path.posix.basename(taskPath)}\`.`
291
+ : ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
257
292
  }
258
293
  if (completionBlocked) {
259
294
  lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
@@ -73,14 +73,17 @@ try {
73
73
  if (fs.existsSync(p)) runtime = JSON.parse(fs.readFileSync(p, 'utf8'));
74
74
  } catch { /* ignore */ }
75
75
 
76
- // Reminder toggle — same key the OpenCode spec-state plugin honors.
76
+ // Reminder toggle.
77
77
  // (The Stop completion gate has its own toggle: spec.completion_gate.)
78
78
  if (runtime.spec && runtime.spec.tollgate === false) process.exit(0);
79
79
 
80
80
  const baseDir = process.env.PROJECT_ROOT || cwd;
81
81
  const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
82
82
  const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
83
- const resolved = RESOLVER.resolveActiveSpec({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
83
+ const resolveWorkflow = typeof RESOLVER.resolveWorkflowCandidate === 'function'
84
+ ? RESOLVER.resolveWorkflowCandidate
85
+ : RESOLVER.resolveActiveSpec;
86
+ const resolved = resolveWorkflow({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
84
87
 
85
88
  if (!resolved) {
86
89
  process.exit(0); // No active spec, do nothing
@@ -108,19 +111,20 @@ try {
108
111
  process.exit(0);
109
112
  }
110
113
 
111
- const activeSpec = resolved.spec;
114
+ const activeSpec = resolved.spec || {};
115
+ const processWorkflow = resolved.layoutKind === 'process-v3';
112
116
  const featureName = resolved.featureName;
113
117
  const specsPath = resolved.specsDir;
114
118
  const runtimeContext = POLICY.deriveRuntimeContext({
115
119
  projectRoot: baseDir,
116
120
  specsRoot: specsPath,
117
- specFile: resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
121
+ specFile: resolved.stateFile || resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
118
122
  featureName,
119
123
  runtimeSession: sessionIdentity(payload),
120
124
  });
121
125
 
122
- const phase = activeSpec.current_phase || activeSpec.phase || 'unknown';
123
- const taskRegistry = activeSpec.task_registry || {};
126
+ const phase = resolved.phase || activeSpec.current_phase || activeSpec.phase || 'unknown';
127
+ const taskRegistry = resolved.taskRegistry || activeSpec.task_registry || {};
124
128
  const flashTasks = POLICY.flashState(taskRegistry);
125
129
  const taskEntries = Object.entries(taskRegistry);
126
130
  const taskCounts = taskEntries.reduce((acc, [, task]) => {
@@ -128,14 +132,32 @@ try {
128
132
  acc[status] = (acc[status] || 0) + 1;
129
133
  return acc;
130
134
  }, {});
135
+ const featureDir = path.join(specsPath, featureName);
131
136
  const taskStatusByPath = new Map(taskEntries.map(([taskPath, task]) => [taskPath, task?.status || 'pending']));
137
+ const dependencyProof = processWorkflow
138
+ ? RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY)
139
+ : {};
140
+ const taskState = taskEntries.map(([taskPath, task]) => [
141
+ taskPath,
142
+ task?.status || 'pending',
143
+ [...(Array.isArray(task?.dependencies) ? task.dependencies : [])].sort(),
144
+ ]).sort(([left], [right]) => left.localeCompare(right));
145
+ const proofState = Object.entries(dependencyProof).map(([taskPath, proof]) => [
146
+ taskPath,
147
+ proof.done,
148
+ proof.valid,
149
+ proof.eligible,
150
+ proof.signature,
151
+ ]);
132
152
  const nextUnblocked = taskEntries.find(([, task]) => {
133
153
  const status = task?.status || 'pending';
134
154
  const deps = Array.isArray(task?.dependencies) ? task.dependencies : [];
135
- return status === 'pending' && deps.every((dep) => taskStatusByPath.get(dep) === 'done');
155
+ return resolved.queueReady !== false && status === 'pending' && deps.every((dep) => (
156
+ processWorkflow ? dependencyProof[dep]?.eligible === true : taskStatusByPath.get(dep) === 'done'
157
+ ));
136
158
  });
137
- const featureDir = path.join(specsPath, featureName);
138
- const featureReceiptPresent = RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
159
+ const featureReceiptPresent = !processWorkflow
160
+ && RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
139
161
 
140
162
  // ── State-change gate: only emit the full tollgate when spec state changed ──
141
163
  // The cache is shared by installed hook copies, so its key must carry the
@@ -152,6 +174,10 @@ try {
152
174
  Head: runtimeContext.head,
153
175
  contextId: runtimeContext.context_id,
154
176
  phase,
177
+ workflowContract: resolved.workflowContract || null,
178
+ queueReady: resolved.queueReady !== false,
179
+ taskState,
180
+ proofState,
155
181
  done: taskCounts.done || 0,
156
182
  total: taskEntries.length,
157
183
  featureReceiptPresent,
@@ -164,7 +190,11 @@ try {
164
190
  }
165
191
 
166
192
  if (stateKey && lastKey === stateKey) {
167
- console.log(`\n> 🔵 Spec \`${featureName}\` @ \`${phase}\` (${taskCounts.done || 0}/${taskEntries.length} tasks done). Tollgate active — sync \`spec.json\` when state changes.\n`);
193
+ const stateTarget = processWorkflow ? 'task Markdown' : '`spec.json`';
194
+ const migration = processWorkflow && resolved.queueReady === false
195
+ ? ' Add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.'
196
+ : '';
197
+ console.log(`\n> 🔵 Spec \`${featureName}\` @ \`${phase}\` (${taskCounts.done || 0}/${taskEntries.length} tasks done). Tollgate active — sync ${stateTarget} when state changes.${migration}\n`);
168
198
  process.exit(0);
169
199
  }
170
200
 
@@ -187,9 +217,17 @@ try {
187
217
  if (flashTasks.length > 0) {
188
218
  lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
189
219
  }
190
- lines.push(`- Sync \`spec.json\` + task Markdown status after verified work; task proof belongs in \`receipts/<task-basename>.md\`.`);
191
- lines.push(`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`);
192
- lines.push(`- Validate with \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\`; hooks revalidate receipt bytes but never grant approval.`);
220
+ if (processWorkflow) {
221
+ if (resolved.queueReady === false) {
222
+ lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
223
+ }
224
+ lines.push('- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.');
225
+ lines.push(`- Workflow source: \`specs/${featureName}/plan.md\`; Stop revalidates every task currently marked done.`);
226
+ } else {
227
+ lines.push('- Sync `spec.json` + task Markdown status after verified work; task proof belongs in `receipts/<task-basename>.md`.');
228
+ lines.push(`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`);
229
+ lines.push(`- Validate with \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\`; hooks revalidate receipt bytes but never grant approval.`);
230
+ }
193
231
  lines.push('');
194
232
 
195
233
  console.log(lines.join('\n'));
@@ -67,7 +67,7 @@ try {
67
67
 
68
68
  // Valve 2: block with an actionable message carrying the exact command.
69
69
  // NOTE: deliberately does NOT mention the runtime.json escape hatch — an
70
- // OpenCode smoke test proved the model reads the advertised override and
70
+ // Smoke testing proved the model reads the advertised override and
71
71
  // simply disables the guard itself. The hatch stays functional for humans.
72
72
  const m = norm.match(/(^|\/)specs\/([^/]+)\/tasks\//);
73
73
  const feature = m ? m[2] : '<feature>';
@@ -80,15 +80,29 @@
80
80
  "obsolete": {
81
81
  "agents": {
82
82
  "claude": ["agents/god-developer.md"],
83
- "codex": ["agents/god_developer.toml"],
84
- "opencode": ["agents/god-developer.md"]
83
+ "codex": ["agents/god_developer.toml"]
85
84
  },
86
85
  "runtimeFiles": [
87
86
  "hooks/skill-router.cjs",
88
87
  "hooks/lib/skill-router-routes.cjs",
89
88
  "scripts/browser-tool.cjs",
90
89
  "skills/generate-graph",
91
- "skills/impact-analysis"
90
+ "skills/impact-analysis",
91
+ "skills/specs/references/archive-workflow.md",
92
+ "skills/specs/references/ask-user-question-gates.md",
93
+ "skills/specs/references/codebase-analysis.md",
94
+ "skills/specs/references/cross-spec-dependency.md",
95
+ "skills/specs/references/research-strategy.md",
96
+ "skills/specs/references/scope-inquiry.md",
97
+ "skills/specs/references/translation-mirror.md",
98
+ "skills/specs/rules/design-discovery-full.md",
99
+ "skills/specs/rules/design-discovery-light.md",
100
+ "skills/specs/rules/design-principles.md",
101
+ "skills/specs/rules/design-review.md",
102
+ "skills/specs/rules/ears-format.md",
103
+ "skills/specs/rules/phase-decision-matrix.md",
104
+ "skills/specs/rules/task-scoring-rubric.md",
105
+ "skills/specs/rules/tasks-generation.md"
92
106
  ],
93
107
  "settingsHookCommandSubstrings": [
94
108
  "hooks/skill-router.cjs"