@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.
- package/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- 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
|
|
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, [
|
|
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, [
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
:
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
//
|
|
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"
|