@haposoft/cafekit 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -182,12 +182,7 @@ const V3_SPECS_BUNDLE = [
182
182
  'SKILL.md',
183
183
  'references/review.md',
184
184
  'references/templates.md',
185
- 'templates/design.md',
186
- 'templates/requirements-init.md',
187
- 'templates/requirements.md',
188
- 'templates/research.md',
189
- 'templates/spec-state.json',
190
- 'templates/task.md'
185
+ 'templates/research.md'
191
186
  ];
192
187
  const OBSOLETE_SPECS_FILES = [
193
188
  'references/archive-workflow.md',
@@ -265,6 +260,99 @@ function parseGeneratedTomlString(content, key) {
265
260
  return JSON.parse(match[1]);
266
261
  }
267
262
 
263
+ test('installed Codex instructions reference all ten runtime rules', () => {
264
+ inTempProject(root => {
265
+ const result = install(root);
266
+ assert.equal(result.status, 0, result.stderr);
267
+ const instructions = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
268
+ const rules = ['workflow', 'ai-dev-rules', 'orchestrator', 'manage-docs', 'skill-workflow-routing', 'skill-domain-routing', 'review-audit-self-decision', 'process-management', 'state-sync', 'hook-protocols'];
269
+ for (const rule of rules) {
270
+ assert.ok(instructions.includes(`.codex/rules/${rule}.md`), rule);
271
+ assert.ok(fs.existsSync(path.join(root, '.codex/rules', `${rule}.md`)), rule);
272
+ }
273
+ });
274
+ });
275
+
276
+ test('installed Codex orchestrator describes high reasoning on the session model', () => {
277
+ inTempProject(root => {
278
+ const source = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/orchestrator.md'));
279
+ const result = install(root);
280
+ assert.equal(result.status, 0, result.stderr);
281
+ const content = fs.readFileSync(path.join(root, '.codex/rules/orchestrator.md'), 'utf8');
282
+ assert.doesNotMatch(content, /strongest|below[^\n]*fable/);
283
+ assert.match(content, /high reasoning effort/);
284
+ assert.match(content, /session model unless[^.]*configur/i);
285
+ assert.deepEqual(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/orchestrator.md')), source);
286
+ });
287
+ });
288
+
289
+ test('installed hook protocols share malformed-output handling and Codex receipt guidance', () => {
290
+ inTempProject(root => {
291
+ const result = installPlatforms(root, ['claude', 'codex']);
292
+ assert.equal(result.status, 0, result.stderr);
293
+ const sentence = 'If hook output is malformed, stop and report the exact failure instead of guessing.';
294
+ for (const runtime of ['.claude', '.codex']) {
295
+ const content = fs.readFileSync(path.join(root, runtime, 'rules/hook-protocols.md'), 'utf8');
296
+ assert.ok(content.includes(sentence), runtime);
297
+ }
298
+ assert.match(fs.readFileSync(path.join(root, '.codex/rules/hook-protocols.md'), 'utf8'), /state-sync\.md/);
299
+ });
300
+ });
301
+
302
+ test('installed Claude ignore rules preserve env samples and ignore temporary files', () => {
303
+ inTempProject(root => {
304
+ const result = installPlatforms(root, ['claude']);
305
+ assert.equal(result.status, 0, result.stderr);
306
+ const lines = fs.readFileSync(path.join(root, '.claude/.gitignore'), 'utf8').split(/\r?\n/);
307
+ for (const line of ['!.env.sample', '!.env.template', '*.tmp']) assert.ok(lines.includes(line), line);
308
+ });
309
+ });
310
+
311
+ const READ_ONLY_ROLE_SUFFIX = '\n\n## Read-only role\n\nRead-only role: never create, never edit, and never delete files, and never run commands that write to disk; return findings in your reply.';
312
+
313
+ function assertInstalledAgentFields(content, name) {
314
+ const source = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/agents', `${name.replace(/_/g, '-')}.md`), 'utf8');
315
+ const { frontmatter, body } = require('../lib/codex-frontmatter').splitFrontmatter(source);
316
+ for (const key of ['name', 'description', 'developer_instructions']) {
317
+ assert.equal((content.match(new RegExp(`^${key} =`, 'gm')) || []).length, 1);
318
+ }
319
+ assert.equal(parseGeneratedTomlString(content, 'name'), name);
320
+ assert.equal(parseGeneratedTomlString(content, 'description'), normalizeCodexBody(frontmatter.description));
321
+ const suffix = ['brainstormer', 'code_auditor', 'inspector', 'researcher'].includes(name)
322
+ ? READ_ONLY_ROLE_SUFFIX : '';
323
+ const expectedBody = name === 'git_ops'
324
+ ? body.slice(0, body.indexOf('When instantiated as a team member'))
325
+ + body.split('\n').find(line => line.includes('NEVER arbitrarily use `force push`')).replace(/^3\. /, '- ') + '\n'
326
+ : body;
327
+ assert.equal(parseGeneratedTomlString(content, 'developer_instructions'), normalizeCodexBody(expectedBody).trim() + suffix);
328
+ }
329
+
330
+ for (const name of ['brainstormer', 'code_auditor', 'inspector', 'researcher']) {
331
+ test(`installed ${name} agent has exactly one read-only sandbox setting`, () => {
332
+ inTempProject(root => {
333
+ const result = install(root);
334
+ assert.equal(result.status, 0, result.stderr);
335
+ const content = fs.readFileSync(path.join(root, '.codex/agents', `${name}.toml`), 'utf8');
336
+ assert.deepEqual(content.match(/^sandbox_mode\s*=.*$/gm) || [], ['sandbox_mode = "read-only"']);
337
+ assert.ok(parseGeneratedTomlString(content, 'developer_instructions').endsWith(READ_ONLY_ROLE_SUFFIX));
338
+ assertInstalledAgentFields(content, name);
339
+ });
340
+ });
341
+ }
342
+
343
+ test('installed implementation agents retain their sandbox defaults', () => {
344
+ inTempProject(root => {
345
+ const result = install(root);
346
+ assert.equal(result.status, 0, result.stderr);
347
+ for (const name of ['implementer', 'test_runner', 'debugger', 'git_ops']) {
348
+ const content = fs.readFileSync(path.join(root, '.codex/agents', `${name}.toml`), 'utf8');
349
+ assert.doesNotMatch(content, /^sandbox_mode\s*=/m);
350
+ assert.doesNotMatch(parseGeneratedTomlString(content, 'developer_instructions'), /## Read-only role/);
351
+ assertInstalledAgentFields(content, name);
352
+ }
353
+ });
354
+ });
355
+
268
356
  function replaceGeneratedTomlString(content, key, value) {
269
357
  const pattern = new RegExp(`^${key} = .+$`, 'gm');
270
358
  const matches = [...content.matchAll(pattern)];
@@ -272,64 +360,6 @@ function replaceGeneratedTomlString(content, key, value) {
272
360
  return content.replace(pattern, `${key} = ${JSON.stringify(value)}`);
273
361
  }
274
362
 
275
- function renderCanonicalVerificationExample(template) {
276
- const values = new Map([
277
- ['SUBJECT_REQ', '1'],
278
- ['X', '1'],
279
- ['PROOF_REQ', '1'],
280
- ['Y', '2'],
281
- ['exact command', 'node --test test/installed.test.js'],
282
- ['exact anchored target', 'src/installed.js#entry'],
283
- ['exact/repository/entrypoint', 'src/installed.js'],
284
- ['observable result', 'the subject behavior and verifier proof both pass'],
285
- ['concrete observable result and proof', 'the subject behavior and verifier proof both pass'],
286
- ['concrete rejected or recovery case', 'invalid input remains rejected and observable'],
287
- ['real entrypoint/caller and grounded anchor expectation', 'the installed entrypoint reaches A-D-01'],
288
- ]);
289
- return template.replace(/\{\{([^}]+)\}\}/g, (placeholder, name) => (
290
- values.has(name) ? values.get(name) : placeholder
291
- ));
292
- }
293
-
294
- function assertInstalledVerificationModel(grounderPath, designTemplate) {
295
- const { parseVerificationDefinitions } = require(grounderPath);
296
- assert.equal(typeof parseVerificationDefinitions, 'function');
297
- const concreteDesign = renderCanonicalVerificationExample(designTemplate);
298
- const errors = [];
299
- const definitions = parseVerificationDefinitions(concreteDesign, errors);
300
- assert.deepEqual(errors, []);
301
- assert.equal(definitions.size, 1);
302
- const definition = definitions.get('V1');
303
- assert.ok(definition);
304
- assert.deepEqual(definition.subject_criteria, ['R1.1']);
305
- assert.deepEqual(definition.proof_criteria, []);
306
- assert.equal(definition.proof_owner, null);
307
- assert.equal(definition.evidence_anchor, null);
308
- assert.deepEqual(definition.decision_refs, ['D1', 'I1', 'C1']);
309
- for (const field of [
310
- 'subject_criteria', 'subject_owner', 'decision_refs', 'method', 'expected',
311
- 'negative', 'reachability'
312
- ]) {
313
- const value = definition[field];
314
- assert.ok(
315
- Array.isArray(value)
316
- ? value.length > 0
317
- : (typeof value === 'string' ? value.trim() !== '' : value && Object.keys(value).length > 0)
318
- );
319
- }
320
- for (const mutation of [
321
- concreteDesign.replace('- **V1**:', '### V1 —'),
322
- concreteDesign.replace('- **V1**:', '| V1 |'),
323
- concreteDesign.replace('; Expected ', '\nExpected '),
324
- concreteDesign.replace('Decision refs ', 'Decisions '),
325
- ]) {
326
- const mutationErrors = [];
327
- const mutated = parseVerificationDefinitions(mutation, mutationErrors);
328
- assert.ok(mutationErrors.length > 0);
329
- assert.equal(mutated.has('V1'), false);
330
- }
331
- }
332
-
333
363
  function markdownSection(content, heading) {
334
364
  const lines = String(content).split('\n');
335
365
  const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
@@ -879,7 +909,7 @@ function brainstormProjectionIssues(files) {
879
909
  }
880
910
  if (!skill.includes('Persist only approved decisions and semantics, only with user authority')
881
911
  || !skill.includes('redact live secrets, credentials, private keys, access tokens, and unnecessary PII;')
882
- || !framework.includes('Do not write "user selected" unless direct user text or the native input tool confirms it.')) {
912
+ || !framework.includes('Do not write "user selected" unless direct user text or the question in the turn\'s final answer confirms it.')) {
883
913
  issues.add('approval-persistence');
884
914
  }
885
915
  for (const [issue, clause] of Object.entries(adaptiveClauses)) {
@@ -887,9 +917,9 @@ function brainstormProjectionIssues(files) {
887
917
  }
888
918
  const visible = `${skill}\n${framework}\n${agent}`;
889
919
  if (/\bAskUserQuestion\b/.test(visible)
890
- || /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a structured user-input request\b/i.test(visible)
891
- || /\ban structured user-input request\b/i.test(visible)
892
- || /\ba structured user-input request\s+(?:calls|batches)\b/i.test(visible)) {
920
+ || /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a question in the turn\'s final answer\b/i.test(visible)
921
+ || /\ban question in the turn\'s final answer\b/i.test(visible)
922
+ || /\ba question in the turn\'s final answer\s+(?:calls|batches)\b/i.test(visible)) {
893
923
  issues.add('projection-grammar');
894
924
  }
895
925
  return [...issues].sort();
@@ -1442,26 +1472,26 @@ test('Codex payload transform emits native skill and subagent syntax', () => {
1442
1472
 
1443
1473
  test('Codex payload transform keeps structured user-input grammar across determiners', () => {
1444
1474
  const cases = [
1445
- ['standalone bare token', 'AskUserQuestion', 'a structured user-input request'],
1446
- ['standalone code token', '`AskUserQuestion`', 'a structured user-input request'],
1447
- ['standalone multi-backtick token', '``AskUserQuestion``', 'a structured user-input request'],
1448
- ['standalone padded multi-backtick token', '`` AskUserQuestion ``', 'a structured user-input request'],
1449
- ['one', 'one AskUserQuestion call', 'one structured user-input request call'],
1450
- ['one multi-backtick', 'one ``AskUserQuestion`` call', 'one structured user-input request call'],
1451
- ['one padded multi-backtick', 'one `` AskUserQuestion `` call', 'one structured user-input request call'],
1452
- ['each', 'each AskUserQuestion call', 'each structured user-input request call'],
1453
- ['every', 'every AskUserQuestion batch', 'every structured user-input request batch'],
1454
- ['another', 'another AskUserQuestion call', 'another structured user-input request call'],
1455
- ['this', 'this AskUserQuestion call', 'this structured user-input request call'],
1456
- ['that', 'that AskUserQuestion call', 'that structured user-input request call'],
1457
- ['a', 'a AskUserQuestion call', 'a structured user-input request call'],
1458
- ['an', 'an AskUserQuestion call', 'a structured user-input request call'],
1459
- ['capitalized each', 'Each AskUserQuestion call', 'Each structured user-input request call'],
1460
- ['capitalized an', 'An AskUserQuestion call', 'A structured user-input request call'],
1461
- ['the', 'the AskUserQuestion call', 'the structured user-input request call'],
1462
- ['numeric', '27 AskUserQuestion calls', '27 structured user-input request calls'],
1463
- ['plural calls', 'AskUserQuestion calls', 'structured user-input request calls'],
1464
- ['plural batches', 'AskUserQuestion batches', 'structured user-input request batches']
1475
+ ['standalone bare token', 'AskUserQuestion', 'a question in the turn\'s final answer'],
1476
+ ['standalone code token', '`AskUserQuestion`', 'a question in the turn\'s final answer'],
1477
+ ['standalone multi-backtick token', '``AskUserQuestion``', 'a question in the turn\'s final answer'],
1478
+ ['standalone padded multi-backtick token', '`` AskUserQuestion ``', 'a question in the turn\'s final answer'],
1479
+ ['one', 'one AskUserQuestion call', 'one question in the turn\'s final answer call'],
1480
+ ['one multi-backtick', 'one ``AskUserQuestion`` call', 'one question in the turn\'s final answer call'],
1481
+ ['one padded multi-backtick', 'one `` AskUserQuestion `` call', 'one question in the turn\'s final answer call'],
1482
+ ['each', 'each AskUserQuestion call', 'each question in the turn\'s final answer call'],
1483
+ ['every', 'every AskUserQuestion batch', 'every question in the turn\'s final answer batch'],
1484
+ ['another', 'another AskUserQuestion call', 'another question in the turn\'s final answer call'],
1485
+ ['this', 'this AskUserQuestion call', 'this question in the turn\'s final answer call'],
1486
+ ['that', 'that AskUserQuestion call', 'that question in the turn\'s final answer call'],
1487
+ ['a', 'a AskUserQuestion call', 'a question in the turn\'s final answer call'],
1488
+ ['an', 'an AskUserQuestion call', 'a question in the turn\'s final answer call'],
1489
+ ['capitalized each', 'Each AskUserQuestion call', 'Each question in the turn\'s final answer call'],
1490
+ ['capitalized an', 'An AskUserQuestion call', 'A question in the turn\'s final answer call'],
1491
+ ['the', 'the AskUserQuestion call', 'the question in the turn\'s final answer call'],
1492
+ ['numeric', '27 AskUserQuestion calls', '27 question in the turn\'s final answer calls'],
1493
+ ['plural calls', 'AskUserQuestion calls', 'question in the turn\'s final answer calls'],
1494
+ ['plural batches', 'AskUserQuestion batches', 'question in the turn\'s final answer batches']
1465
1495
  ];
1466
1496
 
1467
1497
  for (const [name, input, expected] of cases) {
@@ -1469,27 +1499,27 @@ test('Codex payload transform keeps structured user-input grammar across determi
1469
1499
  assert.equal(actual, expected, name);
1470
1500
  assert.equal(normalizeCodexBody(actual, '/fixture/instructions.md'), expected, `${name} idempotence`);
1471
1501
  assert.doesNotMatch(actual, /\bAskUserQuestion\b/, `${name} Claude-only token`);
1472
- assert.doesNotMatch(actual, /\ban structured user-input request\b/i, `${name} incompatible article`);
1502
+ assert.doesNotMatch(actual, /\ban question in the turn\'s final answer\b/i, `${name} incompatible article`);
1473
1503
  assert.doesNotMatch(
1474
1504
  actual,
1475
- /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a structured user-input request\b/i,
1505
+ /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a question in the turn\'s final answer\b/i,
1476
1506
  `${name} doubled article`
1477
1507
  );
1478
1508
  }
1479
1509
 
1480
1510
  assert.equal(
1481
1511
  normalizeCodexBody('one\nAskUserQuestion call', '/fixture/instructions.md'),
1482
- 'one\nstructured user-input request call',
1512
+ 'one\nquestion in the turn\'s final answer call',
1483
1513
  'a single Markdown soft break must preserve its determiner relationship'
1484
1514
  );
1485
1515
  assert.equal(
1486
1516
  normalizeCodexBody('AskUserQuestion\nCalls', '/fixture/instructions.md'),
1487
- 'structured user-input request\nCalls',
1517
+ 'question in the turn\'s final answer\nCalls',
1488
1518
  'a single Markdown soft break must preserve a plural relationship'
1489
1519
  );
1490
1520
  assert.equal(
1491
1521
  normalizeCodexBody('one\n\nAskUserQuestion call', '/fixture/instructions.md'),
1492
- 'one\n\na structured user-input request call',
1522
+ 'one\n\na question in the turn\'s final answer call',
1493
1523
  'a paragraph break must not bind a prior determiner'
1494
1524
  );
1495
1525
  assert.equal(
@@ -1567,9 +1597,9 @@ test('Codex structured-input corpus oracle stays differential and production-awa
1567
1597
  assert.deepEqual(actualPaths, expectedPaths, 'every source occurrence needs an explicit projection oracle');
1568
1598
 
1569
1599
  const instructionExpectedSnippets = new Map([
1570
- ['src/claude/skills/git/SKILL.md', 'Present options via a structured user-input request — header'],
1571
- ['src/claude/skills/scout/SKILL.md', '**Fallback to a structured user-input request:**'],
1572
- ['src/claude/skills/sync/references/rebind-and-audit.md', 'End by asking with a structured user-input request when the host has']
1600
+ ['src/claude/skills/git/SKILL.md', 'Present options via a question in the turn\'s final answer — header'],
1601
+ ['src/claude/skills/scout/SKILL.md', '**Fallback to a question in the turn\'s final answer:**'],
1602
+ ['src/claude/skills/sync/references/rebind-and-audit.md', 'End by asking with a question in the turn\'s final answer when the host has']
1573
1603
  ]);
1574
1604
  for (const [relativePath, expectedSnippet] of instructionExpectedSnippets) {
1575
1605
  const sourcePath = path.join(PACKAGE_ROOT, relativePath);
@@ -1578,7 +1608,7 @@ test('Codex structured-input corpus oracle stays differential and production-awa
1578
1608
  const withSentinel = source.replace(/`?\bAskUserQuestion\b`?/g, sentinel);
1579
1609
  assert.notEqual(withSentinel, source, `${relativePath} differential control must replace a token`);
1580
1610
  const expected = normalizeCodexBody(withSentinel, sourcePath)
1581
- .replaceAll(sentinel, 'a structured user-input request');
1611
+ .replaceAll(sentinel, 'a question in the turn\'s final answer');
1582
1612
  const actual = normalizeCodexBody(source, sourcePath);
1583
1613
  assert.equal(actual, expected, `${relativePath} independent differential oracle`);
1584
1614
  assert.match(actual, new RegExp(expectedSnippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
@@ -1587,7 +1617,7 @@ test('Codex structured-input corpus oracle stays differential and production-awa
1587
1617
  const specMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
1588
1618
  const convertedAgent = convertCodexAgentContent(specMaker, path.basename(SPEC_MAKER_SOURCE_PATH));
1589
1619
  assert.doesNotMatch(convertedAgent, /\bAskUserQuestion\b/);
1590
- assert.doesNotMatch(convertedAgent, /structured user-input request/);
1620
+ assert.doesNotMatch(convertedAgent, /question in the turn\'s final answer/);
1591
1621
 
1592
1622
  const sessionPath = path.join(PACKAGE_ROOT, 'src/claude/hooks/session.cjs');
1593
1623
  const session = normalizeCodexBody(fs.readFileSync(sessionPath, 'utf8'), sessionPath);
@@ -2059,6 +2089,45 @@ function sessionLauncher(projectRoot) {
2059
2089
  return { config, command: config.hooks.SessionStart[0].hooks[0].command };
2060
2090
  }
2061
2091
 
2092
+ test('parity regression: moved Codex checkout rebinds both launcher fields and preserves metadata', () => {
2093
+ inTempProject((root) => {
2094
+ const oldRoot = path.join(root, "old ' quoted project"), newRoot = path.join(root, "new ' quoted project");
2095
+ fs.mkdirSync(oldRoot);
2096
+ assert.equal(install(oldRoot).status, 0);
2097
+ const configPath = path.join(oldRoot, '.codex/hooks.json');
2098
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
2099
+ config.hooks.SessionStart[0].matcher = 'custom-startup';
2100
+ config.hooks.SessionStart[0].hooks[0].timeout = 41;
2101
+ fs.writeFileSync(configPath, JSON.stringify(config));
2102
+ fs.renameSync(oldRoot, newRoot);
2103
+ const movedConfig = path.join(newRoot, '.codex/hooks.json');
2104
+ const before = fs.readFileSync(movedConfig, 'utf8');
2105
+ assert.equal(install(newRoot, ['--dry-run']).status, 0);
2106
+ assert.equal(fs.readFileSync(movedConfig, 'utf8'), before, 'dry run modified launchers');
2107
+ const upgraded = install(newRoot);
2108
+ assert.equal(upgraded.status, 0, `${upgraded.stdout}\n${upgraded.stderr}`);
2109
+ const rebound = sessionLauncher(newRoot);
2110
+ assert.equal(rebound.config.hooks.SessionStart[0].matcher, 'custom-startup');
2111
+ assert.equal(rebound.config.hooks.SessionStart[0].hooks[0].timeout, 41);
2112
+ const launched = runPosixLauncher(rebound.command, newRoot);
2113
+ assert.equal(launched.status, 0, launched.stderr);
2114
+ assert.match(launched.stdout, /Session startup\./);
2115
+ const windowsCommand = rebound.config.hooks.SessionStart[0].hooks[0].commandWindows.replace(/^node /, `"${process.execPath}" `);
2116
+ const launchedWindows = spawnSync(windowsCommand, { cwd: newRoot, encoding: 'utf8', input: JSON.stringify({ session_id: 'moved-windows', hook_event_name: 'SessionStart', source: 'startup', cwd: newRoot }), shell: true });
2117
+ assert.equal(launchedWindows.status, 0, launchedWindows.stderr);
2118
+ assert.match(launchedWindows.stdout, /Session startup\./);
2119
+ for (const { handler } of allHookLaunchers(rebound.config)) {
2120
+ const encoded = handler.commandWindows.match(/\s([A-Za-z0-9_-]+)$/)?.[1];
2121
+ assert.ok(encoded);
2122
+ const target = Buffer.from(encoded, 'base64url').toString('utf8');
2123
+ assert.equal(path.dirname(target), fs.realpathSync(path.join(newRoot, '.codex/hooks')));
2124
+ }
2125
+ const stable = fs.readFileSync(movedConfig, 'utf8');
2126
+ assert.equal(install(newRoot).status, 0);
2127
+ assert.equal(fs.readFileSync(movedConfig, 'utf8'), stable, 'reinstall changed stable hooks config');
2128
+ });
2129
+ });
2130
+
2062
2131
  test('Codex POSIX hook launchers run without Git and outside the repository root', () => {
2063
2132
  inTempProject((root) => {
2064
2133
  // A launcher that asks Git for its root gets nothing here and the outer root one
@@ -2160,22 +2229,12 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2160
2229
  '.codex/rules/state-sync.md',
2161
2230
  '.codex/rules/review-audit-self-decision.md',
2162
2231
  '.codex/rules/process-management.md',
2163
- '.codex/scripts/spec-ground.cjs',
2164
- '.codex/scripts/validate-spec-output.cjs',
2165
2232
  '.agents/.gitignore',
2166
2233
  ...V3_SPECS_BUNDLE.map((file) => `.agents/skills/specs/${file}`)
2167
2234
  ]) {
2168
2235
  assert.equal(fs.existsSync(path.join(root, relative)), true, `missing ${relative}`);
2169
2236
  }
2170
2237
 
2171
- const installedDesign = fs.readFileSync(
2172
- path.join(root, '.agents/skills/specs/templates/design.md'), 'utf8'
2173
- );
2174
- assert.equal((installedDesign.match(/^## Verification Definitions$/gm) || []).length, 1);
2175
- assertInstalledVerificationModel(
2176
- path.join(root, '.codex/scripts/spec-ground.cjs'),
2177
- installedDesign
2178
- );
2179
2238
  const installedSpecsRoot = path.join(root, '.agents/skills/specs');
2180
2239
  assert.throws(
2181
2240
  () => installedAuthoringProjectionIssues(root, '.agents/skills/specs'),
@@ -2191,22 +2250,6 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2191
2250
  );
2192
2251
  assertInstalledAuthoringProjection(root, installedSpecsRoot);
2193
2252
 
2194
- const installedTask = fs.readFileSync(
2195
- path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
2196
- );
2197
- assert.deepEqual(
2198
- [...installedTask.matchAll(/^## (.+)$/gm)].map((match) => match[1]),
2199
- ['Outcome', 'Scope', 'Anchors and Ownership', 'Changes', 'Acceptance', 'Dependencies', 'Verification Plan']
2200
- );
2201
- assert.match(installedTask, /^- \*\*Task role:\*\*/m);
2202
-
2203
- const installedState = JSON.parse(fs.readFileSync(
2204
- path.join(root, '.agents/skills/specs/templates/spec-state.json'), 'utf8'
2205
- ));
2206
- assert.deepEqual(Object.keys(installedState.workflow_policy).sort(), [
2207
- 'assurance_level', 'classified_minimum', 'planning_depth', 'risks', 'version'
2208
- ]);
2209
-
2210
2253
  const agentsGitignore = fs.readFileSync(
2211
2254
  path.join(root, '.agents', '.gitignore'),
2212
2255
  'utf8'
@@ -2217,7 +2260,6 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2217
2260
 
2218
2261
  if (process.platform !== 'win32') {
2219
2262
  for (const [sourceRelative, installedRelative] of [
2220
- ['src/claude/scripts/validate-spec-output.cjs', '.codex/scripts/validate-spec-output.cjs'],
2221
2263
  ['src/claude/skills/chrome-devtools/scripts/install.sh', '.agents/skills/chrome-devtools/scripts/install.sh'],
2222
2264
  ['src/claude/skills/ai-multimodal/scripts/check_setup.py', '.agents/skills/ai-multimodal/scripts/check_setup.py']
2223
2265
  ]) {
@@ -2338,7 +2380,7 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2338
2380
  const questionSkill = path.join(root, '.agents', 'skills', 'ask', 'SKILL.md');
2339
2381
  fs.appendFileSync(questionSkill, '\nUSER-CODEX-SENTINEL\n');
2340
2382
  // Fresh installs are exact. Refresh/upgrade intentionally documents the
2341
- // current Codex limitation: removed skill paths are not pruned.
2383
+ // Untracked obsolete files and edited current skills both stay on refresh.
2342
2384
  const obsoleteSpecsRule = path.join(
2343
2385
  root, '.agents', 'skills', 'specs', 'rules', 'design-principles.md'
2344
2386
  );
@@ -2352,13 +2394,14 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2352
2394
  assert.equal(
2353
2395
  fs.existsSync(obsoleteSpecsRule),
2354
2396
  true,
2355
- 'known limitation: Codex refresh does not prune obsolete skill files'
2397
+ 'Codex refresh preserves untracked obsolete skill files'
2356
2398
  );
2357
2399
 
2358
2400
  const metadataPath = path.join(root, '.codex', 'cafekit.json');
2359
2401
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
2360
2402
  metadata.version = '0.14.1';
2361
2403
  fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
2404
+ fs.writeFileSync(obsoleteSpecsRule, 'LEGACY-CODEX-ORPHAN\n');
2362
2405
 
2363
2406
  const upgrade = install(root);
2364
2407
  assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
@@ -2367,8 +2410,10 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
2367
2410
  assert.equal(
2368
2411
  fs.existsSync(obsoleteSpecsRule),
2369
2412
  true,
2370
- 'known limitation: Codex upgrade does not prune obsolete skill files'
2413
+ 'Codex upgrade preserves untracked obsolete skill files'
2371
2414
  );
2415
+ assert.match(sameVersion.stdout + sameVersion.stderr, /Preserved 1 obsolete runtime/);
2416
+ assert.match(upgrade.stdout + upgrade.stderr, /Preserved 1 obsolete runtime/);
2372
2417
  assert.ok(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8').startsWith(userInstructions));
2373
2418
 
2374
2419
  const forced = install(root, ['--force-overwrite']);
@@ -2598,7 +2643,7 @@ test('Codex installed Brainstorm skill reference and agent preserve proportional
2598
2643
  {
2599
2644
  name: 'decision-provenance-removed',
2600
2645
  source: 'framework',
2601
- from: 'Do not write "user selected" unless direct user text or the native input tool\n confirms it.',
2646
+ from: 'Do not write "user selected" unless direct user text or the question in the turn\'s final answer\n confirms it.',
2602
2647
  to: 'Write "user selected" for inferred defaults.',
2603
2648
  expected: ['approval-persistence']
2604
2649
  },
@@ -2606,7 +2651,7 @@ test('Codex installed Brainstorm skill reference and agent preserve proportional
2606
2651
  name: 'installed-grammar-corruption',
2607
2652
  source: 'skill',
2608
2653
  from: '## Completion bar',
2609
- to: 'one a structured user-input request call\n\n## Completion bar',
2654
+ to: 'one a question in the turn\'s final answer call\n\n## Completion bar',
2610
2655
  expected: ['projection-grammar']
2611
2656
  }
2612
2657
  ];
@@ -3136,8 +3181,6 @@ test('Codex install on top of existing Claude installation preserves content and
3136
3181
  assert.equal(fs.existsSync(path.join(root, '.codex', 'runtime.json')), true, '.codex/runtime.json should exist');
3137
3182
  assert.equal(fs.existsSync(path.join(root, '.codex', 'hooks', 'privacy-block.cjs')), true, '.codex/hooks/privacy-block.cjs should exist');
3138
3183
  assert.equal(fs.existsSync(path.join(root, '.codex', 'rules', 'workflow.md')), true, '.codex/rules/workflow.md should exist');
3139
- assert.equal(fs.existsSync(path.join(root, '.codex', 'scripts', 'spec-ground.cjs')), true, '.codex/scripts/spec-ground.cjs should exist');
3140
- assert.equal(fs.existsSync(path.join(root, '.codex', 'scripts', 'validate-spec-output.cjs')), true, '.codex/scripts/validate-spec-output.cjs should exist');
3141
3184
  assert.equal(fs.existsSync(path.join(root, '.agents', '.gitignore')), true, '.agents/.gitignore should exist');
3142
3185
  assert.equal(fs.existsSync(path.join(root, '.agents', 'skills', 'specs', 'SKILL.md')), true, '.agents/skills/specs/SKILL.md should exist');
3143
3186
 
@@ -3170,23 +3213,54 @@ test('Codex installed code_auditor carries no Strict attestation marker', () =>
3170
3213
  });
3171
3214
  });
3172
3215
 
3173
- test('Codex scaffold resolver rejects symlink template', () => {
3216
+ test('reinstall preserves untracked Codex runtime files while omp keeps its cleanup policy', () => {
3217
+ for (const platform of ['codex', 'omp']) inTempProject(root => {
3218
+ const first = installPlatforms(root, ['codex', 'omp']);
3219
+ assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
3220
+ const runtime = `.${platform}`;
3221
+ const retired = ['change-firewall.cjs', 'spec-final-state.cjs'];
3222
+ for (const name of retired) fs.writeFileSync(path.join(root, runtime, 'scripts', name), 'legacy\n');
3223
+ const retiredSkillFiles = platform === 'codex'
3224
+ ? ['code-review/references/verification-gate.md', 'specs/references/archive-workflow.md'] : [];
3225
+ for (const rel of retiredSkillFiles) {
3226
+ const target = path.join(root, '.agents/skills', rel);
3227
+ fs.mkdirSync(path.dirname(target), { recursive: true });
3228
+ fs.writeFileSync(target, 'legacy\n');
3229
+ }
3230
+ const again = installPlatforms(root, [platform], ['--force-overwrite']);
3231
+ assert.equal(again.status, 0, `${again.stdout}\n${again.stderr}`);
3232
+ const preserved = platform === 'codex';
3233
+ for (const rel of retiredSkillFiles) assert.equal(fs.existsSync(path.join(root, '.agents/skills', rel)), preserved, rel);
3234
+ for (const name of retired) assert.equal(fs.existsSync(path.join(root, runtime, 'scripts', name)), preserved, name);
3235
+ if (preserved) assert.match(again.stdout + again.stderr, /Preserved 4 obsolete runtime/);
3236
+ assert.equal(fs.existsSync(path.join(root, '.agents/skills/code-review/SKILL.md')), true);
3237
+ assert.equal(fs.existsSync(path.join(root, runtime, 'scripts/spec-resolver.cjs')), true);
3238
+ });
3239
+ });
3240
+
3241
+ test('reinstall removes the retired spec.json templates from Claude and Codex but a dry run keeps them', () => {
3174
3242
  inTempProject((root) => {
3175
- const result = install(root);
3176
- assert.equal(result.status, 0);
3177
- const template = path.join(root, '.agents', 'skills', 'specs', 'templates', 'task.md');
3178
- const target = path.join(root, 'outside.md');
3179
- fs.writeFileSync(target, 'evil');
3180
- const original = fs.readFileSync(template);
3181
- fs.unlinkSync(template);
3182
- fs.symlinkSync(target, template);
3183
- const scaffold = path.join(root, '.codex', 'scripts', 'spec-scaffold.cjs');
3184
- const out = spawnSync(process.execPath, [scaffold, 'symlink-test', '--tasks', 'R1-01-foo,R1-02-bar', '--boundaries', '[{"id":"B-OWN","type":"ownership","tasks":["R1-01","R1-02"],"write_sets":{"R1-01":["src/a.js"],"R1-02":["src/b.js"]}}]'], { cwd: root, encoding: 'utf8' });
3185
- // Should fail because template is symlink and resolver rejects it
3186
- assert.notEqual(out.status, 0, 'scaffold should reject symlink template');
3187
- assert.match(`${out.stdout}\n${out.stderr}`, /template not found|symlink/i);
3188
- fs.unlinkSync(template);
3189
- fs.writeFileSync(template, original);
3243
+ const first = installPlatforms(root, ['claude', 'codex']);
3244
+ assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
3245
+ const templateDirs = ['.claude/skills/specs/templates', '.agents/skills/specs/templates'];
3246
+ const retired = ['design.md', 'task.md', 'requirements.md', 'requirements-init.md', 'spec-state.json'];
3247
+ for (const dir of templateDirs) {
3248
+ for (const name of retired) fs.writeFileSync(path.join(root, dir, name), 'legacy\n');
3249
+ }
3250
+
3251
+ const dry = installPlatforms(root, ['claude', 'codex'], ['--force-overwrite', '--dry-run']);
3252
+ assert.equal(dry.status, 0, `${dry.stdout}\n${dry.stderr}`);
3253
+ for (const dir of templateDirs) {
3254
+ for (const name of retired) {
3255
+ assert.equal(fs.existsSync(path.join(root, dir, name)), true, `dry run removed ${dir}/${name}`);
3256
+ }
3257
+ }
3258
+
3259
+ const again = installPlatforms(root, ['claude', 'codex'], ['--force-overwrite']);
3260
+ assert.equal(again.status, 0, `${again.stdout}\n${again.stderr}`);
3261
+ for (const dir of templateDirs) {
3262
+ assert.deepEqual(fs.readdirSync(path.join(root, dir)), ['research.md'], `${dir} keeps only research.md`);
3263
+ }
3190
3264
  });
3191
3265
  });
3192
3266
 
@@ -3282,4 +3356,3 @@ test('Claude and Codex installed rules preserve the ported review and process gu
3282
3356
  }
3283
3357
  });
3284
3358
  });
3285
-