@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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @haposoft/cafekit
|
|
2
2
|
|
|
3
|
-
> Native spec-driven workflow and runtime bundle for Claude Code
|
|
3
|
+
> Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
@@ -28,14 +28,6 @@ Codex CLI install support:
|
|
|
28
28
|
- merges a CafeKit-owned block into root `AGENTS.md`
|
|
29
29
|
- requires no generated `.codex/config.toml` and never changes global trust config
|
|
30
30
|
|
|
31
|
-
OpenCode install support:
|
|
32
|
-
- installs command wrappers under `.opencode/commands/`
|
|
33
|
-
- installs converted OpenCode agents under `.opencode/agents/`
|
|
34
|
-
- installs CafeKit skills under `.opencode/skills/`
|
|
35
|
-
- installs OpenCode plugin runtime files under `.opencode/plugins/`
|
|
36
|
-
- merges `opencode.json` and root `AGENTS.md`
|
|
37
|
-
- omits the Claude `hapo:` command prefix
|
|
38
|
-
|
|
39
31
|
## Install
|
|
40
32
|
|
|
41
33
|
Run in your project root:
|
|
@@ -59,7 +51,7 @@ npx @haposoft/cafekit --platform codex --force-overwrite
|
|
|
59
51
|
|
|
60
52
|
Requirements:
|
|
61
53
|
- Node.js 18+
|
|
62
|
-
- Claude Code
|
|
54
|
+
- Claude Code or Codex CLI; choose a runtime when prompted or use `--platform`
|
|
63
55
|
|
|
64
56
|
## Git ignore policy
|
|
65
57
|
|
|
@@ -75,15 +67,13 @@ plans/*
|
|
|
75
67
|
.cafekit-backup/
|
|
76
68
|
.cafekit.lock
|
|
77
69
|
.claude/
|
|
78
|
-
.opencode/
|
|
79
70
|
.codex/
|
|
80
71
|
.agents/
|
|
81
72
|
```
|
|
82
73
|
|
|
83
74
|
Runtime folders are local — reinstall with `npx @haposoft/cafekit` rather than
|
|
84
|
-
committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore
|
|
85
|
-
|
|
86
|
-
`.agents/.gitignore`. These keep secrets, skill dependencies, session state,
|
|
75
|
+
committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore` or the
|
|
76
|
+
Codex pair `.codex/.gitignore` + `.agents/.gitignore`. These keep secrets, skill dependencies, session state,
|
|
87
77
|
and hook logs out of git even if someone partially un-ignores a runtime.
|
|
88
78
|
Teams may deliberately commit runtime files, but should also commit the
|
|
89
79
|
ownership manifest so selective updates share the same baseline.
|
|
@@ -141,37 +131,11 @@ without requiring Git and remain stable when a session starts in a subdirectory.
|
|
|
141
131
|
CafeKit uses Codex's native status and usage UI instead of installing the
|
|
142
132
|
Claude statusline.
|
|
143
133
|
|
|
144
|
-
OpenCode targets:
|
|
145
|
-
|
|
146
|
-
```text
|
|
147
|
-
.opencode/
|
|
148
|
-
├── .gitignore
|
|
149
|
-
├── commands/
|
|
150
|
-
├── agents/
|
|
151
|
-
├── skills/
|
|
152
|
-
├── rules/
|
|
153
|
-
├── scripts/
|
|
154
|
-
├── references/
|
|
155
|
-
├── plugins/
|
|
156
|
-
├── cafekit.json
|
|
157
|
-
├── runtime.json
|
|
158
|
-
└── package.json
|
|
159
|
-
|
|
160
|
-
AGENTS.md
|
|
161
|
-
opencode.json
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
OpenCode setup also:
|
|
165
|
-
- binds generated commands to matching CafeKit subagents with `agent` + `subtask`
|
|
166
|
-
- configures `permission.skill` and `permission.task`
|
|
167
|
-
- optionally writes `model` to `opencode.json` from `OPENCODE_MODEL`, `OPENCODE_DEFAULT_MODEL`, or installer input
|
|
168
|
-
|
|
169
134
|
To check the installed CafeKit package version:
|
|
170
135
|
|
|
171
136
|
```bash
|
|
172
137
|
cat .claude/cafekit.json
|
|
173
138
|
cat .codex/cafekit.json
|
|
174
|
-
cat .opencode/cafekit.json
|
|
175
139
|
```
|
|
176
140
|
|
|
177
141
|
## Core Skills
|
|
@@ -224,17 +188,6 @@ $hapo-test meet-transcript-mvp --full
|
|
|
224
188
|
$hapo-code-review meet-transcript-mvp --pending
|
|
225
189
|
```
|
|
226
190
|
|
|
227
|
-
OpenCode uses the generated command names without the Claude `hapo:` prefix:
|
|
228
|
-
|
|
229
|
-
```bash
|
|
230
|
-
/question "Which files define the current CafeKit install/runtime behavior?" --repo
|
|
231
|
-
/brainstorm Explore approaches for a Google Meet transcript extension
|
|
232
|
-
/specs Build a Google Meet transcript extension with AI summaries
|
|
233
|
-
/develop meet-transcript-mvp
|
|
234
|
-
/test --full
|
|
235
|
-
/code-review --pending
|
|
236
|
-
```
|
|
237
|
-
|
|
238
191
|
Reconstruct as-is docs from a legacy codebase:
|
|
239
192
|
|
|
240
193
|
```bash
|
|
@@ -253,16 +206,26 @@ hosted or specialized tools may not enter the local hook path.
|
|
|
253
206
|
|
|
254
207
|
## Spec Output
|
|
255
208
|
|
|
256
|
-
Specs are stored under:
|
|
209
|
+
New process-first Specs packets are stored under:
|
|
257
210
|
|
|
258
211
|
```text
|
|
259
212
|
specs/<feature-name>/
|
|
260
|
-
├──
|
|
261
|
-
├──
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
213
|
+
├── plan.md
|
|
214
|
+
├── task-01-<slug>.md
|
|
215
|
+
└── task-NN-<slug>.md
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The process-first workflow has three human decision gates. `hapo:specs` opens
|
|
219
|
+
C1 to fix scope before authoring and C2 to resolve adversarial findings, then
|
|
220
|
+
stops. A later explicit `hapo:develop` invocation executes the tasks and
|
|
221
|
+
presents C3 for closeout only after real execution proof. Each task has one
|
|
222
|
+
`Status:` field and keeps its canonical `## Receipt` inline, including the
|
|
223
|
+
exact Verification Plan command, `Exit: 0`, `Verification: PASS`,
|
|
224
|
+
runtime-derived Base and Head, and current command output.
|
|
225
|
+
|
|
226
|
+
Existing packets with `spec.json`, nested `tasks/task-R*.md`, or separate
|
|
227
|
+
receipts remain supported through the legacy compatibility adapter. New Specs
|
|
228
|
+
work does not author those legacy artifacts.
|
|
266
229
|
|
|
267
230
|
## Development
|
|
268
231
|
|
|
@@ -66,7 +66,7 @@ function initRepo(root, { version = '0.0.1-fixture' } = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function writeLegacyBridge(root, { baseCommit, semanticDigest = cf.sha256Tag('bridge-digest') }) {
|
|
69
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
69
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
70
70
|
schema_version: '1',
|
|
71
71
|
verdict: 'PASS',
|
|
72
72
|
blocking_count: 0,
|
|
@@ -78,7 +78,7 @@ function writeLegacyBridge(root, { baseCommit, semanticDigest = cf.sha256Tag('br
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function writeFeatureSeedPass(root, { reviewReceiptDigest = cf.sha256Tag('seed-review-receipt') } = {}) {
|
|
81
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), {
|
|
81
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/spec.json'), {
|
|
82
82
|
validation: {
|
|
83
83
|
semantic_review_history: {
|
|
84
84
|
entries: [{
|
|
@@ -201,7 +201,7 @@ test('R1.14 negative: an absent or malformed legacy-bridge artifact fails the pr
|
|
|
201
201
|
const inst = cf.createChangeFirewall({ root });
|
|
202
202
|
assert.throws(() => inst.assertLegacyBridgeArtifact(), (error) => error.code === 'legacy_bridge_missing');
|
|
203
203
|
|
|
204
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
204
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
205
205
|
schema_version: '1', verdict: 'FAIL', blocking_count: 0,
|
|
206
206
|
reviewed_criteria: ['R1.1'], semantic_digest: cf.sha256Tag('x'),
|
|
207
207
|
bootWindowBaseCommit: '0'.repeat(40), written_at: new Date().toISOString(),
|
|
@@ -379,7 +379,7 @@ test('C14/R1.8: bootstrapBaseline refuses before the seed PASS exists, and refus
|
|
|
379
379
|
const inst = cf.createChangeFirewall({ root });
|
|
380
380
|
|
|
381
381
|
// feature spec.json exists but carries no semantic_review_history yet (no seed PASS).
|
|
382
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), { validation: {} });
|
|
382
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/spec.json'), { validation: {} });
|
|
383
383
|
assert.throws(
|
|
384
384
|
() => inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: cf.sha256Tag('x'), packageVersion: '0.0.1-fixture' }),
|
|
385
385
|
(error) => error.code === 'seed_pass_missing',
|
|
@@ -2131,4 +2131,3 @@ test('REGRESSION round7 (critical): a committed C10 authority renamed away then
|
|
|
2131
2131
|
assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
|
|
2132
2132
|
});
|
|
2133
2133
|
});
|
|
2134
|
-
|
|
@@ -45,6 +45,16 @@ function bindReceipt(root, value, feature = 'auth', session = 'session-a') {
|
|
|
45
45
|
return value.replaceAll(VALID_BASE, context.base).replaceAll(VALID_HEAD, context.head);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function workflowRuntimeContext(root, feature = 'auth', session = 'session-a') {
|
|
49
|
+
return PROVENANCE.deriveRuntimeContext({
|
|
50
|
+
projectRoot: root,
|
|
51
|
+
specsRoot: path.join(root, 'specs'),
|
|
52
|
+
specFile: path.join(root, 'specs', feature, 'plan.md'),
|
|
53
|
+
featureName: feature,
|
|
54
|
+
runtimeSession: session,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
function installFeatureReceipts(root, session = 'session-a') {
|
|
49
59
|
const specsRoot = path.join(root, 'specs');
|
|
50
60
|
if (!fs.existsSync(specsRoot)) return;
|
|
@@ -128,6 +138,219 @@ function inHookFixture(run, options = {}) {
|
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
|
|
141
|
+
function processTaskContent(title, status, dependency = 'none') {
|
|
142
|
+
return [
|
|
143
|
+
`# ${title}`, '', `Status: ${status}`, '',
|
|
144
|
+
'## Dependencies', '', `- ${dependency}`, '',
|
|
145
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonicalProcessTask(root, title, dependency = 'none') {
|
|
150
|
+
const context = workflowRuntimeContext(root, 'auth', 'session-auth-state');
|
|
151
|
+
return [
|
|
152
|
+
processTaskContent(title, 'done', dependency),
|
|
153
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
154
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
155
|
+
'```text', 'node --test', '1 test passed', '```', '',
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inInstalledProcessSpecStateFixture(run) {
|
|
160
|
+
return inHookFixture((root, hooks) => {
|
|
161
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
162
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
164
|
+
|
|
165
|
+
const writeTask = (basename, title, status, dependency = 'none') => {
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(feature, basename),
|
|
168
|
+
processTaskContent(title, status, dependency),
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
const runState = () => runHook(path.join(hooks, 'spec-state.cjs'), root, {
|
|
172
|
+
cwd: root,
|
|
173
|
+
featureName: 'auth',
|
|
174
|
+
session_id: 'session-auth-state',
|
|
175
|
+
hook_event_name: 'UserPromptSubmit',
|
|
176
|
+
prompt: 'Continue',
|
|
177
|
+
});
|
|
178
|
+
const findCacheFile = () => {
|
|
179
|
+
const cacheDir = path.join(hooks, '.logs');
|
|
180
|
+
const cacheNames = fs.existsSync(cacheDir)
|
|
181
|
+
? fs.readdirSync(cacheDir).filter((file) => file.startsWith('tollgate-'))
|
|
182
|
+
: [];
|
|
183
|
+
assert.equal(cacheNames.length, 1, 'one installed session cache must exist');
|
|
184
|
+
return path.join(cacheDir, cacheNames[0]);
|
|
185
|
+
};
|
|
186
|
+
return run({
|
|
187
|
+
feature,
|
|
188
|
+
findCacheFile,
|
|
189
|
+
hooks,
|
|
190
|
+
root,
|
|
191
|
+
runState,
|
|
192
|
+
writePlan: (content) => fs.writeFileSync(path.join(feature, 'plan.md'), content),
|
|
193
|
+
writeTask,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test('Codex installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
|
|
199
|
+
inInstalledProcessSpecStateFixture(({ findCacheFile, hooks, runState, writeTask }) => {
|
|
200
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
|
|
201
|
+
|
|
202
|
+
const blocked = runState();
|
|
203
|
+
assert.equal(blocked.status, 0, blocked.stderr);
|
|
204
|
+
assert.match(blocked.stdout, /Spec state changed: `auth`/);
|
|
205
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
206
|
+
const cacheFile = findCacheFile();
|
|
207
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
208
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
209
|
+
|
|
210
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
211
|
+
const pending = runState();
|
|
212
|
+
assert.equal(pending.status, 0, pending.stderr);
|
|
213
|
+
assert.match(pending.stdout, /Spec state changed: `auth`/);
|
|
214
|
+
assert.match(pending.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
215
|
+
assert.notEqual(fs.readFileSync(cacheFile, 'utf8'), blockedCache, 'state cache must change');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('Codex installed spec-state requires current canonical dependency proof and keys receipt mutations', () => {
|
|
220
|
+
inInstalledProcessSpecStateFixture(({ feature, findCacheFile, hooks, root, runState, writeTask }) => {
|
|
221
|
+
const predecessor = 'task-01-semantic-review.md';
|
|
222
|
+
const dependent = 'task-02-implementation.md';
|
|
223
|
+
writeTask(predecessor, 'Task 01: semantic review', 'blocked');
|
|
224
|
+
writeTask(dependent, 'Task 02: implementation', 'pending', predecessor);
|
|
225
|
+
const dependentPath = path.join(feature, dependent);
|
|
226
|
+
const unchangedDependent = fs.readFileSync(dependentPath, 'utf8');
|
|
227
|
+
|
|
228
|
+
const blocked = runState();
|
|
229
|
+
assert.equal(blocked.status, 0, blocked.stderr);
|
|
230
|
+
assert.match(blocked.stdout, /Tasks: 0 done \/ 2 total/);
|
|
231
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
232
|
+
const cacheFile = findCacheFile();
|
|
233
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
234
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
235
|
+
|
|
236
|
+
writeTask(predecessor, 'Task 01: semantic review', 'done');
|
|
237
|
+
const missing = runState();
|
|
238
|
+
assert.equal(missing.status, 0, missing.stderr);
|
|
239
|
+
assert.doesNotMatch(missing.stdout, /Next unblocked/);
|
|
240
|
+
const missingCache = fs.readFileSync(cacheFile, 'utf8');
|
|
241
|
+
assert.notEqual(missingCache, blockedCache, 'done without proof must change cached proof state');
|
|
242
|
+
|
|
243
|
+
fs.appendFileSync(path.join(feature, predecessor), '\n## Receipt\n\nVerification: PASS\n');
|
|
244
|
+
const malformed = runState();
|
|
245
|
+
assert.equal(malformed.status, 0, malformed.stderr);
|
|
246
|
+
assert.doesNotMatch(malformed.stdout, /Next unblocked/);
|
|
247
|
+
|
|
248
|
+
fs.writeFileSync(path.join(feature, predecessor), canonicalProcessTask(root, 'Task 01: semantic review'));
|
|
249
|
+
const ready = runState();
|
|
250
|
+
assert.equal(ready.status, 0, ready.stderr);
|
|
251
|
+
assert.match(ready.stdout, /Next unblocked: `task-02-implementation\.md`/);
|
|
252
|
+
const readyCache = fs.readFileSync(cacheFile, 'utf8');
|
|
253
|
+
assert.notEqual(readyCache, missingCache, 'canonical proof must change cached proof state');
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(
|
|
256
|
+
path.join(feature, predecessor),
|
|
257
|
+
fs.readFileSync(path.join(feature, predecessor), 'utf8').replace('Verification: PASS', 'Verification: FAIL'),
|
|
258
|
+
);
|
|
259
|
+
const mutated = runState();
|
|
260
|
+
assert.equal(mutated.status, 0, mutated.stderr);
|
|
261
|
+
assert.doesNotMatch(mutated.stdout, /Next unblocked/);
|
|
262
|
+
assert.notEqual(fs.readFileSync(cacheFile, 'utf8'), readyCache, 'receipt-only mutation must change cache');
|
|
263
|
+
assert.equal(fs.readFileSync(dependentPath, 'utf8'), unchangedDependent, 'dependent must remain pending');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('Codex installed spec-state keeps unversioned pending packets out of Next with migration guidance', () => {
|
|
268
|
+
inInstalledProcessSpecStateFixture(({ feature, runState, writePlan, writeTask }) => {
|
|
269
|
+
const fencedMarkerPlan = '# Auth plan\n\n```markdown\nSpecs-Contract: process-first-ready-v1\n```\n';
|
|
270
|
+
writePlan(fencedMarkerPlan);
|
|
271
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
272
|
+
const taskBytes = fs.readFileSync(path.join(feature, 'task-01-auth.md'));
|
|
273
|
+
const result = runState();
|
|
274
|
+
assert.equal(result.status, 0, result.stderr);
|
|
275
|
+
assert.doesNotMatch(result.stdout, /Next unblocked/);
|
|
276
|
+
assert.match(result.stdout, /Migration required: add `Specs-Contract: process-first-ready-v1`/);
|
|
277
|
+
const cached = runState();
|
|
278
|
+
assert.equal(cached.status, 0, cached.stderr);
|
|
279
|
+
assert.doesNotMatch(cached.stdout, /Next unblocked/);
|
|
280
|
+
assert.match(cached.stdout, /Add `Specs-Contract: process-first-ready-v1`/);
|
|
281
|
+
|
|
282
|
+
writePlan(fencedMarkerPlan.replace('# Auth plan\n', '# Auth plan\nSpecs-Contract: process-first-ready-v1\n'));
|
|
283
|
+
const marked = runState();
|
|
284
|
+
assert.equal(marked.status, 0, marked.stderr);
|
|
285
|
+
assert.match(marked.stdout, /Spec state changed: `auth`/);
|
|
286
|
+
assert.match(marked.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
287
|
+
assert.deepEqual(fs.readFileSync(path.join(feature, 'task-01-auth.md')), taskBytes, 'marker-only transition must not change task bytes');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('Codex resolver ignores fenced workflow annotations and rejects fenced-only Status', () => {
|
|
292
|
+
inInstalledProcessSpecStateFixture(({ feature, root, runState, writePlan, writeTask }) => {
|
|
293
|
+
const resolver = require(path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'));
|
|
294
|
+
const trailingFence = resolver.annotatedMarkdownLines([
|
|
295
|
+
'```markdown', '``` trailing', 'Specs-Contract: process-first-ready-v1',
|
|
296
|
+
'Status: done', '## Receipt', '```',
|
|
297
|
+
].join('\n'));
|
|
298
|
+
assert.deepEqual(trailingFence.slice(2, 5).map(({ outsideFence }) => outsideFence), [false, false, false]);
|
|
299
|
+
const fourSpaceFence = resolver.annotatedMarkdownLines(' ```markdown\nStatus: pending');
|
|
300
|
+
assert.equal(fourSpaceFence[1].outsideFence, true);
|
|
301
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
302
|
+
fs.writeFileSync(path.join(feature, 'task-01-fenced-only.md'), [
|
|
303
|
+
'# Task 01', '', '```markdown', 'Status: done', '```', '',
|
|
304
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '',
|
|
305
|
+
].join('\n'));
|
|
306
|
+
const malformed = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
307
|
+
assert.equal(malformed.error, 'explicit_malformed');
|
|
308
|
+
assert.match(malformed.reason, /must contain exactly one supported Status field/);
|
|
309
|
+
|
|
310
|
+
fs.rmSync(path.join(feature, 'task-01-fenced-only.md'));
|
|
311
|
+
writeTask('task-01-dependent.md', 'Task 01: dependent', 'pending', 'task-02-predecessor.md');
|
|
312
|
+
fs.writeFileSync(path.join(feature, 'task-02-predecessor.md'), [
|
|
313
|
+
'# Task 02', '', 'Status: pending', '', '## Dependencies', '', '- none', '',
|
|
314
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
315
|
+
'~~~~markdown', 'Status: done', '## Dependencies', '- task-01-dependent.md', '## Receipt',
|
|
316
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0', '```text', 'fake pass', '```', '~~~~', '',
|
|
317
|
+
].join('\n'));
|
|
318
|
+
const result = runState();
|
|
319
|
+
assert.equal(result.status, 0, result.stderr);
|
|
320
|
+
assert.match(result.stdout, /Next unblocked: `task-02-predecessor\.md`/);
|
|
321
|
+
assert.doesNotMatch(result.stdout, /Next unblocked: `task-01-dependent\.md`/);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('Codex resolver accepts unique legacy task numbers, rejects ambiguity, and rejects exact cycles', () => {
|
|
326
|
+
inInstalledProcessSpecStateFixture(({ feature, root, writePlan, writeTask }) => {
|
|
327
|
+
const resolver = require(path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'));
|
|
328
|
+
writePlan('# Auth plan\n');
|
|
329
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending');
|
|
330
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', '01');
|
|
331
|
+
fs.writeFileSync(path.join(feature, 'task-03-c.md'), '# Task 03: c\n\nStatus: pending\n');
|
|
332
|
+
const unique = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
333
|
+
assert.deepEqual(unique.taskRegistry['task-02-b.md'].dependencies, ['task-01-a.md']);
|
|
334
|
+
assert.deepEqual(unique.taskRegistry['task-03-c.md'].dependencies, []);
|
|
335
|
+
assert.equal(unique.workflowContract, null);
|
|
336
|
+
assert.equal(unique.queueReady, false);
|
|
337
|
+
|
|
338
|
+
writeTask('task-01-second.md', 'Task 01: second', 'pending');
|
|
339
|
+
const ambiguous = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
340
|
+
assert.equal(ambiguous.error, 'explicit_malformed');
|
|
341
|
+
assert.match(ambiguous.reason, /legacy dependency 01 maps to 2 task basenames/);
|
|
342
|
+
|
|
343
|
+
fs.rmSync(path.join(feature, 'task-01-second.md'));
|
|
344
|
+
fs.rmSync(path.join(feature, 'task-03-c.md'));
|
|
345
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
346
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending', 'task-02-b.md');
|
|
347
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', 'task-01-a.md');
|
|
348
|
+
const cycle = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
349
|
+
assert.equal(cycle.error, 'explicit_malformed');
|
|
350
|
+
assert.match(cycle.reason, /dependency cycle detected: task-01-a\.md -> task-02-b\.md -> task-01-a\.md/);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
131
354
|
test('Codex privacy approval is exact, session-bound, and reusable within its window', () => {
|
|
132
355
|
inHookFixture((root, hooks) => {
|
|
133
356
|
const nested = path.join(root, 'packages', 'app');
|
|
@@ -414,6 +637,165 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
|
|
|
414
637
|
});
|
|
415
638
|
});
|
|
416
639
|
|
|
640
|
+
test('Codex completion gate validates explicit process-v3 inline Receipt', () => {
|
|
641
|
+
inHookFixture((root, hooks) => {
|
|
642
|
+
const nested = path.join(root, 'packages', 'app');
|
|
643
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
644
|
+
const taskFile = path.join(feature, 'task-01-auth.md');
|
|
645
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
646
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
647
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\n');
|
|
648
|
+
fs.writeFileSync(taskFile, [
|
|
649
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
650
|
+
'## Dependencies', '', '- none', '',
|
|
651
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
652
|
+
].join('\n'));
|
|
653
|
+
|
|
654
|
+
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
655
|
+
const payload = {
|
|
656
|
+
cwd: nested,
|
|
657
|
+
featureName: 'auth',
|
|
658
|
+
session_id: 'session-a',
|
|
659
|
+
hook_event_name: 'Stop',
|
|
660
|
+
stop_hook_active: false,
|
|
661
|
+
};
|
|
662
|
+
const blocked = runHook(gate, nested, payload);
|
|
663
|
+
assert.equal(blocked.status, 0, blocked.stderr);
|
|
664
|
+
assert.match(JSON.parse(blocked.stdout).reason, /missing_receipt/);
|
|
665
|
+
|
|
666
|
+
const context = workflowRuntimeContext(root);
|
|
667
|
+
fs.writeFileSync(taskFile, [
|
|
668
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
669
|
+
'## Dependencies', '', '- none', '',
|
|
670
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
671
|
+
'## Receipt', '', '```text', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
672
|
+
`Base: ${context.base}`, `Head: ${context.head}`, 'pass: 1', '```', '',
|
|
673
|
+
].join('\n'));
|
|
674
|
+
const fencedFields = runHook(gate, nested, payload);
|
|
675
|
+
assert.equal(fencedFields.status, 0, fencedFields.stderr);
|
|
676
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /verification_state/);
|
|
677
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /command/);
|
|
678
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /provenance/);
|
|
679
|
+
|
|
680
|
+
fs.writeFileSync(taskFile, [
|
|
681
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
682
|
+
'## Dependencies', '', '- none', '',
|
|
683
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
684
|
+
'## Receipt', '',
|
|
685
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
686
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
687
|
+
'````text', '$ node --test', 'pass: 1', '```', '',
|
|
688
|
+
].join('\n'));
|
|
689
|
+
const malformedOutputFence = runHook(gate, nested, payload);
|
|
690
|
+
assert.equal(malformedOutputFence.status, 0, malformedOutputFence.stderr);
|
|
691
|
+
assert.match(JSON.parse(malformedOutputFence.stdout).reason, /command_output/);
|
|
692
|
+
|
|
693
|
+
fs.writeFileSync(taskFile, [
|
|
694
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
695
|
+
'## Dependencies', '', '- none', '',
|
|
696
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
697
|
+
'## Receipt', '',
|
|
698
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
699
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
700
|
+
'```text', '$ node --test', 'pass: 1', '```', '',
|
|
701
|
+
].join('\n'));
|
|
702
|
+
const passed = runHook(gate, nested, payload);
|
|
703
|
+
assert.equal(passed.status, 0, passed.stderr);
|
|
704
|
+
assert.equal(passed.stdout, '');
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('Codex process-v3 Receipt command must match the exact Verification Plan command', () => {
|
|
709
|
+
inHookFixture((root, hooks) => {
|
|
710
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
711
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
712
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\n');
|
|
713
|
+
const context = workflowRuntimeContext(root);
|
|
714
|
+
fs.writeFileSync(path.join(feature, 'task-01-auth.md'), [
|
|
715
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
716
|
+
'## Dependencies', '', '- none', '',
|
|
717
|
+
'## Verification Plan', '', '- Command: pnpm test', '',
|
|
718
|
+
'## Receipt', '', 'Verification: PASS', 'Command: true', 'Exit: 0',
|
|
719
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
720
|
+
'```text', '$ true', 'pass: 1', '```', '',
|
|
721
|
+
].join('\n'));
|
|
722
|
+
|
|
723
|
+
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
724
|
+
cwd: root,
|
|
725
|
+
session_id: 'session-a',
|
|
726
|
+
hook_event_name: 'Stop',
|
|
727
|
+
stop_hook_active: false,
|
|
728
|
+
});
|
|
729
|
+
assert.equal(result.status, 0, result.stderr);
|
|
730
|
+
assert.notEqual(result.stdout, '', 'a substituted receipt command must block');
|
|
731
|
+
assert.match(JSON.parse(result.stdout).reason, /command_identity/);
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test('Codex process-v3 Stop ignores a completed packet when one packet remains active', () => {
|
|
736
|
+
inHookFixture((root, hooks) => {
|
|
737
|
+
const completed = path.join(root, 'specs', 'auth');
|
|
738
|
+
const active = path.join(root, 'specs', 'active');
|
|
739
|
+
fs.mkdirSync(completed, { recursive: true });
|
|
740
|
+
fs.mkdirSync(active, { recursive: true });
|
|
741
|
+
fs.writeFileSync(path.join(completed, 'plan.md'), '# Auth plan\n');
|
|
742
|
+
fs.writeFileSync(path.join(active, 'plan.md'), '# Active plan\n');
|
|
743
|
+
const context = workflowRuntimeContext(root);
|
|
744
|
+
fs.writeFileSync(path.join(completed, 'task-01-auth.md'), [
|
|
745
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
746
|
+
'## Dependencies', '', '- none', '',
|
|
747
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
748
|
+
'## Receipt', '',
|
|
749
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
750
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
751
|
+
'```text', '$ node --test', 'pass: 1', '```', '',
|
|
752
|
+
].join('\n'));
|
|
753
|
+
fs.writeFileSync(path.join(active, 'task-01-active.md'), [
|
|
754
|
+
'# Task 01: active', '', 'Status: pending', '',
|
|
755
|
+
'## Dependencies', '', '- none', '',
|
|
756
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
757
|
+
].join('\n'));
|
|
758
|
+
|
|
759
|
+
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
760
|
+
cwd: root,
|
|
761
|
+
session_id: 'session-a',
|
|
762
|
+
hook_event_name: 'Stop',
|
|
763
|
+
stop_hook_active: false,
|
|
764
|
+
});
|
|
765
|
+
assert.equal(result.status, 0, result.stderr);
|
|
766
|
+
assert.equal(result.stdout, '');
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test('Codex process-v3 Stop accepts two completed packets with valid Receipts', () => {
|
|
771
|
+
inHookFixture((root, hooks) => {
|
|
772
|
+
for (const featureName of ['auth', 'other']) {
|
|
773
|
+
const feature = path.join(root, 'specs', featureName);
|
|
774
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
775
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), `# ${featureName} plan\n`);
|
|
776
|
+
const context = workflowRuntimeContext(root, featureName);
|
|
777
|
+
fs.writeFileSync(path.join(feature, `task-01-${featureName}.md`), [
|
|
778
|
+
`# Task 01: ${featureName}`, '', 'Status: done', '',
|
|
779
|
+
'## Dependencies', '', '- none', '',
|
|
780
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
781
|
+
'## Receipt', '',
|
|
782
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
783
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
784
|
+
'```text', '$ node --test', 'pass: 1', '```', '',
|
|
785
|
+
].join('\n'));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
789
|
+
cwd: root,
|
|
790
|
+
session_id: 'session-a',
|
|
791
|
+
hook_event_name: 'Stop',
|
|
792
|
+
stop_hook_active: false,
|
|
793
|
+
});
|
|
794
|
+
assert.equal(result.status, 0, result.stderr);
|
|
795
|
+
assert.equal(result.stdout, '');
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
417
799
|
test('empty or forged Codex hook authority fails closed', () => {
|
|
418
800
|
inHookFixture((root, hooks) => {
|
|
419
801
|
const gate = path.join(hooks, 'spec-gate.cjs');
|