@haposoft/cafekit 0.16.0-rc.7 → 0.16.0
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 +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +295 -0
- package/bin/__tests__/codex-native.test.js +2289 -34
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +2131 -16
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +24 -13
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +87 -4
- package/src/claude/hooks/__tests__/state.test.js +254 -3
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +32 -5
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +48 -57
- package/src/claude/skills/specs/references/review.md +26 -8
- package/src/claude/skills/specs/references/templates.md +76 -31
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +34 -5
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
4
5
|
const fs = require('node:fs');
|
|
5
6
|
const os = require('node:os');
|
|
6
7
|
const path = require('node:path');
|
|
@@ -8,6 +9,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
9
|
const { test } = require('node:test');
|
|
9
10
|
|
|
10
11
|
const {
|
|
12
|
+
convertCodexAgentContent,
|
|
11
13
|
normalizeCodexBody,
|
|
12
14
|
transformManagedCodexContent,
|
|
13
15
|
upsertManagedCodexBlock
|
|
@@ -29,6 +31,153 @@ const MANIFEST = JSON.parse(
|
|
|
29
31
|
fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8')
|
|
30
32
|
);
|
|
31
33
|
const SPECS_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/specs');
|
|
34
|
+
const SPEC_MAKER_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/spec-maker.md');
|
|
35
|
+
const BRAINSTORM_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/brainstorm');
|
|
36
|
+
const BRAINSTORM_SKILL_SOURCE_PATH = path.join(BRAINSTORM_SOURCE_ROOT, 'SKILL.md');
|
|
37
|
+
const BRAINSTORM_REFERENCE_SOURCE_PATH = path.join(
|
|
38
|
+
BRAINSTORM_SOURCE_ROOT,
|
|
39
|
+
'references/question-framework.md'
|
|
40
|
+
);
|
|
41
|
+
const BRAINSTORM_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/brainstormer.md');
|
|
42
|
+
const BRAINSTORM_SOURCE_PATHS = [
|
|
43
|
+
BRAINSTORM_SKILL_SOURCE_PATH,
|
|
44
|
+
BRAINSTORM_REFERENCE_SOURCE_PATH,
|
|
45
|
+
BRAINSTORM_AGENT_SOURCE_PATH
|
|
46
|
+
];
|
|
47
|
+
const RESEARCH_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/research');
|
|
48
|
+
const RESEARCH_SKILL_SOURCE_PATH = path.join(RESEARCH_SOURCE_ROOT, 'SKILL.md');
|
|
49
|
+
const RESEARCH_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/researcher.md');
|
|
50
|
+
const DEVELOP_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/develop');
|
|
51
|
+
const DEVELOP_BUNDLE = [
|
|
52
|
+
'SKILL.md',
|
|
53
|
+
'references/quality-gate.md',
|
|
54
|
+
'references/parallel-waves.md',
|
|
55
|
+
'references/subagent-patterns.md'
|
|
56
|
+
];
|
|
57
|
+
const TEST_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/test');
|
|
58
|
+
const TEST_BUNDLE = [
|
|
59
|
+
'SKILL.md',
|
|
60
|
+
'references/execution-strategy.md',
|
|
61
|
+
'references/failure-triage.md',
|
|
62
|
+
'references/test-memory.md'
|
|
63
|
+
];
|
|
64
|
+
const TEST_RUNNER_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/test-runner.md');
|
|
65
|
+
const DOCS_ADAPTIVE_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/docs');
|
|
66
|
+
const DOCS_ADAPTIVE_BUNDLE = [
|
|
67
|
+
'SKILL.md',
|
|
68
|
+
'references/init-workflow.md',
|
|
69
|
+
'references/update-workflow.md',
|
|
70
|
+
'references/standard-docs-workflow.md'
|
|
71
|
+
];
|
|
72
|
+
const HOTFIX_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/hotfix');
|
|
73
|
+
const HOTFIX_BUNDLE = [
|
|
74
|
+
'SKILL.md',
|
|
75
|
+
'references/diagnosis-protocol.md',
|
|
76
|
+
'references/review-cycle.md',
|
|
77
|
+
'references/parallel-patterns.md',
|
|
78
|
+
'references/prevention-gate.md',
|
|
79
|
+
'references/workflow-specialized.md'
|
|
80
|
+
];
|
|
81
|
+
const DEBUG_SKILL_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/skills/debug/SKILL.md');
|
|
82
|
+
const DEBUG_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/debugger.md');
|
|
83
|
+
const DEBUG_REFERENCE_FILES = [
|
|
84
|
+
'core-philosophy.md',
|
|
85
|
+
'root-cause-tracing.md',
|
|
86
|
+
'verification-protocol.md',
|
|
87
|
+
'log-ci-analysis.md',
|
|
88
|
+
'parallel-agent-hydration.md',
|
|
89
|
+
'side-effect-gate.md'
|
|
90
|
+
];
|
|
91
|
+
const CODE_REVIEW_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/code-review');
|
|
92
|
+
const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
|
|
93
|
+
['Interaction/UI', 'entry journey; visible/loading/empty/error states; input/focus/keyboard; accessibility; responsive/native/device behavior'],
|
|
94
|
+
['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
|
|
95
|
+
['Data/schema', 'authority/storage/transaction; version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
|
|
96
|
+
['Async/state', 'initial/terminal states; event + guard + effect + next + error; ordering/concurrency; duplicate/retry; writer/lock acquire/contention/release; cancellation; rollback/recovery'],
|
|
97
|
+
['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
|
|
98
|
+
['Runtime/deploy', 'config/env/flags; registration/packaging; OS/arch; rollout/rollback; health/logging; operator recovery'],
|
|
99
|
+
['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
|
|
100
|
+
['AI/model', 'provider/model/prompt/tool schema; nondeterminism/bounds; safety/privacy; fallback; cost/token limit; eval oracle'],
|
|
101
|
+
['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle']
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
function boundaryTableHasRequiredRows(table) {
|
|
105
|
+
const [header, ...rows] = table;
|
|
106
|
+
const labels = rows.map((row) => row[0]);
|
|
107
|
+
return JSON.stringify(header || []) === JSON.stringify(['Boundary', 'Required contract when material'])
|
|
108
|
+
&& new Set(labels).size === labels.length
|
|
109
|
+
&& IMPLEMENTATION_READINESS_BOUNDARY_ROWS.every((expected) =>
|
|
110
|
+
rows.some((row) => JSON.stringify(row) === JSON.stringify(expected)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const IMPLEMENTATION_READINESS_CLAUSES = {
|
|
114
|
+
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.',
|
|
115
|
+
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.',
|
|
116
|
+
exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
|
|
117
|
+
proofPlanLines: [
|
|
118
|
+
'- Command: `<exact runnable command>`',
|
|
119
|
+
'- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
|
|
120
|
+
'- Reachability: <known command/caller/environment per required level; `UNKNOWN` only when the path cannot yet be established>',
|
|
121
|
+
'- Oracle: <externally observable success or failure>',
|
|
122
|
+
'- Counterexample: <material alternative behavior that must make this proof fail>',
|
|
123
|
+
'- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>'
|
|
124
|
+
],
|
|
125
|
+
proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
|
|
126
|
+
namedProbeOwnership: 'Aggregate suites name the\nowning concrete probe.',
|
|
127
|
+
proofLevelSeparation: 'Levels stay separate and never promote one another.',
|
|
128
|
+
disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
|
|
129
|
+
proofLevelMapping: 'For every required level in each referenced CP row, map its named probe and\nreachability here; one command may own several explicitly named level probes.',
|
|
130
|
+
disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
|
|
131
|
+
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.',
|
|
132
|
+
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.',
|
|
133
|
+
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:',
|
|
134
|
+
distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
|
|
135
|
+
closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
|
|
136
|
+
unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
|
|
137
|
+
scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.'
|
|
138
|
+
};
|
|
139
|
+
const ADAPTIVE_COVERAGE_PROFILE_HEADER = [
|
|
140
|
+
'ID', 'Outcome', 'Change kinds', 'Material surfaces', 'Ambiguity/action',
|
|
141
|
+
'Risk/evidence', 'Required proof'
|
|
142
|
+
];
|
|
143
|
+
const ADAPTIVE_COVERAGE_PROFILE_ROW = [
|
|
144
|
+
'CP-01', '<externally observable outcome>', '<all kinds>', '<all material surfaces>',
|
|
145
|
+
'<state + action>', '<level + evidence>', '<source/installed/live set>'
|
|
146
|
+
];
|
|
147
|
+
const ADAPTIVE_COVERAGE_AMBIGUITY_ROWS = [
|
|
148
|
+
['State', 'Required action'],
|
|
149
|
+
['`none`', 'proceed'],
|
|
150
|
+
['`examples-needed`', 'add two or three examples only for an already decided rule; promote to `decision-needed` if an example changes observable behavior'],
|
|
151
|
+
['`decision-needed`', 'ask the user at C1/C2 and keep affected tasks blocked'],
|
|
152
|
+
['`design-needed`', 'after user-owned decisions settle, route material competing technical designs through Brainstorm']
|
|
153
|
+
];
|
|
154
|
+
const ADAPTIVE_REVIEWER_ROWS = [
|
|
155
|
+
['Groups', 'Reviewers', 'Roles', 'Claim budget'],
|
|
156
|
+
['1-2', '2', 'Fact Checker plus all matching material lenses', 'about 5 per group'],
|
|
157
|
+
['3-5', '3', 'Fact Checker plus all matching material lenses', 'about 10 per group'],
|
|
158
|
+
['6+', '4', 'Fact Checker plus all matching material lenses', 'at least 15 total']
|
|
159
|
+
];
|
|
160
|
+
const ADAPTIVE_COVERAGE_CLAUSES = {
|
|
161
|
+
riskFirst: 'Classify material risk before choosing a workflow; user wording never lowers an observed floor.',
|
|
162
|
+
criticalFloor: '`critical`: auth/secrets/privacy; destructive/irreversible work or possible data loss/corruption; money/privilege/safety; production-state mutation.',
|
|
163
|
+
elevatedFloor: '`elevated`: cross-component contracts, compatibility, concurrency, external integration, or installed/runtime behavior.',
|
|
164
|
+
frontmatterGate: 'skip only when a change is clear, isolated, reversible, routine, and likely limited to one or two files.',
|
|
165
|
+
directGate: 'Work directly only when the cause and change are clear, isolated, reversible,\n`routine`, and likely limited to one or two files.',
|
|
166
|
+
splitRoute: 'Split three or more independent\nsubsystems; otherwise use one Specs packet for any material work that does not qualify for direct work or Brainstorm-only exploration.',
|
|
167
|
+
independentSubsystem: 'A subsystem is independent only when its outcome, boundary, and verification/deployment path can move through the lifecycle separately.',
|
|
168
|
+
profileAuthority: 'For a Specs route, `plan.md` owns one `## Coverage profile` row per externally observable outcome; direct and Brainstorm-only routes do not persist it.',
|
|
169
|
+
openKinds: 'Change kinds are multi-valued (`add`, `modify`, `fix`, `refactor`, `remove`, `migrate`, `integrate`), and unfamiliar kinds or surfaces use `other:<verbatim>` rather than disappearing.',
|
|
170
|
+
scopedUnion: 'Each task references its CP IDs; authoring, review, edge, and proof obligations union only inside affected rows/tasks.',
|
|
171
|
+
profileRederivation: 'Rederive affected CP rows after any accepted scope, outcome, criteria, ownership, dependency, risk, or proof delta before task status.',
|
|
172
|
+
plannedProof: '`Required proof` is a planned level set, not execution\nevidence: known but unrun proof may be `pending`; `UNKNOWN` reachability blocks\n`pending`; missing, failed, or unavailable required evidence blocks `done`/C3.',
|
|
173
|
+
proofSeparation: 'Levels stay separate and never promote one another.',
|
|
174
|
+
liveLimit: 'Source/static checks prove the written contract, not live-model adherence.',
|
|
175
|
+
specMakerAuthority: 'they are the canonical risk and coverage authority. Do not duplicate\ntheir taxonomy here.',
|
|
176
|
+
specMakerAmbiguity: 'Apply the canonical ambiguity action; examples never decide observable behavior.',
|
|
177
|
+
specMakerRoute: 'Apply their risk-first route before C1 and stop when the\nrequest qualifies for direct work; hand off when it requires Brainstorm-only exploration.',
|
|
178
|
+
reviewRisk: 'Keep Fact Checker as the baseline. Assign every remaining material CP risk to a\nnamed reviewer lens; a critical row includes both relevant security-adversary and\nfailure-mode coverage, and nonmaterial lenses are not added.',
|
|
179
|
+
reviewCapacity: 'Reviewer count is fixed by the table, not lens count. Give each reviewer a distinct primary lens; when material lenses exceed reviewers, combine related named lenses on one reviewer and keep every material lens assigned.'
|
|
180
|
+
};
|
|
32
181
|
const V3_SPECS_BUNDLE = [
|
|
33
182
|
'SKILL.md',
|
|
34
183
|
'references/review.md',
|
|
@@ -79,6 +228,21 @@ function install(root, extraArgs = []) {
|
|
|
79
228
|
);
|
|
80
229
|
}
|
|
81
230
|
|
|
231
|
+
function installPlatforms(root, platforms, extraArgs = []) {
|
|
232
|
+
return spawnSync(
|
|
233
|
+
process.execPath,
|
|
234
|
+
[INSTALLER, '--platform', platforms.join(','), '--yes', ...extraArgs],
|
|
235
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function installedCatalog(root, runtime) {
|
|
240
|
+
const script = path.join(root, runtime === 'codex' ? '.codex' : '.claude', 'scripts', 'generate-skill-catalog.cjs');
|
|
241
|
+
const result = spawnSync(process.execPath, [script, '--json'], { cwd: root, encoding: 'utf8' });
|
|
242
|
+
assert.equal(result.status, 0, result.stderr);
|
|
243
|
+
return JSON.parse(result.stdout);
|
|
244
|
+
}
|
|
245
|
+
|
|
82
246
|
function allFiles(root, predicate) {
|
|
83
247
|
const found = [];
|
|
84
248
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
@@ -101,6 +265,13 @@ function parseGeneratedTomlString(content, key) {
|
|
|
101
265
|
return JSON.parse(match[1]);
|
|
102
266
|
}
|
|
103
267
|
|
|
268
|
+
function replaceGeneratedTomlString(content, key, value) {
|
|
269
|
+
const pattern = new RegExp(`^${key} = .+$`, 'gm');
|
|
270
|
+
const matches = [...content.matchAll(pattern)];
|
|
271
|
+
assert.equal(matches.length, 1, `expected exactly one TOML key: ${key}`);
|
|
272
|
+
return content.replace(pattern, `${key} = ${JSON.stringify(value)}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
104
275
|
function renderCanonicalVerificationExample(template) {
|
|
105
276
|
const values = new Map([
|
|
106
277
|
['SUBJECT_REQ', '1'],
|
|
@@ -159,8 +330,225 @@ function assertInstalledVerificationModel(grounderPath, designTemplate) {
|
|
|
159
330
|
}
|
|
160
331
|
}
|
|
161
332
|
|
|
162
|
-
function
|
|
333
|
+
function markdownSection(content, heading) {
|
|
334
|
+
const lines = String(content).split('\n');
|
|
335
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
336
|
+
if (start < 0) return '';
|
|
337
|
+
const end = lines.findIndex((line, index) => index > start && /^##\s+/.test(line));
|
|
338
|
+
return lines.slice(start + 1, end < 0 ? undefined : end).join('\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function markdownTableUnderHeading(content, heading) {
|
|
342
|
+
const lines = markdownSection(content, heading).split('\n');
|
|
343
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
|
|
344
|
+
if (tableStart < 0) return [];
|
|
345
|
+
const rows = [];
|
|
346
|
+
for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
|
|
347
|
+
const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
|
|
348
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
349
|
+
rows.push(cells);
|
|
350
|
+
}
|
|
351
|
+
return rows;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function normalizeMarkdownWhitespace(content) {
|
|
355
|
+
return String(content).replace(/\s+/g, ' ').trim();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function markdownBetweenHeadings(content, startHeading, endHeading) {
|
|
359
|
+
const value = String(content);
|
|
360
|
+
const startMarker = `## ${startHeading}`;
|
|
361
|
+
const endMarker = `## ${endHeading}`;
|
|
362
|
+
const startIndex = value.indexOf(startMarker);
|
|
363
|
+
const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
|
|
364
|
+
if (startIndex < 0 || endIndex < 0) return '';
|
|
365
|
+
return value.slice(startIndex + startMarker.length, endIndex);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function implementationReadinessIssues(input) {
|
|
369
|
+
const keys = input && typeof input === 'object' && !Array.isArray(input)
|
|
370
|
+
? Object.keys(input).sort()
|
|
371
|
+
: [];
|
|
372
|
+
if (keys.join(',') !== 'review,templates'
|
|
373
|
+
|| typeof input.templates !== 'string' || typeof input.review !== 'string') {
|
|
374
|
+
throw new TypeError('implementation-readiness checker expects templates and review UTF-8 strings');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const issues = new Set();
|
|
378
|
+
const { templates, review } = input;
|
|
379
|
+
const authoring = markdownSection(templates, 'No-invention and conditional boundary contracts');
|
|
380
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
|
|
381
|
+
issues.add('no-invention');
|
|
382
|
+
}
|
|
383
|
+
const contradictoryNoInvention = /\b(?:exception|however)\b.{0,160}\bimplementation handoff\b.{0,80}\b(?:may|can)\s+(?:proceed|continue)\b.{0,160}\bunresolved\b/i;
|
|
384
|
+
if (contradictoryNoInvention.test(normalizeMarkdownWhitespace(authoring))) {
|
|
385
|
+
issues.add('no-invention');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
389
|
+
templates,
|
|
390
|
+
'No-invention and conditional boundary contracts'
|
|
391
|
+
);
|
|
392
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
|
|
393
|
+
|| !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
|
|
394
|
+
|| IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 9
|
|
395
|
+
|| !boundaryTableHasRequiredRows(boundaryTable)) {
|
|
396
|
+
issues.add('boundary-contract');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const proof = markdownSection(templates, 'Verification Plan');
|
|
400
|
+
const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
|
|
401
|
+
const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
|
|
402
|
+
templates,
|
|
403
|
+
'Verification Plan',
|
|
404
|
+
'Canonical inline Receipt'
|
|
405
|
+
));
|
|
406
|
+
const normalizedProofClauses = [
|
|
407
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
|
|
408
|
+
IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
409
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
410
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
411
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping
|
|
412
|
+
].map(normalizeMarkdownWhitespace);
|
|
413
|
+
const reviewNegativeControls = normalizeMarkdownWhitespace(
|
|
414
|
+
markdownSection(review, 'B2 — fresh-context red team')
|
|
415
|
+
);
|
|
416
|
+
if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
|
|
417
|
+
|| normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
|
|
418
|
+
|| !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
|
|
419
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
|
|
420
|
+
))) {
|
|
421
|
+
issues.add('proof-chain');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const failureGuidance = markdownSection(templates, 'Twelve edge-case dimensions');
|
|
425
|
+
if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
|
|
426
|
+
issues.add('failure-semantics');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const evidenceRules = markdownSection(review, 'B1 — evidence rule');
|
|
430
|
+
if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
|
|
431
|
+
issues.add('privacy-identifiers');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const closure = markdownSection(review, 'Accepted-repair closure');
|
|
435
|
+
const normalizedClosure = normalizeMarkdownWhitespace(closure);
|
|
436
|
+
const closureHeader = markdownTableUnderHeading(review, 'Accepted-repair closure')[0] || [];
|
|
437
|
+
if (JSON.stringify(closureHeader) !== JSON.stringify([
|
|
438
|
+
'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure'
|
|
439
|
+
])
|
|
440
|
+
|| !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
|
|
441
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
|
|
442
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
|
|
443
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
|
|
444
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
|
|
445
|
+
issues.add('repair-closure');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return [...issues].sort();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function adaptiveCoverageIssues(input) {
|
|
452
|
+
const expectedKeys = ['review', 'skill', 'specMaker', 'templates'];
|
|
453
|
+
const keys = input && typeof input === 'object' && !Array.isArray(input)
|
|
454
|
+
? Object.keys(input).sort()
|
|
455
|
+
: [];
|
|
456
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)
|
|
457
|
+
|| expectedKeys.some((key) => typeof input[key] !== 'string')) {
|
|
458
|
+
throw new TypeError('adaptive-coverage checker expects skill, specMaker, templates, and review UTF-8 strings');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const issues = new Set();
|
|
462
|
+
const skill = normalizeMarkdownWhitespace(input.skill);
|
|
463
|
+
const templates = normalizeMarkdownWhitespace(input.templates);
|
|
464
|
+
const review = normalizeMarkdownWhitespace(input.review);
|
|
465
|
+
const specMaker = normalizeMarkdownWhitespace(input.specMaker);
|
|
466
|
+
const has = (source, clause) => source.includes(normalizeMarkdownWhitespace(clause));
|
|
467
|
+
|
|
468
|
+
const riskClauses = [
|
|
469
|
+
ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
|
|
470
|
+
ADAPTIVE_COVERAGE_CLAUSES.criticalFloor,
|
|
471
|
+
ADAPTIVE_COVERAGE_CLAUSES.elevatedFloor,
|
|
472
|
+
ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
|
|
473
|
+
ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
474
|
+
ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
|
|
475
|
+
ADAPTIVE_COVERAGE_CLAUSES.independentSubsystem
|
|
476
|
+
];
|
|
477
|
+
const riskDowngrade = /\buser\b.{0,80}\b(?:may|can)\b.{0,80}\blower\b.{0,40}\brisk\b/i;
|
|
478
|
+
const riskyDirectOverride = /\b(?:exception|even if|regardless)\b.{0,160}\b(?:auth|secret|privacy|destructive|irreversible|data loss|corruption|production[- ]state|critical)\b.{0,160}\b(?:direct|work directly|go direct)\b/i;
|
|
479
|
+
if (riskClauses.some((clause) => !has(skill, clause))
|
|
480
|
+
|| riskDowngrade.test(skill) || riskyDirectOverride.test(skill)) {
|
|
481
|
+
issues.add('risk-first-routing');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const profileTable = markdownTableUnderHeading(input.templates, 'Coverage profile');
|
|
485
|
+
const profileHeadingCount = (input.templates.match(/^## Coverage profile\s*$/gm) || []).length;
|
|
486
|
+
const taskReferencesCoverage = /## Coverage\s*\n- <exact `CP-NN` IDs owned by this task>/.test(input.templates);
|
|
487
|
+
if (JSON.stringify(profileTable[0] || []) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_HEADER)
|
|
488
|
+
|| profileHeadingCount !== 1 || profileTable.length !== 2
|
|
489
|
+
|| JSON.stringify(profileTable[1]) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_ROW)
|
|
490
|
+
|| !taskReferencesCoverage
|
|
491
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileAuthority)
|
|
492
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.openKinds)) {
|
|
493
|
+
issues.add('coverage-profile-shape');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const ambiguityTable = markdownTableUnderHeading(input.templates, 'Example Mapping rule');
|
|
497
|
+
if (JSON.stringify(ambiguityTable) !== JSON.stringify(ADAPTIVE_COVERAGE_AMBIGUITY_ROWS)
|
|
498
|
+
|| !templates.includes('retention of 30 versus 90 days is `decision-needed`')) {
|
|
499
|
+
issues.add('ambiguity-actions');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const globalCeremony = /\b(?:critical|security|failure|proof|review|edge|obligations?|lenses?)\b[^.!?\n]{0,120}\b(?:union|apply|spread|require)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b|\b(?:union|apply|spread)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b/i;
|
|
503
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.scopedUnion)
|
|
504
|
+
|| globalCeremony.test(templates)
|
|
505
|
+
|| !review.includes('nonmaterial lenses are not added')) {
|
|
506
|
+
issues.add('scoped-coverage');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const rederiveSources = [templates, skill, specMaker, review];
|
|
510
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileRederivation)
|
|
511
|
+
|| rederiveSources.some((source) => !/rederive affected cp rows?/.test(source.toLowerCase()))) {
|
|
512
|
+
issues.add('profile-lifecycle');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const statusMatrix = markdownTableUnderHeading(input.templates, 'Status matrix');
|
|
516
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.plannedProof)
|
|
517
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.proofSeparation)
|
|
518
|
+
|| !has(templates, IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping)
|
|
519
|
+
|| !has(skill, ADAPTIVE_COVERAGE_CLAUSES.liveLimit)
|
|
520
|
+
|| !statusMatrix.some((row) => row[0] === 'accepted finding open or `UNKNOWN` reachability' && row[1] === '`blocked`')) {
|
|
521
|
+
issues.add('proof-lifecycle');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const reviewerRowsPresent = ADAPTIVE_REVIEWER_ROWS.every((row) =>
|
|
525
|
+
input.review.includes(`| ${row.join(' | ')} |`));
|
|
526
|
+
if (!has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewRisk)
|
|
527
|
+
|| !has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity)
|
|
528
|
+
|| !reviewerRowsPresent) {
|
|
529
|
+
issues.add('reviewer-routing');
|
|
530
|
+
}
|
|
531
|
+
if (!specMaker.includes('skills/specs/SKILL.md')
|
|
532
|
+
|| !specMaker.includes('skills/specs/references/templates.md')
|
|
533
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority)
|
|
534
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity)
|
|
535
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute)) {
|
|
536
|
+
issues.add('spec-maker-authority');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
540
|
+
input.templates,
|
|
541
|
+
'No-invention and conditional boundary contracts'
|
|
542
|
+
);
|
|
543
|
+
if (!boundaryTableHasRequiredRows(boundaryTable)) {
|
|
544
|
+
issues.add('adaptive-boundary-lenses');
|
|
545
|
+
}
|
|
546
|
+
return [...issues].sort();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function authoringProjectionIssues(files) {
|
|
163
550
|
const issues = [];
|
|
551
|
+
issues.push(...implementationReadinessIssues({ templates: files.templates, review: files.review }));
|
|
164
552
|
const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
|
|
165
553
|
if (!files.skill.includes('Task files are flat beside `plan.md`')
|
|
166
554
|
|| !files.skill.includes('Do not create a nested task directory.')) {
|
|
@@ -187,7 +575,7 @@ function installedAuthoringProjectionIssues(files) {
|
|
|
187
575
|
]) {
|
|
188
576
|
if (!files.templates.includes(field)) issues.push(`receipt-${field.toLowerCase()}`);
|
|
189
577
|
}
|
|
190
|
-
if (!/^name:
|
|
578
|
+
if (!/^name: cf-specs$/m.test(files.skill)) issues.push('codex-skill-name');
|
|
191
579
|
const bundle = [files.skill, files.review, files.templates, files.legacyTemplates].join('\n');
|
|
192
580
|
const runtimeProjection = `${bundle}\n${files.codex.replace(foreignRuntimeBoundary, '')}`;
|
|
193
581
|
if (!files.codex.includes(foreignRuntimeBoundary)
|
|
@@ -196,11 +584,11 @@ function installedAuthoringProjectionIssues(files) {
|
|
|
196
584
|
}
|
|
197
585
|
for (const claudeOnly of [
|
|
198
586
|
'AskUserQuestion', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList',
|
|
199
|
-
'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/
|
|
587
|
+
'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/cf:', 'cf:'
|
|
200
588
|
]) {
|
|
201
589
|
if (runtimeProjection.includes(claudeOnly)) issues.push(`claude-vocabulary-${claudeOnly}`);
|
|
202
590
|
}
|
|
203
|
-
for (const command of ['$
|
|
591
|
+
for (const command of ['$cf-specs', '$cf-develop', '$cf-sync']) {
|
|
204
592
|
if (!files.codex.includes(command)) issues.push(`codex-command-${command}`);
|
|
205
593
|
}
|
|
206
594
|
for (const state of ['pending', 'in_progress', 'paused', 'blocked', 'done']) {
|
|
@@ -211,11 +599,346 @@ function installedAuthoringProjectionIssues(files) {
|
|
|
211
599
|
|| !files.codex.includes('C1') || !files.codex.includes('C2') || !files.codex.includes('C3')) {
|
|
212
600
|
issues.push('codex-process-v3');
|
|
213
601
|
}
|
|
214
|
-
return issues;
|
|
602
|
+
return [...new Set(issues)].sort();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function canonicalInstalledSpecsRoot(projectRoot, installedRoot) {
|
|
606
|
+
assert.equal(path.isAbsolute(projectRoot), true, 'project root must be absolute');
|
|
607
|
+
assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
|
|
608
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
609
|
+
const canonicalInstalled = fs.realpathSync(installedRoot);
|
|
610
|
+
const relative = path.relative(canonicalProject, canonicalInstalled);
|
|
611
|
+
assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
|
|
612
|
+
assert.equal(
|
|
613
|
+
canonicalInstalled,
|
|
614
|
+
fs.realpathSync(path.join(canonicalProject, '.agents/skills/specs')),
|
|
615
|
+
'checker must read the native installed Codex Specs root'
|
|
616
|
+
);
|
|
617
|
+
return canonicalInstalled;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function readInstalledAuthoringProjection(projectRoot, installedRoot) {
|
|
621
|
+
const canonicalRoot = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
622
|
+
const read = (relative) => fs.readFileSync(path.join(canonicalRoot, relative), 'utf8');
|
|
623
|
+
const specMakerToml = fs.readFileSync(
|
|
624
|
+
path.join(projectRoot, '.codex/agents/spec_maker.toml'),
|
|
625
|
+
'utf8'
|
|
626
|
+
);
|
|
627
|
+
return {
|
|
628
|
+
skill: read('SKILL.md'),
|
|
629
|
+
review: read('references/review.md'),
|
|
630
|
+
templates: read('references/templates.md'),
|
|
631
|
+
specMaker: parseGeneratedTomlString(specMakerToml, 'developer_instructions'),
|
|
632
|
+
legacyTemplates: V3_SPECS_BUNDLE.filter((relative) => relative.startsWith('templates/')).map(read).join('\n'),
|
|
633
|
+
codex: [
|
|
634
|
+
fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'),
|
|
635
|
+
fs.readFileSync(path.join(projectRoot, '.codex/rules/state-sync.md'), 'utf8')
|
|
636
|
+
].join('\n')
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function installedAuthoringProjectionIssues(projectRoot, installedRoot) {
|
|
641
|
+
return authoringProjectionIssues(readInstalledAuthoringProjection(projectRoot, installedRoot));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function installedAdaptiveCoverageIssues(projectRoot, installedRoot) {
|
|
645
|
+
const { skill, specMaker, templates, review } = readInstalledAuthoringProjection(
|
|
646
|
+
projectRoot,
|
|
647
|
+
installedRoot
|
|
648
|
+
);
|
|
649
|
+
return adaptiveCoverageIssues({ skill, specMaker, templates, review });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function assertDisposableInstalledMutationTarget({
|
|
653
|
+
projectRoot,
|
|
654
|
+
installedScope,
|
|
655
|
+
targetPath,
|
|
656
|
+
expectedTargetPath,
|
|
657
|
+
canonicalSourcePaths,
|
|
658
|
+
scopeLabel
|
|
659
|
+
}) {
|
|
660
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
661
|
+
const canonicalScope = fs.realpathSync(installedScope);
|
|
662
|
+
const targetLstat = fs.lstatSync(targetPath);
|
|
663
|
+
assert.equal(targetLstat.isSymbolicLink(), false, 'installed mutation target must not be a symlink');
|
|
664
|
+
assert.equal(targetLstat.isFile(), true, 'installed mutation target must be a regular file');
|
|
665
|
+
const canonicalTarget = fs.realpathSync(targetPath);
|
|
666
|
+
assert.equal(
|
|
667
|
+
canonicalTarget,
|
|
668
|
+
fs.realpathSync(expectedTargetPath),
|
|
669
|
+
`installed mutation target must be the exact ${scopeLabel} projection`
|
|
670
|
+
);
|
|
671
|
+
const scopeRelative = path.relative(canonicalScope, canonicalTarget);
|
|
672
|
+
const projectRelative = path.relative(canonicalProject, canonicalTarget);
|
|
673
|
+
assert.ok(
|
|
674
|
+
scopeRelative && scopeRelative !== '..'
|
|
675
|
+
&& !scopeRelative.startsWith(`..${path.sep}`)
|
|
676
|
+
&& !path.isAbsolute(scopeRelative),
|
|
677
|
+
`installed mutation target must stay inside ${scopeLabel}`
|
|
678
|
+
);
|
|
679
|
+
assert.ok(
|
|
680
|
+
projectRelative && projectRelative !== '..'
|
|
681
|
+
&& !projectRelative.startsWith(`..${path.sep}`)
|
|
682
|
+
&& !path.isAbsolute(projectRelative),
|
|
683
|
+
'installed mutation target must stay inside the disposable project'
|
|
684
|
+
);
|
|
685
|
+
assert.ok(
|
|
686
|
+
Array.isArray(canonicalSourcePaths) && canonicalSourcePaths.length > 0,
|
|
687
|
+
'installed mutation guard requires the full canonical source set'
|
|
688
|
+
);
|
|
689
|
+
const targetStat = fs.statSync(canonicalTarget);
|
|
690
|
+
for (const sourcePath of canonicalSourcePaths) {
|
|
691
|
+
const canonicalSource = fs.realpathSync(sourcePath);
|
|
692
|
+
assert.notEqual(
|
|
693
|
+
canonicalTarget,
|
|
694
|
+
canonicalSource,
|
|
695
|
+
'installed mutation target must not resolve to any canonical source'
|
|
696
|
+
);
|
|
697
|
+
const sourceStat = fs.statSync(canonicalSource);
|
|
698
|
+
assert.notDeepEqual(
|
|
699
|
+
[targetStat.dev, targetStat.ino],
|
|
700
|
+
[sourceStat.dev, sourceStat.ino],
|
|
701
|
+
`installed mutation target must not share an inode with canonical source set: ${sourcePath}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
assert.equal(targetStat.nlink, 1, 'installed mutation target must have exactly one hard link');
|
|
705
|
+
return canonicalTarget;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function assertInstalledMutationTarget(projectRoot, installedRoot, relative) {
|
|
709
|
+
const canonicalInstalled = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
710
|
+
const targetPath = path.join(canonicalInstalled, relative);
|
|
711
|
+
return assertDisposableInstalledMutationTarget({
|
|
712
|
+
projectRoot,
|
|
713
|
+
installedScope: canonicalInstalled,
|
|
714
|
+
targetPath,
|
|
715
|
+
expectedTargetPath: path.join(projectRoot, '.agents/skills/specs', relative),
|
|
716
|
+
canonicalSourcePaths: canonicalProjectionSourcePaths(),
|
|
717
|
+
scopeLabel: '.agents/skills/specs'
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function assertInstalledSpecMakerMutationTarget(projectRoot) {
|
|
722
|
+
const installedScope = path.join(projectRoot, '.codex/agents');
|
|
723
|
+
const targetPath = path.join(installedScope, 'spec_maker.toml');
|
|
724
|
+
return assertDisposableInstalledMutationTarget({
|
|
725
|
+
projectRoot,
|
|
726
|
+
installedScope,
|
|
727
|
+
targetPath,
|
|
728
|
+
expectedTargetPath: targetPath,
|
|
729
|
+
canonicalSourcePaths: canonicalProjectionSourcePaths(),
|
|
730
|
+
scopeLabel: '.codex/agents'
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function canonicalProjectionSourcePaths() {
|
|
735
|
+
return [
|
|
736
|
+
...V3_SPECS_BUNDLE.map((relative) => path.join(SPECS_SOURCE_ROOT, relative)),
|
|
737
|
+
SPEC_MAKER_SOURCE_PATH
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function canonicalProjectionSourceBytes() {
|
|
742
|
+
const sourcePaths = canonicalProjectionSourcePaths();
|
|
743
|
+
return new Map(sourcePaths.map((sourcePath) => [
|
|
744
|
+
sourcePath,
|
|
745
|
+
fs.readFileSync(sourcePath)
|
|
746
|
+
]));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function sha256(content) {
|
|
750
|
+
return createHash('sha256').update(content).digest('hex');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function canonicalBrainstormSourceBytes() {
|
|
754
|
+
return new Map(BRAINSTORM_SOURCE_PATHS.map((sourcePath) => [
|
|
755
|
+
sourcePath,
|
|
756
|
+
fs.readFileSync(sourcePath)
|
|
757
|
+
]));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes) {
|
|
761
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
762
|
+
const actual = fs.readFileSync(sourcePath);
|
|
763
|
+
assert.deepEqual(
|
|
764
|
+
actual,
|
|
765
|
+
expected,
|
|
766
|
+
`canonical Brainstorm source changed: ${path.relative(PACKAGE_ROOT, sourcePath)}`
|
|
767
|
+
);
|
|
768
|
+
assert.equal(sha256(actual), sha256(expected));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function readInstalledBrainstormProjection(projectRoot) {
|
|
773
|
+
const skillRoot = path.join(projectRoot, '.agents/skills/brainstorm');
|
|
774
|
+
const agentPath = path.join(projectRoot, '.codex/agents/brainstormer.toml');
|
|
775
|
+
return {
|
|
776
|
+
skill: fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'),
|
|
777
|
+
framework: fs.readFileSync(path.join(skillRoot, 'references/question-framework.md'), 'utf8'),
|
|
778
|
+
agent: parseGeneratedTomlString(fs.readFileSync(agentPath, 'utf8'), 'developer_instructions')
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function brainstormProjectionClausePrefix(content, index) {
|
|
783
|
+
const starts = ['.', '!', '?', ';', '\n'].map((marker) => content.lastIndexOf(marker, index - 1));
|
|
784
|
+
return content.slice(Math.max(...starts) + 1, index).trim();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function brainstormProjectionIsLocallyNegated(content, index) {
|
|
788
|
+
const clausePrefix = brainstormProjectionClausePrefix(content, index);
|
|
789
|
+
const localBoundary = Math.max(
|
|
790
|
+
clausePrefix.lastIndexOf(','),
|
|
791
|
+
clausePrefix.lastIndexOf(':'),
|
|
792
|
+
clausePrefix.lastIndexOf('—')
|
|
793
|
+
);
|
|
794
|
+
const localPrefix = clausePrefix.slice(localBoundary + 1).trim().toLowerCase();
|
|
795
|
+
const directNegation = /\b(?:never|do not|does not|must not|may not|should not|will not|cannot|can't|is forbidden to|are forbidden to|is not (?:permitted|allowed) to|are not (?:permitted|allowed) to|not (?:permitted|allowed) to)(?:\s+(?:ever|directly|automatically|implicitly|explicitly|immediately|intentionally|silently))*\s*$/;
|
|
796
|
+
if (directNegation.test(localPrefix)) return true;
|
|
797
|
+
const fullPrefix = clausePrefix.toLowerCase();
|
|
798
|
+
if (/\b(?:but|yet|however|except|instead)\b[^.!?;\n]*$/.test(fullPrefix)) return false;
|
|
799
|
+
return /\b(?:never|do not|does not|must not|may not|should not|will not|cannot|can't|is forbidden to|are forbidden to|is not (?:permitted|allowed) to|are not (?:permitted|allowed) to|not (?:permitted|allowed) to)\b(?:(?!\b(?:but|yet|however|except|instead)\b).){0,160}\b(?:and|or|nor)(?:\s+then)?(?:\s+(?:ever|directly|automatically|implicitly|explicitly|immediately|intentionally|silently))*\s*$/.test(fullPrefix);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function brainstormProjectionHasUnauthorizedAuthority(content) {
|
|
803
|
+
const actionPattern = /\b(?:ask|question|contact|invoke|start|dispatch|launch|route|forward|run|execute|hand(?:\s+|-)?off|write|edit|update|mutate|delegate|claim)\b/gi;
|
|
804
|
+
const actions = [...content.matchAll(actionPattern)];
|
|
805
|
+
for (let index = 0; index < actions.length; index += 1) {
|
|
806
|
+
const match = actions[index];
|
|
807
|
+
const normalizedAction = match[0].toLowerCase().replace(/[-\s]+/g, ' ');
|
|
808
|
+
const action = normalizedAction === 'handoff' ? 'hand off' : normalizedAction;
|
|
809
|
+
const start = match.index + match[0].length;
|
|
810
|
+
const nextAction = actions[index + 1]?.index ?? content.length;
|
|
811
|
+
const punctuation = content.slice(start).search(/[.!?;\n]/);
|
|
812
|
+
const punctuationEnd = punctuation < 0 ? content.length : start + punctuation;
|
|
813
|
+
const tail = content.slice(start, Math.min(start + 100, nextAction, punctuationEnd));
|
|
814
|
+
const hasTarget = ['ask', 'question', 'contact'].includes(action)
|
|
815
|
+
? /\b(?:the )?user\b/i.test(tail)
|
|
816
|
+
: ['invoke', 'start', 'dispatch', 'launch', 'route', 'forward', 'run', 'execute', 'hand off'].includes(action)
|
|
817
|
+
? /\b(?:Specs|Fix|Develop)\b/i.test(tail)
|
|
818
|
+
: ['write', 'edit', 'update', 'mutate'].includes(action)
|
|
819
|
+
? /\b(?:files?|task state|shared state)\b/i.test(tail)
|
|
820
|
+
: action === 'delegate'
|
|
821
|
+
? /\b(?:work|tasks?)\b/i.test(tail)
|
|
822
|
+
: /\bapproval\b/i.test(tail);
|
|
823
|
+
if (hasTarget && !brainstormProjectionIsLocallyNegated(content, match.index)) return true;
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function brainstormProjectionIssues(files) {
|
|
829
|
+
const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
830
|
+
const skill = compact(files.skill);
|
|
831
|
+
const framework = compact(files.framework);
|
|
832
|
+
const agent = compact(files.agent);
|
|
833
|
+
const issues = new Set();
|
|
834
|
+
const specialistBoundary = 'Do not ask the user directly, write files, mutate shared task state, delegate work, invoke Specs/Fix/Develop, or claim approval.';
|
|
835
|
+
const specialistHandoff = 'Non-bug exploration may end in chat; feature/docs delivery may only prepare a future explicit Specs invocation; bug handoff requires evidenced root cause and the user\'s explicit fix request.';
|
|
836
|
+
const agentAuthorityRemainder = agent
|
|
837
|
+
.replace(specialistBoundary, '')
|
|
838
|
+
.replace(specialistHandoff, '');
|
|
839
|
+
const adaptiveClauses = {
|
|
840
|
+
'adaptive-direct-precedence': 'Route Direct first, then apply controls only to requests that remain in Brainstorm.',
|
|
841
|
+
'adaptive-ordered-depth': 'With no Deep signal, use Standard. `--deep` raises Standard to Deep.',
|
|
842
|
+
'adaptive-leading-flags': 'Parse controls only from the leading consecutive token segment.',
|
|
843
|
+
'adaptive-lens-trigger-skip': 'failure isolation for partial or cascading failure across boundaries',
|
|
844
|
+
'adaptive-evidence-semantics': 'Missing evidence forces feasibility `unknown` and confidence `low`.',
|
|
845
|
+
'adaptive-numeric-estimates': 'A numeric estimate requires range, unit, basis, evidence, and assumptions; otherwise report `unknown`.',
|
|
846
|
+
'adaptive-pre-tool-redaction': 'Before an external visual tool or adviser handoff, minimize context and redact secrets, credentials, private keys, access tokens, and unnecessary PII.',
|
|
847
|
+
'adaptive-adviser-gate': '`--advice` invokes `brainstormer` only after the material-choice gate;',
|
|
848
|
+
'adaptive-decision-freshness': 'The first section records target identity, current source revision and worktree state or `[UNVERIFIED]`, an evidence-as-of value, and what change invalidates the brief.',
|
|
849
|
+
'adaptive-non-authority': 'Neither overlay writes, approves, persists, dispatches, or completes work.'
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
if (!skill.includes('leave Brainstorm before scout, questions, approval, or persistence.')
|
|
853
|
+
|| !skill.includes('Hydration is not a terminal route; continue to exactly one intent route below.')) {
|
|
854
|
+
issues.add('front-door-routing');
|
|
855
|
+
}
|
|
856
|
+
if (!skill.includes('Then use `cf-debug` until root cause is evidenced.')
|
|
857
|
+
|| !skill.includes('Hand off to `cf-fix` only when the user explicitly requested a fix')) {
|
|
858
|
+
issues.add('bug-routing');
|
|
859
|
+
}
|
|
860
|
+
if (!skill.includes('Do not request design approval, persist a report, or invoke another workflow without a new explicit request.')) {
|
|
861
|
+
issues.add('exploration-stop');
|
|
862
|
+
}
|
|
863
|
+
if (!skill.includes('For a material choice, compare 2–3 mechanically distinct viable approaches')
|
|
864
|
+
|| !skill.includes('Never create strawman options.')
|
|
865
|
+
|| !agent.includes('never invent strawmen to fill a quota.')) {
|
|
866
|
+
issues.add('option-cardinality');
|
|
867
|
+
}
|
|
868
|
+
if (!agent.includes('You advise `cf-brainstorm`; you do not replace its routing, question, approval, persistence, or handoff ownership.')
|
|
869
|
+
|| !agent.includes(specialistBoundary)
|
|
870
|
+
|| brainstormProjectionHasUnauthorizedAuthority(agentAuthorityRemainder)) {
|
|
871
|
+
issues.add('specialist-boundary');
|
|
872
|
+
}
|
|
873
|
+
if (!agent.includes('If the request is a symptom without an evidenced root cause, return it to `cf-debug`.')
|
|
874
|
+
|| !agent.includes('If the controller has not identified whether the work is feature delivery, an explicitly authorized fix, or non-bug exploration, request that routing context instead of guessing.')) {
|
|
875
|
+
issues.add('specialist-routing');
|
|
876
|
+
}
|
|
877
|
+
if (!agent.includes(specialistHandoff)) {
|
|
878
|
+
issues.add('specialist-handoff');
|
|
879
|
+
}
|
|
880
|
+
if (!skill.includes('Persist only approved decisions and semantics, only with user authority')
|
|
881
|
+
|| !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.')) {
|
|
883
|
+
issues.add('approval-persistence');
|
|
884
|
+
}
|
|
885
|
+
for (const [issue, clause] of Object.entries(adaptiveClauses)) {
|
|
886
|
+
if (!skill.includes(clause)) issues.add(issue);
|
|
887
|
+
}
|
|
888
|
+
const visible = `${skill}\n${framework}\n${agent}`;
|
|
889
|
+
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)) {
|
|
893
|
+
issues.add('projection-grammar');
|
|
894
|
+
}
|
|
895
|
+
return [...issues].sort();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function assertInstalledBrainstormMutationTarget(projectRoot, source) {
|
|
899
|
+
const skillRoot = path.join(projectRoot, '.agents/skills/brainstorm');
|
|
900
|
+
if (source === 'agent') {
|
|
901
|
+
const agentRoot = path.join(projectRoot, '.codex/agents');
|
|
902
|
+
const targetPath = path.join(agentRoot, 'brainstormer.toml');
|
|
903
|
+
return assertDisposableInstalledMutationTarget({
|
|
904
|
+
projectRoot,
|
|
905
|
+
installedScope: agentRoot,
|
|
906
|
+
targetPath,
|
|
907
|
+
expectedTargetPath: targetPath,
|
|
908
|
+
canonicalSourcePaths: BRAINSTORM_SOURCE_PATHS,
|
|
909
|
+
scopeLabel: '.codex/agents'
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
const relative = source === 'skill' ? 'SKILL.md' : 'references/question-framework.md';
|
|
913
|
+
const targetPath = path.join(skillRoot, relative);
|
|
914
|
+
return assertDisposableInstalledMutationTarget({
|
|
915
|
+
projectRoot,
|
|
916
|
+
installedScope: skillRoot,
|
|
917
|
+
targetPath,
|
|
918
|
+
expectedTargetPath: targetPath,
|
|
919
|
+
canonicalSourcePaths: BRAINSTORM_SOURCE_PATHS,
|
|
920
|
+
scopeLabel: '.agents/skills/brainstorm'
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function assertCanonicalSpecsSourceBytesUnchanged(sourceBytes) {
|
|
925
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
926
|
+
const actual = fs.readFileSync(sourcePath);
|
|
927
|
+
assert.deepEqual(
|
|
928
|
+
actual,
|
|
929
|
+
expected,
|
|
930
|
+
`canonical source bytes changed during installed mutation: ${path.relative(PACKAGE_ROOT, sourcePath)}`
|
|
931
|
+
);
|
|
932
|
+
assert.equal(
|
|
933
|
+
sha256(actual),
|
|
934
|
+
sha256(expected),
|
|
935
|
+
`canonical source SHA changed during installed mutation: ${path.relative(PACKAGE_ROOT, sourcePath)}`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
215
938
|
}
|
|
216
939
|
|
|
217
|
-
function assertInstalledAuthoringProjection(root) {
|
|
218
|
-
|
|
940
|
+
function assertInstalledAuthoringProjection(root, installedRoot) {
|
|
941
|
+
canonicalInstalledSpecsRoot(root, installedRoot);
|
|
219
942
|
const relativeFiles = (directory) => allFiles(directory, () => true)
|
|
220
943
|
.map((file) => path.relative(directory, file).split(path.sep).join('/'))
|
|
221
944
|
.sort();
|
|
@@ -228,25 +951,460 @@ function assertInstalledAuthoringProjection(root) {
|
|
|
228
951
|
const actual = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
|
|
229
952
|
assert.equal(actual, expected, `Codex Specs projection drifted: ${relative}`);
|
|
230
953
|
}
|
|
954
|
+
const sourceSpecMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
|
|
955
|
+
const installedSpecMakerPath = path.join(root, '.codex/agents/spec_maker.toml');
|
|
956
|
+
assert.equal(
|
|
957
|
+
fs.readFileSync(installedSpecMakerPath, 'utf8'),
|
|
958
|
+
convertCodexAgentContent(sourceSpecMaker, path.basename(SPEC_MAKER_SOURCE_PATH)),
|
|
959
|
+
'Codex spec_maker projection drifted'
|
|
960
|
+
);
|
|
231
961
|
for (const relative of OBSOLETE_SPECS_FILES) {
|
|
232
962
|
assert.equal(fs.existsSync(path.join(installedRoot, relative)), false, `obsolete Specs file installed: ${relative}`);
|
|
233
963
|
}
|
|
234
964
|
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
965
|
+
const files = readInstalledAuthoringProjection(root, installedRoot);
|
|
966
|
+
const sourceBytes = canonicalProjectionSourceBytes();
|
|
967
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
|
|
968
|
+
assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), []);
|
|
969
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
970
|
+
|
|
971
|
+
if (process.platform !== 'win32') {
|
|
972
|
+
for (const [installedPath, sourcePath, assertTarget] of [
|
|
973
|
+
[
|
|
974
|
+
path.join(installedRoot, 'references/templates.md'),
|
|
975
|
+
path.join(SPECS_SOURCE_ROOT, 'references/templates.md'),
|
|
976
|
+
() => assertInstalledMutationTarget(root, installedRoot, 'references/templates.md')
|
|
977
|
+
],
|
|
978
|
+
[
|
|
979
|
+
installedSpecMakerPath,
|
|
980
|
+
SPEC_MAKER_SOURCE_PATH,
|
|
981
|
+
() => assertInstalledSpecMakerMutationTarget(root)
|
|
982
|
+
]
|
|
983
|
+
]) {
|
|
984
|
+
const backupPath = `${installedPath}.cafekit-backup`;
|
|
985
|
+
fs.renameSync(installedPath, backupPath);
|
|
986
|
+
try {
|
|
987
|
+
fs.linkSync(sourcePath, installedPath);
|
|
988
|
+
assert.throws(assertTarget, /must not share an inode with canonical source/);
|
|
989
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
990
|
+
} finally {
|
|
991
|
+
try { fs.unlinkSync(installedPath); } catch { /* best-effort disposable cleanup */ }
|
|
992
|
+
fs.renameSync(backupPath, installedPath);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const crossSourceRoot = path.join(root, 'cross-source-native-control');
|
|
997
|
+
const crossSourceProject = path.join(crossSourceRoot, 'project');
|
|
998
|
+
const crossSourceScope = path.join(crossSourceProject, '.agents/skills/specs');
|
|
999
|
+
const crossSourceTarget = path.join(crossSourceScope, 'target-a.md');
|
|
1000
|
+
const crossSourcePaths = [
|
|
1001
|
+
path.join(crossSourceRoot, 'sources/source-a.md'),
|
|
1002
|
+
path.join(crossSourceRoot, 'sources/source-b.md')
|
|
1003
|
+
];
|
|
1004
|
+
fs.mkdirSync(crossSourceScope, { recursive: true });
|
|
1005
|
+
fs.mkdirSync(path.dirname(crossSourcePaths[0]), { recursive: true });
|
|
1006
|
+
fs.writeFileSync(crossSourceTarget, 'native installed target A\n');
|
|
1007
|
+
fs.writeFileSync(crossSourcePaths[0], 'native canonical source A\n');
|
|
1008
|
+
fs.writeFileSync(crossSourcePaths[1], 'native canonical source B\n');
|
|
1009
|
+
const targetBytes = fs.readFileSync(crossSourceTarget);
|
|
1010
|
+
const sourceState = new Map(crossSourcePaths.map((sourcePath) => {
|
|
1011
|
+
const bytes = fs.readFileSync(sourcePath);
|
|
1012
|
+
return [sourcePath, { bytes, sha: sha256(bytes) }];
|
|
1013
|
+
}));
|
|
1014
|
+
assert.notEqual(crossSourcePaths[0], crossSourcePaths[1], 'cross-source control requires A != B');
|
|
1015
|
+
fs.unlinkSync(crossSourceTarget);
|
|
1016
|
+
fs.linkSync(crossSourcePaths[1], crossSourceTarget);
|
|
1017
|
+
try {
|
|
1018
|
+
assert.throws(
|
|
1019
|
+
() => fs.writeFileSync(
|
|
1020
|
+
assertDisposableInstalledMutationTarget({
|
|
1021
|
+
projectRoot: crossSourceProject,
|
|
1022
|
+
installedScope: crossSourceScope,
|
|
1023
|
+
targetPath: crossSourceTarget,
|
|
1024
|
+
expectedTargetPath: crossSourceTarget,
|
|
1025
|
+
canonicalSourcePaths: crossSourcePaths,
|
|
1026
|
+
scopeLabel: '.agents/skills/specs'
|
|
1027
|
+
}),
|
|
1028
|
+
'forbidden native cross-source mutation\n'
|
|
1029
|
+
),
|
|
1030
|
+
/must not share an inode with canonical source set/
|
|
1031
|
+
);
|
|
1032
|
+
} finally {
|
|
1033
|
+
fs.unlinkSync(crossSourceTarget);
|
|
1034
|
+
fs.writeFileSync(crossSourceTarget, targetBytes);
|
|
1035
|
+
fs.writeFileSync(crossSourcePaths[1], sourceState.get(crossSourcePaths[1]).bytes);
|
|
1036
|
+
}
|
|
1037
|
+
assert.deepEqual(fs.readFileSync(crossSourceTarget), targetBytes, 'cross-source control must restore exact target bytes');
|
|
1038
|
+
for (const [sourcePath, expected] of sourceState) {
|
|
1039
|
+
const actual = fs.readFileSync(sourcePath);
|
|
1040
|
+
assert.deepEqual(actual, expected.bytes, 'cross-source rejection must preserve every source byte');
|
|
1041
|
+
assert.equal(sha256(actual), expected.sha, 'cross-source rejection must preserve every source SHA');
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const boundaryMutation = (name, boundary, replacements) => {
|
|
1046
|
+
const row = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === boundary);
|
|
1047
|
+
assert.ok(row, `${name} references unknown boundary ${boundary}`);
|
|
1048
|
+
let weakened = row[1];
|
|
1049
|
+
for (const [from, to] of replacements) {
|
|
1050
|
+
const next = weakened.replace(from, to);
|
|
1051
|
+
assert.notEqual(next, weakened, `${name} weakening anchor must exist in ${boundary}`);
|
|
1052
|
+
weakened = next;
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
name,
|
|
1056
|
+
issue: 'boundary-contract',
|
|
1057
|
+
relative: 'references/templates.md',
|
|
1058
|
+
from: `| ${row[0]} | ${row[1]} |`,
|
|
1059
|
+
to: `| ${row[0]} | ${weakened} |`
|
|
1060
|
+
};
|
|
248
1061
|
};
|
|
249
|
-
|
|
1062
|
+
|
|
1063
|
+
const readinessMutations = [
|
|
1064
|
+
{
|
|
1065
|
+
name: 'no-invention-blocking',
|
|
1066
|
+
issue: 'no-invention',
|
|
1067
|
+
relative: 'references/templates.md',
|
|
1068
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
1069
|
+
to: 'Before implementation handoff, note ambiguous choices without blocking handoff.'
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
name: 'no-invention-contradictory-override',
|
|
1073
|
+
issue: 'no-invention',
|
|
1074
|
+
relative: 'references/templates.md',
|
|
1075
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
1076
|
+
to: `${IMPLEMENTATION_READINESS_CLAUSES.noInvention}\n\nException: implementation handoff may proceed with an unresolved material choice.`
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
name: 'material-boundary-definition',
|
|
1080
|
+
issue: 'boundary-contract',
|
|
1081
|
+
relative: 'references/templates.md',
|
|
1082
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.materialDefinition,
|
|
1083
|
+
to: 'A boundary is material when it seems relevant to the task.'
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
name: 'exact-boundary-choices',
|
|
1087
|
+
issue: 'boundary-contract',
|
|
1088
|
+
relative: 'references/templates.md',
|
|
1089
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices,
|
|
1090
|
+
to: 'For every required row, describe the listed choices generally.'
|
|
1091
|
+
},
|
|
1092
|
+
boundaryMutation('interaction-accessibility', 'Interaction/UI', [
|
|
1093
|
+
['input/focus/keyboard; ', ''],
|
|
1094
|
+
['accessibility; ', '']
|
|
1095
|
+
]),
|
|
1096
|
+
boundaryMutation('api-success-and-error-semantics', 'API/CLI', [
|
|
1097
|
+
['success output; ', ''],
|
|
1098
|
+
['error/status/exit; ', '']
|
|
1099
|
+
]),
|
|
1100
|
+
boundaryMutation('schema-shape-and-unknown-fields', 'Data/schema', [
|
|
1101
|
+
['exact keys/nesting/types; ', ''],
|
|
1102
|
+
['unknown-field behavior; ', '']
|
|
1103
|
+
]),
|
|
1104
|
+
boundaryMutation('schema-enum-and-format', 'Data/schema', [
|
|
1105
|
+
['enum/format/', '']
|
|
1106
|
+
]),
|
|
1107
|
+
boundaryMutation('state-lock-lifecycle', 'Async/state', [
|
|
1108
|
+
['writer/lock acquire/contention/release; ', 'writer/lock; ']
|
|
1109
|
+
]),
|
|
1110
|
+
boundaryMutation('filesystem-segment-grammar', 'Filesystem/security', [
|
|
1111
|
+
['trusted/untrusted segment grammar; ', '']
|
|
1112
|
+
]),
|
|
1113
|
+
boundaryMutation('filesystem-stale-lock-reclaim', 'Filesystem/security', [
|
|
1114
|
+
['lock/stale reclaim; ', '']
|
|
1115
|
+
]),
|
|
1116
|
+
boundaryMutation('runtime-rollout-and-recovery', 'Runtime/deploy', [
|
|
1117
|
+
['rollout/rollback; ', ''],
|
|
1118
|
+
['operator recovery', '']
|
|
1119
|
+
]),
|
|
1120
|
+
boundaryMutation('retention-clock-and-endpoints', 'Time/retention', [
|
|
1121
|
+
['clock source; ', ''],
|
|
1122
|
+
['unit/precision/timezone; ', ''],
|
|
1123
|
+
['endpoints and inclusion/comparator; ', '']
|
|
1124
|
+
]),
|
|
1125
|
+
boundaryMutation('ai-model-safety-and-eval', 'AI/model', [
|
|
1126
|
+
['safety/privacy; ', ''],
|
|
1127
|
+
['eval oracle', '']
|
|
1128
|
+
]),
|
|
1129
|
+
boundaryMutation('proof-level-partition', 'Integration/proof', [
|
|
1130
|
+
['proof level (`source`/`installed`/`live`)', 'proof level']
|
|
1131
|
+
]),
|
|
1132
|
+
{
|
|
1133
|
+
name: 'concrete-named-probe',
|
|
1134
|
+
issue: 'proof-chain',
|
|
1135
|
+
relative: 'references/templates.md',
|
|
1136
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
|
|
1137
|
+
to: '- Named probe: <suite label>'
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
name: 'aggregate-suite-probe-owner',
|
|
1141
|
+
issue: 'proof-chain',
|
|
1142
|
+
relative: 'references/templates.md',
|
|
1143
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
1144
|
+
to: 'Aggregate suites may cite only the suite label.'
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: 'reachability-levels',
|
|
1148
|
+
issue: 'proof-chain',
|
|
1149
|
+
relative: 'references/templates.md',
|
|
1150
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[2],
|
|
1151
|
+
to: '- Reachability: <entrypoint or consumer>'
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: 'proof-level-separation',
|
|
1155
|
+
issue: 'proof-chain',
|
|
1156
|
+
relative: 'references/templates.md',
|
|
1157
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
1158
|
+
to: 'Proof may be promoted between source, installed, and live levels.'
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
name: 'disposable-template-negative-controls',
|
|
1162
|
+
issue: 'proof-chain',
|
|
1163
|
+
relative: 'references/templates.md',
|
|
1164
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
1165
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: 'disposable-review-negative-controls',
|
|
1169
|
+
issue: 'proof-chain',
|
|
1170
|
+
relative: 'references/review.md',
|
|
1171
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls,
|
|
1172
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
name: 'required-proof-level-mapping',
|
|
1176
|
+
issue: 'proof-chain',
|
|
1177
|
+
relative: 'references/templates.md',
|
|
1178
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
|
|
1179
|
+
to: 'Map one required proof level to one probe; other levels may remain implicit.'
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
name: 'artifact-path-and-digest-or-ephemeral',
|
|
1183
|
+
issue: 'proof-chain',
|
|
1184
|
+
relative: 'references/templates.md',
|
|
1185
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
|
|
1186
|
+
to: '- Artifacts: <required artifact path, or none>'
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
name: 'proof-counterexample',
|
|
1190
|
+
issue: 'proof-chain',
|
|
1191
|
+
relative: 'references/templates.md',
|
|
1192
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[4],
|
|
1193
|
+
to: '- Counterexample: <example>'
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
name: 'repair-and-proof-columns',
|
|
1197
|
+
issue: 'repair-closure',
|
|
1198
|
+
relative: 'references/review.md',
|
|
1199
|
+
from: '| ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |',
|
|
1200
|
+
to: '| ID | Decision | Original counterexample | Repaired at | Evidence | Replay | Closure |'
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
name: 'fresh-original-counterexample-replay',
|
|
1204
|
+
issue: 'repair-closure',
|
|
1205
|
+
relative: 'references/review.md',
|
|
1206
|
+
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:',
|
|
1207
|
+
to: 'After applying an accepted C2 finding, record the repair under this review-log header:'
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
name: 'distinct-repair-and-proof-evidence',
|
|
1211
|
+
issue: 'repair-closure',
|
|
1212
|
+
relative: 'references/review.md',
|
|
1213
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
|
|
1214
|
+
to: '`Repaired at` and `Proved at` may cite the same repair edit.'
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: 'unknown-blocks-handoff',
|
|
1218
|
+
issue: 'repair-closure',
|
|
1219
|
+
relative: 'references/review.md',
|
|
1220
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks,
|
|
1221
|
+
to: '`PASS` closes it; `FAIL` and `UNKNOWN` may continue to implementation handoff.'
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: 'crash-versus-catchable-failure',
|
|
1225
|
+
issue: 'failure-semantics',
|
|
1226
|
+
relative: 'references/templates.md',
|
|
1227
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
|
|
1228
|
+
to: 'Crash and catchable failure both mean an error occurred.'
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: 'privacy-identifier-surface',
|
|
1232
|
+
issue: 'privacy-identifiers',
|
|
1233
|
+
relative: 'references/review.md',
|
|
1234
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
|
|
1235
|
+
to: 'Any privacy/security claim names the sensitive data at risk.'
|
|
1236
|
+
}
|
|
1237
|
+
];
|
|
1238
|
+
assert.equal(readinessMutations.length, 30, 'installed readiness mutation set must match canonical source');
|
|
1239
|
+
|
|
1240
|
+
for (const { name, issue, relative, from, to } of readinessMutations) {
|
|
1241
|
+
const target = assertInstalledMutationTarget(root, installedRoot, relative);
|
|
1242
|
+
const installedBytes = fs.readFileSync(target, 'utf8');
|
|
1243
|
+
const anchorIndex = installedBytes.indexOf(from);
|
|
1244
|
+
assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
|
|
1245
|
+
assert.equal(
|
|
1246
|
+
installedBytes.indexOf(from, anchorIndex + from.length),
|
|
1247
|
+
-1,
|
|
1248
|
+
`${name} mutation anchor must be unique in installed bytes`
|
|
1249
|
+
);
|
|
1250
|
+
const weakened = `${installedBytes.slice(0, anchorIndex)}${to}${installedBytes.slice(anchorIndex + from.length)}`;
|
|
1251
|
+
try {
|
|
1252
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), weakened);
|
|
1253
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1254
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [issue], name);
|
|
1255
|
+
} finally {
|
|
1256
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), installedBytes);
|
|
1257
|
+
}
|
|
1258
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], `${name} restore`);
|
|
1259
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const integrationRow = IMPLEMENTATION_READINESS_BOUNDARY_ROWS
|
|
1263
|
+
.find(([label]) => label === 'Integration/proof');
|
|
1264
|
+
const integrationLine = `| ${integrationRow.join(' | ')} |`;
|
|
1265
|
+
const templatesTarget = assertInstalledMutationTarget(
|
|
1266
|
+
root,
|
|
1267
|
+
installedRoot,
|
|
1268
|
+
'references/templates.md'
|
|
1269
|
+
);
|
|
1270
|
+
const templatesBytes = fs.readFileSync(templatesTarget, 'utf8');
|
|
1271
|
+
const extendedTemplates = templatesBytes.replace(
|
|
1272
|
+
integrationLine,
|
|
1273
|
+
`${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
|
|
1274
|
+
);
|
|
1275
|
+
assert.notEqual(extendedTemplates, templatesBytes, 'open-boundary positive control must change installed templates');
|
|
1276
|
+
try {
|
|
1277
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), extendedTemplates);
|
|
1278
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
|
|
1279
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1280
|
+
} finally {
|
|
1281
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), templatesBytes);
|
|
1282
|
+
}
|
|
1283
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], 'open-boundary restore');
|
|
1284
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1285
|
+
|
|
1286
|
+
const adaptiveMutations = [
|
|
1287
|
+
['frontmatter-risk-bypass', 'skill', ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
|
|
1288
|
+
'skip for any clear one-file or two-file change.', ['risk-first-routing']],
|
|
1289
|
+
['destructive-routine-direct', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
1290
|
+
'Work directly when the change is routine and likely limited to one or two files.', ['risk-first-routing']],
|
|
1291
|
+
['destructive-direct-exception', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
1292
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.directGate} Exception: destructive one-file work labeled routine may go direct.`, ['risk-first-routing']],
|
|
1293
|
+
['user-risk-downgrade', 'skill', ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
|
|
1294
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.riskFirst} A user may lower critical risk to routine.`, ['risk-first-routing']],
|
|
1295
|
+
['four-subsystem-split', 'skill', ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
|
|
1296
|
+
'Split four or more independent subsystems; otherwise use one Specs packet for substantial work.', ['risk-first-routing']],
|
|
1297
|
+
['forced-single-kind', 'templates', ADAPTIVE_COVERAGE_CLAUSES.openKinds,
|
|
1298
|
+
'Choose one primary change kind and ignore unfamiliar kinds or surfaces.', ['coverage-profile-shape']],
|
|
1299
|
+
['missing-profile-column', 'templates', '| ID | Outcome | Change kinds | Material surfaces | Ambiguity/action | Risk/evidence | Required proof |',
|
|
1300
|
+
'| ID | Outcome | Change kinds | Material surfaces | Risk/evidence | Required proof |', ['coverage-profile-shape']],
|
|
1301
|
+
['duplicate-profile-heading', 'templates', '## Coverage profile\n',
|
|
1302
|
+
'## Coverage profile\n\n## Coverage profile\n', ['coverage-profile-shape']],
|
|
1303
|
+
['truncated-profile-row', 'templates', `| ${ADAPTIVE_COVERAGE_PROFILE_ROW.join(' | ')} |`,
|
|
1304
|
+
'| CP-01 | <externally observable outcome> |', ['coverage-profile-shape']],
|
|
1305
|
+
['examples-promote-to-design', 'templates', `| ${ADAPTIVE_COVERAGE_AMBIGUITY_ROWS[2].join(' | ')} |`,
|
|
1306
|
+
'| `examples-needed` | add examples and promote to `design-needed` if behavior changes |', ['ambiguity-actions']],
|
|
1307
|
+
['examples-select-retention', 'templates', 'retention of 30 versus 90 days is\n`decision-needed`',
|
|
1308
|
+
'retention of 30 versus 90 days may remain\n`examples-needed`', ['ambiguity-actions']],
|
|
1309
|
+
['global-critical-ceremony', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1310
|
+
'Each task copies its CP values; authoring, review, edge, and proof obligations union across every task.', ['scoped-coverage']],
|
|
1311
|
+
['scoped-union-contradiction', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1312
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Exception: critical proof obligations apply across every CP row.`, ['scoped-coverage']],
|
|
1313
|
+
['stale-profile-after-c2', 'templates', ADAPTIVE_COVERAGE_CLAUSES.profileRederivation,
|
|
1314
|
+
'Keep existing coverage rows after accepted plan changes.', ['profile-lifecycle']],
|
|
1315
|
+
['planned-proof-blocks-start', 'templates', ADAPTIVE_COVERAGE_CLAUSES.plannedProof,
|
|
1316
|
+
'`Required proof` is execution evidence: known but unrun proof blocks `pending`; `UNKNOWN` may proceed; missing evidence may still reach `done`/C3.', ['proof-lifecycle']],
|
|
1317
|
+
['source-promotes-live', 'templates', ADAPTIVE_COVERAGE_CLAUSES.proofSeparation,
|
|
1318
|
+
'Source proof may promote installed and live proof.', ['proof-lifecycle']],
|
|
1319
|
+
['unmapped-proof-level', 'templates', IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
|
|
1320
|
+
'A task may map only one required proof level to its probe.', ['proof-lifecycle']],
|
|
1321
|
+
['critical-reviewer-omitted', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewRisk,
|
|
1322
|
+
'Keep Fact Checker as the baseline and choose any remaining reviewer; critical rows need no matching risk role.', ['reviewer-routing', 'scoped-coverage']],
|
|
1323
|
+
['reviewer-lens-overflow', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity,
|
|
1324
|
+
'Each reviewer owns exactly one lens; skip excess material lenses when the fixed reviewer count is full.', ['reviewer-routing']],
|
|
1325
|
+
['spec-maker-local-taxonomy', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority,
|
|
1326
|
+
'this agent owns a separate risk and coverage taxonomy.', ['spec-maker-authority']],
|
|
1327
|
+
['spec-maker-examples-decide', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity,
|
|
1328
|
+
'Use examples to settle every ambiguous observable behavior.', ['spec-maker-authority']],
|
|
1329
|
+
['spec-maker-skips-brainstorm', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute,
|
|
1330
|
+
'Apply the risk-first route before C1 and stop only for direct work.', ['spec-maker-authority']],
|
|
1331
|
+
['static-proves-live', 'skill', ADAPTIVE_COVERAGE_CLAUSES.liveLimit,
|
|
1332
|
+
'Source/static checks prove live-model adherence.', ['proof-lifecycle']]
|
|
1333
|
+
];
|
|
1334
|
+
assert.equal(adaptiveMutations.length, 23, 'installed adaptive mutation set must match canonical source');
|
|
1335
|
+
|
|
1336
|
+
const adaptiveTarget = (source) => {
|
|
1337
|
+
if (source === 'specMaker') {
|
|
1338
|
+
const target = assertInstalledSpecMakerMutationTarget(root);
|
|
1339
|
+
const bytes = fs.readFileSync(target);
|
|
1340
|
+
const content = bytes.toString('utf8');
|
|
1341
|
+
return {
|
|
1342
|
+
target,
|
|
1343
|
+
bytes,
|
|
1344
|
+
content: parseGeneratedTomlString(content, 'developer_instructions'),
|
|
1345
|
+
render: (value) => replaceGeneratedTomlString(content, 'developer_instructions', value)
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
const relative = {
|
|
1349
|
+
skill: 'SKILL.md',
|
|
1350
|
+
templates: 'references/templates.md',
|
|
1351
|
+
review: 'references/review.md'
|
|
1352
|
+
}[source];
|
|
1353
|
+
assert.ok(relative, `unknown adaptive mutation source: ${source}`);
|
|
1354
|
+
const target = assertInstalledMutationTarget(root, installedRoot, relative);
|
|
1355
|
+
const bytes = fs.readFileSync(target);
|
|
1356
|
+
return { target, bytes, content: bytes.toString('utf8'), render: (value) => value };
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
for (const [name, source, from, to, expected] of adaptiveMutations) {
|
|
1360
|
+
const targetState = adaptiveTarget(source);
|
|
1361
|
+
const anchorIndex = targetState.content.indexOf(from);
|
|
1362
|
+
assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
|
|
1363
|
+
assert.equal(
|
|
1364
|
+
targetState.content.indexOf(from, anchorIndex + from.length),
|
|
1365
|
+
-1,
|
|
1366
|
+
`${name} mutation anchor must be unique in installed bytes`
|
|
1367
|
+
);
|
|
1368
|
+
const weakened = `${targetState.content.slice(0, anchorIndex)}${to}${targetState.content.slice(anchorIndex + from.length)}`;
|
|
1369
|
+
try {
|
|
1370
|
+
fs.writeFileSync(adaptiveTarget(source).target, targetState.render(weakened));
|
|
1371
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1372
|
+
assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [...expected].sort(), name);
|
|
1373
|
+
} finally {
|
|
1374
|
+
fs.writeFileSync(adaptiveTarget(source).target, targetState.bytes);
|
|
1375
|
+
}
|
|
1376
|
+
assert.deepEqual(fs.readFileSync(adaptiveTarget(source).target), targetState.bytes, `${name} byte restore`);
|
|
1377
|
+
assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], `${name} restore`);
|
|
1378
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const intactAdaptive = readInstalledAuthoringProjection(root, installedRoot);
|
|
1382
|
+
const adaptivePositiveControls = [
|
|
1383
|
+
['open material surface', intactAdaptive.templates.replace(
|
|
1384
|
+
integrationLine,
|
|
1385
|
+
`${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
|
|
1386
|
+
)],
|
|
1387
|
+
['task CP reference requirement', intactAdaptive.templates.replace(
|
|
1388
|
+
ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1389
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Require every task to reference a CP row.`
|
|
1390
|
+
)]
|
|
1391
|
+
];
|
|
1392
|
+
for (const [name, templates] of adaptivePositiveControls) {
|
|
1393
|
+
assert.notEqual(templates, intactAdaptive.templates, `${name} positive control must change installed templates`);
|
|
1394
|
+
const target = assertInstalledMutationTarget(root, installedRoot, 'references/templates.md');
|
|
1395
|
+
const original = fs.readFileSync(target);
|
|
1396
|
+
try {
|
|
1397
|
+
fs.writeFileSync(target, templates);
|
|
1398
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1399
|
+
assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], name);
|
|
1400
|
+
} finally {
|
|
1401
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), original);
|
|
1402
|
+
}
|
|
1403
|
+
assert.deepEqual(fs.readFileSync(target), original, `${name} byte restore`);
|
|
1404
|
+
assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], `${name} restore`);
|
|
1405
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
250
1408
|
const mutations = [
|
|
251
1409
|
['skill', (value) => value.replace('Task files are flat beside `plan.md`', 'Task files may be nested')],
|
|
252
1410
|
['review', (value) => value.replace('list at 15', 'list without a cap')],
|
|
@@ -255,8 +1413,14 @@ function assertInstalledAuthoringProjection(root) {
|
|
|
255
1413
|
];
|
|
256
1414
|
for (const [key, mutate] of mutations) {
|
|
257
1415
|
const changed = { ...files, [key]: mutate(files[key]) };
|
|
258
|
-
assert.notDeepEqual(
|
|
1416
|
+
assert.notDeepEqual(authoringProjectionIssues(changed), []);
|
|
259
1417
|
}
|
|
1418
|
+
assert.equal(
|
|
1419
|
+
fs.readFileSync(installedSpecMakerPath, 'utf8'),
|
|
1420
|
+
convertCodexAgentContent(sourceSpecMaker, path.basename(SPEC_MAKER_SOURCE_PATH)),
|
|
1421
|
+
'Codex spec_maker projection must be byte-exact after mutation restores'
|
|
1422
|
+
);
|
|
1423
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
260
1424
|
}
|
|
261
1425
|
|
|
262
1426
|
test('Codex payload transform emits native skill and subagent syntax', () => {
|
|
@@ -269,13 +1433,165 @@ test('Codex payload transform emits native skill and subagent syntax', () => {
|
|
|
269
1433
|
transformed,
|
|
270
1434
|
/spawn_agent\(agent_type="implementer", fork_turns="none", message="Implement it", task_name="code_feature"\)/
|
|
271
1435
|
);
|
|
272
|
-
assert.match(transformed, /\$
|
|
1436
|
+
assert.match(transformed, /\$cf-specs auth/);
|
|
273
1437
|
assert.match(transformed, /`send_message`/);
|
|
274
1438
|
assert.match(transformed, /`exec_command`/);
|
|
275
1439
|
assert.match(transformed, /`apply_patch`/);
|
|
276
1440
|
assert.doesNotMatch(transformed, /Agent\(|subagent_type|\/specs\b|Claude Code/);
|
|
277
1441
|
});
|
|
278
1442
|
|
|
1443
|
+
test('Codex payload transform keeps structured user-input grammar across determiners', () => {
|
|
1444
|
+
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']
|
|
1465
|
+
];
|
|
1466
|
+
|
|
1467
|
+
for (const [name, input, expected] of cases) {
|
|
1468
|
+
const actual = normalizeCodexBody(input, '/fixture/instructions.md');
|
|
1469
|
+
assert.equal(actual, expected, name);
|
|
1470
|
+
assert.equal(normalizeCodexBody(actual, '/fixture/instructions.md'), expected, `${name} idempotence`);
|
|
1471
|
+
assert.doesNotMatch(actual, /\bAskUserQuestion\b/, `${name} Claude-only token`);
|
|
1472
|
+
assert.doesNotMatch(actual, /\ban structured user-input request\b/i, `${name} incompatible article`);
|
|
1473
|
+
assert.doesNotMatch(
|
|
1474
|
+
actual,
|
|
1475
|
+
/\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a structured user-input request\b/i,
|
|
1476
|
+
`${name} doubled article`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
assert.equal(
|
|
1481
|
+
normalizeCodexBody('one\nAskUserQuestion call', '/fixture/instructions.md'),
|
|
1482
|
+
'one\nstructured user-input request call',
|
|
1483
|
+
'a single Markdown soft break must preserve its determiner relationship'
|
|
1484
|
+
);
|
|
1485
|
+
assert.equal(
|
|
1486
|
+
normalizeCodexBody('AskUserQuestion\nCalls', '/fixture/instructions.md'),
|
|
1487
|
+
'structured user-input request\nCalls',
|
|
1488
|
+
'a single Markdown soft break must preserve a plural relationship'
|
|
1489
|
+
);
|
|
1490
|
+
assert.equal(
|
|
1491
|
+
normalizeCodexBody('one\n\nAskUserQuestion call', '/fixture/instructions.md'),
|
|
1492
|
+
'one\n\na structured user-input request call',
|
|
1493
|
+
'a paragraph break must not bind a prior determiner'
|
|
1494
|
+
);
|
|
1495
|
+
assert.equal(
|
|
1496
|
+
normalizeCodexBody('MyAskUserQuestionHelper AskUserQuestionFactory', '/fixture/instructions.md'),
|
|
1497
|
+
'MyAskUserQuestionHelper AskUserQuestionFactory',
|
|
1498
|
+
'balanced token matching must preserve identifiers'
|
|
1499
|
+
);
|
|
1500
|
+
assert.equal(
|
|
1501
|
+
normalizeCodexBody('[tool](https://example.test/AskUserQuestion)', '/fixture/instructions.md'),
|
|
1502
|
+
'[tool](https://example.test/AskUserQuestion)',
|
|
1503
|
+
'Markdown link destinations must stay byte-exact'
|
|
1504
|
+
);
|
|
1505
|
+
assert.equal(
|
|
1506
|
+
normalizeCodexBody('https://example.test/AskUserQuestion', '/fixture/instructions.md'),
|
|
1507
|
+
'https://example.test/AskUserQuestion',
|
|
1508
|
+
'raw URL tokens must stay byte-exact'
|
|
1509
|
+
);
|
|
1510
|
+
assert.equal(
|
|
1511
|
+
normalizeCodexBody('one ``AskUserQuestion``` call', '/fixture/instructions.md'),
|
|
1512
|
+
'one ``AskUserQuestion``` call',
|
|
1513
|
+
'mismatched backtick runs must stay byte-exact'
|
|
1514
|
+
);
|
|
1515
|
+
assert.equal(
|
|
1516
|
+
normalizeCodexBody('``Use AskUserQuestion here``', '/fixture/instructions.md'),
|
|
1517
|
+
'``Use AskUserQuestion here``',
|
|
1518
|
+
'non-token code spans must stay byte-exact'
|
|
1519
|
+
);
|
|
1520
|
+
assert.equal(
|
|
1521
|
+
normalizeCodexBody('```js\nAskUserQuestion\n````', '/fixture/instructions.md'),
|
|
1522
|
+
'```js\nAskUserQuestion\n````',
|
|
1523
|
+
'a longer closing backtick fence must protect its body'
|
|
1524
|
+
);
|
|
1525
|
+
assert.equal(
|
|
1526
|
+
normalizeCodexBody('~~~text\nAskUserQuestion\n~~~~', '/fixture/instructions.md'),
|
|
1527
|
+
'~~~text\nAskUserQuestion\n~~~~',
|
|
1528
|
+
'a longer closing tilde fence must protect its body'
|
|
1529
|
+
);
|
|
1530
|
+
const markerCollision = '\uE000CAFEKIT_0_ASK_999\uE001';
|
|
1531
|
+
assert.equal(
|
|
1532
|
+
normalizeCodexBody(markerCollision, '/fixture/instructions.md'),
|
|
1533
|
+
markerCollision,
|
|
1534
|
+
'literal internal-marker-shaped input must stay byte-exact'
|
|
1535
|
+
);
|
|
1536
|
+
const codeMarkerCollision = '\uE000CAFEKIT_0_CODE_0\uE001 and ``not token``';
|
|
1537
|
+
assert.equal(
|
|
1538
|
+
normalizeCodexBody(codeMarkerCollision, '/fixture/instructions.md'),
|
|
1539
|
+
codeMarkerCollision,
|
|
1540
|
+
'mask restoration must not replace a literal marker-shaped prefix'
|
|
1541
|
+
);
|
|
1542
|
+
assert.equal(
|
|
1543
|
+
normalizeCodexBody('one AskUserQuestion call', '/fixture/session.cjs'),
|
|
1544
|
+
'one AskUserQuestion call',
|
|
1545
|
+
'non-instruction assets must preserve executable text'
|
|
1546
|
+
);
|
|
1547
|
+
assert.equal(
|
|
1548
|
+
normalizeCodexBody('No structured input token here.', '/fixture/instructions.md'),
|
|
1549
|
+
'No structured input token here.',
|
|
1550
|
+
'token-free instruction text must stay byte-exact'
|
|
1551
|
+
);
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
test('Codex structured-input corpus oracle stays differential and production-aware', () => {
|
|
1555
|
+
const sourceRoot = path.join(PACKAGE_ROOT, 'src/claude');
|
|
1556
|
+
const occurrenceFiles = allFiles(sourceRoot, (file) => /\.(?:md|mdx|txt|cjs|js)$/i.test(file))
|
|
1557
|
+
.filter((file) => fs.readFileSync(file, 'utf8').includes('AskUserQuestion'));
|
|
1558
|
+
const relative = (file) => path.relative(PACKAGE_ROOT, file).split(path.sep).join('/');
|
|
1559
|
+
const actualPaths = occurrenceFiles.map(relative).sort();
|
|
1560
|
+
const expectedPaths = [
|
|
1561
|
+
'src/claude/agents/spec-maker.md',
|
|
1562
|
+
'src/claude/hooks/session.cjs',
|
|
1563
|
+
'src/claude/skills/git/SKILL.md',
|
|
1564
|
+
'src/claude/skills/inspect/SKILL.md'
|
|
1565
|
+
];
|
|
1566
|
+
assert.deepEqual(actualPaths, expectedPaths, 'every source occurrence needs an explicit projection oracle');
|
|
1567
|
+
|
|
1568
|
+
const instructionExpectedSnippets = new Map([
|
|
1569
|
+
['src/claude/skills/git/SKILL.md', 'Present options via a structured user-input request — header'],
|
|
1570
|
+
['src/claude/skills/inspect/SKILL.md', '**Fallback to a structured user-input request:**']
|
|
1571
|
+
]);
|
|
1572
|
+
for (const [relativePath, expectedSnippet] of instructionExpectedSnippets) {
|
|
1573
|
+
const sourcePath = path.join(PACKAGE_ROOT, relativePath);
|
|
1574
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
1575
|
+
const sentinel = '__CAFEKIT_STRUCTURED_INPUT_SENTINEL__';
|
|
1576
|
+
const withSentinel = source.replace(/`?\bAskUserQuestion\b`?/g, sentinel);
|
|
1577
|
+
assert.notEqual(withSentinel, source, `${relativePath} differential control must replace a token`);
|
|
1578
|
+
const expected = normalizeCodexBody(withSentinel, sourcePath)
|
|
1579
|
+
.replaceAll(sentinel, 'a structured user-input request');
|
|
1580
|
+
const actual = normalizeCodexBody(source, sourcePath);
|
|
1581
|
+
assert.equal(actual, expected, `${relativePath} independent differential oracle`);
|
|
1582
|
+
assert.match(actual, new RegExp(expectedSnippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const specMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
|
|
1586
|
+
const convertedAgent = convertCodexAgentContent(specMaker, path.basename(SPEC_MAKER_SOURCE_PATH));
|
|
1587
|
+
assert.doesNotMatch(convertedAgent, /\bAskUserQuestion\b/);
|
|
1588
|
+
assert.doesNotMatch(convertedAgent, /structured user-input request/);
|
|
1589
|
+
|
|
1590
|
+
const sessionPath = path.join(PACKAGE_ROOT, 'src/claude/hooks/session.cjs');
|
|
1591
|
+
const session = normalizeCodexBody(fs.readFileSync(sessionPath, 'utf8'), sessionPath);
|
|
1592
|
+
assert.equal((session.match(/\bAskUserQuestion\b/g) || []).length, 2);
|
|
1593
|
+
});
|
|
1594
|
+
|
|
279
1595
|
test('Codex payload transform preserves executable keyword arguments', () => {
|
|
280
1596
|
const transformed = normalizeCodexBody(
|
|
281
1597
|
'parser = ArgumentParser(description="Analyze it")\n' +
|
|
@@ -618,6 +1934,43 @@ test('Codex dry-run leaves both managed roots untouched', () => {
|
|
|
618
1934
|
});
|
|
619
1935
|
});
|
|
620
1936
|
|
|
1937
|
+
test('Codex install preserves the adaptive diagnostic-only Debug contract', () => {
|
|
1938
|
+
inTempProject((root) => {
|
|
1939
|
+
const result = install(root);
|
|
1940
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
1941
|
+
|
|
1942
|
+
const installedSkill = fs.readFileSync(
|
|
1943
|
+
path.join(root, '.agents/skills/debug/SKILL.md'), 'utf8'
|
|
1944
|
+
);
|
|
1945
|
+
assert.match(installedSkill, /^name: cf-debug$/m);
|
|
1946
|
+
assert.match(installedSkill, /## Proportional depth/);
|
|
1947
|
+
assert.match(installedSkill, /Evidence Timeline/);
|
|
1948
|
+
assert.match(installedSkill, /### Elimination Path/);
|
|
1949
|
+
assert.match(installedSkill, /### Recurrence-Prevention Handoff/);
|
|
1950
|
+
assert.match(installedSkill, /`cf-debug` is read-only for product code/);
|
|
1951
|
+
|
|
1952
|
+
const installedAgent = fs.readFileSync(
|
|
1953
|
+
path.join(root, '.codex/agents/debugger.toml'), 'utf8'
|
|
1954
|
+
);
|
|
1955
|
+
assert.equal(
|
|
1956
|
+
installedAgent,
|
|
1957
|
+
convertCodexAgentContent(fs.readFileSync(DEBUG_AGENT_SOURCE_PATH, 'utf8'), 'debugger.md')
|
|
1958
|
+
);
|
|
1959
|
+
assert.match(installedAgent, /Never implement the repair/);
|
|
1960
|
+
|
|
1961
|
+
for (const relative of DEBUG_REFERENCE_FILES) {
|
|
1962
|
+
const source = fs.readFileSync(
|
|
1963
|
+
path.join(PACKAGE_ROOT, 'src/claude/references/debugger', relative), 'utf8'
|
|
1964
|
+
);
|
|
1965
|
+
const installed = fs.readFileSync(
|
|
1966
|
+
path.join(root, '.codex/references/debugger', relative), 'utf8'
|
|
1967
|
+
);
|
|
1968
|
+
assert.equal(installed, normalizeCodexBody(source, relative), relative);
|
|
1969
|
+
}
|
|
1970
|
+
assert.equal(fs.existsSync(DEBUG_SKILL_SOURCE_PATH), true);
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
|
|
621
1974
|
test('Codex Windows hook launchers stay project-bound without Git from nested cwd', () => {
|
|
622
1975
|
inTempProject((root) => {
|
|
623
1976
|
const projectRoot = path.join(root, 'project with spaces');
|
|
@@ -682,12 +2035,12 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
|
|
|
682
2035
|
});
|
|
683
2036
|
});
|
|
684
2037
|
|
|
685
|
-
test('Codex
|
|
2038
|
+
test('Codex installed Specs and spec-maker reject adaptive coverage mutations', () => {
|
|
686
2039
|
inTempProject((root) => {
|
|
687
2040
|
const userInstructions = '# User rules\n\nKeep this exact.\n';
|
|
688
2041
|
fs.writeFileSync(path.join(root, 'AGENTS.md'), userInstructions);
|
|
689
2042
|
|
|
690
|
-
const first = install(root);
|
|
2043
|
+
const first = install(root, ['--with-document-skills']);
|
|
691
2044
|
assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
|
|
692
2045
|
assert.equal(fs.existsSync(path.join(root, '.claude')), false);
|
|
693
2046
|
assert.equal(fs.existsSync(path.join(root, '.codex', 'config.toml')), false);
|
|
@@ -699,6 +2052,8 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
699
2052
|
'.codex/rules/workflow.md',
|
|
700
2053
|
'.codex/rules/hook-protocols.md',
|
|
701
2054
|
'.codex/rules/state-sync.md',
|
|
2055
|
+
'.codex/rules/review-audit-self-decision.md',
|
|
2056
|
+
'.codex/rules/process-management.md',
|
|
702
2057
|
'.codex/scripts/spec-ground.cjs',
|
|
703
2058
|
'.codex/scripts/validate-spec-output.cjs',
|
|
704
2059
|
'.agents/.gitignore',
|
|
@@ -715,7 +2070,20 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
715
2070
|
path.join(root, '.codex/scripts/spec-ground.cjs'),
|
|
716
2071
|
installedDesign
|
|
717
2072
|
);
|
|
718
|
-
|
|
2073
|
+
const installedSpecsRoot = path.join(root, '.agents/skills/specs');
|
|
2074
|
+
assert.throws(
|
|
2075
|
+
() => installedAuthoringProjectionIssues(root, '.agents/skills/specs'),
|
|
2076
|
+
/must be absolute/
|
|
2077
|
+
);
|
|
2078
|
+
assert.throws(
|
|
2079
|
+
() => installedAuthoringProjectionIssues(root, SPECS_SOURCE_ROOT),
|
|
2080
|
+
/must stay inside project/
|
|
2081
|
+
);
|
|
2082
|
+
assert.throws(
|
|
2083
|
+
() => installedAuthoringProjectionIssues(root, path.dirname(root)),
|
|
2084
|
+
/must stay inside project/
|
|
2085
|
+
);
|
|
2086
|
+
assertInstalledAuthoringProjection(root, installedSpecsRoot);
|
|
719
2087
|
|
|
720
2088
|
const installedTask = fs.readFileSync(
|
|
721
2089
|
path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
|
|
@@ -758,6 +2126,12 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
758
2126
|
const name = path.basename(fileName, '.md').replace(/-/g, '_');
|
|
759
2127
|
const agentPath = path.join(root, '.codex', 'agents', `${name}.toml`);
|
|
760
2128
|
const content = fs.readFileSync(agentPath, 'utf8');
|
|
2129
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'src', 'claude', 'agents', fileName);
|
|
2130
|
+
assert.equal(
|
|
2131
|
+
content,
|
|
2132
|
+
convertCodexAgentContent(fs.readFileSync(sourcePath, 'utf8'), fileName),
|
|
2133
|
+
`Codex agent projection drifted: ${fileName}`
|
|
2134
|
+
);
|
|
761
2135
|
assert.equal(parseGeneratedTomlString(content, 'name'), name);
|
|
762
2136
|
assert.ok(parseGeneratedTomlString(content, 'description').length > 8);
|
|
763
2137
|
assert.ok(parseGeneratedTomlString(content, 'developer_instructions').length > 20);
|
|
@@ -776,6 +2150,27 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
776
2150
|
assert.equal(new Set(skillNames).size, skillNames.length);
|
|
777
2151
|
assert.ok(skillNames.every((name) => /^[a-z0-9-]+$/.test(name)));
|
|
778
2152
|
|
|
2153
|
+
const scoutSkill = fs.readFileSync(
|
|
2154
|
+
path.join(root, '.agents', 'skills', 'inspect', 'SKILL.md'),
|
|
2155
|
+
'utf8'
|
|
2156
|
+
);
|
|
2157
|
+
assert.match(scoutSkill, /^name:\s*cf-scout$/m);
|
|
2158
|
+
assert.equal(
|
|
2159
|
+
fs.existsSync(path.join(root, '.agents', 'skills', 'scout')),
|
|
2160
|
+
false,
|
|
2161
|
+
'public Scout rename must keep the manifest-owned inspect directory'
|
|
2162
|
+
);
|
|
2163
|
+
const askSkill = fs.readFileSync(
|
|
2164
|
+
path.join(root, '.agents', 'skills', 'question', 'SKILL.md'),
|
|
2165
|
+
'utf8'
|
|
2166
|
+
);
|
|
2167
|
+
assert.match(askSkill, /^name:\s*cf-ask$/m);
|
|
2168
|
+
assert.equal(
|
|
2169
|
+
fs.existsSync(path.join(root, '.agents', 'skills', 'ask')),
|
|
2170
|
+
false,
|
|
2171
|
+
'public Ask rename must keep the manifest-owned question directory'
|
|
2172
|
+
);
|
|
2173
|
+
|
|
779
2174
|
const catalog = spawnSync(
|
|
780
2175
|
process.execPath,
|
|
781
2176
|
[path.join(root, '.codex', 'scripts', 'generate-skill-catalog.cjs'), '--json'],
|
|
@@ -789,12 +2184,13 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
789
2184
|
fs.realpathSync(path.join(root, '.agents', 'skills'))
|
|
790
2185
|
);
|
|
791
2186
|
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
2187
|
+
for (const retired of MANIFEST.obsolete.skills) {
|
|
2188
|
+
assert.equal(
|
|
2189
|
+
fs.existsSync(path.join(root, '.agents', 'skills', retired)),
|
|
2190
|
+
false,
|
|
2191
|
+
`retired skill should not be installed: ${retired}`
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
798
2194
|
|
|
799
2195
|
const multimodalScript = fs.readFileSync(
|
|
800
2196
|
path.join(root, '.agents', 'skills', 'ai-multimodal', 'scripts', 'gemini_batch_process.py'),
|
|
@@ -815,10 +2211,10 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
815
2211
|
assert.doesNotMatch(visible, /If you are Codex CLI or any other runtime, ignore this entire Codex block/);
|
|
816
2212
|
assert.doesNotMatch(
|
|
817
2213
|
visible.replaceAll(foreignRuntimeBoundary, ''),
|
|
818
|
-
/\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/
|
|
2214
|
+
/\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/cf:|\bcf:|Claude Code/,
|
|
819
2215
|
);
|
|
820
2216
|
assert.doesNotMatch(visible, /@@PRIVACY_PROMPT_START@@|Claude Tasks/);
|
|
821
|
-
assert.match(visible, /\$
|
|
2217
|
+
assert.match(visible, /\$cf-specs/);
|
|
822
2218
|
|
|
823
2219
|
const agentsMd = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
824
2220
|
assert.ok(agentsMd.startsWith(userInstructions));
|
|
@@ -845,6 +2241,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
845
2241
|
|
|
846
2242
|
const sameVersion = install(root);
|
|
847
2243
|
assert.equal(sameVersion.status, 0, `${sameVersion.stdout}\n${sameVersion.stderr}`);
|
|
2244
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
848
2245
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
849
2246
|
assert.equal(
|
|
850
2247
|
fs.existsSync(obsoleteSpecsRule),
|
|
@@ -859,6 +2256,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
859
2256
|
|
|
860
2257
|
const upgrade = install(root);
|
|
861
2258
|
assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
|
|
2259
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
862
2260
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
863
2261
|
assert.equal(
|
|
864
2262
|
fs.existsSync(obsoleteSpecsRule),
|
|
@@ -873,6 +2271,732 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
873
2271
|
});
|
|
874
2272
|
});
|
|
875
2273
|
|
|
2274
|
+
test('Codex installed Brainstorm skill reference and agent preserve proportional routing parity', () => {
|
|
2275
|
+
inTempProject((root) => {
|
|
2276
|
+
const sourceBytes = canonicalBrainstormSourceBytes();
|
|
2277
|
+
const result = install(root);
|
|
2278
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2279
|
+
|
|
2280
|
+
const installedSkillPath = path.join(root, '.agents/skills/brainstorm/SKILL.md');
|
|
2281
|
+
const installedReferencePath = path.join(
|
|
2282
|
+
root,
|
|
2283
|
+
'.agents/skills/brainstorm/references/question-framework.md'
|
|
2284
|
+
);
|
|
2285
|
+
const installedAgentPath = path.join(root, '.codex/agents/brainstormer.toml');
|
|
2286
|
+
const sourceSkill = fs.readFileSync(BRAINSTORM_SKILL_SOURCE_PATH, 'utf8');
|
|
2287
|
+
const sourceReference = fs.readFileSync(BRAINSTORM_REFERENCE_SOURCE_PATH, 'utf8');
|
|
2288
|
+
const sourceAgent = fs.readFileSync(BRAINSTORM_AGENT_SOURCE_PATH, 'utf8');
|
|
2289
|
+
|
|
2290
|
+
assert.equal(
|
|
2291
|
+
fs.readFileSync(installedSkillPath, 'utf8'),
|
|
2292
|
+
normalizeCodexBody(sourceSkill, BRAINSTORM_SKILL_SOURCE_PATH),
|
|
2293
|
+
'installed Brainstorm skill must equal the generic Codex projection'
|
|
2294
|
+
);
|
|
2295
|
+
assert.equal(
|
|
2296
|
+
fs.readFileSync(installedReferencePath, 'utf8'),
|
|
2297
|
+
normalizeCodexBody(sourceReference, BRAINSTORM_REFERENCE_SOURCE_PATH),
|
|
2298
|
+
'installed Brainstorm reference must equal the generic Codex projection'
|
|
2299
|
+
);
|
|
2300
|
+
assert.equal(
|
|
2301
|
+
fs.readFileSync(installedAgentPath, 'utf8'),
|
|
2302
|
+
convertCodexAgentContent(sourceAgent, path.basename(BRAINSTORM_AGENT_SOURCE_PATH)),
|
|
2303
|
+
'installed brainstormer agent must equal the Codex TOML projection'
|
|
2304
|
+
);
|
|
2305
|
+
|
|
2306
|
+
assert.deepEqual(brainstormProjectionIssues(readInstalledBrainstormProjection(root)), []);
|
|
2307
|
+
assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
|
|
2308
|
+
|
|
2309
|
+
const safelyStrengthened = readInstalledBrainstormProjection(root);
|
|
2310
|
+
safelyStrengthened.agent += '\nThe specialist may never ask the user directly or contact the user. Do not write files or mutate shared task state. Do not delegate work or delegate tasks. The specialist is not permitted to launch Specs or run Develop.';
|
|
2311
|
+
assert.deepEqual(
|
|
2312
|
+
brainstormProjectionIssues(safelyStrengthened),
|
|
2313
|
+
[],
|
|
2314
|
+
'safe specialist prohibitions must not produce authority issues'
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
const mutations = [
|
|
2318
|
+
{
|
|
2319
|
+
name: 'adaptive-direct-precedence-removed', source: 'skill',
|
|
2320
|
+
from: 'Route Direct first, then\napply controls only to requests that remain in Brainstorm.',
|
|
2321
|
+
to: 'Apply controls before Direct classification.', expected: ['adaptive-direct-precedence']
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
name: 'adaptive-ordered-depth-removed', source: 'skill',
|
|
2325
|
+
from: 'With no Deep signal, use Standard.', to: 'Deep is always the default.',
|
|
2326
|
+
expected: ['adaptive-ordered-depth']
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
name: 'adaptive-leading-flags-removed', source: 'skill',
|
|
2330
|
+
from: 'Parse controls only from the leading consecutive token segment.',
|
|
2331
|
+
to: 'Parse flag-like tokens anywhere.', expected: ['adaptive-leading-flags']
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
name: 'adaptive-failure-isolation-removed', source: 'skill',
|
|
2335
|
+
from: 'failure isolation for partial or cascading failure\nacross boundaries',
|
|
2336
|
+
to: 'generic failure notes', expected: ['adaptive-lens-trigger-skip']
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: 'adaptive-evidence-fallback-removed', source: 'skill',
|
|
2340
|
+
from: 'Missing evidence\nforces feasibility `unknown` and confidence `low`.',
|
|
2341
|
+
to: 'Missing evidence permits a confident guess.', expected: ['adaptive-evidence-semantics']
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: 'adaptive-numeric-evidence-removed', source: 'skill',
|
|
2345
|
+
from: 'A numeric estimate requires\nrange, unit, basis, evidence, and assumptions; otherwise report `unknown`.',
|
|
2346
|
+
to: 'A numeric estimate may be a best-effort number.', expected: ['adaptive-numeric-estimates']
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
name: 'adaptive-pre-tool-redaction-removed', source: 'skill',
|
|
2350
|
+
from: 'Before an\nexternal visual tool or adviser handoff, minimize context and redact secrets,\ncredentials, private keys, access tokens, and unnecessary PII.',
|
|
2351
|
+
to: 'Forward full context to every external tool and adviser.', expected: ['adaptive-pre-tool-redaction']
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
name: 'adaptive-adviser-gate-removed', source: 'skill',
|
|
2355
|
+
from: '`--advice` invokes\n`brainstormer` only after the material-choice gate;',
|
|
2356
|
+
to: '`--advice` invokes `brainstormer` before routing;', expected: ['adaptive-adviser-gate']
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
name: 'adaptive-decision-freshness-removed', source: 'skill',
|
|
2360
|
+
from: 'The first section records target\nidentity, current source revision and worktree state or `[UNVERIFIED]`, an\nevidence-as-of value, and what change invalidates the brief.',
|
|
2361
|
+
to: 'The handoff has no revision or freshness binding.', expected: ['adaptive-decision-freshness']
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
name: 'adaptive-overlay-authority-removed', source: 'skill',
|
|
2365
|
+
from: 'Neither overlay\nwrites, approves, persists, dispatches, or completes work.',
|
|
2366
|
+
to: 'Overlays may persist, approve, dispatch, and complete work.', expected: ['adaptive-non-authority']
|
|
2367
|
+
},
|
|
2368
|
+
{
|
|
2369
|
+
name: 'debug-route-removed',
|
|
2370
|
+
source: 'skill',
|
|
2371
|
+
from: 'Then use `cf-debug` until',
|
|
2372
|
+
to: 'Skip diagnosis and choose a remedy before',
|
|
2373
|
+
expected: ['bug-routing']
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
name: 'fix-authority-removed',
|
|
2377
|
+
source: 'skill',
|
|
2378
|
+
from: 'only when the user explicitly requested a fix',
|
|
2379
|
+
to: 'automatically whenever root cause is known',
|
|
2380
|
+
expected: ['bug-routing']
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
name: 'exploration-stop-removed',
|
|
2384
|
+
source: 'skill',
|
|
2385
|
+
from: 'Do not request design approval, persist a report, or\n invoke another workflow without a new explicit request.',
|
|
2386
|
+
to: 'Request approval, persist a report, and invoke another workflow.',
|
|
2387
|
+
expected: ['exploration-stop']
|
|
2388
|
+
},
|
|
2389
|
+
{
|
|
2390
|
+
name: 'conditional-options-removed',
|
|
2391
|
+
source: 'skill',
|
|
2392
|
+
from: 'For a material choice, compare 2–3 mechanically distinct viable approaches',
|
|
2393
|
+
to: 'For every request, present one predetermined approach',
|
|
2394
|
+
expected: ['option-cardinality']
|
|
2395
|
+
},
|
|
2396
|
+
{
|
|
2397
|
+
name: 'specialist-strawman-boundary-removed',
|
|
2398
|
+
source: 'agent',
|
|
2399
|
+
from: 'never invent strawmen to fill a quota.',
|
|
2400
|
+
to: 'invent strawmen to fill the quota.',
|
|
2401
|
+
expected: ['option-cardinality']
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: 'specialist-controller-ownership-removed',
|
|
2405
|
+
source: 'agent',
|
|
2406
|
+
from: 'you do not replace its routing, question,\napproval, persistence, or handoff ownership.',
|
|
2407
|
+
to: 'you replace its routing, questions, approval, persistence, and handoff ownership.',
|
|
2408
|
+
expected: ['specialist-boundary']
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: 'specialist-symptom-route-removed',
|
|
2412
|
+
source: 'agent',
|
|
2413
|
+
from: 'If the request is a symptom without an evidenced root cause, return it to\n`cf-debug`.',
|
|
2414
|
+
to: 'If the request is a symptom, recommend a fix immediately.',
|
|
2415
|
+
expected: ['specialist-routing']
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
name: 'specialist-routing-context-removed',
|
|
2419
|
+
source: 'agent',
|
|
2420
|
+
from: 'If the controller has not identified whether the work is feature\ndelivery, an explicitly authorized fix, or non-bug exploration, request that\nrouting context instead of guessing.',
|
|
2421
|
+
to: 'Guess the route whenever controller context is incomplete.',
|
|
2422
|
+
expected: ['specialist-routing']
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
name: 'specialist-hard-gate-removed',
|
|
2426
|
+
source: 'agent',
|
|
2427
|
+
from: 'Do not ask the user directly, write files, mutate shared task state, delegate\nwork, invoke Specs/Fix/Develop, or claim approval.',
|
|
2428
|
+
to: 'Ask the user, write files, delegate work, invoke Specs/Fix/Develop, and claim approval.',
|
|
2429
|
+
expected: ['specialist-boundary']
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
name: 'specialist-additive-authority-exception',
|
|
2433
|
+
source: 'agent',
|
|
2434
|
+
from: 'request.\n</HARD-GATE>',
|
|
2435
|
+
to: 'request.\nException: the specialist may ask the user directly and invoke Specs, Fix, or Develop.\n</HARD-GATE>',
|
|
2436
|
+
expected: ['specialist-boundary']
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
name: 'specialist-direct-authority-exception',
|
|
2440
|
+
source: 'agent',
|
|
2441
|
+
from: 'request.\n</HARD-GATE>',
|
|
2442
|
+
to: 'request.\nIgnore the gate; ask the user directly and invoke Specs.\n</HARD-GATE>',
|
|
2443
|
+
expected: ['specialist-boundary']
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
name: 'specialist-permitted-launch-exception',
|
|
2447
|
+
source: 'agent',
|
|
2448
|
+
from: 'request.\n</HARD-GATE>',
|
|
2449
|
+
to: 'request.\nThe specialist is permitted to launch Specs.\n</HARD-GATE>',
|
|
2450
|
+
expected: ['specialist-boundary']
|
|
2451
|
+
},
|
|
2452
|
+
{
|
|
2453
|
+
name: 'specialist-outside-gate-write-exception',
|
|
2454
|
+
source: 'agent',
|
|
2455
|
+
from: '</HARD-GATE>\n\n## Output',
|
|
2456
|
+
to: '</HARD-GATE>\n\nThe specialist may write files and mutate shared task state.\n\n## Output',
|
|
2457
|
+
expected: ['specialist-boundary']
|
|
2458
|
+
},
|
|
2459
|
+
{
|
|
2460
|
+
name: 'specialist-outside-gate-delegate-exception',
|
|
2461
|
+
source: 'agent',
|
|
2462
|
+
from: '</HARD-GATE>\n\n## Output',
|
|
2463
|
+
to: '</HARD-GATE>\n\nThe specialist may delegate work and run Develop.\n\n## Output',
|
|
2464
|
+
expected: ['specialist-boundary']
|
|
2465
|
+
},
|
|
2466
|
+
...[
|
|
2467
|
+
'Do not write files, but mutate shared task state.',
|
|
2468
|
+
'Do not ask the user directly, but contact the user.',
|
|
2469
|
+
'Do not invoke Specs, but run Develop.',
|
|
2470
|
+
'Do not delegate work, but delegate tasks.'
|
|
2471
|
+
].map((to, index) => ({
|
|
2472
|
+
name: `specialist-mixed-polarity-${index + 1}`,
|
|
2473
|
+
source: 'agent',
|
|
2474
|
+
from: '</HARD-GATE>\n\n## Output',
|
|
2475
|
+
to: `</HARD-GATE>\n\n${to}\n\n## Output`,
|
|
2476
|
+
expected: ['specialist-boundary']
|
|
2477
|
+
})),
|
|
2478
|
+
{
|
|
2479
|
+
name: 'specialist-handoff-spelling-exception',
|
|
2480
|
+
source: 'agent',
|
|
2481
|
+
from: '</HARD-GATE>\n\n## Output',
|
|
2482
|
+
to: '</HARD-GATE>\n\nThe specialist may handoff to Specs.\n\n## Output',
|
|
2483
|
+
expected: ['specialist-boundary']
|
|
2484
|
+
},
|
|
2485
|
+
{
|
|
2486
|
+
name: 'specialist-handoff-authority-removed',
|
|
2487
|
+
source: 'agent',
|
|
2488
|
+
from: 'Non-bug exploration may end\nin chat; feature/docs delivery may only prepare a future explicit Specs\ninvocation; bug handoff requires evidenced root cause and the user\'s explicit fix\nrequest.',
|
|
2489
|
+
to: 'Every route may invoke delivery workflows automatically.',
|
|
2490
|
+
expected: ['specialist-handoff']
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
name: 'decision-provenance-removed',
|
|
2494
|
+
source: 'framework',
|
|
2495
|
+
from: 'Do not write "user selected" unless direct user text or the native input tool\n confirms it.',
|
|
2496
|
+
to: 'Write "user selected" for inferred defaults.',
|
|
2497
|
+
expected: ['approval-persistence']
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
name: 'installed-grammar-corruption',
|
|
2501
|
+
source: 'skill',
|
|
2502
|
+
from: '## Completion bar',
|
|
2503
|
+
to: 'one a structured user-input request call\n\n## Completion bar',
|
|
2504
|
+
expected: ['projection-grammar']
|
|
2505
|
+
}
|
|
2506
|
+
];
|
|
2507
|
+
|
|
2508
|
+
for (const mutation of mutations) {
|
|
2509
|
+
const target = assertInstalledBrainstormMutationTarget(root, mutation.source);
|
|
2510
|
+
const original = fs.readFileSync(target);
|
|
2511
|
+
const fullContent = original.toString('utf8');
|
|
2512
|
+
const projectedContent = mutation.source === 'agent'
|
|
2513
|
+
? parseGeneratedTomlString(fullContent, 'developer_instructions')
|
|
2514
|
+
: fullContent;
|
|
2515
|
+
const anchor = projectedContent.indexOf(mutation.from);
|
|
2516
|
+
assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
|
|
2517
|
+
assert.equal(
|
|
2518
|
+
projectedContent.indexOf(mutation.from, anchor + mutation.from.length),
|
|
2519
|
+
-1,
|
|
2520
|
+
`${mutation.name} mutation anchor must be unique`
|
|
2521
|
+
);
|
|
2522
|
+
const weakened = `${projectedContent.slice(0, anchor)}${mutation.to}${projectedContent.slice(anchor + mutation.from.length)}`;
|
|
2523
|
+
const rendered = mutation.source === 'agent'
|
|
2524
|
+
? replaceGeneratedTomlString(fullContent, 'developer_instructions', weakened)
|
|
2525
|
+
: weakened;
|
|
2526
|
+
try {
|
|
2527
|
+
fs.writeFileSync(assertInstalledBrainstormMutationTarget(root, mutation.source), rendered);
|
|
2528
|
+
assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
|
|
2529
|
+
assert.deepEqual(
|
|
2530
|
+
brainstormProjectionIssues(readInstalledBrainstormProjection(root)),
|
|
2531
|
+
mutation.expected,
|
|
2532
|
+
mutation.name
|
|
2533
|
+
);
|
|
2534
|
+
} finally {
|
|
2535
|
+
fs.writeFileSync(assertInstalledBrainstormMutationTarget(root, mutation.source), original);
|
|
2536
|
+
}
|
|
2537
|
+
assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
|
|
2538
|
+
assert.deepEqual(brainstormProjectionIssues(readInstalledBrainstormProjection(root)), []);
|
|
2539
|
+
assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
test('Codex installed Develop preserves plan-native execution and references', () => {
|
|
2545
|
+
inTempProject((root) => {
|
|
2546
|
+
const sourceBytes = new Map(DEVELOP_BUNDLE.map((relative) => [
|
|
2547
|
+
path.join(DEVELOP_SOURCE_ROOT, relative),
|
|
2548
|
+
fs.readFileSync(path.join(DEVELOP_SOURCE_ROOT, relative))
|
|
2549
|
+
]));
|
|
2550
|
+
const result = install(root);
|
|
2551
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2552
|
+
|
|
2553
|
+
const installedRoot = path.join(root, '.agents/skills/develop');
|
|
2554
|
+
for (const relative of DEVELOP_BUNDLE) {
|
|
2555
|
+
const sourcePath = path.join(DEVELOP_SOURCE_ROOT, relative);
|
|
2556
|
+
assert.equal(
|
|
2557
|
+
fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
|
|
2558
|
+
normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
|
|
2559
|
+
`Codex Develop projection drifted: ${relative}`
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
const installedSkill = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
|
|
2563
|
+
assert.match(installedSkill, /\$cf-develop <feature>/);
|
|
2564
|
+
assert.match(installedSkill, /\.codex\/scripts\/workflow-policy\.cjs/);
|
|
2565
|
+
assert.doesNotMatch(installedSkill, /\/cf:develop|\.claude\/scripts/);
|
|
2566
|
+
assert.equal(fs.existsSync(path.join(root, '.claude/skills/develop')), false);
|
|
2567
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
2568
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
test('Codex installed Research preserves adaptive evidence semantics', () => {
|
|
2574
|
+
inTempProject((root) => {
|
|
2575
|
+
const sourceBytes = new Map([
|
|
2576
|
+
[RESEARCH_SKILL_SOURCE_PATH, fs.readFileSync(RESEARCH_SKILL_SOURCE_PATH)],
|
|
2577
|
+
[RESEARCH_AGENT_SOURCE_PATH, fs.readFileSync(RESEARCH_AGENT_SOURCE_PATH)],
|
|
2578
|
+
]);
|
|
2579
|
+
const result = install(root);
|
|
2580
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2581
|
+
|
|
2582
|
+
const installedSkillPath = path.join(root, '.agents/skills/research/SKILL.md');
|
|
2583
|
+
const installedAgentPath = path.join(root, '.codex/agents/researcher.toml');
|
|
2584
|
+
assert.equal(
|
|
2585
|
+
fs.readFileSync(installedSkillPath, 'utf8'),
|
|
2586
|
+
normalizeCodexBody(fs.readFileSync(RESEARCH_SKILL_SOURCE_PATH, 'utf8'), RESEARCH_SKILL_SOURCE_PATH)
|
|
2587
|
+
);
|
|
2588
|
+
assert.equal(
|
|
2589
|
+
fs.readFileSync(installedAgentPath, 'utf8'),
|
|
2590
|
+
convertCodexAgentContent(fs.readFileSync(RESEARCH_AGENT_SOURCE_PATH, 'utf8'), 'researcher.md')
|
|
2591
|
+
);
|
|
2592
|
+
const installedSkill = fs.readFileSync(installedSkillPath, 'utf8');
|
|
2593
|
+
const installedAgent = parseGeneratedTomlString(
|
|
2594
|
+
fs.readFileSync(installedAgentPath, 'utf8'), 'developer_instructions'
|
|
2595
|
+
);
|
|
2596
|
+
assert.match(installedSkill, /^name: cf-research$/m);
|
|
2597
|
+
assert.match(installedSkill, /Quick \| One low-risk, reversible fact or known option/);
|
|
2598
|
+
assert.match(installedSkill, /research sequentially with the same evidence bar/);
|
|
2599
|
+
assert.match(installedSkill, /Default to a concise answer in chat/);
|
|
2600
|
+
assert.match(installedSkill, /confirmed \| inferred \| unresolved/);
|
|
2601
|
+
assert.match(installedAgent, /do not implement code,\s+write files, mutate task state/);
|
|
2602
|
+
assert.match(installedAgent, /owns any\s+authorized persistence/);
|
|
2603
|
+
assert.doesNotMatch(installedSkill, /must (?:instantly |always )?delegate/i);
|
|
2604
|
+
|
|
2605
|
+
const manifest = JSON.parse(
|
|
2606
|
+
fs.readFileSync(path.join(root, '.codex/cafekit-manifest.json'), 'utf8')
|
|
2607
|
+
);
|
|
2608
|
+
assert.ok(manifest.files['.agents/skills/research/SKILL.md']);
|
|
2609
|
+
assert.ok(manifest.files['.codex/agents/researcher.toml']);
|
|
2610
|
+
assert.equal(fs.existsSync(path.join(root, '.claude/skills/research')), false);
|
|
2611
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
2612
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
test('Codex installed Test preserves plan-native proof and references', () => {
|
|
2618
|
+
inTempProject((root) => {
|
|
2619
|
+
const sourcePaths = [
|
|
2620
|
+
...TEST_BUNDLE.map((relative) => path.join(TEST_SOURCE_ROOT, relative)),
|
|
2621
|
+
TEST_RUNNER_SOURCE_PATH,
|
|
2622
|
+
path.join(CODE_REVIEW_SOURCE_ROOT, 'SKILL.md')
|
|
2623
|
+
];
|
|
2624
|
+
const sourceBytes = new Map(sourcePaths.map((sourcePath) => [
|
|
2625
|
+
sourcePath, fs.readFileSync(sourcePath)
|
|
2626
|
+
]));
|
|
2627
|
+
const result = install(root);
|
|
2628
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2629
|
+
|
|
2630
|
+
const installedRoot = path.join(root, '.agents/skills/test');
|
|
2631
|
+
for (const relative of TEST_BUNDLE) {
|
|
2632
|
+
const sourcePath = path.join(TEST_SOURCE_ROOT, relative);
|
|
2633
|
+
assert.equal(
|
|
2634
|
+
fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
|
|
2635
|
+
normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
|
|
2636
|
+
`Codex Test projection drifted: ${relative}`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
const installedSkill = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
|
|
2640
|
+
assert.match(installedSkill, /\$cf-test/);
|
|
2641
|
+
assert.match(installedSkill, /test-proof-v1/);
|
|
2642
|
+
assert.doesNotMatch(installedSkill, /\/cf:test/);
|
|
2643
|
+
|
|
2644
|
+
const installedRunnerPath = path.join(root, '.codex/agents/test_runner.toml');
|
|
2645
|
+
assert.equal(
|
|
2646
|
+
fs.readFileSync(installedRunnerPath, 'utf8'),
|
|
2647
|
+
convertCodexAgentContent(
|
|
2648
|
+
fs.readFileSync(TEST_RUNNER_SOURCE_PATH, 'utf8'),
|
|
2649
|
+
path.basename(TEST_RUNNER_SOURCE_PATH)
|
|
2650
|
+
),
|
|
2651
|
+
'Codex test_runner projection drifted'
|
|
2652
|
+
);
|
|
2653
|
+
const installedRunner = parseGeneratedTomlString(
|
|
2654
|
+
fs.readFileSync(installedRunnerPath, 'utf8'), 'developer_instructions'
|
|
2655
|
+
);
|
|
2656
|
+
assert.match(installedRunner, /test-proof-v1/);
|
|
2657
|
+
assert.match(installedRunner, /sole writer of[\s\S]*Status and inline Receipt/i);
|
|
2658
|
+
|
|
2659
|
+
const reviewSourcePath = path.join(CODE_REVIEW_SOURCE_ROOT, 'SKILL.md');
|
|
2660
|
+
const installedReview = fs.readFileSync(path.join(root, '.agents/skills/code-review/SKILL.md'), 'utf8');
|
|
2661
|
+
assert.equal(installedReview, normalizeCodexBody(
|
|
2662
|
+
fs.readFileSync(reviewSourcePath, 'utf8'), reviewSourcePath
|
|
2663
|
+
));
|
|
2664
|
+
assert.match(installedReview, /controller-validated `test-proof-v1`/);
|
|
2665
|
+
assert.match(installedReview, /Never create or search for a separate[\s\S]*process-first receipt/i);
|
|
2666
|
+
assert.equal(fs.existsSync(path.join(root, '.claude/skills/test')), false);
|
|
2667
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
2668
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
function hotfixProjectionIssues(files) {
|
|
2674
|
+
const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
2675
|
+
const skill = compact(files.skill);
|
|
2676
|
+
const review = compact(files.review);
|
|
2677
|
+
const parallel = compact(files.parallel);
|
|
2678
|
+
const specialized = compact(files.specialized);
|
|
2679
|
+
const issues = new Set();
|
|
2680
|
+
if (!skill.includes('name: cf-fix')
|
|
2681
|
+
|| !skill.includes('# Fix — root-cause repair workflow')
|
|
2682
|
+
|| skill.includes('name: cf-hotfix')) {
|
|
2683
|
+
issues.add('public-rename');
|
|
2684
|
+
}
|
|
2685
|
+
if (!skill.includes('## Proportional depth')
|
|
2686
|
+
|| !skill.includes('Quick mode only reduces depth; it never skips scout, pre-fix evidence, diagnosis, or before/after verification.')) {
|
|
2687
|
+
issues.add('adaptive-depth');
|
|
2688
|
+
}
|
|
2689
|
+
if (!skill.includes('`Timeline: skipped - <reason>` or `- skipped: <reason>`')
|
|
2690
|
+
|| !skill.includes('`Recurrence-Prevention Handoff`, when present, carries evidence-backed candidates only.')
|
|
2691
|
+
|| !skill.includes('routes back to diagnosis (`cf-debug`)')) {
|
|
2692
|
+
issues.add('debug-handoff');
|
|
2693
|
+
}
|
|
2694
|
+
if (!skill.includes('report `PASS | PASS_WITH_WARNINGS | FAIL | BLOCKED`')
|
|
2695
|
+
|| !skill.includes('The definition of `PASS` defers to `cf-code-review`.')
|
|
2696
|
+
|| skill.includes('Confidence score')
|
|
2697
|
+
|| !review.includes('Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry')
|
|
2698
|
+
|| !review.includes('only a fresh literal `PASS` enters finalization')
|
|
2699
|
+
|| review.includes('"Approve anyway"')
|
|
2700
|
+
|| review.includes('"Approve with known issues"')
|
|
2701
|
+
|| review.includes('at most one Medium')) {
|
|
2702
|
+
issues.add('verdict-surface');
|
|
2703
|
+
}
|
|
2704
|
+
if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
|
|
2705
|
+
|| !skill.includes('The active runtime exposes an Explore/delegation capability.')
|
|
2706
|
+
|| !skill.includes('at least two distinct, non-overlapping scopes')
|
|
2707
|
+
|| !parallel.includes('The user explicitly requested or permitted delegation or parallel agents.')) {
|
|
2708
|
+
issues.add('delegation-gate');
|
|
2709
|
+
}
|
|
2710
|
+
if (!skill.includes('The original symptom no longer reproduces with the exact pre-fix command/user flow.')
|
|
2711
|
+
|| !skill.includes('Do not silently patch around the regression.')) {
|
|
2712
|
+
issues.add('side-effect-gate');
|
|
2713
|
+
}
|
|
2714
|
+
if (!skill.includes('## Bounded repair frame')
|
|
2715
|
+
|| !skill.includes('Quick/local does not add a separate framing ceremony')
|
|
2716
|
+
|| !skill.includes('**Outcome:**')
|
|
2717
|
+
|| !skill.includes('**Constraints:**')
|
|
2718
|
+
|| !skill.includes('**Non-goals:**')
|
|
2719
|
+
|| !skill.includes('**Acceptance:**')) {
|
|
2720
|
+
issues.add('bounded-repair-frame');
|
|
2721
|
+
}
|
|
2722
|
+
if (!skill.includes('after diagnosis, research only unresolved external facts')
|
|
2723
|
+
|| !skill.includes('`cf-brainstorm` to compare 2-3 options')
|
|
2724
|
+
|| !skill.includes('When diagnosis leaves one safe direct repair, skip research and')) {
|
|
2725
|
+
issues.add('deep-decision-route');
|
|
2726
|
+
}
|
|
2727
|
+
if (!skill.includes('Load only the matching')
|
|
2728
|
+
|| !specialized.includes('Load only the matching section')
|
|
2729
|
+
|| (specialized.match(/\*\*Baseline:\*\*/g) || []).length < 5
|
|
2730
|
+
|| (specialized.match(/\*\*Proof:\*\*/g) || []).length < 5) {
|
|
2731
|
+
issues.add('specialized-proof-overlays');
|
|
2732
|
+
}
|
|
2733
|
+
if (!parallel.includes('Diagnosis still starts only')
|
|
2734
|
+
|| !parallel.includes('after the required scout outputs are synthesized')
|
|
2735
|
+
|| !parallel.includes('Research begins only after Step 2 diagnosis')
|
|
2736
|
+
|| parallel.includes('scout + diagnose + research together')
|
|
2737
|
+
|| parallel.includes("You don't need to wait for scouting")) {
|
|
2738
|
+
issues.add('scout-before-diagnosis');
|
|
2739
|
+
}
|
|
2740
|
+
return [...issues].sort();
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
test('Codex installed Fix preserves the adaptive repair contract', () => {
|
|
2744
|
+
inTempProject((root) => {
|
|
2745
|
+
const sourceBytes = new Map(HOTFIX_BUNDLE.map((relative) => [
|
|
2746
|
+
path.join(HOTFIX_SOURCE_ROOT, relative),
|
|
2747
|
+
fs.readFileSync(path.join(HOTFIX_SOURCE_ROOT, relative))
|
|
2748
|
+
]));
|
|
2749
|
+
const result = install(root);
|
|
2750
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2751
|
+
|
|
2752
|
+
const installedRoot = path.join(root, '.agents/skills/hotfix');
|
|
2753
|
+
assert.equal(fs.existsSync(path.join(root, '.agents/skills/fix')), false,
|
|
2754
|
+
'public Fix rename must keep the manifest-owned hotfix directory');
|
|
2755
|
+
for (const relative of HOTFIX_BUNDLE) {
|
|
2756
|
+
const sourcePath = path.join(HOTFIX_SOURCE_ROOT, relative);
|
|
2757
|
+
assert.equal(
|
|
2758
|
+
fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
|
|
2759
|
+
normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
|
|
2760
|
+
`Codex Fix projection drifted: ${relative}`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
const readProjection = () => ({
|
|
2764
|
+
skill: fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8'),
|
|
2765
|
+
review: fs.readFileSync(path.join(installedRoot, 'references/review-cycle.md'), 'utf8'),
|
|
2766
|
+
parallel: fs.readFileSync(path.join(installedRoot, 'references/parallel-patterns.md'), 'utf8'),
|
|
2767
|
+
specialized: fs.readFileSync(path.join(installedRoot, 'references/workflow-specialized.md'), 'utf8'),
|
|
2768
|
+
});
|
|
2769
|
+
const installedSkill = readProjection().skill;
|
|
2770
|
+
assert.match(installedSkill, /name: cf-fix/);
|
|
2771
|
+
assert.doesNotMatch(installedSkill, /name: cf-hotfix|\/cf:fix|cf:debug|cf:code-review/);
|
|
2772
|
+
assert.deepEqual(hotfixProjectionIssues(readProjection()), []);
|
|
2773
|
+
|
|
2774
|
+
const mutations = [
|
|
2775
|
+
{
|
|
2776
|
+
name: 'public-name-reverts', file: 'SKILL.md',
|
|
2777
|
+
from: 'name: cf-fix', to: 'name: cf-hotfix',
|
|
2778
|
+
expected: ['public-rename']
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
name: 'quick-skips-diagnosis', file: 'SKILL.md',
|
|
2782
|
+
from: 'Quick mode only reduces depth', to: 'Quick mode may shorten scope',
|
|
2783
|
+
expected: ['adaptive-depth']
|
|
2784
|
+
},
|
|
2785
|
+
{
|
|
2786
|
+
name: 'handoff-drops-skip-forms', file: 'SKILL.md',
|
|
2787
|
+
from: '`Timeline: skipped - <reason>` or `- skipped: <reason>`', to: '`Timeline: omitted`',
|
|
2788
|
+
expected: ['debug-handoff']
|
|
2789
|
+
},
|
|
2790
|
+
{
|
|
2791
|
+
name: 'warnings-auto-accept', file: 'references/review-cycle.md',
|
|
2792
|
+
from: 'Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry',
|
|
2793
|
+
to: 'Only `FAIL` enters remediation retry; `PASS_WITH_WARNINGS` auto-approves',
|
|
2794
|
+
expected: ['verdict-surface']
|
|
2795
|
+
},
|
|
2796
|
+
{
|
|
2797
|
+
name: 'warnings-user-approve', file: 'references/review-cycle.md',
|
|
2798
|
+
from: 'only a fresh literal `PASS` enters finalization', to: '"Approve anyway" enters finalization',
|
|
2799
|
+
expected: ['verdict-surface']
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
name: 'pass-redefined-locally', file: 'references/review-cycle.md',
|
|
2803
|
+
from: 'The definition of `PASS` defers to `cf-code-review`; Fix never redefines it with local severity thresholds.',
|
|
2804
|
+
to: 'The definition of `PASS` is local: no Critical, no High, at most one Medium.',
|
|
2805
|
+
expected: ['verdict-surface']
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
name: 'confidence-score-returns', file: 'SKILL.md',
|
|
2809
|
+
from: '**Report:** root cause, changes made',
|
|
2810
|
+
to: '**Report:** Confidence score, root cause, changes made',
|
|
2811
|
+
expected: ['verdict-surface']
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
name: 'delegation-loses-user-clause', file: 'SKILL.md',
|
|
2815
|
+
from: 'The user explicitly requested or permitted delegation or parallel agents.',
|
|
2816
|
+
to: 'Delegation is at the agent\'s discretion.',
|
|
2817
|
+
expected: ['delegation-gate']
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: 'sweep-permits-silent-patch', file: 'SKILL.md',
|
|
2821
|
+
from: 'Do not silently patch around the regression.', to: 'Patch around regressions quietly.',
|
|
2822
|
+
expected: ['side-effect-gate']
|
|
2823
|
+
},
|
|
2824
|
+
{
|
|
2825
|
+
name: 'quick-gains-mandatory-frame', file: 'SKILL.md',
|
|
2826
|
+
from: 'Quick/local does not add a separate framing ceremony', to: 'Quick/local always requires a separate framing ceremony',
|
|
2827
|
+
expected: ['bounded-repair-frame']
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
name: 'deep-researches-before-diagnosis', file: 'SKILL.md',
|
|
2831
|
+
from: 'after diagnosis, research only unresolved external facts', to: 'research broadly before diagnosis',
|
|
2832
|
+
expected: ['deep-decision-route']
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
name: 'overlays-load-everything', file: 'references/workflow-specialized.md',
|
|
2836
|
+
from: 'Load only the matching section', to: 'Load every section',
|
|
2837
|
+
expected: ['specialized-proof-overlays']
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
name: 'diagnosis-starts-before-scout', file: 'references/parallel-patterns.md',
|
|
2841
|
+
from: 'Diagnosis still starts only', to: 'Diagnosis may start',
|
|
2842
|
+
expected: ['scout-before-diagnosis']
|
|
2843
|
+
},
|
|
2844
|
+
{
|
|
2845
|
+
name: 'research-starts-before-diagnosis', file: 'references/parallel-patterns.md',
|
|
2846
|
+
from: 'Research begins only after Step 2 diagnosis', to: 'Research may begin before Step 2 diagnosis',
|
|
2847
|
+
expected: ['scout-before-diagnosis']
|
|
2848
|
+
}
|
|
2849
|
+
];
|
|
2850
|
+
for (const mutation of mutations) {
|
|
2851
|
+
const target = path.join(installedRoot, mutation.file);
|
|
2852
|
+
const original = fs.readFileSync(target);
|
|
2853
|
+
const content = original.toString('utf8');
|
|
2854
|
+
const anchor = content.indexOf(mutation.from);
|
|
2855
|
+
assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
|
|
2856
|
+
assert.equal(
|
|
2857
|
+
content.indexOf(mutation.from, anchor + mutation.from.length),
|
|
2858
|
+
-1,
|
|
2859
|
+
`${mutation.name} mutation anchor must be unique`
|
|
2860
|
+
);
|
|
2861
|
+
const weakened = `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`;
|
|
2862
|
+
try {
|
|
2863
|
+
fs.writeFileSync(target, weakened);
|
|
2864
|
+
assert.deepEqual(hotfixProjectionIssues(readProjection()), mutation.expected, mutation.name);
|
|
2865
|
+
} finally {
|
|
2866
|
+
fs.writeFileSync(target, original);
|
|
2867
|
+
}
|
|
2868
|
+
assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
|
|
2869
|
+
assert.deepEqual(hotfixProjectionIssues(readProjection()), []);
|
|
2870
|
+
}
|
|
2871
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
2872
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
function docsProjectionIssues(files) {
|
|
2878
|
+
const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
2879
|
+
const skill = compact(files.skill);
|
|
2880
|
+
const init = compact(files.init);
|
|
2881
|
+
const update = compact(files.update);
|
|
2882
|
+
const standard = compact(files.standard);
|
|
2883
|
+
const issues = new Set();
|
|
2884
|
+
if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
|
|
2885
|
+
|| !skill.includes('The active runtime exposes an Explore/delegation capability.')
|
|
2886
|
+
|| !skill.includes('at least two distinct, non-overlapping scopes')
|
|
2887
|
+
|| !skill.includes('Otherwise continue sequentially in the main agent')
|
|
2888
|
+
|| !skill.includes('only through the Delegation Gate above')
|
|
2889
|
+
|| !init.includes('only through the Delegation Gate in `../SKILL.md`')
|
|
2890
|
+
|| !update.includes('only through the Delegation Gate in `../SKILL.md`')
|
|
2891
|
+
|| skill.includes('when delegation is available')
|
|
2892
|
+
|| init.includes('when delegation is available')
|
|
2893
|
+
|| update.includes('when delegation is available')) {
|
|
2894
|
+
issues.add('delegation-gate');
|
|
2895
|
+
}
|
|
2896
|
+
if (!skill.includes('## Post-Task Docs Checkpoint')
|
|
2897
|
+
|| !skill.includes('`none`: report that no docs change is needed and edit nothing.')
|
|
2898
|
+
|| !skill.includes('A checkpoint never invents a new document')
|
|
2899
|
+
|| !skill.includes('never auto-selects `init` and overrides')
|
|
2900
|
+
|| !skill.includes('report the gap and recommend an explicit')
|
|
2901
|
+
|| !standard.includes('checkpoint contract in `../SKILL.md`')
|
|
2902
|
+
|| skill.includes('checkpoint may create')) {
|
|
2903
|
+
issues.add('docs-checkpoint');
|
|
2904
|
+
}
|
|
2905
|
+
if (!skill.includes('Type: Observed | Inferred | Unknown')
|
|
2906
|
+
|| !skill.includes('Confidence: High | Medium | Low')
|
|
2907
|
+
|| skill.includes('evidence is optional')) {
|
|
2908
|
+
issues.add('evidence-taxonomy');
|
|
2909
|
+
}
|
|
2910
|
+
if (!skill.includes('## Reconstruction Is Not Specs')
|
|
2911
|
+
|| skill.includes('are advisory')) {
|
|
2912
|
+
issues.add('reconstruction-boundary');
|
|
2913
|
+
}
|
|
2914
|
+
return [...issues].sort();
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
test('Codex installed Docs preserves the adaptive contract', () => {
|
|
2918
|
+
inTempProject((root) => {
|
|
2919
|
+
const sourceBytes = new Map(DOCS_ADAPTIVE_BUNDLE.map((relative) => [
|
|
2920
|
+
path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative),
|
|
2921
|
+
fs.readFileSync(path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative))
|
|
2922
|
+
]));
|
|
2923
|
+
const result = install(root, ['--with-document-skills']);
|
|
2924
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2925
|
+
|
|
2926
|
+
const installedRoot = path.join(root, '.agents/skills/docs');
|
|
2927
|
+
for (const relative of DOCS_ADAPTIVE_BUNDLE) {
|
|
2928
|
+
const sourcePath = path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative);
|
|
2929
|
+
assert.equal(
|
|
2930
|
+
fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
|
|
2931
|
+
normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
|
|
2932
|
+
`Codex Docs projection drifted: ${relative}`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
const readProjection = () => ({
|
|
2936
|
+
skill: fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8'),
|
|
2937
|
+
init: fs.readFileSync(path.join(installedRoot, 'references/init-workflow.md'), 'utf8'),
|
|
2938
|
+
update: fs.readFileSync(path.join(installedRoot, 'references/update-workflow.md'), 'utf8'),
|
|
2939
|
+
standard: fs.readFileSync(path.join(installedRoot, 'references/standard-docs-workflow.md'), 'utf8'),
|
|
2940
|
+
});
|
|
2941
|
+
assert.match(readProjection().skill, /name: cf-docs/);
|
|
2942
|
+
assert.deepEqual(docsProjectionIssues(readProjection()), []);
|
|
2943
|
+
|
|
2944
|
+
const mutations = [
|
|
2945
|
+
{
|
|
2946
|
+
name: 'gate-loses-user-clause', file: 'SKILL.md',
|
|
2947
|
+
from: 'The user explicitly requested or permitted delegation or parallel agents.',
|
|
2948
|
+
to: 'Delegation is at the agent\'s discretion.',
|
|
2949
|
+
expected: ['delegation-gate']
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
name: 'keeper-ungated-returns', file: 'SKILL.md',
|
|
2953
|
+
from: 'only through the Delegation Gate above', to: 'when delegation is available',
|
|
2954
|
+
expected: ['delegation-gate']
|
|
2955
|
+
},
|
|
2956
|
+
{
|
|
2957
|
+
name: 'checkpoint-invents-doc', file: 'SKILL.md',
|
|
2958
|
+
from: 'A checkpoint never invents a new document', to: 'A checkpoint may create missing documents',
|
|
2959
|
+
expected: ['docs-checkpoint']
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
name: 'taxonomy-drops-inferred', file: 'SKILL.md',
|
|
2963
|
+
from: 'Type: Observed | Inferred | Unknown', to: 'Type: Observed | Unknown',
|
|
2964
|
+
expected: ['evidence-taxonomy']
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
name: 'reconstruction-becomes-advisory', file: 'SKILL.md',
|
|
2968
|
+
from: '## Reconstruction Is Not Specs',
|
|
2969
|
+
to: '## Reconstruction Is Not Specs\n\nThe prohibitions below are advisory.',
|
|
2970
|
+
expected: ['reconstruction-boundary']
|
|
2971
|
+
}
|
|
2972
|
+
];
|
|
2973
|
+
for (const mutation of mutations) {
|
|
2974
|
+
const target = path.join(installedRoot, mutation.file);
|
|
2975
|
+
const original = fs.readFileSync(target);
|
|
2976
|
+
const content = original.toString('utf8');
|
|
2977
|
+
const anchor = content.indexOf(mutation.from);
|
|
2978
|
+
assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
|
|
2979
|
+
assert.equal(
|
|
2980
|
+
content.indexOf(mutation.from, anchor + mutation.from.length),
|
|
2981
|
+
-1,
|
|
2982
|
+
`${mutation.name} mutation anchor must be unique`
|
|
2983
|
+
);
|
|
2984
|
+
const weakened = `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`;
|
|
2985
|
+
try {
|
|
2986
|
+
fs.writeFileSync(target, weakened);
|
|
2987
|
+
assert.deepEqual(docsProjectionIssues(readProjection()), mutation.expected, mutation.name);
|
|
2988
|
+
} finally {
|
|
2989
|
+
fs.writeFileSync(target, original);
|
|
2990
|
+
}
|
|
2991
|
+
assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
|
|
2992
|
+
assert.deepEqual(docsProjectionIssues(readProjection()), []);
|
|
2993
|
+
}
|
|
2994
|
+
for (const [sourcePath, expected] of sourceBytes) {
|
|
2995
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
});
|
|
2999
|
+
|
|
876
3000
|
test('Codex install on top of existing Claude installation preserves content and adds Codex', () => {
|
|
877
3001
|
inTempProject((root) => {
|
|
878
3002
|
// Setup existing Claude installation with content
|
|
@@ -961,3 +3085,134 @@ test('Codex scaffold resolver rejects symlink template', () => {
|
|
|
961
3085
|
fs.writeFileSync(template, original);
|
|
962
3086
|
});
|
|
963
3087
|
});
|
|
3088
|
+
|
|
3089
|
+
test('Claude and Codex installed Route preserve proportional live-catalog semantics', () => {
|
|
3090
|
+
inTempProject((root) => {
|
|
3091
|
+
const result = installPlatforms(root, ['claude', 'codex']);
|
|
3092
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
3093
|
+
const sourceRoot = path.join(PACKAGE_ROOT, 'src/claude/skills/route');
|
|
3094
|
+
const expectedFiles = [
|
|
3095
|
+
'SKILL.md', 'references/task-taxonomy.md',
|
|
3096
|
+
'references/chaining-patterns.md', 'references/agent-timing.md'
|
|
3097
|
+
];
|
|
3098
|
+
const expectedRules = ['skill-workflow-routing.md', 'skill-domain-routing.md'];
|
|
3099
|
+
for (const runtime of ['claude', 'codex']) {
|
|
3100
|
+
const installedRoot = path.join(root, runtime === 'codex' ? '.agents/skills/route' : '.claude/skills/route');
|
|
3101
|
+
for (const relative of expectedFiles) {
|
|
3102
|
+
const source = fs.readFileSync(path.join(sourceRoot, relative), 'utf8');
|
|
3103
|
+
const installed = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
|
|
3104
|
+
const expected = runtime === 'codex'
|
|
3105
|
+
? normalizeCodexBody(source, path.join(sourceRoot, relative))
|
|
3106
|
+
: source;
|
|
3107
|
+
assert.equal(installed, expected, `${runtime}:${relative}`);
|
|
3108
|
+
}
|
|
3109
|
+
for (const relative of expectedRules) {
|
|
3110
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'src/claude/rules', relative);
|
|
3111
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
3112
|
+
const installed = fs.readFileSync(path.join(
|
|
3113
|
+
root, runtime === 'codex' ? '.codex/rules' : '.claude/rules', relative
|
|
3114
|
+
), 'utf8');
|
|
3115
|
+
const expected = runtime === 'codex' ? normalizeCodexBody(source, sourcePath) : source;
|
|
3116
|
+
assert.equal(installed, expected, `${runtime}:rules/${relative}`);
|
|
3117
|
+
}
|
|
3118
|
+
const route = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
|
|
3119
|
+
assert.match(route, /names a valid installed skill[\s\S]*one installed skill clearly covers[\s\S]*Direct factual conversation/);
|
|
3120
|
+
assert.match(route, /highest-link[\s\S]*risk[\s\S]*number of material domains/);
|
|
3121
|
+
assert.match(route, /never expand it/);
|
|
3122
|
+
const catalog = installedCatalog(root, runtime);
|
|
3123
|
+
assert.ok(catalog.skills.some((skill) => skill.public_id === 'cf:route'));
|
|
3124
|
+
assert.equal(catalog.skills.some((skill) => skill.public_id === 'cf:docs'), false);
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
});
|
|
3128
|
+
|
|
3129
|
+
test('combined installs bind each catalog to its native runtime inventory', () => {
|
|
3130
|
+
inTempProject((root) => {
|
|
3131
|
+
const result = installPlatforms(root, ['claude', 'codex']);
|
|
3132
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
3133
|
+
fs.cpSync(
|
|
3134
|
+
path.join(PACKAGE_ROOT, 'src/claude/skills/docs'),
|
|
3135
|
+
path.join(root, '.claude/skills/docs'),
|
|
3136
|
+
{ recursive: true }
|
|
3137
|
+
);
|
|
3138
|
+
const claude = installedCatalog(root, 'claude');
|
|
3139
|
+
const codex = installedCatalog(root, 'codex');
|
|
3140
|
+
assert.equal(claude.root, fs.realpathSync(path.join(root, '.claude/skills')));
|
|
3141
|
+
assert.equal(codex.root, fs.realpathSync(path.join(root, '.agents/skills')));
|
|
3142
|
+
assert.equal(claude.skills.some((skill) => skill.public_id === 'cf:docs'), true);
|
|
3143
|
+
assert.equal(codex.skills.some((skill) => skill.public_id === 'cf:docs'), false);
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
test('Claude and Codex installed rules preserve the ported review and process guidance', () => {
|
|
3148
|
+
inTempProject((root) => {
|
|
3149
|
+
const result = installPlatforms(root, ['claude', 'codex']);
|
|
3150
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
3151
|
+
const ported = ['review-audit-self-decision.md', 'process-management.md'];
|
|
3152
|
+
const renameOnly = (content) => content
|
|
3153
|
+
.replace(/\/cf:([a-z0-9-]+)/gi, (_match, name) => `$cf-${name}`)
|
|
3154
|
+
.replace(/\bcf:([a-z0-9-]+)/gi, (_match, name) => `cf-${name}`);
|
|
3155
|
+
for (const relative of ported) {
|
|
3156
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'src/claude/rules', relative);
|
|
3157
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
3158
|
+
assert.equal(
|
|
3159
|
+
fs.existsSync(path.join(PACKAGE_ROOT, 'src/codex/rules', relative)),
|
|
3160
|
+
false,
|
|
3161
|
+
`${relative} must not be shadowed by a Codex native override`
|
|
3162
|
+
);
|
|
3163
|
+
assert.equal(
|
|
3164
|
+
fs.readFileSync(path.join(root, '.claude/rules', relative), 'utf8'),
|
|
3165
|
+
source,
|
|
3166
|
+
`claude:rules/${relative}`
|
|
3167
|
+
);
|
|
3168
|
+
const installedCodex = fs.readFileSync(path.join(root, '.codex/rules', relative), 'utf8');
|
|
3169
|
+
assert.equal(installedCodex, normalizeCodexBody(source, sourcePath), `codex:rules/${relative}`);
|
|
3170
|
+
assert.equal(
|
|
3171
|
+
installedCodex,
|
|
3172
|
+
renameOnly(source),
|
|
3173
|
+
`codex:rules/${relative} must differ from source only by skill-name renames`
|
|
3174
|
+
);
|
|
3175
|
+
}
|
|
3176
|
+
const agentsMd = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
3177
|
+
const pointerLines = agentsMd
|
|
3178
|
+
.split('\n')
|
|
3179
|
+
.filter((line) => ported.some((relative) => line.includes(relative)));
|
|
3180
|
+
assert.equal(pointerLines.length, ported.length, 'AGENTS.md must point at both ported rules');
|
|
3181
|
+
for (const relative of ported) {
|
|
3182
|
+
assert.ok(
|
|
3183
|
+
pointerLines.some((line) => line.includes(`.codex/rules/${relative}`)),
|
|
3184
|
+
`AGENTS.md must name .codex/rules/${relative}`
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
const pointerText = pointerLines.join('\n');
|
|
3188
|
+
for (const forbidden of ['cf:', 'Claude Code', 'Claude Tasks', 'SendMessage', '.claude/']) {
|
|
3189
|
+
assert.equal(
|
|
3190
|
+
pointerText.includes(forbidden),
|
|
3191
|
+
false,
|
|
3192
|
+
`AGENTS.md pointer must stay Codex-native: ${forbidden}`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
test('installed Route degrades safely when an agent is absent', () => {
|
|
3199
|
+
inTempProject((root) => {
|
|
3200
|
+
const result = installPlatforms(root, ['claude', 'codex']);
|
|
3201
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
3202
|
+
const cases = [
|
|
3203
|
+
['claude', '.claude/agents/researcher.md', '.claude/skills/route'],
|
|
3204
|
+
['codex', '.codex/agents/researcher.toml', '.agents/skills/route'],
|
|
3205
|
+
];
|
|
3206
|
+
for (const [runtime, agentRelative, routeRelative] of cases) {
|
|
3207
|
+
fs.rmSync(path.join(root, agentRelative), { force: true });
|
|
3208
|
+
assert.equal(fs.existsSync(path.join(root, agentRelative)), false);
|
|
3209
|
+
const timing = fs.readFileSync(path.join(root, routeRelative, 'references/agent-timing.md'), 'utf8');
|
|
3210
|
+
const route = fs.readFileSync(path.join(root, routeRelative, 'SKILL.md'), 'utf8');
|
|
3211
|
+
const normalizedRoute = route.replace(/\s+/g, ' ');
|
|
3212
|
+
assert.match(timing, /If the preferred agent is absent[\s\S]*never synthesize a role/);
|
|
3213
|
+
assert.match(normalizedRoute, /continue inline when safe or return the named gap/);
|
|
3214
|
+
assert.match(normalizedRoute, /Never synthesize an unavailable agent/);
|
|
3215
|
+
assert.doesNotMatch(`${timing}\n${route}`, /researcher(?:\.md|\.toml)/i, runtime);
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
});
|