@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
|
@@ -28,6 +28,68 @@ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
|
|
|
28
28
|
const MANIFEST = JSON.parse(
|
|
29
29
|
fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8')
|
|
30
30
|
);
|
|
31
|
+
const SPECS_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/specs');
|
|
32
|
+
const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
|
|
33
|
+
['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
|
|
34
|
+
['Schema', 'version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
|
|
35
|
+
['State/concurrency', 'initial/terminal states; event + guard + effect + next + error; ordering; duplicate/retry; writer/lock acquire/contention/release; rollback/recovery'],
|
|
36
|
+
['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
|
|
37
|
+
['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
|
|
38
|
+
['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle']
|
|
39
|
+
];
|
|
40
|
+
const IMPLEMENTATION_READINESS_CLAUSES = {
|
|
41
|
+
noInvention: 'Before implementation handoff, apply the **no-invention gate**: if two implementations conform to the packet text yet can produce different externally observable output, state, error, security, or compatibility behavior, surface the missing choice as an explicit C1 or C2 question and block handoff.',
|
|
42
|
+
materialDefinition: 'A boundary is material when the task creates, changes, or depends on it and a different choice changes an external observation, security, durable data, compatibility, or proof reachability. Require only the matching material row; omit nonmaterial categories.',
|
|
43
|
+
exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
|
|
44
|
+
proofPlanLines: [
|
|
45
|
+
'- Command: `<exact runnable command>`',
|
|
46
|
+
'- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
|
|
47
|
+
'- Reachability: <real entrypoint/caller at each `source`/`installed`/`live` level; `UNKNOWN` if unexecuted>',
|
|
48
|
+
'- Oracle: <externally observable success or failure>',
|
|
49
|
+
'- Counterexample: <material alternative behavior that must make this proof fail>',
|
|
50
|
+
'- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>'
|
|
51
|
+
],
|
|
52
|
+
proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
|
|
53
|
+
namedProbeOwnership: 'Aggregate suites\nname the owning concrete probe.',
|
|
54
|
+
proofLevelSeparation: 'Proof at `source`/`installed`/`live` stays\nseparate; one level never promotes another.',
|
|
55
|
+
disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
|
|
56
|
+
disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
|
|
57
|
+
failureSemantics: '`Crash` means abrupt unhandled termination before the claimed catch point; a catchable failure returns/raises an error or exits nonzero. Never use them interchangeably.',
|
|
58
|
+
privacyIdentifiers: 'Any privacy/security claim names the exact identifier surface at risk, such as an env var, header, path, token class, or field name; generic “sensitive data” is insufficient.',
|
|
59
|
+
freshReplay: 'After applying an accepted C2 finding, a fresh-context closure pass records and freshly replays its original counterexample after the repair under this exact review-log header:',
|
|
60
|
+
distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
|
|
61
|
+
closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
|
|
62
|
+
unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
|
|
63
|
+
scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.'
|
|
64
|
+
};
|
|
65
|
+
const V3_SPECS_BUNDLE = [
|
|
66
|
+
'SKILL.md',
|
|
67
|
+
'references/review.md',
|
|
68
|
+
'references/templates.md',
|
|
69
|
+
'templates/design.md',
|
|
70
|
+
'templates/requirements-init.md',
|
|
71
|
+
'templates/requirements.md',
|
|
72
|
+
'templates/research.md',
|
|
73
|
+
'templates/spec-state.json',
|
|
74
|
+
'templates/task.md'
|
|
75
|
+
];
|
|
76
|
+
const OBSOLETE_SPECS_FILES = [
|
|
77
|
+
'references/archive-workflow.md',
|
|
78
|
+
'references/ask-user-question-gates.md',
|
|
79
|
+
'references/codebase-analysis.md',
|
|
80
|
+
'references/cross-spec-dependency.md',
|
|
81
|
+
'references/research-strategy.md',
|
|
82
|
+
'references/scope-inquiry.md',
|
|
83
|
+
'references/translation-mirror.md',
|
|
84
|
+
'rules/design-discovery-full.md',
|
|
85
|
+
'rules/design-discovery-light.md',
|
|
86
|
+
'rules/design-principles.md',
|
|
87
|
+
'rules/design-review.md',
|
|
88
|
+
'rules/ears-format.md',
|
|
89
|
+
'rules/phase-decision-matrix.md',
|
|
90
|
+
'rules/task-scoring-rubric.md',
|
|
91
|
+
'rules/tasks-generation.md'
|
|
92
|
+
];
|
|
31
93
|
|
|
32
94
|
function inTempProject(run) {
|
|
33
95
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-native-'));
|
|
@@ -130,95 +192,498 @@ function assertInstalledVerificationModel(grounderPath, designTemplate) {
|
|
|
130
192
|
}
|
|
131
193
|
}
|
|
132
194
|
|
|
133
|
-
function
|
|
134
|
-
const lines =
|
|
135
|
-
const
|
|
136
|
-
|
|
195
|
+
function markdownSection(content, heading) {
|
|
196
|
+
const lines = String(content).split('\n');
|
|
197
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
198
|
+
if (start < 0) return '';
|
|
199
|
+
const end = lines.findIndex((line, index) => index > start && /^##\s+/.test(line));
|
|
200
|
+
return lines.slice(start + 1, end < 0 ? undefined : end).join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function markdownTableUnderHeading(content, heading) {
|
|
204
|
+
const lines = markdownSection(content, heading).split('\n');
|
|
205
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
|
|
206
|
+
if (tableStart < 0) return [];
|
|
137
207
|
const rows = [];
|
|
138
|
-
for (let index =
|
|
208
|
+
for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
|
|
139
209
|
const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
|
|
140
|
-
if (
|
|
210
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
211
|
+
rows.push(cells);
|
|
141
212
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeMarkdownWhitespace(content) {
|
|
217
|
+
return String(content).replace(/\s+/g, ' ').trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function markdownBetweenHeadings(content, startHeading, endHeading) {
|
|
221
|
+
const value = String(content);
|
|
222
|
+
const startMarker = `## ${startHeading}`;
|
|
223
|
+
const endMarker = `## ${endHeading}`;
|
|
224
|
+
const startIndex = value.indexOf(startMarker);
|
|
225
|
+
const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
|
|
226
|
+
if (startIndex < 0 || endIndex < 0) return '';
|
|
227
|
+
return value.slice(startIndex + startMarker.length, endIndex);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function implementationReadinessIssues(templates, review) {
|
|
231
|
+
if (typeof templates !== 'string' || typeof review !== 'string') {
|
|
232
|
+
throw new TypeError('implementation-readiness checker expects templates and review UTF-8 strings');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const issues = new Set();
|
|
236
|
+
const authoring = markdownSection(templates, 'No-invention and conditional boundary contracts');
|
|
237
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
|
|
238
|
+
issues.add('no-invention');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
242
|
+
templates,
|
|
243
|
+
'No-invention and conditional boundary contracts'
|
|
244
|
+
);
|
|
245
|
+
const expectedBoundaryTable = [
|
|
246
|
+
['Boundary', 'Required contract when material'],
|
|
247
|
+
...IMPLEMENTATION_READINESS_BOUNDARY_ROWS
|
|
248
|
+
];
|
|
249
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
|
|
250
|
+
|| !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
|
|
251
|
+
|| IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 6
|
|
252
|
+
|| boundaryTable.length !== expectedBoundaryTable.length
|
|
253
|
+
|| JSON.stringify(boundaryTable) !== JSON.stringify(expectedBoundaryTable)) {
|
|
254
|
+
issues.add('boundary-contract');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const proof = markdownSection(templates, 'Verification Plan');
|
|
258
|
+
const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
|
|
259
|
+
const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
|
|
260
|
+
templates,
|
|
261
|
+
'Verification Plan',
|
|
262
|
+
'Canonical inline Receipt'
|
|
263
|
+
));
|
|
264
|
+
const normalizedProofClauses = [
|
|
265
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
|
|
266
|
+
IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
267
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
268
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls
|
|
269
|
+
].map(normalizeMarkdownWhitespace);
|
|
270
|
+
const reviewNegativeControls = normalizeMarkdownWhitespace(
|
|
271
|
+
markdownSection(review, 'B2 — fresh-context red team')
|
|
272
|
+
);
|
|
273
|
+
if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
|
|
274
|
+
|| normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
|
|
275
|
+
|| !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
|
|
276
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
|
|
277
|
+
))) {
|
|
278
|
+
issues.add('proof-chain');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const failureGuidance = markdownSection(templates, 'Twelve edge-case dimensions');
|
|
282
|
+
if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
|
|
283
|
+
issues.add('failure-semantics');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const evidenceRules = markdownSection(review, 'B1 — evidence rule');
|
|
287
|
+
if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
|
|
288
|
+
issues.add('privacy-identifiers');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const closure = markdownSection(review, 'Accepted-repair closure');
|
|
292
|
+
const normalizedClosure = normalizeMarkdownWhitespace(closure);
|
|
293
|
+
const closureHeader = markdownTableUnderHeading(review, 'Accepted-repair closure')[0] || [];
|
|
294
|
+
if (JSON.stringify(closureHeader) !== JSON.stringify([
|
|
295
|
+
'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure'
|
|
296
|
+
])
|
|
297
|
+
|| !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
|
|
298
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
|
|
299
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
|
|
300
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
|
|
301
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
|
|
302
|
+
issues.add('repair-closure');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return [...issues].sort();
|
|
147
306
|
}
|
|
148
307
|
|
|
149
|
-
function
|
|
308
|
+
function authoringProjectionIssues(files) {
|
|
150
309
|
const issues = [];
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
154
|
-
||
|
|
155
|
-
issues.push('
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const
|
|
174
|
-
|
|
310
|
+
issues.push(...implementationReadinessIssues(files.templates, files.review));
|
|
311
|
+
const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
|
|
312
|
+
if (!files.skill.includes('Task files are flat beside `plan.md`')
|
|
313
|
+
|| !files.skill.includes('Do not create a nested task directory.')) {
|
|
314
|
+
issues.push('flat-layout-entrypoint');
|
|
315
|
+
}
|
|
316
|
+
if (!files.templates.includes('The primary layout is always flat:')
|
|
317
|
+
|| !files.templates.includes('task-NN-<slug>.md')) {
|
|
318
|
+
issues.push('flat-layout-template');
|
|
319
|
+
}
|
|
320
|
+
for (const gate of ['C1 — Scope', 'C2 — Findings', 'C3 — Done']) {
|
|
321
|
+
if (!files.skill.includes(gate)) issues.push(`human-gate-${gate.slice(0, 2).toLowerCase()}`);
|
|
322
|
+
}
|
|
323
|
+
if (!files.review.includes('fresh-context red team')
|
|
324
|
+
|| !/Cap the presented\s+list at 15/.test(files.review)
|
|
325
|
+
|| !files.review.includes('at most two review-and-repair rounds')) {
|
|
326
|
+
issues.push('adversarial-review');
|
|
327
|
+
}
|
|
328
|
+
if (!files.skill.includes('inline `## Receipt`')
|
|
329
|
+
|| !files.templates.includes('## Canonical inline Receipt')) {
|
|
330
|
+
issues.push('inline-receipt');
|
|
331
|
+
}
|
|
332
|
+
for (const field of [
|
|
333
|
+
'Verification: PASS', 'Command:', 'Exit: 0', 'Base:', 'Head:'
|
|
334
|
+
]) {
|
|
335
|
+
if (!files.templates.includes(field)) issues.push(`receipt-${field.toLowerCase()}`);
|
|
336
|
+
}
|
|
337
|
+
if (!/^name: hapo-specs$/m.test(files.skill)) issues.push('codex-skill-name');
|
|
338
|
+
const bundle = [files.skill, files.review, files.templates, files.legacyTemplates].join('\n');
|
|
339
|
+
const runtimeProjection = `${bundle}\n${files.codex.replace(foreignRuntimeBoundary, '')}`;
|
|
340
|
+
if (!files.codex.includes(foreignRuntimeBoundary)
|
|
341
|
+
|| files.codex.includes('If you are Codex CLI or any other runtime, ignore this entire Codex block')) {
|
|
342
|
+
issues.push('codex-ownership-boundary');
|
|
175
343
|
}
|
|
176
|
-
if (/^##\s+Evidence\s*$/m.test(files.tasks)) issues.push('task-evidence');
|
|
177
|
-
if (/\(P\)/.test(files.tasks)) issues.push('task-priority-marker');
|
|
178
344
|
for (const claudeOnly of [
|
|
179
345
|
'AskUserQuestion', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList',
|
|
180
|
-
'WebSearch', 'WebFetch', 'SendMessage'
|
|
346
|
+
'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/hapo:', 'hapo:'
|
|
181
347
|
]) {
|
|
182
|
-
if (
|
|
348
|
+
if (runtimeProjection.includes(claudeOnly)) issues.push(`claude-vocabulary-${claudeOnly}`);
|
|
183
349
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
issues.push(
|
|
350
|
+
for (const command of ['$hapo-specs', '$hapo-develop', '$hapo-sync']) {
|
|
351
|
+
if (!files.codex.includes(command)) issues.push(`codex-command-${command}`);
|
|
352
|
+
}
|
|
353
|
+
for (const state of ['pending', 'in_progress', 'paused', 'blocked', 'done']) {
|
|
354
|
+
if (!files.codex.includes(`\`${state}\``)) issues.push(`lifecycle-${state}`);
|
|
189
355
|
}
|
|
190
|
-
|
|
356
|
+
if (!files.codex.includes('flat `task-NN-*.md` files')
|
|
357
|
+
|| !files.codex.includes('inline `## Receipt`')
|
|
358
|
+
|| !files.codex.includes('C1') || !files.codex.includes('C2') || !files.codex.includes('C3')) {
|
|
359
|
+
issues.push('codex-process-v3');
|
|
360
|
+
}
|
|
361
|
+
return [...new Set(issues)].sort();
|
|
191
362
|
}
|
|
192
363
|
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
364
|
+
function canonicalInstalledSpecsRoot(projectRoot, installedRoot) {
|
|
365
|
+
assert.equal(path.isAbsolute(projectRoot), true, 'project root must be absolute');
|
|
366
|
+
assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
|
|
367
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
368
|
+
const canonicalInstalled = fs.realpathSync(installedRoot);
|
|
369
|
+
const relative = path.relative(canonicalProject, canonicalInstalled);
|
|
370
|
+
assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
|
|
371
|
+
assert.equal(
|
|
372
|
+
canonicalInstalled,
|
|
373
|
+
fs.realpathSync(path.join(canonicalProject, '.agents/skills/specs')),
|
|
374
|
+
'checker must read the native installed Codex Specs root'
|
|
375
|
+
);
|
|
376
|
+
return canonicalInstalled;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function readInstalledAuthoringProjection(projectRoot, installedRoot) {
|
|
380
|
+
const canonicalRoot = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
381
|
+
const read = (relative) => fs.readFileSync(path.join(canonicalRoot, relative), 'utf8');
|
|
382
|
+
return {
|
|
383
|
+
skill: read('SKILL.md'),
|
|
384
|
+
review: read('references/review.md'),
|
|
385
|
+
templates: read('references/templates.md'),
|
|
386
|
+
legacyTemplates: V3_SPECS_BUNDLE.filter((relative) => relative.startsWith('templates/')).map(read).join('\n'),
|
|
387
|
+
codex: [
|
|
388
|
+
fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'),
|
|
389
|
+
fs.readFileSync(path.join(projectRoot, '.codex/rules/state-sync.md'), 'utf8')
|
|
390
|
+
].join('\n')
|
|
211
391
|
};
|
|
212
|
-
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function installedAuthoringProjectionIssues(projectRoot, installedRoot) {
|
|
395
|
+
return authoringProjectionIssues(readInstalledAuthoringProjection(projectRoot, installedRoot));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function assertInstalledMutationTarget(projectRoot, installedRoot, relative) {
|
|
399
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
400
|
+
const canonicalInstalled = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
401
|
+
const targetPath = path.join(canonicalInstalled, relative);
|
|
402
|
+
const targetLstat = fs.lstatSync(targetPath);
|
|
403
|
+
assert.equal(targetLstat.isSymbolicLink(), false, 'installed mutation target must not be a symlink');
|
|
404
|
+
assert.equal(targetLstat.isFile(), true, 'installed mutation target must be a regular file');
|
|
405
|
+
const canonicalTarget = fs.realpathSync(targetPath);
|
|
406
|
+
const installedRelative = path.relative(canonicalInstalled, canonicalTarget);
|
|
407
|
+
const projectRelative = path.relative(canonicalProject, canonicalTarget);
|
|
408
|
+
assert.ok(
|
|
409
|
+
installedRelative && installedRelative !== '..'
|
|
410
|
+
&& !installedRelative.startsWith(`..${path.sep}`)
|
|
411
|
+
&& !path.isAbsolute(installedRelative),
|
|
412
|
+
'installed mutation target must stay inside .agents/skills/specs'
|
|
413
|
+
);
|
|
414
|
+
assert.ok(
|
|
415
|
+
projectRelative && projectRelative !== '..'
|
|
416
|
+
&& !projectRelative.startsWith(`..${path.sep}`)
|
|
417
|
+
&& !path.isAbsolute(projectRelative),
|
|
418
|
+
'installed mutation target must stay inside the disposable project'
|
|
419
|
+
);
|
|
420
|
+
const canonicalSource = fs.realpathSync(path.join(SPECS_SOURCE_ROOT, relative));
|
|
421
|
+
assert.notEqual(
|
|
422
|
+
canonicalTarget,
|
|
423
|
+
canonicalSource,
|
|
424
|
+
'installed mutation target must not resolve to canonical source'
|
|
425
|
+
);
|
|
426
|
+
const targetStat = fs.statSync(canonicalTarget);
|
|
427
|
+
const sourceStat = fs.statSync(canonicalSource);
|
|
428
|
+
assert.notDeepEqual(
|
|
429
|
+
[targetStat.dev, targetStat.ino],
|
|
430
|
+
[sourceStat.dev, sourceStat.ino],
|
|
431
|
+
'installed mutation target must not share an inode with canonical source'
|
|
432
|
+
);
|
|
433
|
+
return canonicalTarget;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assertCanonicalSpecsSourceBytesUnchanged(sourceBytes) {
|
|
437
|
+
for (const [relative, expected] of sourceBytes) {
|
|
438
|
+
assert.deepEqual(
|
|
439
|
+
fs.readFileSync(path.join(SPECS_SOURCE_ROOT, relative)),
|
|
440
|
+
expected,
|
|
441
|
+
`canonical Specs source bytes changed during installed mutation: ${relative}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function assertInstalledAuthoringProjection(root, installedRoot) {
|
|
447
|
+
canonicalInstalledSpecsRoot(root, installedRoot);
|
|
448
|
+
const relativeFiles = (directory) => allFiles(directory, () => true)
|
|
449
|
+
.map((file) => path.relative(directory, file).split(path.sep).join('/'))
|
|
450
|
+
.sort();
|
|
451
|
+
assert.deepEqual(relativeFiles(SPECS_SOURCE_ROOT), V3_SPECS_BUNDLE);
|
|
452
|
+
assert.deepEqual(relativeFiles(installedRoot), V3_SPECS_BUNDLE);
|
|
453
|
+
|
|
454
|
+
for (const relative of V3_SPECS_BUNDLE) {
|
|
455
|
+
const sourcePath = path.join(SPECS_SOURCE_ROOT, relative);
|
|
456
|
+
const expected = normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath);
|
|
457
|
+
const actual = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
|
|
458
|
+
assert.equal(actual, expected, `Codex Specs projection drifted: ${relative}`);
|
|
459
|
+
}
|
|
460
|
+
for (const relative of OBSOLETE_SPECS_FILES) {
|
|
461
|
+
assert.equal(fs.existsSync(path.join(installedRoot, relative)), false, `obsolete Specs file installed: ${relative}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const files = readInstalledAuthoringProjection(root, installedRoot);
|
|
465
|
+
const sourceBytes = new Map(V3_SPECS_BUNDLE.map((relative) => [
|
|
466
|
+
relative,
|
|
467
|
+
fs.readFileSync(path.join(SPECS_SOURCE_ROOT, relative))
|
|
468
|
+
]));
|
|
469
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
|
|
470
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
471
|
+
|
|
472
|
+
if (process.platform !== 'win32') {
|
|
473
|
+
const relative = 'references/templates.md';
|
|
474
|
+
const installedPath = path.join(installedRoot, relative);
|
|
475
|
+
const backupPath = `${installedPath}.cafekit-backup`;
|
|
476
|
+
const sourcePath = path.join(SPECS_SOURCE_ROOT, relative);
|
|
477
|
+
fs.renameSync(installedPath, backupPath);
|
|
478
|
+
try {
|
|
479
|
+
fs.linkSync(sourcePath, installedPath);
|
|
480
|
+
assert.throws(
|
|
481
|
+
() => assertInstalledMutationTarget(root, installedRoot, relative),
|
|
482
|
+
/must not share an inode with canonical source/
|
|
483
|
+
);
|
|
484
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
485
|
+
} finally {
|
|
486
|
+
try { fs.unlinkSync(installedPath); } catch { /* best-effort disposable cleanup */ }
|
|
487
|
+
fs.renameSync(backupPath, installedPath);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const boundaryMutation = (name, boundary, replacements) => {
|
|
492
|
+
const row = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === boundary);
|
|
493
|
+
assert.ok(row, `${name} references unknown boundary ${boundary}`);
|
|
494
|
+
let weakened = row[1];
|
|
495
|
+
for (const [from, to] of replacements) {
|
|
496
|
+
const next = weakened.replace(from, to);
|
|
497
|
+
assert.notEqual(next, weakened, `${name} weakening anchor must exist in ${boundary}`);
|
|
498
|
+
weakened = next;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name,
|
|
502
|
+
issue: 'boundary-contract',
|
|
503
|
+
relative: 'references/templates.md',
|
|
504
|
+
from: `| ${row[0]} | ${row[1]} |`,
|
|
505
|
+
to: `| ${row[0]} | ${weakened} |`
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const readinessMutations = [
|
|
510
|
+
{
|
|
511
|
+
name: 'no-invention-blocking',
|
|
512
|
+
issue: 'no-invention',
|
|
513
|
+
relative: 'references/templates.md',
|
|
514
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
515
|
+
to: 'Before implementation handoff, note ambiguous choices without blocking handoff.'
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'material-boundary-definition',
|
|
519
|
+
issue: 'boundary-contract',
|
|
520
|
+
relative: 'references/templates.md',
|
|
521
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.materialDefinition,
|
|
522
|
+
to: 'A boundary is material when it seems relevant to the task.'
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: 'exact-boundary-choices',
|
|
526
|
+
issue: 'boundary-contract',
|
|
527
|
+
relative: 'references/templates.md',
|
|
528
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices,
|
|
529
|
+
to: 'For every required row, describe the listed choices generally.'
|
|
530
|
+
},
|
|
531
|
+
boundaryMutation('api-success-and-error-semantics', 'API/CLI', [
|
|
532
|
+
['success output; ', ''],
|
|
533
|
+
['error/status/exit; ', '']
|
|
534
|
+
]),
|
|
535
|
+
boundaryMutation('schema-shape-and-unknown-fields', 'Schema', [
|
|
536
|
+
['exact keys/nesting/types; ', ''],
|
|
537
|
+
['unknown-field behavior; ', '']
|
|
538
|
+
]),
|
|
539
|
+
boundaryMutation('state-lock-lifecycle', 'State/concurrency', [
|
|
540
|
+
['writer/lock acquire/contention/release', 'writer/lock']
|
|
541
|
+
]),
|
|
542
|
+
boundaryMutation('filesystem-segment-grammar', 'Filesystem/security', [
|
|
543
|
+
['trusted/untrusted segment grammar; ', '']
|
|
544
|
+
]),
|
|
545
|
+
boundaryMutation('filesystem-stale-lock-reclaim', 'Filesystem/security', [
|
|
546
|
+
['lock/stale reclaim; ', '']
|
|
547
|
+
]),
|
|
548
|
+
boundaryMutation('retention-clock-and-endpoints', 'Time/retention', [
|
|
549
|
+
['clock source; ', ''],
|
|
550
|
+
['unit/precision/timezone; ', ''],
|
|
551
|
+
['endpoints and inclusion/comparator; ', '']
|
|
552
|
+
]),
|
|
553
|
+
boundaryMutation('proof-level-partition', 'Integration/proof', [
|
|
554
|
+
['proof level (`source`/`installed`/`live`)', 'proof level']
|
|
555
|
+
]),
|
|
556
|
+
{
|
|
557
|
+
name: 'concrete-named-probe',
|
|
558
|
+
issue: 'proof-chain',
|
|
559
|
+
relative: 'references/templates.md',
|
|
560
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
|
|
561
|
+
to: '- Named probe: <suite label>'
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'aggregate-suite-probe-owner',
|
|
565
|
+
issue: 'proof-chain',
|
|
566
|
+
relative: 'references/templates.md',
|
|
567
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
568
|
+
to: 'Aggregate suites may cite only the suite label.'
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'reachability-levels',
|
|
572
|
+
issue: 'proof-chain',
|
|
573
|
+
relative: 'references/templates.md',
|
|
574
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[2],
|
|
575
|
+
to: '- Reachability: <entrypoint or consumer>'
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'proof-level-separation',
|
|
579
|
+
issue: 'proof-chain',
|
|
580
|
+
relative: 'references/templates.md',
|
|
581
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
582
|
+
to: 'Proof may be promoted between source, installed, and live levels.'
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'disposable-template-negative-controls',
|
|
586
|
+
issue: 'proof-chain',
|
|
587
|
+
relative: 'references/templates.md',
|
|
588
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
589
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: 'disposable-review-negative-controls',
|
|
593
|
+
issue: 'proof-chain',
|
|
594
|
+
relative: 'references/review.md',
|
|
595
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls,
|
|
596
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: 'artifact-path-and-digest-or-ephemeral',
|
|
600
|
+
issue: 'proof-chain',
|
|
601
|
+
relative: 'references/templates.md',
|
|
602
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
|
|
603
|
+
to: '- Artifacts: <required artifact path, or none>'
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'proof-counterexample',
|
|
607
|
+
issue: 'proof-chain',
|
|
608
|
+
relative: 'references/templates.md',
|
|
609
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[4],
|
|
610
|
+
to: '- Counterexample: <example>'
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: 'repair-and-proof-columns',
|
|
614
|
+
issue: 'repair-closure',
|
|
615
|
+
relative: 'references/review.md',
|
|
616
|
+
from: '| ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |',
|
|
617
|
+
to: '| ID | Decision | Original counterexample | Repaired at | Evidence | Replay | Closure |'
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'fresh-original-counterexample-replay',
|
|
621
|
+
issue: 'repair-closure',
|
|
622
|
+
relative: 'references/review.md',
|
|
623
|
+
from: 'After applying an accepted C2 finding, a fresh-context closure pass records and\nfreshly replays its original counterexample after the repair under this exact review-log header:',
|
|
624
|
+
to: 'After applying an accepted C2 finding, record the repair under this review-log header:'
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: 'distinct-repair-and-proof-evidence',
|
|
628
|
+
issue: 'repair-closure',
|
|
629
|
+
relative: 'references/review.md',
|
|
630
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
|
|
631
|
+
to: '`Repaired at` and `Proved at` may cite the same repair edit.'
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'unknown-blocks-handoff',
|
|
635
|
+
issue: 'repair-closure',
|
|
636
|
+
relative: 'references/review.md',
|
|
637
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks,
|
|
638
|
+
to: '`PASS` closes it; `FAIL` and `UNKNOWN` may continue to implementation handoff.'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'crash-versus-catchable-failure',
|
|
642
|
+
issue: 'failure-semantics',
|
|
643
|
+
relative: 'references/templates.md',
|
|
644
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
|
|
645
|
+
to: 'Crash and catchable failure both mean an error occurred.'
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: 'privacy-identifier-surface',
|
|
649
|
+
issue: 'privacy-identifiers',
|
|
650
|
+
relative: 'references/review.md',
|
|
651
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
|
|
652
|
+
to: 'Any privacy/security claim names the sensitive data at risk.'
|
|
653
|
+
}
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
for (const { name, issue, relative, from, to } of readinessMutations) {
|
|
657
|
+
const target = assertInstalledMutationTarget(root, installedRoot, relative);
|
|
658
|
+
const installedBytes = fs.readFileSync(target, 'utf8');
|
|
659
|
+
const anchorIndex = installedBytes.indexOf(from);
|
|
660
|
+
assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
|
|
661
|
+
assert.equal(
|
|
662
|
+
installedBytes.indexOf(from, anchorIndex + from.length),
|
|
663
|
+
-1,
|
|
664
|
+
`${name} mutation anchor must be unique in installed bytes`
|
|
665
|
+
);
|
|
666
|
+
const weakened = `${installedBytes.slice(0, anchorIndex)}${to}${installedBytes.slice(anchorIndex + from.length)}`;
|
|
667
|
+
try {
|
|
668
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), weakened);
|
|
669
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
670
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [issue], name);
|
|
671
|
+
} finally {
|
|
672
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), installedBytes);
|
|
673
|
+
}
|
|
674
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], `${name} restore`);
|
|
675
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
676
|
+
}
|
|
677
|
+
|
|
213
678
|
const mutations = [
|
|
214
|
-
['
|
|
215
|
-
['review', (value) =>
|
|
216
|
-
['
|
|
217
|
-
['
|
|
679
|
+
['skill', (value) => value.replace('Task files are flat beside `plan.md`', 'Task files may be nested')],
|
|
680
|
+
['review', (value) => value.replace('list at 15', 'list without a cap')],
|
|
681
|
+
['templates', (value) => value.replace('Verification: PASS', 'Verification: UNKNOWN')],
|
|
682
|
+
['codex', (value) => `${value}\nUse TaskUpdate for state changes.`]
|
|
218
683
|
];
|
|
219
684
|
for (const [key, mutate] of mutations) {
|
|
220
685
|
const changed = { ...files, [key]: mutate(files[key]) };
|
|
221
|
-
assert.notDeepEqual(
|
|
686
|
+
assert.notDeepEqual(authoringProjectionIssues(changed), []);
|
|
222
687
|
}
|
|
223
688
|
}
|
|
224
689
|
|
|
@@ -271,7 +736,7 @@ test('platform resolver keeps saved runtimes and newly detected Codex', async ()
|
|
|
271
736
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-detect-'));
|
|
272
737
|
const originalCwd = process.cwd();
|
|
273
738
|
try {
|
|
274
|
-
for (const folder of ['.claude', '.
|
|
739
|
+
for (const folder of ['.claude', '.codex']) {
|
|
275
740
|
fs.mkdirSync(path.join(root, folder), { recursive: true });
|
|
276
741
|
}
|
|
277
742
|
fs.writeFileSync(
|
|
@@ -287,7 +752,7 @@ test('platform resolver keeps saved runtimes and newly detected Codex', async ()
|
|
|
287
752
|
t: (key) => key
|
|
288
753
|
};
|
|
289
754
|
await resolvePlatforms(ctx);
|
|
290
|
-
assert.deepEqual(ctx.platforms, ['claude', '
|
|
755
|
+
assert.deepEqual(ctx.platforms, ['claude', 'codex']);
|
|
291
756
|
} finally {
|
|
292
757
|
process.chdir(originalCwd);
|
|
293
758
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -645,7 +1110,7 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
|
|
|
645
1110
|
});
|
|
646
1111
|
});
|
|
647
1112
|
|
|
648
|
-
test('Codex install is
|
|
1113
|
+
test('Codex fresh install is exact while refresh and upgrade preserve existing files', () => {
|
|
649
1114
|
inTempProject((root) => {
|
|
650
1115
|
const userInstructions = '# User rules\n\nKeep this exact.\n';
|
|
651
1116
|
fs.writeFileSync(path.join(root, 'AGENTS.md'), userInstructions);
|
|
@@ -653,7 +1118,6 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
653
1118
|
const first = install(root);
|
|
654
1119
|
assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
|
|
655
1120
|
assert.equal(fs.existsSync(path.join(root, '.claude')), false);
|
|
656
|
-
assert.equal(fs.existsSync(path.join(root, '.opencode')), false);
|
|
657
1121
|
assert.equal(fs.existsSync(path.join(root, '.codex', 'config.toml')), false);
|
|
658
1122
|
|
|
659
1123
|
for (const relative of [
|
|
@@ -666,10 +1130,7 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
666
1130
|
'.codex/scripts/spec-ground.cjs',
|
|
667
1131
|
'.codex/scripts/validate-spec-output.cjs',
|
|
668
1132
|
'.agents/.gitignore',
|
|
669
|
-
|
|
670
|
-
'.agents/skills/specs/templates/design.md',
|
|
671
|
-
'.agents/skills/specs/templates/task.md',
|
|
672
|
-
'.agents/skills/specs/templates/spec-state.json'
|
|
1133
|
+
...V3_SPECS_BUNDLE.map((file) => `.agents/skills/specs/${file}`)
|
|
673
1134
|
]) {
|
|
674
1135
|
assert.equal(fs.existsSync(path.join(root, relative)), true, `missing ${relative}`);
|
|
675
1136
|
}
|
|
@@ -682,7 +1143,20 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
682
1143
|
path.join(root, '.codex/scripts/spec-ground.cjs'),
|
|
683
1144
|
installedDesign
|
|
684
1145
|
);
|
|
685
|
-
|
|
1146
|
+
const installedSpecsRoot = path.join(root, '.agents/skills/specs');
|
|
1147
|
+
assert.throws(
|
|
1148
|
+
() => installedAuthoringProjectionIssues(root, '.agents/skills/specs'),
|
|
1149
|
+
/must be absolute/
|
|
1150
|
+
);
|
|
1151
|
+
assert.throws(
|
|
1152
|
+
() => installedAuthoringProjectionIssues(root, SPECS_SOURCE_ROOT),
|
|
1153
|
+
/must stay inside project/
|
|
1154
|
+
);
|
|
1155
|
+
assert.throws(
|
|
1156
|
+
() => installedAuthoringProjectionIssues(root, path.dirname(root)),
|
|
1157
|
+
/must stay inside project/
|
|
1158
|
+
);
|
|
1159
|
+
assertInstalledAuthoringProjection(root, installedSpecsRoot);
|
|
686
1160
|
|
|
687
1161
|
const installedTask = fs.readFileSync(
|
|
688
1162
|
path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
|
|
@@ -777,7 +1251,13 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
777
1251
|
...allFiles(path.join(root, '.agents', 'skills'), (file) => file.endsWith('.md'))
|
|
778
1252
|
];
|
|
779
1253
|
const visible = modelVisibleFiles.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
|
|
780
|
-
|
|
1254
|
+
const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
|
|
1255
|
+
assert.match(visible, new RegExp(foreignRuntimeBoundary));
|
|
1256
|
+
assert.doesNotMatch(visible, /If you are Codex CLI or any other runtime, ignore this entire Codex block/);
|
|
1257
|
+
assert.doesNotMatch(
|
|
1258
|
+
visible.replaceAll(foreignRuntimeBoundary, ''),
|
|
1259
|
+
/\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/hapo:|\bhapo:|Claude Code/,
|
|
1260
|
+
);
|
|
781
1261
|
assert.doesNotMatch(visible, /@@PRIVACY_PROMPT_START@@|Claude Tasks/);
|
|
782
1262
|
assert.match(visible, /\$hapo-specs/);
|
|
783
1263
|
|
|
@@ -796,10 +1276,23 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
796
1276
|
|
|
797
1277
|
const questionSkill = path.join(root, '.agents', 'skills', 'question', 'SKILL.md');
|
|
798
1278
|
fs.appendFileSync(questionSkill, '\nUSER-CODEX-SENTINEL\n');
|
|
1279
|
+
// Fresh installs are exact. Refresh/upgrade intentionally documents the
|
|
1280
|
+
// current Codex limitation: removed skill paths are not pruned.
|
|
1281
|
+
const obsoleteSpecsRule = path.join(
|
|
1282
|
+
root, '.agents', 'skills', 'specs', 'rules', 'design-principles.md'
|
|
1283
|
+
);
|
|
1284
|
+
fs.mkdirSync(path.dirname(obsoleteSpecsRule), { recursive: true });
|
|
1285
|
+
fs.writeFileSync(obsoleteSpecsRule, 'LEGACY-CODEX-ORPHAN\n');
|
|
799
1286
|
|
|
800
1287
|
const sameVersion = install(root);
|
|
801
1288
|
assert.equal(sameVersion.status, 0, `${sameVersion.stdout}\n${sameVersion.stderr}`);
|
|
1289
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
802
1290
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
1291
|
+
assert.equal(
|
|
1292
|
+
fs.existsSync(obsoleteSpecsRule),
|
|
1293
|
+
true,
|
|
1294
|
+
'known limitation: Codex refresh does not prune obsolete skill files'
|
|
1295
|
+
);
|
|
803
1296
|
|
|
804
1297
|
const metadataPath = path.join(root, '.codex', 'cafekit.json');
|
|
805
1298
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
@@ -808,7 +1301,13 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
808
1301
|
|
|
809
1302
|
const upgrade = install(root);
|
|
810
1303
|
assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
|
|
1304
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
811
1305
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
1306
|
+
assert.equal(
|
|
1307
|
+
fs.existsSync(obsoleteSpecsRule),
|
|
1308
|
+
true,
|
|
1309
|
+
'known limitation: Codex upgrade does not prune obsolete skill files'
|
|
1310
|
+
);
|
|
812
1311
|
assert.ok(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8').startsWith(userInstructions));
|
|
813
1312
|
|
|
814
1313
|
const forced = install(root, ['--force-overwrite']);
|