@opengsd/gsd-core 1.11.0 → 1.12.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-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -9
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +15 -55
- package/agents/gsd-planner.md +6 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +160 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +3 -2
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +3 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +34 -16
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +134 -1
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +122 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -13,6 +13,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
16
17
|
const security_cjs_1 = require("./security.cjs");
|
|
17
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
19
|
const ioMod = require("./io.cjs");
|
|
@@ -54,7 +55,7 @@ const planningWorkspace = require("./planning-workspace.cjs");
|
|
|
54
55
|
const { planningDir, planningPaths } = planningWorkspace;
|
|
55
56
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
57
|
const frontmatter = require("./frontmatter.cjs");
|
|
57
|
-
const { extractFrontmatter } = frontmatter;
|
|
58
|
+
const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar } = frontmatter;
|
|
58
59
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
59
60
|
const modelProfiles = require("./model-profiles.cjs");
|
|
60
61
|
const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
|
|
@@ -159,11 +160,11 @@ function cmdGenerateSlug(text, raw) {
|
|
|
159
160
|
if (!text) {
|
|
160
161
|
error('text required for slug generation');
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
// #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
|
|
164
|
+
// (generateSlugInternal, core-utils.cts) instead of re-implementing it —
|
|
165
|
+
// this call site previously diverged from it (Cyrillic collapsed to "",
|
|
166
|
+
// and truncation could leave a trailing hyphen; #2848/#2849).
|
|
167
|
+
const slug = coreUtilsMod.generateSlugInternal(text) ?? '';
|
|
167
168
|
const result = { slug };
|
|
168
169
|
output(result, raw, slug);
|
|
169
170
|
}
|
|
@@ -529,7 +530,12 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
529
530
|
: resolveEffortInternal(cwd, agentType, effortOpts);
|
|
530
531
|
const fastMode = resolveFastModeInternal(cwd, agentType, fastModeOpts);
|
|
531
532
|
const runtime = config['runtime'] || 'claude';
|
|
532
|
-
|
|
533
|
+
// #3007: pass the resolved model so the per-model advertised-effort ceiling
|
|
534
|
+
// (CODEX_MODEL_EFFORT) is reachable from this production seam. `model` may
|
|
535
|
+
// be a tier alias or a non-Codex id for other runtimes — that's fine and
|
|
536
|
+
// must not be special-cased here: advertisedCodexEffort() falls back to the
|
|
537
|
+
// family baseline for any id it doesn't recognize.
|
|
538
|
+
const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, effort, model);
|
|
533
539
|
const fastModeSupported = model_catalog_cjs_1.RUNTIMES_WITH_FAST_MODE.has(runtime);
|
|
534
540
|
// #3534 (10a): the effective effort — what the installed agent will actually
|
|
535
541
|
// run at. `effort` above is the config cascade; for the claude runtime the
|
|
@@ -584,6 +590,9 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
584
590
|
effort_rendered: rendered.value,
|
|
585
591
|
effort_param: rendered.param,
|
|
586
592
|
effort_propagation: rendered.channel,
|
|
593
|
+
effort_requested: rendered.requested,
|
|
594
|
+
effort_clamped: rendered.clamped,
|
|
595
|
+
effort_clamp_reason: rendered.reason,
|
|
587
596
|
effort_effective: effortEffective,
|
|
588
597
|
effort_effective_source: effortEffectiveSource,
|
|
589
598
|
fast_mode: fastMode,
|
|
@@ -638,46 +647,109 @@ function effortSurfaceForHost(cwd, host) {
|
|
|
638
647
|
}
|
|
639
648
|
}
|
|
640
649
|
/**
|
|
641
|
-
* #488 — Replace or inject the
|
|
650
|
+
* #488 — Replace or inject the `<key>:` value in YAML frontmatter.
|
|
642
651
|
* Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
|
|
652
|
+
* #3706: key-parameterised so the same line-editor serves both claude's
|
|
653
|
+
* `effort:` and OpenCode's `variant:`. #3706: all offsets (eol, openLen,
|
|
654
|
+
* closingStart) are derived from the MATCHED BLOCK, not the start of the
|
|
655
|
+
* file, and the existing-key replace is scoped to the frontmatter span only.
|
|
643
656
|
*/
|
|
644
|
-
function
|
|
645
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
657
|
+
function setFrontmatterKeyLine(content, key, value) {
|
|
646
658
|
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
647
659
|
const match = fmRe.exec(content);
|
|
648
660
|
if (!match)
|
|
649
661
|
return content;
|
|
650
662
|
const fmBody = match[1];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
663
|
+
// Both writers of these frontmatter keys — this sync path and the
|
|
664
|
+
// install-side `frontmatterScalar` in runtime-artifact-conversion.cts —
|
|
665
|
+
// now share one escaping rule: quote via `agentScalarNeedsDoubleQuoting` +
|
|
666
|
+
// `escapeDoubleQuotedScalar` (both from frontmatter.cts) rather than each
|
|
667
|
+
// interpolating `value` raw/differently.
|
|
668
|
+
const renderedValue = agentScalarNeedsDoubleQuoting(value) ? `"${escapeDoubleQuotedScalar(value)}"` : value;
|
|
669
|
+
// EOL comes from the MATCHED BLOCK, not the start of the file. With a
|
|
670
|
+
// preamble the two can disagree, and on a CRLF document that misaligns every
|
|
671
|
+
// offset below by one byte and mangles the opening fence.
|
|
672
|
+
const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
|
|
654
673
|
const openLen = 3 + eol.length;
|
|
655
|
-
const
|
|
656
|
-
|
|
674
|
+
const bodyStart = match.index + openLen;
|
|
675
|
+
const closingStart = bodyStart + fmBody.length;
|
|
676
|
+
// #3706: key is now generic (not just the literal 'effort'/'variant'
|
|
677
|
+
// callers happen to pass today) — escape it before interpolating into the
|
|
678
|
+
// RegExp so a future caller can't have its key metacharacters reinterpreted.
|
|
679
|
+
const keyLineRe = new RegExp(`^(${(0, pattern_cjs_1.escapeRegex)(key)}:)[ \\t]*.*$`, 'm');
|
|
680
|
+
if (keyLineRe.test(fmBody)) {
|
|
681
|
+
// #3706: a duplicated `<key>:` line is already invalid YAML, but a
|
|
682
|
+
// non-first-wins reader (last-wins) would otherwise honour a stale
|
|
683
|
+
// second occurrence left behind by a naive single-hit replace, while
|
|
684
|
+
// this function's own single-hit read reports "in sync" — a
|
|
685
|
+
// permanently non-converging state. Use a GLOBAL replace with a
|
|
686
|
+
// first-hit flag so every occurrence collapses to exactly one, IN THE
|
|
687
|
+
// POSITION of the first occurrence (never delete-then-append, which
|
|
688
|
+
// would move the key to the end of the frontmatter and churn every
|
|
689
|
+
// already-generated single-occurrence file).
|
|
690
|
+
const escaped = (0, pattern_cjs_1.escapeRegex)(key);
|
|
691
|
+
let seen = false;
|
|
692
|
+
const newBody = fmBody.replace(new RegExp(`^${escaped}:[ \\t]*.*(\\r?\\n?)`, 'gm'), (_m, nl) => {
|
|
693
|
+
if (!seen) {
|
|
694
|
+
seen = true;
|
|
695
|
+
return `${key}: ${renderedValue}${nl}`;
|
|
696
|
+
}
|
|
697
|
+
return '';
|
|
698
|
+
});
|
|
699
|
+
// Replace INSIDE the frontmatter span only: a whole-file /m replace would
|
|
700
|
+
// rewrite an earlier preamble line that happens to start with this key.
|
|
701
|
+
return content.slice(0, bodyStart) + newBody + content.slice(closingStart);
|
|
702
|
+
}
|
|
703
|
+
return content.slice(0, closingStart) + `${key}: ${renderedValue}${eol}` + content.slice(closingStart);
|
|
657
704
|
}
|
|
658
705
|
/**
|
|
659
|
-
* #3533 (10d) — remove exactly the frontmatter
|
|
706
|
+
* #3533 (10d) — remove exactly the frontmatter `<key>:` line (and its line
|
|
660
707
|
* ending) so an agent configured for `inherit` carries NO key. Mirrors the
|
|
661
708
|
* codex-agent-toml strip discipline: targeted line removal, EOL-aware, every
|
|
662
709
|
* other byte (comments, sibling keys, the body) untouched.
|
|
710
|
+
* #3706: key-parameterised so the same line-editor serves both claude's
|
|
711
|
+
* `effort:` and OpenCode's `variant:`. #3706: openLen is derived from the
|
|
712
|
+
* MATCHED BLOCK, not the start of the file — a preamble on a CRLF document
|
|
713
|
+
* would otherwise misalign every offset below.
|
|
663
714
|
*/
|
|
664
|
-
function
|
|
715
|
+
function removeFrontmatterKeyLine(content, key) {
|
|
665
716
|
// Scoped to the FIRST frontmatter block (not a whole-file /m match): a
|
|
666
|
-
// preamble or body line starting with
|
|
717
|
+
// preamble or body line starting with `<key>:` (a fenced config example,
|
|
667
718
|
// a thematic-break flanked fragment) must never be the line removed.
|
|
668
719
|
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
669
720
|
const match = fmRe.exec(content);
|
|
670
721
|
if (!match)
|
|
671
722
|
return content;
|
|
672
723
|
const fmBody = match[1];
|
|
673
|
-
|
|
724
|
+
// #3706: same generic-key escape as setFrontmatterKeyLine above.
|
|
725
|
+
const lineRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'm');
|
|
674
726
|
if (!lineRe.test(fmBody))
|
|
675
727
|
return content;
|
|
676
|
-
|
|
677
|
-
|
|
728
|
+
// A duplicate `<key>:` mapping key is already invalid YAML (a document with
|
|
729
|
+
// two `effort:`/`variant:` lines does not parse), so this is robustness
|
|
730
|
+
// against a malformed document, not a live corruption path. Still, "a null
|
|
731
|
+
// target means the key must not exist" is an invariant this function must
|
|
732
|
+
// leave true on disk — a non-global replace here would strip only the
|
|
733
|
+
// FIRST occurrence and require a second run to converge. Use a fresh
|
|
734
|
+
// global RegExp for the strip so every occurrence in the frontmatter body
|
|
735
|
+
// is removed in one pass.
|
|
736
|
+
const stripAllRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'gm');
|
|
737
|
+
const strippedFm = fmBody.replace(stripAllRe, '');
|
|
738
|
+
// Same rule as setFrontmatterKeyLine: the EOL must come from the matched
|
|
739
|
+
// block, not the start of the file, or a preambled CRLF document misaligns.
|
|
740
|
+
const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
|
|
741
|
+
const openLen = 3 + eol.length;
|
|
678
742
|
const closingStart = match.index + openLen + fmBody.length;
|
|
679
743
|
return content.slice(0, match.index + openLen) + strippedFm + content.slice(closingStart);
|
|
680
744
|
}
|
|
745
|
+
/** #488 — Replace or inject the `effort:` value in YAML frontmatter. */
|
|
746
|
+
function setEffortFrontmatter(content, effortValue) {
|
|
747
|
+
return setFrontmatterKeyLine(content, 'effort', effortValue);
|
|
748
|
+
}
|
|
749
|
+
/** #3533 (10d) — remove exactly the frontmatter `effort:` line (and its line ending). */
|
|
750
|
+
function removeEffortFrontmatter(content) {
|
|
751
|
+
return removeFrontmatterKeyLine(content, 'effort');
|
|
752
|
+
}
|
|
681
753
|
/**
|
|
682
754
|
* #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
|
|
683
755
|
* match the current effort config, without requiring a full reinstall.
|
|
@@ -701,6 +773,14 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
701
773
|
cmdEffortSyncCodex(raw, dryRun, opts.configDir);
|
|
702
774
|
return;
|
|
703
775
|
}
|
|
776
|
+
// #3706: install now bakes OpenCode's resolved effort into agent
|
|
777
|
+
// frontmatter under the `variant:` key (not `effort:`), so OpenCode gets
|
|
778
|
+
// its own sync path — mirroring the codex branch above — rather than
|
|
779
|
+
// falling into the generic "does not use effort: frontmatter" skip.
|
|
780
|
+
if (runtime === 'opencode') {
|
|
781
|
+
cmdEffortSyncOpencode(cwd, raw, dryRun, opts.configDir);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
704
784
|
if (runtime !== 'claude') {
|
|
705
785
|
output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
|
|
706
786
|
return;
|
|
@@ -729,14 +809,46 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
729
809
|
catch {
|
|
730
810
|
return false;
|
|
731
811
|
}
|
|
732
|
-
});
|
|
812
|
+
}).sort(); // #3706: sorted like the codex and
|
|
813
|
+
// opencode branches — readdir order is platform-dependent, so leaving it unsorted makes the
|
|
814
|
+
// reported `changes` ordering differ across machines for identical inputs.
|
|
733
815
|
const changes = [];
|
|
734
816
|
let synced = 0;
|
|
735
817
|
let skipped = 0;
|
|
818
|
+
// Local-only counter: reads AND writes are both guarded in this loop (an
|
|
819
|
+
// unreadable or unwritable agent file must not abort the whole sweep), but
|
|
820
|
+
// this result shape (`{synced, skipped, changes, dry_run, agents_dir}`) is
|
|
821
|
+
// long-standing and widely consumed, so it deliberately gains NO new key
|
|
822
|
+
// (no `read_failures`/`write_failures`, unlike the codex/opencode branches
|
|
823
|
+
// below). Instead every per-file failure — read or write — is folded into
|
|
824
|
+
// `skipped` and rides the raw-mode summary token below — `output()`'s
|
|
825
|
+
// third argument is never merged into the emitted JSON object (see io.cts
|
|
826
|
+
// `output()`: it is only read when `raw === true`, entirely replacing the
|
|
827
|
+
// JSON payload), so flipping it to `'failed'` costs nothing in the wire
|
|
828
|
+
// shape while still surfacing the failure to a raw-mode caller. The three
|
|
829
|
+
// branches differ on that reporting shape, but are now also consistent in
|
|
830
|
+
// HOW they publish: every write below goes through the same tmp-file +
|
|
831
|
+
// chmod + retryRenameSync atomic-publish sequence used by
|
|
832
|
+
// cmdEffortSyncCodex and cmdEffortSyncOpencode, so a fault mid-write can
|
|
833
|
+
// never leave an agent file truncated or empty.
|
|
834
|
+
let fileFailureCount = 0;
|
|
736
835
|
for (const file of files) {
|
|
737
836
|
const agentName = file.replace(/\.md$/, '');
|
|
738
837
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
739
|
-
|
|
838
|
+
let content;
|
|
839
|
+
try {
|
|
840
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// An unreadable agent file must not abort the whole sweep. Deliberately
|
|
844
|
+
// NOT adding a new field here: this result shape (`{synced, skipped,
|
|
845
|
+
// changes, dry_run, agents_dir}`) is long-standing and widely consumed,
|
|
846
|
+
// so the failure is folded into `skipped` only, with no
|
|
847
|
+
// read_failures/write_failures list — see `fileFailureCount` above.
|
|
848
|
+
skipped++;
|
|
849
|
+
fileFailureCount++;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
740
852
|
// Resolve using install-time logic: home defaults merged with project config.
|
|
741
853
|
const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
|
|
742
854
|
// #3533 (10d): 'inherit' means the key must NOT exist. An absent key is
|
|
@@ -744,45 +856,154 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
744
856
|
// drift and the sync re-added a hand-stripped key on every apply. A
|
|
745
857
|
// present key under inherit is stripped, reported as {from, to: null}.
|
|
746
858
|
if (universalEffort === 'inherit') {
|
|
747
|
-
// eslint-disable-next-line local/no-unbounded-quantifier -- same lazy `*?` bounded by the `^---$/m` closing anchor as the concrete-path fmMatch below; duplicated here so the inherit branch validates against the same frontmatter span the strip targets
|
|
748
859
|
const fmMatchInherit = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
749
860
|
if (!fmMatchInherit) {
|
|
750
861
|
skipped++;
|
|
751
862
|
continue;
|
|
752
863
|
}
|
|
753
|
-
|
|
754
|
-
|
|
864
|
+
// Presence and value are distinct questions: `effort:` with an EMPTY
|
|
865
|
+
// value is a key that IS present but whose captured value is null (the
|
|
866
|
+
// `(.+?)` group requires at least one char). Deciding "already correct"
|
|
867
|
+
// from a null value alone is wrong here — it would leave an
|
|
868
|
+
// unresolvable `effort: null` key on disk forever. Test presence with
|
|
869
|
+
// its own regex, and only compare values once presence is known.
|
|
870
|
+
const effortPresentInherit = /^effort:/m.test(fmMatchInherit[1]);
|
|
871
|
+
if (!effortPresentInherit) {
|
|
755
872
|
skipped++;
|
|
756
873
|
continue;
|
|
757
874
|
}
|
|
758
|
-
|
|
759
|
-
|
|
875
|
+
const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit[1]);
|
|
876
|
+
// `effortPresentInherit` is guaranteed true here (checked above), so a
|
|
877
|
+
// failed value match means the key is present with an EMPTY value —
|
|
878
|
+
// report `''`, not `null`, so "present-but-empty" is never conflated
|
|
879
|
+
// with "absent" in the sync output.
|
|
760
880
|
if (!dryRun) {
|
|
761
|
-
|
|
881
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
882
|
+
// cmdEffortSyncCodex/cmdEffortSyncOpencode: write to a sibling tmp
|
|
883
|
+
// file, chmod it to match filePath's existing (masked) mode, then
|
|
884
|
+
// retryRenameSync it over the target so filePath is either the old
|
|
885
|
+
// bytes or the new ones, never half-written and never dropped to a
|
|
886
|
+
// default mode. On any failure the tmp file is unlinked (best-effort)
|
|
887
|
+
// and the write is reported (folded into `skipped`/`fileFailureCount`,
|
|
888
|
+
// no new field), not thrown, so the remaining agents still get
|
|
889
|
+
// processed. ONE failure path for this site — no nested try/catch.
|
|
890
|
+
const tmpPathInherit = `${filePath}.tmp.${process.pid}`;
|
|
891
|
+
// Stat filePath BEFORE the write so its mode can be passed at
|
|
892
|
+
// CREATION time — a plain `writeFileSync(tmpPath, data)` creates the
|
|
893
|
+
// tmp file at the default `0666 & ~umask` even when filePath is more
|
|
894
|
+
// restrictive. Best-effort only: a stat failure must not abort the
|
|
895
|
+
// sync, since the content write is what matters, not the mode.
|
|
896
|
+
let originalModeInherit;
|
|
897
|
+
try {
|
|
898
|
+
originalModeInherit = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
899
|
+
}
|
|
900
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
901
|
+
try {
|
|
902
|
+
node_fs_1.default.writeFileSync(tmpPathInherit, removeEffortFrontmatter(content), originalModeInherit !== undefined ? { mode: originalModeInherit } : undefined);
|
|
903
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
904
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
905
|
+
// from an earlier crashed run would be reused (truncated) at its
|
|
906
|
+
// OLD mode instead, and this chmod is what corrects that case.
|
|
907
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
908
|
+
// the content write is what matters, not the mode.
|
|
909
|
+
try {
|
|
910
|
+
if (originalModeInherit !== undefined)
|
|
911
|
+
node_fs_1.default.chmodSync(tmpPathInherit, originalModeInherit);
|
|
912
|
+
}
|
|
913
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
914
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathInherit, filePath);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
try {
|
|
918
|
+
node_fs_1.default.unlinkSync(tmpPathInherit);
|
|
919
|
+
}
|
|
920
|
+
catch { /* already gone or never created */ }
|
|
921
|
+
skipped++;
|
|
922
|
+
fileFailureCount++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
762
925
|
}
|
|
926
|
+
changes.push({ agent: agentName, from: effortMatchInherit ? effortMatchInherit[1] : '', to: null });
|
|
927
|
+
synced++;
|
|
763
928
|
continue;
|
|
764
929
|
}
|
|
930
|
+
// `runtime` is guaranteed 'claude' by the guard above (#3007: only
|
|
931
|
+
// codex's 'ultra' rejection can produce a null value).
|
|
765
932
|
const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, universalEffort);
|
|
766
933
|
const newEffortValue = rendered.value;
|
|
767
|
-
// eslint-disable-next-line local/no-unbounded-quantifier -- lazy `*?` bounded by the `^---$/m` closing anchor, no nested quantifier, measured linear to 5MB (no-closing-marker adversarial input)
|
|
768
934
|
const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
769
935
|
if (!fmMatch) {
|
|
770
936
|
skipped++;
|
|
771
937
|
continue;
|
|
772
938
|
}
|
|
939
|
+
// Presence and value are distinct questions here too: `currentEffort`
|
|
940
|
+
// reads null both when the key is ABSENT and when it is present with an
|
|
941
|
+
// EMPTY value. `effortPresent` disambiguates those two for the reported
|
|
942
|
+
// `from` below (never `null` when the key is present but empty) — but it
|
|
943
|
+
// has no bearing on the skip check that follows: `newEffortValue` is
|
|
944
|
+
// never null on this path (guarded above), so an absent key already
|
|
945
|
+
// yields `currentEffort === null !== newEffortValue` without consulting
|
|
946
|
+
// presence separately.
|
|
947
|
+
const effortPresent = /^effort:/m.test(fmMatch[1]);
|
|
773
948
|
const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
|
|
774
|
-
|
|
949
|
+
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
950
|
+
// states `effortPresent` deliberately disambiguates — collapsing both to
|
|
951
|
+
// `null` here would make the reported `from` lie about which case fired.
|
|
952
|
+
const currentEffort = effortPresent ? (effortMatch ? effortMatch[1] : '') : null;
|
|
775
953
|
if (currentEffort === newEffortValue) {
|
|
776
954
|
skipped++;
|
|
777
955
|
continue;
|
|
778
956
|
}
|
|
779
|
-
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
780
|
-
synced++;
|
|
781
957
|
if (!dryRun) {
|
|
782
|
-
|
|
958
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
959
|
+
// cmdEffortSyncCodex/cmdEffortSyncOpencode: write to a sibling tmp
|
|
960
|
+
// file, chmod it to match filePath's existing (masked) mode, then
|
|
961
|
+
// retryRenameSync it over the target so filePath is either the old
|
|
962
|
+
// bytes or the new ones, never half-written and never dropped to a
|
|
963
|
+
// default mode. On any failure the tmp file is unlinked (best-effort)
|
|
964
|
+
// and the write is reported (folded into `skipped`/`fileFailureCount`,
|
|
965
|
+
// no new field), not thrown, so the remaining agents still get
|
|
966
|
+
// processed. ONE failure path for this site — no nested try/catch.
|
|
967
|
+
const tmpPathSet = `${filePath}.tmp.${process.pid}`;
|
|
968
|
+
// Stat filePath BEFORE the write so its mode can be passed at CREATION
|
|
969
|
+
// time — a plain `writeFileSync(tmpPath, data)` creates the tmp file
|
|
970
|
+
// at the default `0666 & ~umask` even when filePath is more
|
|
971
|
+
// restrictive. Best-effort only: a stat failure must not abort the
|
|
972
|
+
// sync, since the content write is what matters, not the mode.
|
|
973
|
+
let originalModeSet;
|
|
974
|
+
try {
|
|
975
|
+
originalModeSet = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
976
|
+
}
|
|
977
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
978
|
+
try {
|
|
979
|
+
node_fs_1.default.writeFileSync(tmpPathSet, setEffortFrontmatter(content, newEffortValue), originalModeSet !== undefined ? { mode: originalModeSet } : undefined);
|
|
980
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
981
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
982
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
983
|
+
// mode instead, and this chmod is what corrects that case.
|
|
984
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
985
|
+
// the content write is what matters, not the mode.
|
|
986
|
+
try {
|
|
987
|
+
if (originalModeSet !== undefined)
|
|
988
|
+
node_fs_1.default.chmodSync(tmpPathSet, originalModeSet);
|
|
989
|
+
}
|
|
990
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
991
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathSet, filePath);
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
try {
|
|
995
|
+
node_fs_1.default.unlinkSync(tmpPathSet);
|
|
996
|
+
}
|
|
997
|
+
catch { /* already gone or never created */ }
|
|
998
|
+
skipped++;
|
|
999
|
+
fileFailureCount++;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
783
1002
|
}
|
|
1003
|
+
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
1004
|
+
synced++;
|
|
784
1005
|
}
|
|
785
|
-
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
|
|
1006
|
+
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, fileFailureCount > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
786
1007
|
}
|
|
787
1008
|
/**
|
|
788
1009
|
* ADR-2313 D7 (#3243) — the Codex branch of `cmdEffortSync`. Strips a stale
|
|
@@ -793,8 +1014,9 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
793
1014
|
* document is refused and reported, never partially rewritten (40-design.md
|
|
794
1015
|
* "Reconciliation" — parseCodexAgentToml is the STRICT half of the reader/
|
|
795
1016
|
* writer split). Result shape is additive over the claude branch's
|
|
796
|
-
* `{synced, skipped, changes, dry_run, agents_dir}` — `refused
|
|
797
|
-
* `write_failures` are new fields, never a reshape of
|
|
1017
|
+
* `{synced, skipped, changes, dry_run, agents_dir}` — `refused`,
|
|
1018
|
+
* `write_failures`, and `read_failures` are new fields, never a reshape of
|
|
1019
|
+
* the existing ones.
|
|
798
1020
|
*/
|
|
799
1021
|
function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
800
1022
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
@@ -822,12 +1044,25 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
822
1044
|
const changes = [];
|
|
823
1045
|
const refused = [];
|
|
824
1046
|
const writeFailures = [];
|
|
1047
|
+
const readFailures = [];
|
|
825
1048
|
let synced = 0;
|
|
826
1049
|
let skipped = 0;
|
|
827
1050
|
for (const file of files) {
|
|
828
1051
|
const agentName = file.replace(/\.toml$/, '');
|
|
829
1052
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
830
|
-
|
|
1053
|
+
let content;
|
|
1054
|
+
try {
|
|
1055
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
1056
|
+
}
|
|
1057
|
+
catch (err) {
|
|
1058
|
+
// An unreadable agent file must not abort the whole sweep — mirrors the
|
|
1059
|
+
// opencode branch's own read guard, reported under its own
|
|
1060
|
+
// `read_failures` key so a caller can tell "never read" apart from
|
|
1061
|
+
// "read but write failed".
|
|
1062
|
+
skipped++;
|
|
1063
|
+
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
831
1066
|
const parsed = (0, codex_agent_toml_cjs_1.parseCodexAgentToml)(content);
|
|
832
1067
|
if (!parsed.ok) {
|
|
833
1068
|
// Never partially rewritten (40-design.md, ADR-2313 reader/writer
|
|
@@ -869,8 +1104,32 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
869
1104
|
// bare fs.renameSync) carries the transient-Windows-lock retry per
|
|
870
1105
|
// DEFECT.WINDOWS-FS-OPS.
|
|
871
1106
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1107
|
+
// Stat filePath BEFORE the write so the original mode is available to
|
|
1108
|
+
// pass at creation time, not just at chmod time afterward — otherwise
|
|
1109
|
+
// the tmp file is briefly created at the default `0666 & ~umask`
|
|
1110
|
+
// (world-readable under a typical 022 umask) even when filePath is
|
|
1111
|
+
// e.g. 0600, exposing its contents for the window between creation and
|
|
1112
|
+
// chmod. Best-effort: a stat failure must not abort the sync, since the
|
|
1113
|
+
// content write is what matters, not the mode.
|
|
1114
|
+
let originalMode;
|
|
872
1115
|
try {
|
|
873
|
-
node_fs_1.default.
|
|
1116
|
+
originalMode = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
1117
|
+
}
|
|
1118
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1119
|
+
try {
|
|
1120
|
+
node_fs_1.default.writeFileSync(tmpPath, (0, codex_agent_toml_cjs_1.renderCodexAgentToml)(doc), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1121
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
1122
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1123
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
1124
|
+
// mode instead, and this chmod is what corrects that case. Mask off
|
|
1125
|
+
// the file-type bits fs.statSync().mode carries (POSIX leaves
|
|
1126
|
+
// chmod's handling of those unspecified); best-effort only, since
|
|
1127
|
+
// the content write is what matters, not the mode.
|
|
1128
|
+
try {
|
|
1129
|
+
if (originalMode !== undefined)
|
|
1130
|
+
node_fs_1.default.chmodSync(tmpPath, originalMode);
|
|
1131
|
+
}
|
|
1132
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
874
1133
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
875
1134
|
}
|
|
876
1135
|
catch (err) {
|
|
@@ -888,7 +1147,177 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
888
1147
|
changes.push(...pendingChanges);
|
|
889
1148
|
synced++;
|
|
890
1149
|
}
|
|
891
|
-
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, refused, write_failures: writeFailures }, raw, synced > 0 ? 'changed' : 'ok');
|
|
1150
|
+
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, refused, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* #3706 — the OpenCode branch of `cmdEffortSync`. Maintains the `variant:`
|
|
1154
|
+
* frontmatter key install now bakes into every `~/.config/opencode/agents/
|
|
1155
|
+
* gsd-*.md` (or configDir-relative equivalent), mirroring exactly what
|
|
1156
|
+
* install writes: a resolved universal effort clamped through
|
|
1157
|
+
* `clampEffortForHost('opencode', ...)`. Null means the key must be ABSENT —
|
|
1158
|
+
* #3533 (10d): an absent key is the correct state under `inherit`, and a
|
|
1159
|
+
* level OpenCode does not accept must never be written, so both collapse to
|
|
1160
|
+
* the same `target: null` and the same removal path. Result shape is
|
|
1161
|
+
* additive over the claude branch, matching the CODEX branch's
|
|
1162
|
+
* `{synced, skipped, changes, dry_run, agents_dir, write_failures}`.
|
|
1163
|
+
*/
|
|
1164
|
+
function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
1165
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1166
|
+
const { getGlobalConfigDir } = require('./runtime-homes.cjs');
|
|
1167
|
+
const agentsDir = node_path_1.default.join(configDir || getGlobalConfigDir('opencode'), 'agents');
|
|
1168
|
+
if (!node_fs_1.default.existsSync(agentsDir)) {
|
|
1169
|
+
output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// Skip symlinks — matches the claude branch's existing guard (only write
|
|
1173
|
+
// regular files, never follow a symlink into clobbering its target).
|
|
1174
|
+
const files = node_fs_1.default
|
|
1175
|
+
.readdirSync(agentsDir)
|
|
1176
|
+
.filter(f => {
|
|
1177
|
+
if (!f.startsWith('gsd-') || !f.endsWith('.md'))
|
|
1178
|
+
return false;
|
|
1179
|
+
try {
|
|
1180
|
+
return node_fs_1.default.lstatSync(node_path_1.default.join(agentsDir, f)).isFile();
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
})
|
|
1186
|
+
.sort();
|
|
1187
|
+
// Use install-time resolvers: they merge ~/.gsd/defaults.json with project
|
|
1188
|
+
// config, matching the exact logic used when agents were originally
|
|
1189
|
+
// installed. Resolved once, outside the loop, like the claude branch.
|
|
1190
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1191
|
+
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs');
|
|
1192
|
+
const effortCfg = readGsdEffectiveEffortConfig(cwd);
|
|
1193
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1194
|
+
const { clampEffortForHost } = require('./model-catalog.cjs');
|
|
1195
|
+
const changes = [];
|
|
1196
|
+
const writeFailures = [];
|
|
1197
|
+
const readFailures = [];
|
|
1198
|
+
let synced = 0;
|
|
1199
|
+
let skipped = 0;
|
|
1200
|
+
for (const file of files) {
|
|
1201
|
+
const agentName = file.replace(/\.md$/, '');
|
|
1202
|
+
const filePath = node_path_1.default.join(agentsDir, file);
|
|
1203
|
+
let content;
|
|
1204
|
+
try {
|
|
1205
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
// An unreadable agent file must not abort the whole sweep — degrade
|
|
1209
|
+
// like the write path below does, and report it under its own
|
|
1210
|
+
// `read_failures` key so a caller can tell "never read" apart from
|
|
1211
|
+
// "read but write failed".
|
|
1212
|
+
skipped++;
|
|
1213
|
+
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
// `target === null` covers both "no effort configured" (inherit) and "a
|
|
1217
|
+
// level OpenCode does not accept" — both must produce NO `variant:` key,
|
|
1218
|
+
// exactly what install writes.
|
|
1219
|
+
const universal = effortCfg ? resolveInstallTimeEffort(effortCfg, agentName) : null;
|
|
1220
|
+
const target = universal ? clampEffortForHost('opencode', universal) : null;
|
|
1221
|
+
const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
1222
|
+
if (!fmMatch) {
|
|
1223
|
+
skipped++;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
// Presence and value are distinct questions: `variant:` with an EMPTY
|
|
1227
|
+
// value is a key that IS present but whose captured value is null (the
|
|
1228
|
+
// `(.+?)` group requires at least one char). Deciding "already correct"
|
|
1229
|
+
// from a null-vs-null comparison alone is wrong when target is also
|
|
1230
|
+
// null — it would leave an unresolvable `variant: null` key on disk
|
|
1231
|
+
// forever. Test presence with its own regex, and only compare values
|
|
1232
|
+
// once presence is known.
|
|
1233
|
+
const variantPresent = /^variant:/m.test(fmMatch[1]);
|
|
1234
|
+
const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
|
|
1235
|
+
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
1236
|
+
// states this code deliberately tracks via `variantPresent` above — a
|
|
1237
|
+
// reported `from` that collapses both to `null` would make "no key" and
|
|
1238
|
+
// "empty key" indistinguishable in the sync output, even though only one
|
|
1239
|
+
// of them actually has a `variant:` line to remove.
|
|
1240
|
+
const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
|
|
1241
|
+
if (target === null) {
|
|
1242
|
+
if (!variantPresent) {
|
|
1243
|
+
skipped++;
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
else if (variantPresent && currentVariant === target) {
|
|
1248
|
+
skipped++;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
changes.push({ agent: agentName, from: currentVariant, to: target });
|
|
1252
|
+
synced++;
|
|
1253
|
+
if (!dryRun) {
|
|
1254
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
1255
|
+
// cmdEffortSyncCodex above: write to a sibling tmp file, chmod it to
|
|
1256
|
+
// match filePath's existing (masked) mode, then retryRenameSync it over
|
|
1257
|
+
// the target so filePath is either the old bytes or the new ones, never
|
|
1258
|
+
// half-written and never dropped to a default mode. On failure the
|
|
1259
|
+
// write is reported, not thrown, so the remaining agents still get
|
|
1260
|
+
// processed.
|
|
1261
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1262
|
+
// Stat filePath BEFORE the write so its mode can be passed at CREATION
|
|
1263
|
+
// time — a plain `writeFileSync(tmpPath, data)` creates the tmp file at
|
|
1264
|
+
// the default `0666 & ~umask` (world-readable under a typical 022
|
|
1265
|
+
// umask) even when filePath is e.g. 0600, exposing its contents for
|
|
1266
|
+
// the window between creation and the chmod below. Mask off the
|
|
1267
|
+
// file-type bits (e.g. S_IFREG 0o100000) that fs.statSync().mode
|
|
1268
|
+
// carries alongside the permission bits — POSIX leaves chmod's
|
|
1269
|
+
// handling of those bits unspecified, and the remote matrix runs Linux
|
|
1270
|
+
// only (Darwin tolerating the full mode is not evidence it is safe
|
|
1271
|
+
// there). Best-effort only: a stat failure must not abort the sync,
|
|
1272
|
+
// since the content write is what matters, not the mode.
|
|
1273
|
+
let originalMode;
|
|
1274
|
+
try {
|
|
1275
|
+
originalMode = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
1276
|
+
}
|
|
1277
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1278
|
+
try {
|
|
1279
|
+
node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1280
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
1281
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1282
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
1283
|
+
// mode instead, and this chmod is what corrects that case.
|
|
1284
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
1285
|
+
// the content write is what matters, not the mode.
|
|
1286
|
+
try {
|
|
1287
|
+
if (originalMode !== undefined)
|
|
1288
|
+
node_fs_1.default.chmodSync(tmpPath, originalMode);
|
|
1289
|
+
}
|
|
1290
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1291
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1292
|
+
}
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
try {
|
|
1295
|
+
node_fs_1.default.unlinkSync(tmpPath);
|
|
1296
|
+
}
|
|
1297
|
+
catch { /* already gone or never created */ }
|
|
1298
|
+
changes.pop();
|
|
1299
|
+
synced--;
|
|
1300
|
+
skipped++;
|
|
1301
|
+
writeFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Any failure — a write OR a read — must not report 'ok' or 'changed':
|
|
1307
|
+
// either would hide that at least one agent's on-disk state is now unknown
|
|
1308
|
+
// (unread) or unchanged despite being reported as a pending change (write
|
|
1309
|
+
// failed after being pushed onto `changes`/`synced`). `write_failures` and
|
|
1310
|
+
// `read_failures` take priority over the synced-count-derived summary below,
|
|
1311
|
+
// even when other agents in the same run succeeded.
|
|
1312
|
+
//
|
|
1313
|
+
// Known limitation, deliberately not fixed here: `output()` only honors its
|
|
1314
|
+
// third argument when `raw === true`, and this command's process always
|
|
1315
|
+
// exits 0 regardless of the summary string — so `if gsd-tools effort sync;
|
|
1316
|
+
// then` reads success in a shell even on a run where every write failed.
|
|
1317
|
+
// Making the exit code reflect failure would be a CLI-contract change
|
|
1318
|
+
// affecting all three cmdEffortSync* branches (claude, codex, opencode) and
|
|
1319
|
+
// is out of scope for this fix.
|
|
1320
|
+
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
892
1321
|
}
|
|
893
1322
|
/**
|
|
894
1323
|
* Detect the phase number for a commit from its `--files` path list.
|
|
@@ -1059,7 +1488,10 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1059
1488
|
// — see #2528 for the parallel drift problem in phase-locator/phase),
|
|
1060
1489
|
// so this is the canonical path-segment-bound read, not a fourth copy.
|
|
1061
1490
|
const phaseNum = detectPhaseNumberFromFiles(files);
|
|
1062
|
-
|
|
1491
|
+
// #3734: a 999.x/0.x backlog sentinel is a parking-lot entry, not a real
|
|
1492
|
+
// phase — the phase arm must never branch-mutate for it (isSentinelPhaseId
|
|
1493
|
+
// is the invariant's single owner, src/phase-id.cts).
|
|
1494
|
+
if (phaseNum && !isSentinelPhaseId(phaseNum)) {
|
|
1063
1495
|
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
1064
1496
|
if (phaseInfo) {
|
|
1065
1497
|
branchName = config['phase_branch_template']
|
|
@@ -1239,8 +1671,26 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1239
1671
|
if (canScope) {
|
|
1240
1672
|
commitArgs.push('--', ...stagedPaths);
|
|
1241
1673
|
}
|
|
1242
|
-
|
|
1674
|
+
// #3886: `git commit` runs pre-commit hooks (husky/lint-staged routinely
|
|
1675
|
+
// idles ~4s on Windows before any task) — 10s is too tight, and a timeout
|
|
1676
|
+
// kill is NOT an ordinary failure. Same band as the push call below.
|
|
1677
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd, timeout: COMMIT_TIMEOUT_MS });
|
|
1243
1678
|
if (commitResult.exitCode !== 0) {
|
|
1679
|
+
// #3886: a SIGTERM'd git commit is a timeout, not commit_failed — the
|
|
1680
|
+
// partial stderr it flushed (often incidental CRLF warnings) is noise,
|
|
1681
|
+
// and the kill can leave a stale index.lock that blocks the next
|
|
1682
|
+
// attempt. Report the distinct reason and surface the lock path.
|
|
1683
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
1684
|
+
const result = {
|
|
1685
|
+
committed: false,
|
|
1686
|
+
hash: null,
|
|
1687
|
+
reason: 'commit_timeout',
|
|
1688
|
+
timed_out: true,
|
|
1689
|
+
error: commitTimeoutMessage(cwd, commitResult.stderr, commitResult.stdout),
|
|
1690
|
+
};
|
|
1691
|
+
output(result, raw, 'failed');
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1244
1694
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
1245
1695
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
1246
1696
|
output(result, raw, 'nothing');
|
|
@@ -1380,8 +1830,21 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
1380
1830
|
const commitArgs = canScopeSub
|
|
1381
1831
|
? ['commit', '-m', message, '--', ...stagedRelPaths]
|
|
1382
1832
|
: ['commit', '-m', message];
|
|
1383
|
-
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
1833
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd, timeout: COMMIT_TIMEOUT_MS });
|
|
1384
1834
|
if (commitResult.exitCode !== 0) {
|
|
1835
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
1836
|
+
// #3886 (subrepo counterpart): timeout ≠ error; surface the stale-lock
|
|
1837
|
+
// path a killed commit can leave in the subrepo.
|
|
1838
|
+
repos[repo] = {
|
|
1839
|
+
committed: false,
|
|
1840
|
+
hash: null,
|
|
1841
|
+
files: repoFiles,
|
|
1842
|
+
reason: 'commit_timeout',
|
|
1843
|
+
timed_out: true,
|
|
1844
|
+
error: commitTimeoutMessage(repoCwd, commitResult.stderr, commitResult.stdout),
|
|
1845
|
+
};
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1385
1848
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
1386
1849
|
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
1387
1850
|
continue;
|
|
@@ -1511,9 +1974,15 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
1511
1974
|
const commitArgs = canScopePr
|
|
1512
1975
|
? ['commit', '-m', commitMessage, '--', ...changedFiles]
|
|
1513
1976
|
: ['commit', '-m', commitMessage];
|
|
1514
|
-
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
1977
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd, timeout: COMMIT_TIMEOUT_MS });
|
|
1515
1978
|
if (commitResult.exitCode !== 0) {
|
|
1516
1979
|
rollback();
|
|
1980
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
1981
|
+
// #3886 (PR-subrepo counterpart): name the timeout and the stale lock
|
|
1982
|
+
// instead of echoing the killed hook's partial stderr.
|
|
1983
|
+
error(`git commit timed out after ${COMMIT_TIMEOUT_MS / 1000}s in ${repo} (killed mid-hook; ` +
|
|
1984
|
+
`a stale lock may remain at ${resolveIndexLockPath(repoCwd)} — remove it if no git process is running)`);
|
|
1985
|
+
}
|
|
1517
1986
|
error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
|
|
1518
1987
|
}
|
|
1519
1988
|
// 6. Capture commit hash
|
|
@@ -2275,6 +2744,36 @@ function buildCommitDocsGuardHookScript() {
|
|
|
2275
2744
|
];
|
|
2276
2745
|
return lines.join('\n') + '\n';
|
|
2277
2746
|
}
|
|
2747
|
+
/**
|
|
2748
|
+
* #3886: the timeout band for `git commit` — pre-commit hooks (husky +
|
|
2749
|
+
* lint-staged idles ~4s on Windows before any task) routinely exceed the 10s
|
|
2750
|
+
* plumbing default; 30s is the same band the push call uses. Shared by all
|
|
2751
|
+
* three commit sites AND their timeout messages, so the number and the text
|
|
2752
|
+
* cannot drift apart.
|
|
2753
|
+
*/
|
|
2754
|
+
const COMMIT_TIMEOUT_MS = 30_000;
|
|
2755
|
+
/**
|
|
2756
|
+
* #3886: resolve where a killed `git commit` would leave its stale
|
|
2757
|
+
* index.lock — via `git rev-parse --git-path index.lock`, never a literal
|
|
2758
|
+
* `.git/index.lock` join (#3588 row 8's class: a linked worktree's `.git` is
|
|
2759
|
+
* a FILE pointing at `<gitdir>/worktrees/<name>/`, so the literal path
|
|
2760
|
+
* cannot exist there while the real lock blocks the next commit). Best
|
|
2761
|
+
* effort: any resolution failure falls back to the literal join, and the
|
|
2762
|
+
* message already hedges with "may remain".
|
|
2763
|
+
*/
|
|
2764
|
+
function resolveIndexLockPath(cwd) {
|
|
2765
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--git-path', 'index.lock'], { cwd });
|
|
2766
|
+
if (result.exitCode !== 0)
|
|
2767
|
+
return node_path_1.default.join(cwd, '.git', 'index.lock');
|
|
2768
|
+
const raw = result.stdout.trim();
|
|
2769
|
+
return raw ? (node_path_1.default.isAbsolute(raw) ? raw : node_path_1.default.join(cwd, raw)) : node_path_1.default.join(cwd, '.git', 'index.lock');
|
|
2770
|
+
}
|
|
2771
|
+
/** #3886: shared timeout message shape for all three commit sites. */
|
|
2772
|
+
function commitTimeoutMessage(cwd, stderr, stdout) {
|
|
2773
|
+
return (`git commit timed out after ${COMMIT_TIMEOUT_MS / 1000}s (killed mid-hook; a stale lock may remain at ` +
|
|
2774
|
+
`${resolveIndexLockPath(cwd)} — remove it if no git process is running). ` +
|
|
2775
|
+
`Partial stderr: ${stderr || stdout || '(none)'}`);
|
|
2776
|
+
}
|
|
2278
2777
|
/**
|
|
2279
2778
|
* Resolve the real git hooks directory for `cwd` via `git rev-parse
|
|
2280
2779
|
* --git-path hooks` — never a literal `.git/hooks` join (#3588 row 8: a
|