@opengsd/gsd-core 1.10.0 → 1.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -28,6 +28,7 @@ const { extractFrontmatter } = frontmatterMod;
|
|
|
28
28
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
29
29
|
const security_cjs_1 = require("./security.cjs");
|
|
30
30
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
31
|
+
const ui_frontend_evidence_cjs_1 = require("./ui-frontend-evidence.cjs");
|
|
31
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
33
|
const verifyModule = require("./verify.cjs");
|
|
33
34
|
const { cmdVerifySchemaDrift, cmdVerifyCodebaseDrift } = verifyModule;
|
|
@@ -45,6 +46,12 @@ const { evaluatePredicate } = gatePredicateEval;
|
|
|
45
46
|
const apiCoverageMod = require("./api-coverage.cjs");
|
|
46
47
|
const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
|
|
47
48
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
51
|
+
const { scanPhasePlans } = planScanMod;
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
54
|
+
const { SCOPE } = planningScopeMod;
|
|
48
55
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
49
56
|
function normalizePhrase(text) {
|
|
50
57
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
@@ -128,14 +135,13 @@ function gateEnabled(projectDir) {
|
|
|
128
135
|
function loadPlanContents(phaseDir) {
|
|
129
136
|
if (!node_fs_1.default.existsSync(phaseDir))
|
|
130
137
|
return [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
138
|
+
// #3183 (lint-plan-count-drift): source live plan files from the single
|
|
139
|
+
// owner (scanPhasePlans) instead of a local `-PLAN.md` readdirSync filter
|
|
140
|
+
// — picks up bare PLAN.md and nested plans/, and excludes plans marked
|
|
141
|
+
// `status: superseded`, which the prior root-only exact-suffix filter did
|
|
142
|
+
// neither for.
|
|
143
|
+
return scanPhasePlans(phaseDir).planFiles
|
|
144
|
+
.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)));
|
|
139
145
|
}
|
|
140
146
|
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
|
141
147
|
// #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
|
|
@@ -337,6 +343,7 @@ function recentCommitMessages(projectDir) {
|
|
|
337
343
|
encoding: 'utf-8',
|
|
338
344
|
maxBuffer: 4 * 1024 * 1024,
|
|
339
345
|
windowsHide: true,
|
|
346
|
+
timeout: 15_000,
|
|
340
347
|
});
|
|
341
348
|
}
|
|
342
349
|
catch {
|
|
@@ -411,8 +418,11 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
|
|
|
411
418
|
return;
|
|
412
419
|
}
|
|
413
420
|
const planContents = loadPlanContents(phaseDir);
|
|
421
|
+
// #3183 (lint-plan-count-drift): same single-owner sourcing as
|
|
422
|
+
// loadPlanContents above — scanPhasePlans's summaryFiles instead of a
|
|
423
|
+
// local `-SUMMARY.md` readdirSync filter.
|
|
414
424
|
const summaryParts = node_fs_1.default.existsSync(phaseDir)
|
|
415
|
-
?
|
|
425
|
+
? scanPhasePlans(phaseDir).summaryFiles.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)))
|
|
416
426
|
: [];
|
|
417
427
|
const haystack = [
|
|
418
428
|
planContents.join('\n\n'),
|
|
@@ -442,8 +452,9 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
|
|
|
442
452
|
* ui-plan-gate: given a phase number, checks whether the phase has frontend
|
|
443
453
|
* indicators and whether a *-UI-SPEC.md already exists in the phase directory.
|
|
444
454
|
*
|
|
445
|
-
* Returns JSON: { frontend
|
|
446
|
-
* block = frontend && !hasUiSpec (gate fires when
|
|
455
|
+
* Returns JSON: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine }
|
|
456
|
+
* block = frontend && hasFrontendEvidence && !hasUiSpec (#3312: gate fires when
|
|
457
|
+
* UI work is detected AND the repo has static frontend evidence but no spec exists)
|
|
447
458
|
*
|
|
448
459
|
* Invocable as: gsd_run check ui-plan-gate <phase>
|
|
449
460
|
*
|
|
@@ -477,10 +488,21 @@ function findUiSpecInDir(phaseDir) {
|
|
|
477
488
|
* (b) Runs checkUiPresence (frontend detection) — no reimplementation.
|
|
478
489
|
* (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
|
|
479
490
|
*
|
|
480
|
-
* Returns: { frontend, hasUiSpec, block, uiSpecPath, phaseLookupFailed }
|
|
481
|
-
* block = frontend && !hasUiSpec
|
|
491
|
+
* Returns: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine, phaseLookupFailed }
|
|
492
|
+
* block = frontend && hasFrontendEvidence && !hasUiSpec (#3312)
|
|
482
493
|
* phaseLookupFailed = ROADMAP.md present but phase header not found (surfaced for
|
|
483
494
|
* onError:halt gates so a missing phase doesn't silently bypass)
|
|
495
|
+
*
|
|
496
|
+
* #3312 — structural corroboration: `frontend` is a vocabulary signal only. A
|
|
497
|
+
* hyphen is a word boundary, so a phase naming the repo `dashboard-financeiro`
|
|
498
|
+
* matches the token `dashboard` exactly like the real compound `micro-frontend`
|
|
499
|
+
* (the boundary rule of #3718 is intentional and untouched). The gate therefore
|
|
500
|
+
* blocks only when the token match is corroborated by static frontend evidence
|
|
501
|
+
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep or
|
|
502
|
+
* a component-framework file). This mirrors the sibling post-wave gate
|
|
503
|
+
* computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
|
|
504
|
+
* matchedToken/matchedLine surface what tripped the sniffer so an operator can
|
|
505
|
+
* judge the flag in one second instead of reaching for --skip-ui.
|
|
484
506
|
*/
|
|
485
507
|
function computeUiPlanGate(projectDir, phase) {
|
|
486
508
|
// (a) Read the phase section text using the same two-pass lookup as roadmap.get-phase.
|
|
@@ -509,6 +531,9 @@ function computeUiPlanGate(projectDir, phase) {
|
|
|
509
531
|
// (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
|
|
510
532
|
const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
|
|
511
533
|
const frontend = presenceResult.hasUI;
|
|
534
|
+
// (b') #3312 — static structural corroboration. Only probed when the sniffer
|
|
535
|
+
// matched (evidence is irrelevant otherwise); failures degrade to false.
|
|
536
|
+
const hasFrontendEvidence = frontend ? (0, ui_frontend_evidence_cjs_1.hasStaticFrontendEvidence)(projectDir) : false;
|
|
512
537
|
// (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md
|
|
513
538
|
let phaseDir = '';
|
|
514
539
|
try {
|
|
@@ -528,10 +553,13 @@ function computeUiPlanGate(projectDir, phase) {
|
|
|
528
553
|
catch { /* phase dir lookup failure → hasUiSpec=false */ }
|
|
529
554
|
const uiSpecPath = findUiSpecInDir(phaseDir);
|
|
530
555
|
const hasUiSpec = uiSpecPath !== '';
|
|
531
|
-
// block = frontend phase with no UI-SPEC
|
|
532
|
-
const block = frontend && !hasUiSpec;
|
|
556
|
+
// block = frontend phase with structural frontend evidence and no UI-SPEC (#3312)
|
|
557
|
+
const block = frontend && hasFrontendEvidence && !hasUiSpec;
|
|
533
558
|
const result = {
|
|
534
|
-
frontend, hasUiSpec, block,
|
|
559
|
+
frontend, hasFrontendEvidence, hasUiSpec, block,
|
|
560
|
+
uiSpecPath: hasUiSpec ? uiSpecPath : null,
|
|
561
|
+
matchedToken: presenceResult.matchedToken,
|
|
562
|
+
matchedLine: presenceResult.matchedLine,
|
|
535
563
|
};
|
|
536
564
|
if (phaseLookupFailed)
|
|
537
565
|
result.phaseLookupFailed = true;
|
|
@@ -613,6 +641,7 @@ function computeUiSafetyGate(projectDir, phase) {
|
|
|
613
641
|
encoding: 'utf-8',
|
|
614
642
|
maxBuffer: 2 * 1024 * 1024,
|
|
615
643
|
windowsHide: true,
|
|
644
|
+
timeout: 10_000,
|
|
616
645
|
});
|
|
617
646
|
hasUiFiles = changed.split('\n').some((f) => f.trim() && (UI_FILE_EXTENSIONS_RE.test(f) || UI_PATH_PATTERNS_RE.test(f)));
|
|
618
647
|
}
|
|
@@ -679,7 +708,9 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
679
708
|
const tddPlanFiles = [];
|
|
680
709
|
if (phaseDir) {
|
|
681
710
|
try {
|
|
682
|
-
|
|
711
|
+
// #3183: canonical plan set (root+nested, superseded-excluded) from the
|
|
712
|
+
// single owner, rather than a root-only hand-rolled readdirSync filter.
|
|
713
|
+
const files = scanPhasePlans(phaseDir).planFiles;
|
|
683
714
|
for (const file of files) {
|
|
684
715
|
const planPath = node_path_1.default.join(phaseDir, file);
|
|
685
716
|
const content = readIfExists(planPath);
|
|
@@ -728,17 +759,17 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
728
759
|
let green = false;
|
|
729
760
|
let refactor = false;
|
|
730
761
|
try {
|
|
731
|
-
const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
762
|
+
const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
732
763
|
red = redCommit.trim().length > 0;
|
|
733
764
|
}
|
|
734
765
|
catch { /* git unavailable or no match */ }
|
|
735
766
|
try {
|
|
736
|
-
const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
767
|
+
const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
737
768
|
green = greenCommit.trim().length > 0;
|
|
738
769
|
}
|
|
739
770
|
catch { /* git unavailable or no match */ }
|
|
740
771
|
try {
|
|
741
|
-
const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
772
|
+
const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
742
773
|
refactor = refactorCommit.trim().length > 0;
|
|
743
774
|
}
|
|
744
775
|
catch { /* git unavailable or no match */ }
|
|
@@ -1207,12 +1238,26 @@ function isRealReadFailure(err) {
|
|
|
1207
1238
|
function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
1208
1239
|
const chunks = [];
|
|
1209
1240
|
let readError = null;
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1241
|
+
// A MISSING phase directory is fine (no plans yet → fall through to the
|
|
1242
|
+
// roadmap). Checked up front (rather than via a readdirSync catch) because
|
|
1243
|
+
// #3183 (lint-plan-count-drift) now sources the plan-file list from the
|
|
1244
|
+
// single owner (scanPhasePlans) instead of a local `-PLAN\.md$` readdirSync
|
|
1245
|
+
// filter — picks up bare PLAN.md and nested plans/, and excludes
|
|
1246
|
+
// superseded plans, none of which the prior root-only exact-suffix filter
|
|
1247
|
+
// did.
|
|
1248
|
+
if (node_fs_1.default.existsSync(phaseDir)) {
|
|
1249
|
+
const scan = scanPhasePlans(phaseDir);
|
|
1250
|
+
if (scan.scope === SCOPE.UNREADABLE) {
|
|
1251
|
+
// Directory exists but scanPhasePlans's own readdirSync(phaseDir) call
|
|
1252
|
+
// failed (EACCES/EIO race) — a real read failure the gate must not
|
|
1253
|
+
// silently pass (#2365 review), mirroring the prior isRealReadFailure
|
|
1254
|
+
// branch below for the readdirSync-throws case.
|
|
1255
|
+
return {
|
|
1256
|
+
text: '',
|
|
1257
|
+
readError: 'could not read the phase directory: scanPhasePlans reported scope UNREADABLE',
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const plans = [...scan.planFiles].sort();
|
|
1216
1261
|
for (const p of plans) {
|
|
1217
1262
|
try {
|
|
1218
1263
|
chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
|
|
@@ -1226,17 +1271,6 @@ function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
|
1226
1271
|
}
|
|
1227
1272
|
}
|
|
1228
1273
|
}
|
|
1229
|
-
catch (err) {
|
|
1230
|
-
// A MISSING phase directory is fine (no plans yet → fall through to the
|
|
1231
|
-
// roadmap). A directory that exists but cannot be enumerated (EACCES/EIO)
|
|
1232
|
-
// is a real read failure the gate must not silently pass (#2365 review).
|
|
1233
|
-
if (isRealReadFailure(err)) {
|
|
1234
|
-
return {
|
|
1235
|
-
text: '',
|
|
1236
|
-
readError: `could not read the phase directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
1274
|
if (readError)
|
|
1241
1275
|
return { text: chunks.join('\n\n'), readError };
|
|
1242
1276
|
if (chunks.join('').trim().length > 0)
|
|
@@ -470,13 +470,43 @@ function emitWorkflowScript(input) {
|
|
|
470
470
|
lines.push('// read-only in a Workflow script — set it via the caller\'s token directive.');
|
|
471
471
|
}
|
|
472
472
|
lines.push('');
|
|
473
|
+
// #3302: the generated script must hand the per-agent executor results back
|
|
474
|
+
// to the orchestrator so it can feed the wave merge chain. Emitted right
|
|
475
|
+
// after the header comments (meta stays the first statement): a helper that
|
|
476
|
+
// extracts the executor's <worktree_metadata> JSON (agents/gsd-executor.md
|
|
477
|
+
// <worktree_metadata_capture>) from one agent() result, plus the outcomes
|
|
478
|
+
// accumulator the stage barriers below push into. `metadata` is null when
|
|
479
|
+
// the result carried no parseable block — the LOUD-failure input the
|
|
480
|
+
// orchestrator halts on for expects_worktree plans (never a silent skip).
|
|
481
|
+
lines.push('// #3302: extract the executor-returned <worktree_metadata> JSON so the');
|
|
482
|
+
lines.push('// orchestrator can record it into WAVE_WORKTREE_MANIFEST after the run');
|
|
483
|
+
lines.push('// (worktree.record-agent -> worktree.cleanup-wave, the same manifest-scoped');
|
|
484
|
+
lines.push('// merge chain inline dispatch feeds). null = absent/unparseable/interrupted.');
|
|
485
|
+
lines.push('function gsdWorktreeMetadata(agentResult) {');
|
|
486
|
+
lines.push(' if (typeof agentResult !== \'string\') return null;');
|
|
487
|
+
lines.push(' const m = agentResult.match(/<worktree_metadata>([\\s\\S]*?)<\\/worktree_metadata>/);');
|
|
488
|
+
lines.push(' if (m === null) return null;');
|
|
489
|
+
lines.push(' try {');
|
|
490
|
+
lines.push(' const parsed = JSON.parse(m[1]);');
|
|
491
|
+
lines.push(' return (parsed !== null && typeof parsed === \'object\') ? parsed : null;');
|
|
492
|
+
lines.push(' } catch (e) {');
|
|
493
|
+
lines.push(' return null;');
|
|
494
|
+
lines.push(' }');
|
|
495
|
+
lines.push('}');
|
|
496
|
+
lines.push('const gsdAgentOutcomes = [];');
|
|
497
|
+
lines.push('');
|
|
473
498
|
const stagesByWave = [];
|
|
474
499
|
let totalPlans = 0;
|
|
500
|
+
let worktreePlans = 0;
|
|
475
501
|
for (let wi = 0; wi < waves.length; wi++) {
|
|
476
502
|
const wave = waves[wi];
|
|
477
503
|
const stages = partitionStages(wave.plans);
|
|
478
504
|
stagesByWave.push(stages);
|
|
479
505
|
totalPlans += wave.plans.length;
|
|
506
|
+
for (const p of wave.plans) {
|
|
507
|
+
if (p.use_worktree !== false)
|
|
508
|
+
worktreePlans += 1;
|
|
509
|
+
}
|
|
480
510
|
lines.push('// Wave ' + wave.id);
|
|
481
511
|
// Title must match this wave's meta.phases entry EXACTLY.
|
|
482
512
|
lines.push('phase(' + quoteString('Wave ' + wave.id) + ')');
|
|
@@ -491,17 +521,37 @@ function emitWorkflowScript(input) {
|
|
|
491
521
|
// threw "parallel() expects an array of functions" (#2590). Passing
|
|
492
522
|
// agent() results directly would also start every agent eagerly, before
|
|
493
523
|
// parallel() could bound concurrency.
|
|
494
|
-
|
|
524
|
+
// #3302: capture the barrier's resolved results (one per thunk, in thunk
|
|
525
|
+
// order — the documented parallel() contract) so each plan's outcome can
|
|
526
|
+
// be tagged and returned below. Discarding them stranded every
|
|
527
|
+
// worktree-wf_* branch: the merge chain had no input (#3302).
|
|
528
|
+
lines.push('const gsdStage_' + wi + '_' + si + ' = await parallel([');
|
|
495
529
|
for (const p of stagePlans) {
|
|
496
530
|
lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
|
|
497
531
|
}
|
|
498
532
|
lines.push('])');
|
|
533
|
+
// Positional tagging is decided at EMIT time from the validated manifest,
|
|
534
|
+
// so attribution survives out-of-order completion and needs no runtime
|
|
535
|
+
// introspection. expects_worktree mirrors agentOptions' own per-plan
|
|
536
|
+
// decision (use_worktree !== false).
|
|
537
|
+
lines.push('gsdAgentOutcomes.push(');
|
|
538
|
+
for (let pi = 0; pi < stagePlans.length; pi++) {
|
|
539
|
+
const p = stagePlans[pi];
|
|
540
|
+
const tail = pi < stagePlans.length - 1 ? ',' : '';
|
|
541
|
+
lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: gsdWorktreeMetadata(gsdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
|
|
542
|
+
}
|
|
543
|
+
lines.push(')');
|
|
499
544
|
}
|
|
500
545
|
if (wi < waves.length - 1)
|
|
501
546
|
lines.push('');
|
|
502
547
|
}
|
|
503
|
-
|
|
504
|
-
|
|
548
|
+
// #3302: the script's top-level return value is what the Workflow tool hands
|
|
549
|
+
// back to the orchestrator. One { plan, expects_worktree, metadata } entry
|
|
550
|
+
// per dispatched plan; the orchestrator records every worktree entry via
|
|
551
|
+
// `gsd_run query worktree.record-agent` and HALTS on a null metadata entry
|
|
552
|
+
// for an expects_worktree plan (see the execute:wave:pre fragment) — a
|
|
553
|
+
// silently-empty manifest is the exact #3302 failure mode.
|
|
554
|
+
lines.push('return gsdAgentOutcomes');
|
|
505
555
|
const script = lines.join('\n');
|
|
506
556
|
return {
|
|
507
557
|
ok: true,
|
|
@@ -509,6 +559,9 @@ function emitWorkflowScript(input) {
|
|
|
509
559
|
summary: {
|
|
510
560
|
waves: waves.length,
|
|
511
561
|
plans: totalPlans,
|
|
562
|
+
// #3302: the number of record-agent entries the orchestrator must end up
|
|
563
|
+
// with in WAVE_WORKTREE_MANIFEST after the run — the loud count check.
|
|
564
|
+
worktreePlans,
|
|
512
565
|
stagesByWave,
|
|
513
566
|
resumeRunId: runId,
|
|
514
567
|
budgetTokens,
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex Agent TOML — typed IR for `~/.codex/agents/<agent>.toml` (#3243, ADR-2313).
|
|
4
|
+
*
|
|
5
|
+
* A genuine leaf: node builtins only. This is a **document model**, not a policy —
|
|
6
|
+
* it knows how to parse/render/strip two known keys (`model`,
|
|
7
|
+
* `model_reasoning_effort`) from a Codex agent `.toml`. It does NOT know which
|
|
8
|
+
* `model` values are illegal for Codex (that predicate — Anthropic-flavored
|
|
9
|
+
* detection — stays in `model-catalog.cts`; callers decide what to strip).
|
|
10
|
+
*
|
|
11
|
+
* Moved here (not copied) from `agent-install-check.cts` (#3242, Phase 2), which
|
|
12
|
+
* wrote the hard half: block-range detection, BOM stripping, TOML value
|
|
13
|
+
* unquoting, and the lenient header scan. That module's behavior is UNCHANGED —
|
|
14
|
+
* it imports `stripBOM`/`scanTomlLines` from here and its regression suite
|
|
15
|
+
* (`tests/agent-install-check.test.cjs`) is the proof.
|
|
16
|
+
*
|
|
17
|
+
* ── The reconciliation (40-design.md) ──────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* Phase 2's reader and this phase's writer disagree on how to handle an
|
|
20
|
+
* unterminated `developer_instructions` block, deliberately:
|
|
21
|
+
*
|
|
22
|
+
* - The READER (`scanTomlLines`, used directly by `checkCodexModelPosture`)
|
|
23
|
+
* stays LENIENT: an unterminated block still excludes "the rest of the
|
|
24
|
+
* file" from the header scan (findDeveloperInstructionsBlockRange's
|
|
25
|
+
* existing fallback), because misreading prompt prose as a pin is only a
|
|
26
|
+
* false positive — it wastes a user's time, nothing more.
|
|
27
|
+
* - The WRITER (`parseCodexAgentToml`, used by the Codex sync) is STRICT: an
|
|
28
|
+
* unterminated block makes the whole document `{ok:false}`, because a
|
|
29
|
+
* writer that proceeds on a malformed document risks rewriting it.
|
|
30
|
+
*
|
|
31
|
+
* One block-range detector, two call sites, two policies — never two detectors
|
|
32
|
+
* that could silently drift from each other.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.PARSE_REASON = void 0;
|
|
36
|
+
exports.stripBOM = stripBOM;
|
|
37
|
+
exports.unquoteTomlValue = unquoteTomlValue;
|
|
38
|
+
exports.findDeveloperInstructionsBlockRange = findDeveloperInstructionsBlockRange;
|
|
39
|
+
exports.scanTomlLines = scanTomlLines;
|
|
40
|
+
exports.parseCodexAgentToml = parseCodexAgentToml;
|
|
41
|
+
exports.renderCodexAgentToml = renderCodexAgentToml;
|
|
42
|
+
exports.stripModel = stripModel;
|
|
43
|
+
exports.stripReasoningEffort = stripReasoningEffort;
|
|
44
|
+
/** Frozen reason enum for a failed {@link parseCodexAgentToml}. */
|
|
45
|
+
exports.PARSE_REASON = Object.freeze({
|
|
46
|
+
UNTERMINATED_BLOCK: 'unterminated_block',
|
|
47
|
+
});
|
|
48
|
+
// The UTF-8 BOM codepoint, spelled as an escape rather than the literal
|
|
49
|
+
// character so the source file never carries an invisible codepoint.
|
|
50
|
+
const BOM_CHAR = String.fromCharCode(0xfeff);
|
|
51
|
+
// Strips a leading UTF-8 BOM (U+FEFF), which fs.readFileSync(..., 'utf8') does not
|
|
52
|
+
// strip on its own, and unwraps a TOML basic/literal string value's surrounding
|
|
53
|
+
// quotes so `model = "sonnet"` yields `sonnet`, not `"sonnet"`.
|
|
54
|
+
function stripBOM(content) {
|
|
55
|
+
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
56
|
+
}
|
|
57
|
+
function unquoteTomlValue(rawValue) {
|
|
58
|
+
const trimmed = rawValue.trim();
|
|
59
|
+
const quoted = trimmed.match(/^"([^"]*)"/) ?? trimmed.match(/^'([^']*)'/);
|
|
60
|
+
return quoted ? quoted[1] : trimmed;
|
|
61
|
+
}
|
|
62
|
+
// The `developer_instructions` block is a TOML multi-line literal string
|
|
63
|
+
// (`developer_instructions = '''...'''`) that `generateCodexAgentToml` always
|
|
64
|
+
// emits after the header fields. Prompt prose inside that block discusses models
|
|
65
|
+
// constantly, so a `model = ...`-shaped line inside it must never be read as a
|
|
66
|
+
// live pin — but the block can legally appear anywhere in the file (a
|
|
67
|
+
// hand-reordered agent can move `model` after it), and another key's *value* can
|
|
68
|
+
// legally contain the literal text `developer_instructions = '''` (e.g. a
|
|
69
|
+
// `description` field quoting it) without that being the real block opener. So
|
|
70
|
+
// instead of truncating the file at the first textual occurrence of the marker
|
|
71
|
+
// anywhere in the content, this locates the block by its anchored line-start
|
|
72
|
+
// opener (`^\s*developer_instructions\s*=\s*'''`, never a mid-line/mid-value
|
|
73
|
+
// match) and its closing `'''` line, and excludes only the lines between them —
|
|
74
|
+
// every other line in the file, before AND after the block, is scanned.
|
|
75
|
+
//
|
|
76
|
+
// If no opener is found, nothing is excluded (the whole file is scanned) and
|
|
77
|
+
// `terminated` is trivially true. If the block IS opened but never closed before
|
|
78
|
+
// EOF (malformed file), `terminated` is false: the lenient reader (scanTomlLines)
|
|
79
|
+
// still treats the rest of the file as inside the block (the safe direction for
|
|
80
|
+
// a reader — see module header comment); the strict writer (parseCodexAgentToml)
|
|
81
|
+
// reads `terminated` and refuses instead. The emitter always uses `'''` (a TOML
|
|
82
|
+
// literal string), never a `"""` basic multi-line string, so only `'''` is
|
|
83
|
+
// treated as the block delimiter here.
|
|
84
|
+
function findDeveloperInstructionsBlockRange(lines) {
|
|
85
|
+
const openIndex = lines.findIndex((line) => /^\s*developer_instructions\s*=\s*'''/.test(line));
|
|
86
|
+
if (openIndex === -1) {
|
|
87
|
+
return { start: -1, end: -1, terminated: true };
|
|
88
|
+
}
|
|
89
|
+
const afterOpenMarker = lines[openIndex].replace(/^\s*developer_instructions\s*=\s*'''/, '');
|
|
90
|
+
if (afterOpenMarker.includes("'''")) {
|
|
91
|
+
// Same-line block: developer_instructions = '''one line'''
|
|
92
|
+
return { start: openIndex, end: openIndex, terminated: true };
|
|
93
|
+
}
|
|
94
|
+
for (let i = openIndex + 1; i < lines.length; i++) {
|
|
95
|
+
if (lines[i].includes("'''")) {
|
|
96
|
+
return { start: openIndex, end: i, terminated: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { start: openIndex, end: lines.length - 1, terminated: false };
|
|
100
|
+
}
|
|
101
|
+
// Line-oriented scan of every line OUTSIDE the `developer_instructions` block
|
|
102
|
+
// (see findDeveloperInstructionsBlockRange). Full-key-name anchoring —
|
|
103
|
+
// `^([A-Za-z_][\w]*)\s*=` for a bare key, or `^"([^"]*)"\s*=` / `^'([^']*)'\s*=`
|
|
104
|
+
// for TOML's legal quoted-key forms, normalized to the same key name — means
|
|
105
|
+
// `model_verbosity` / `model_reasoning_effort` never satisfy a `model` probe,
|
|
106
|
+
// and vice versa; `#`-prefixed lines (after trimming leading whitespace) are
|
|
107
|
+
// treated as comments, never live pins. Shared by both scanTomlLines (the
|
|
108
|
+
// lenient reader, boolean-only for reasoning effort) and parseCodexAgentToml
|
|
109
|
+
// (the strict writer, which also needs the effort's value and both keys' line
|
|
110
|
+
// indices so stripModel/stripReasoningEffort can remove exactly one line).
|
|
111
|
+
function scanHeaderLines(lines, blockStart, blockEnd) {
|
|
112
|
+
let model = null;
|
|
113
|
+
let modelLineIndex = null;
|
|
114
|
+
let reasoningEffort = null;
|
|
115
|
+
let reasoningEffortLineIndex = null;
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
if (blockStart !== -1 && i >= blockStart && i <= blockEnd)
|
|
118
|
+
continue;
|
|
119
|
+
const trimmed = lines[i].trim();
|
|
120
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
121
|
+
continue;
|
|
122
|
+
const match = trimmed.match(/^(?:"([^"]*)"|'([^']*)'|([A-Za-z_][\w]*))\s*=\s*(.*)$/);
|
|
123
|
+
if (!match)
|
|
124
|
+
continue;
|
|
125
|
+
const key = match[1] ?? match[2] ?? match[3];
|
|
126
|
+
const rawValue = match[4];
|
|
127
|
+
if (key === 'model') {
|
|
128
|
+
model = unquoteTomlValue(rawValue);
|
|
129
|
+
modelLineIndex = i;
|
|
130
|
+
}
|
|
131
|
+
else if (key === 'model_reasoning_effort') {
|
|
132
|
+
reasoningEffort = unquoteTomlValue(rawValue);
|
|
133
|
+
reasoningEffortLineIndex = i;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The LENIENT reader entry point (Phase 2, moved verbatim in behavior). Never
|
|
140
|
+
* fails: an unterminated block falls back to "rest of file is inside the
|
|
141
|
+
* block" via {@link findDeveloperInstructionsBlockRange}'s own fallback.
|
|
142
|
+
* `content` is expected already BOM-stripped (callers pass `stripBOM(raw)`).
|
|
143
|
+
*/
|
|
144
|
+
function scanTomlLines(content) {
|
|
145
|
+
const lines = content.split(/\r?\n/);
|
|
146
|
+
const { start, end } = findDeveloperInstructionsBlockRange(lines);
|
|
147
|
+
const { model, reasoningEffort } = scanHeaderLines(lines, start, end);
|
|
148
|
+
return { model, hasReasoningEffort: reasoningEffort !== null };
|
|
149
|
+
}
|
|
150
|
+
// Splits `content` into `{lines, terminators}` where `terminators[i]` is the
|
|
151
|
+
// terminator that FOLLOWS `lines[i]` (`'\r\n'`, `'\r'`, `'\n'`, or `''` for a
|
|
152
|
+
// line with none — only possible as the file's last line). The two arrays are
|
|
153
|
+
// always the same length and there is NEVER a phantom trailing entry: a
|
|
154
|
+
// source ending in a terminator (the common case) yields exactly as many
|
|
155
|
+
// lines as it has content lines, not one more. `render` is then a plain
|
|
156
|
+
// `lines[i] + terminators[i]` concatenation with no special-casing of "the
|
|
157
|
+
// last line" — see `renderCodexAgentToml`.
|
|
158
|
+
//
|
|
159
|
+
// `String#split` with a capturing group interleaves the delimiters into the
|
|
160
|
+
// result array — `"a\r\nb\nc".split(/(\r\n|\r|\n)/)` yields
|
|
161
|
+
// `["a","\r\n","b","\n","c"]` — so even indices are line content and odd
|
|
162
|
+
// indices are that line's terminator. When `content` ends WITH a terminator,
|
|
163
|
+
// `split` appends one extra empty-string element after the last real
|
|
164
|
+
// terminator (e.g. `"a\n".split(...)` → `["a","\n",""]`); that trailing `""`
|
|
165
|
+
// is not a real line, it is `split`'s "nothing after the last delimiter"
|
|
166
|
+
// marker, so the loop below stops before consuming it instead of recording it
|
|
167
|
+
// as a phantom empty final line (the defect this replaced — see A29: a doc
|
|
168
|
+
// with a phantom last element made every removal rule reason about the wrong
|
|
169
|
+
// element for any trailing-newline-terminated file, the common case). `\r\n`
|
|
170
|
+
// is tried before the bare `\r` alternative so a CRLF is never misread as a
|
|
171
|
+
// lone-CR line followed by an empty LF-terminated line.
|
|
172
|
+
function splitPreservingTerminators(content) {
|
|
173
|
+
if (content === '')
|
|
174
|
+
return { lines: [], terminators: [] };
|
|
175
|
+
const parts = content.split(/(\r\n|\r|\n)/);
|
|
176
|
+
const lastIndex = parts.length - 1;
|
|
177
|
+
const lines = [];
|
|
178
|
+
const terminators = [];
|
|
179
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
180
|
+
if (i === lastIndex && parts[i] === '')
|
|
181
|
+
break; // split's post-terminator marker, not a real line
|
|
182
|
+
lines.push(parts[i]);
|
|
183
|
+
terminators.push(parts[i + 1] ?? '');
|
|
184
|
+
}
|
|
185
|
+
return { lines, terminators };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The STRICT parse entry point (Phase 3, the writer's half of the
|
|
189
|
+
* reconciliation). Returns `{ok:false, reason:UNTERMINATED_BLOCK}` rather than
|
|
190
|
+
* guessing when the `developer_instructions` block is opened but never closed.
|
|
191
|
+
* On success, `doc` carries enough (the original `lines`/`terminators`, BOM/
|
|
192
|
+
* trailing-newline flags, and the two resolved values with their line indices)
|
|
193
|
+
* for {@link renderCodexAgentToml} to reproduce the source byte-identically —
|
|
194
|
+
* including a source with mixed line-ending styles — and for
|
|
195
|
+
* {@link stripModel}/{@link stripReasoningEffort} to remove exactly one line
|
|
196
|
+
* and its own terminator.
|
|
197
|
+
*/
|
|
198
|
+
function parseCodexAgentToml(content) {
|
|
199
|
+
const hadBOM = content.charCodeAt(0) === 0xfeff;
|
|
200
|
+
const stripped = stripBOM(content);
|
|
201
|
+
// Informational only — see CodexAgentDoc.eol's docstring. Never used by
|
|
202
|
+
// renderCodexAgentToml.
|
|
203
|
+
const eol = stripped.includes('\r\n') ? '\r\n' : '\n';
|
|
204
|
+
const trailingNewline = /(\r\n|\r|\n)$/.test(stripped);
|
|
205
|
+
const { lines, terminators } = splitPreservingTerminators(stripped);
|
|
206
|
+
const { start, end, terminated } = findDeveloperInstructionsBlockRange(lines);
|
|
207
|
+
if (start !== -1 && !terminated) {
|
|
208
|
+
return { ok: false, reason: exports.PARSE_REASON.UNTERMINATED_BLOCK };
|
|
209
|
+
}
|
|
210
|
+
const { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex } = scanHeaderLines(lines, start, end);
|
|
211
|
+
const doc = {
|
|
212
|
+
lines,
|
|
213
|
+
terminators,
|
|
214
|
+
eol,
|
|
215
|
+
hadBOM,
|
|
216
|
+
trailingNewline,
|
|
217
|
+
blockRange: { start, end },
|
|
218
|
+
model,
|
|
219
|
+
modelLineIndex,
|
|
220
|
+
reasoningEffort,
|
|
221
|
+
reasoningEffortLineIndex,
|
|
222
|
+
};
|
|
223
|
+
return { ok: true, doc };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Renders `doc` back to a string. For an unmodified doc this is
|
|
227
|
+
* byte-identical to the original `parseCodexAgentToml` input (matrix row
|
|
228
|
+
* A14) — it never re-derives line content, only rejoins each line with its
|
|
229
|
+
* OWN recorded terminator (`terminators[i]`, never the whole-file `eol`) and
|
|
230
|
+
* re-prepends a BOM if one was present. This is a plain concatenation of the
|
|
231
|
+
* surviving `[line, terminator]` pieces, so a source with mixed `\r\n`/`\n`/
|
|
232
|
+
* lone-`\r` line endings round-trips exactly, and a strip
|
|
233
|
+
* ({@link stripModel}/{@link stripReasoningEffort}) removes only the target
|
|
234
|
+
* line and its own terminator — every other line's ending is untouched.
|
|
235
|
+
*/
|
|
236
|
+
function renderCodexAgentToml(doc) {
|
|
237
|
+
let body = '';
|
|
238
|
+
for (let i = 0; i < doc.lines.length; i++) {
|
|
239
|
+
body += doc.lines[i] + (doc.terminators[i] ?? '');
|
|
240
|
+
}
|
|
241
|
+
return doc.hadBOM ? BOM_CHAR + body : body;
|
|
242
|
+
}
|
|
243
|
+
// Removes exactly one line (by index) — AND its own terminator — from
|
|
244
|
+
// `doc.lines`/`doc.terminators`, re-indexing the block range and the OTHER
|
|
245
|
+
// key's line index so a subsequent strip/render still sees a consistent doc.
|
|
246
|
+
// Never touches any other line's content or terminator.
|
|
247
|
+
//
|
|
248
|
+
// The one exception is when `index` names the file's LAST line: a plain
|
|
249
|
+
// slice-out would drop the removed line's terminator but leave the
|
|
250
|
+
// *previous* line's terminator standing in its place, which silently
|
|
251
|
+
// invents (or drops) a trailing newline the source never had — a middle-line
|
|
252
|
+
// removal never has this problem because the terminator that survives (the
|
|
253
|
+
// one that WAS between the previous line and the removed one) is exactly the
|
|
254
|
+
// terminator the new neighbors should have between them. For a last-line
|
|
255
|
+
// removal, the file's trailing-newline-or-not status lives in whether the
|
|
256
|
+
// REMOVED line's own terminator was empty (that is what `trailingNewline`
|
|
257
|
+
// was computed from) — so the new last line inherits the removed line's
|
|
258
|
+
// EMPTINESS only: if the removed terminator was `''`, the new last line's
|
|
259
|
+
// terminator is cleared to `''` too. If the removed terminator was
|
|
260
|
+
// non-empty, the source already ended with a newline and the new last line
|
|
261
|
+
// already has the right one (its OWN, unchanged) — overwriting it with the
|
|
262
|
+
// removed line's terminator would silently change the new last line's own
|
|
263
|
+
// ending style on a mixed-EOL source (see A26). Removing the only remaining
|
|
264
|
+
// line is the degenerate case: there is no new last line, so the result is
|
|
265
|
+
// the empty document.
|
|
266
|
+
function removeLine(doc, index, which) {
|
|
267
|
+
const isLastLine = index === doc.lines.length - 1;
|
|
268
|
+
let lines;
|
|
269
|
+
let terminators;
|
|
270
|
+
if (doc.lines.length === 1) {
|
|
271
|
+
lines = [];
|
|
272
|
+
terminators = [];
|
|
273
|
+
}
|
|
274
|
+
else if (isLastLine) {
|
|
275
|
+
lines = doc.lines.slice(0, index);
|
|
276
|
+
terminators = doc.terminators.slice(0, index);
|
|
277
|
+
// Inherit the removed line's EMPTINESS, never its STYLE: if the removed
|
|
278
|
+
// line had no terminator (the source had no trailing newline), the new
|
|
279
|
+
// last line's terminator becomes '' too. Otherwise the source DID end
|
|
280
|
+
// with a newline, and the new last line already has the right one — its
|
|
281
|
+
// OWN terminator (already carried over by the slice above), which may
|
|
282
|
+
// differ in style from the removed line's (a mixed-EOL source) — so it is
|
|
283
|
+
// left unchanged rather than overwritten.
|
|
284
|
+
if (doc.terminators[index] === '') {
|
|
285
|
+
terminators[terminators.length - 1] = '';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
lines = doc.lines.slice(0, index).concat(doc.lines.slice(index + 1));
|
|
290
|
+
terminators = doc.terminators.slice(0, index).concat(doc.terminators.slice(index + 1));
|
|
291
|
+
}
|
|
292
|
+
const reindex = (i) => (i === null ? null : i > index ? i - 1 : i);
|
|
293
|
+
const blockRange = { ...doc.blockRange };
|
|
294
|
+
if (blockRange.start !== -1) {
|
|
295
|
+
if (blockRange.start > index)
|
|
296
|
+
blockRange.start -= 1;
|
|
297
|
+
if (blockRange.end > index)
|
|
298
|
+
blockRange.end -= 1;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...doc,
|
|
302
|
+
lines,
|
|
303
|
+
terminators,
|
|
304
|
+
blockRange,
|
|
305
|
+
model: which === 'model' ? null : doc.model,
|
|
306
|
+
modelLineIndex: which === 'model' ? null : reindex(doc.modelLineIndex),
|
|
307
|
+
reasoningEffort: which === 'reasoningEffort' ? null : doc.reasoningEffort,
|
|
308
|
+
reasoningEffortLineIndex: which === 'reasoningEffort' ? null : reindex(doc.reasoningEffortLineIndex),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Returns a new doc with the `model` line removed (a no-op copy if there was
|
|
313
|
+
* no `model` line). Every other byte — comments, other keys, the
|
|
314
|
+
* `developer_instructions` block, line endings, BOM — is untouched.
|
|
315
|
+
*/
|
|
316
|
+
function stripModel(doc) {
|
|
317
|
+
if (doc.modelLineIndex === null)
|
|
318
|
+
return doc;
|
|
319
|
+
return removeLine(doc, doc.modelLineIndex, 'model');
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Returns a new doc with the `model_reasoning_effort` line removed (a no-op
|
|
323
|
+
* copy if there was none). Every other byte is untouched.
|
|
324
|
+
*/
|
|
325
|
+
function stripReasoningEffort(doc) {
|
|
326
|
+
if (doc.reasoningEffortLineIndex === null)
|
|
327
|
+
return doc;
|
|
328
|
+
return removeLine(doc, doc.reasoningEffortLineIndex, 'reasoningEffort');
|
|
329
|
+
}
|