@opengsd/gsd-core 1.11.0 → 1.13.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/.opencode/plugins/gsd-core.js +12 -0
- 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 +78 -42
- 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 +91 -112
- package/agents/gsd-planner.md +20 -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 +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- 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-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- 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-aliases.cjs +16 -0
- 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 +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -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 +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- 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/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- 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 +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -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 +195 -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/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- 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-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- 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 +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -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 +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- 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/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- 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 +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- 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 +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- 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 +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- 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/tdd-red-evidence.cjs +133 -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 +2542 -387
- 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/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- 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-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- 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 +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -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/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- 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 +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- 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/mvp-concepts.md +2 -2
- 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/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- 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-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- 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/verifier-evidence-gate.md +160 -0
- 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 +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -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 +29 -5
- 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 +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- 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 +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- 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 +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- 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 +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- 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 +36 -17
- 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-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- 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 +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -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 +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -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 +189 -4
- 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/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- 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 +198 -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 +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- 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 +124 -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/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- 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-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- 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,
|
|
@@ -616,6 +625,11 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
616
625
|
* applies here exactly as everywhere else: an unknown host, a missing axis, or the
|
|
617
626
|
* `undocumented` sentinel all degrade to the safe floor rather than being trusted.
|
|
618
627
|
* Never throws — a lookup failure yields `'none'`, which renders no argument.
|
|
628
|
+
*
|
|
629
|
+
* On the module's export surface for #4255: the reviewer-lane effort resolver renders a lane's own
|
|
630
|
+
* configured level through this same negotiation, so a lane can never emit an argument for a host
|
|
631
|
+
* whose negotiated surface does not accept one. One negotiation, both channels — a second copy in
|
|
632
|
+
* the lane path is exactly how the two would drift.
|
|
619
633
|
*/
|
|
620
634
|
function effortSurfaceForHost(cwd, host) {
|
|
621
635
|
void cwd;
|
|
@@ -638,46 +652,109 @@ function effortSurfaceForHost(cwd, host) {
|
|
|
638
652
|
}
|
|
639
653
|
}
|
|
640
654
|
/**
|
|
641
|
-
* #488 — Replace or inject the
|
|
655
|
+
* #488 — Replace or inject the `<key>:` value in YAML frontmatter.
|
|
642
656
|
* Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
|
|
657
|
+
* #3706: key-parameterised so the same line-editor serves both claude's
|
|
658
|
+
* `effort:` and OpenCode's `variant:`. #3706: all offsets (eol, openLen,
|
|
659
|
+
* closingStart) are derived from the MATCHED BLOCK, not the start of the
|
|
660
|
+
* file, and the existing-key replace is scoped to the frontmatter span only.
|
|
643
661
|
*/
|
|
644
|
-
function
|
|
645
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
662
|
+
function setFrontmatterKeyLine(content, key, value) {
|
|
646
663
|
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
647
664
|
const match = fmRe.exec(content);
|
|
648
665
|
if (!match)
|
|
649
666
|
return content;
|
|
650
667
|
const fmBody = match[1];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
668
|
+
// Both writers of these frontmatter keys — this sync path and the
|
|
669
|
+
// install-side `frontmatterScalar` in runtime-artifact-conversion.cts —
|
|
670
|
+
// now share one escaping rule: quote via `agentScalarNeedsDoubleQuoting` +
|
|
671
|
+
// `escapeDoubleQuotedScalar` (both from frontmatter.cts) rather than each
|
|
672
|
+
// interpolating `value` raw/differently.
|
|
673
|
+
const renderedValue = agentScalarNeedsDoubleQuoting(value) ? `"${escapeDoubleQuotedScalar(value)}"` : value;
|
|
674
|
+
// EOL comes from the MATCHED BLOCK, not the start of the file. With a
|
|
675
|
+
// preamble the two can disagree, and on a CRLF document that misaligns every
|
|
676
|
+
// offset below by one byte and mangles the opening fence.
|
|
677
|
+
const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
|
|
654
678
|
const openLen = 3 + eol.length;
|
|
655
|
-
const
|
|
656
|
-
|
|
679
|
+
const bodyStart = match.index + openLen;
|
|
680
|
+
const closingStart = bodyStart + fmBody.length;
|
|
681
|
+
// #3706: key is now generic (not just the literal 'effort'/'variant'
|
|
682
|
+
// callers happen to pass today) — escape it before interpolating into the
|
|
683
|
+
// RegExp so a future caller can't have its key metacharacters reinterpreted.
|
|
684
|
+
const keyLineRe = new RegExp(`^(${(0, pattern_cjs_1.escapeRegex)(key)}:)[ \\t]*.*$`, 'm');
|
|
685
|
+
if (keyLineRe.test(fmBody)) {
|
|
686
|
+
// #3706: a duplicated `<key>:` line is already invalid YAML, but a
|
|
687
|
+
// non-first-wins reader (last-wins) would otherwise honour a stale
|
|
688
|
+
// second occurrence left behind by a naive single-hit replace, while
|
|
689
|
+
// this function's own single-hit read reports "in sync" — a
|
|
690
|
+
// permanently non-converging state. Use a GLOBAL replace with a
|
|
691
|
+
// first-hit flag so every occurrence collapses to exactly one, IN THE
|
|
692
|
+
// POSITION of the first occurrence (never delete-then-append, which
|
|
693
|
+
// would move the key to the end of the frontmatter and churn every
|
|
694
|
+
// already-generated single-occurrence file).
|
|
695
|
+
const escaped = (0, pattern_cjs_1.escapeRegex)(key);
|
|
696
|
+
let seen = false;
|
|
697
|
+
const newBody = fmBody.replace(new RegExp(`^${escaped}:[ \\t]*.*(\\r?\\n?)`, 'gm'), (_m, nl) => {
|
|
698
|
+
if (!seen) {
|
|
699
|
+
seen = true;
|
|
700
|
+
return `${key}: ${renderedValue}${nl}`;
|
|
701
|
+
}
|
|
702
|
+
return '';
|
|
703
|
+
});
|
|
704
|
+
// Replace INSIDE the frontmatter span only: a whole-file /m replace would
|
|
705
|
+
// rewrite an earlier preamble line that happens to start with this key.
|
|
706
|
+
return content.slice(0, bodyStart) + newBody + content.slice(closingStart);
|
|
707
|
+
}
|
|
708
|
+
return content.slice(0, closingStart) + `${key}: ${renderedValue}${eol}` + content.slice(closingStart);
|
|
657
709
|
}
|
|
658
710
|
/**
|
|
659
|
-
* #3533 (10d) — remove exactly the frontmatter
|
|
711
|
+
* #3533 (10d) — remove exactly the frontmatter `<key>:` line (and its line
|
|
660
712
|
* ending) so an agent configured for `inherit` carries NO key. Mirrors the
|
|
661
713
|
* codex-agent-toml strip discipline: targeted line removal, EOL-aware, every
|
|
662
714
|
* other byte (comments, sibling keys, the body) untouched.
|
|
715
|
+
* #3706: key-parameterised so the same line-editor serves both claude's
|
|
716
|
+
* `effort:` and OpenCode's `variant:`. #3706: openLen is derived from the
|
|
717
|
+
* MATCHED BLOCK, not the start of the file — a preamble on a CRLF document
|
|
718
|
+
* would otherwise misalign every offset below.
|
|
663
719
|
*/
|
|
664
|
-
function
|
|
720
|
+
function removeFrontmatterKeyLine(content, key) {
|
|
665
721
|
// Scoped to the FIRST frontmatter block (not a whole-file /m match): a
|
|
666
|
-
// preamble or body line starting with
|
|
722
|
+
// preamble or body line starting with `<key>:` (a fenced config example,
|
|
667
723
|
// a thematic-break flanked fragment) must never be the line removed.
|
|
668
724
|
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
669
725
|
const match = fmRe.exec(content);
|
|
670
726
|
if (!match)
|
|
671
727
|
return content;
|
|
672
728
|
const fmBody = match[1];
|
|
673
|
-
|
|
729
|
+
// #3706: same generic-key escape as setFrontmatterKeyLine above.
|
|
730
|
+
const lineRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'm');
|
|
674
731
|
if (!lineRe.test(fmBody))
|
|
675
732
|
return content;
|
|
676
|
-
|
|
677
|
-
|
|
733
|
+
// A duplicate `<key>:` mapping key is already invalid YAML (a document with
|
|
734
|
+
// two `effort:`/`variant:` lines does not parse), so this is robustness
|
|
735
|
+
// against a malformed document, not a live corruption path. Still, "a null
|
|
736
|
+
// target means the key must not exist" is an invariant this function must
|
|
737
|
+
// leave true on disk — a non-global replace here would strip only the
|
|
738
|
+
// FIRST occurrence and require a second run to converge. Use a fresh
|
|
739
|
+
// global RegExp for the strip so every occurrence in the frontmatter body
|
|
740
|
+
// is removed in one pass.
|
|
741
|
+
const stripAllRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'gm');
|
|
742
|
+
const strippedFm = fmBody.replace(stripAllRe, '');
|
|
743
|
+
// Same rule as setFrontmatterKeyLine: the EOL must come from the matched
|
|
744
|
+
// block, not the start of the file, or a preambled CRLF document misaligns.
|
|
745
|
+
const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
|
|
746
|
+
const openLen = 3 + eol.length;
|
|
678
747
|
const closingStart = match.index + openLen + fmBody.length;
|
|
679
748
|
return content.slice(0, match.index + openLen) + strippedFm + content.slice(closingStart);
|
|
680
749
|
}
|
|
750
|
+
/** #488 — Replace or inject the `effort:` value in YAML frontmatter. */
|
|
751
|
+
function setEffortFrontmatter(content, effortValue) {
|
|
752
|
+
return setFrontmatterKeyLine(content, 'effort', effortValue);
|
|
753
|
+
}
|
|
754
|
+
/** #3533 (10d) — remove exactly the frontmatter `effort:` line (and its line ending). */
|
|
755
|
+
function removeEffortFrontmatter(content) {
|
|
756
|
+
return removeFrontmatterKeyLine(content, 'effort');
|
|
757
|
+
}
|
|
681
758
|
/**
|
|
682
759
|
* #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
|
|
683
760
|
* match the current effort config, without requiring a full reinstall.
|
|
@@ -701,6 +778,14 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
701
778
|
cmdEffortSyncCodex(raw, dryRun, opts.configDir);
|
|
702
779
|
return;
|
|
703
780
|
}
|
|
781
|
+
// #3706: install now bakes OpenCode's resolved effort into agent
|
|
782
|
+
// frontmatter under the `variant:` key (not `effort:`), so OpenCode gets
|
|
783
|
+
// its own sync path — mirroring the codex branch above — rather than
|
|
784
|
+
// falling into the generic "does not use effort: frontmatter" skip.
|
|
785
|
+
if (runtime === 'opencode') {
|
|
786
|
+
cmdEffortSyncOpencode(cwd, raw, dryRun, opts.configDir);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
704
789
|
if (runtime !== 'claude') {
|
|
705
790
|
output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
|
|
706
791
|
return;
|
|
@@ -729,14 +814,46 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
729
814
|
catch {
|
|
730
815
|
return false;
|
|
731
816
|
}
|
|
732
|
-
});
|
|
817
|
+
}).sort(); // #3706: sorted like the codex and
|
|
818
|
+
// opencode branches — readdir order is platform-dependent, so leaving it unsorted makes the
|
|
819
|
+
// reported `changes` ordering differ across machines for identical inputs.
|
|
733
820
|
const changes = [];
|
|
734
821
|
let synced = 0;
|
|
735
822
|
let skipped = 0;
|
|
823
|
+
// Local-only counter: reads AND writes are both guarded in this loop (an
|
|
824
|
+
// unreadable or unwritable agent file must not abort the whole sweep), but
|
|
825
|
+
// this result shape (`{synced, skipped, changes, dry_run, agents_dir}`) is
|
|
826
|
+
// long-standing and widely consumed, so it deliberately gains NO new key
|
|
827
|
+
// (no `read_failures`/`write_failures`, unlike the codex/opencode branches
|
|
828
|
+
// below). Instead every per-file failure — read or write — is folded into
|
|
829
|
+
// `skipped` and rides the raw-mode summary token below — `output()`'s
|
|
830
|
+
// third argument is never merged into the emitted JSON object (see io.cts
|
|
831
|
+
// `output()`: it is only read when `raw === true`, entirely replacing the
|
|
832
|
+
// JSON payload), so flipping it to `'failed'` costs nothing in the wire
|
|
833
|
+
// shape while still surfacing the failure to a raw-mode caller. The three
|
|
834
|
+
// branches differ on that reporting shape, but are now also consistent in
|
|
835
|
+
// HOW they publish: every write below goes through the same tmp-file +
|
|
836
|
+
// chmod + retryRenameSync atomic-publish sequence used by
|
|
837
|
+
// cmdEffortSyncCodex and cmdEffortSyncOpencode, so a fault mid-write can
|
|
838
|
+
// never leave an agent file truncated or empty.
|
|
839
|
+
let fileFailureCount = 0;
|
|
736
840
|
for (const file of files) {
|
|
737
841
|
const agentName = file.replace(/\.md$/, '');
|
|
738
842
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
739
|
-
|
|
843
|
+
let content;
|
|
844
|
+
try {
|
|
845
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// An unreadable agent file must not abort the whole sweep. Deliberately
|
|
849
|
+
// NOT adding a new field here: this result shape (`{synced, skipped,
|
|
850
|
+
// changes, dry_run, agents_dir}`) is long-standing and widely consumed,
|
|
851
|
+
// so the failure is folded into `skipped` only, with no
|
|
852
|
+
// read_failures/write_failures list — see `fileFailureCount` above.
|
|
853
|
+
skipped++;
|
|
854
|
+
fileFailureCount++;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
740
857
|
// Resolve using install-time logic: home defaults merged with project config.
|
|
741
858
|
const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
|
|
742
859
|
// #3533 (10d): 'inherit' means the key must NOT exist. An absent key is
|
|
@@ -744,45 +861,154 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
744
861
|
// drift and the sync re-added a hand-stripped key on every apply. A
|
|
745
862
|
// present key under inherit is stripped, reported as {from, to: null}.
|
|
746
863
|
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
864
|
const fmMatchInherit = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
749
865
|
if (!fmMatchInherit) {
|
|
750
866
|
skipped++;
|
|
751
867
|
continue;
|
|
752
868
|
}
|
|
753
|
-
|
|
754
|
-
|
|
869
|
+
// Presence and value are distinct questions: `effort:` with an EMPTY
|
|
870
|
+
// value is a key that IS present but whose captured value is null (the
|
|
871
|
+
// `(.+?)` group requires at least one char). Deciding "already correct"
|
|
872
|
+
// from a null value alone is wrong here — it would leave an
|
|
873
|
+
// unresolvable `effort: null` key on disk forever. Test presence with
|
|
874
|
+
// its own regex, and only compare values once presence is known.
|
|
875
|
+
const effortPresentInherit = /^effort:/m.test(fmMatchInherit[1]);
|
|
876
|
+
if (!effortPresentInherit) {
|
|
755
877
|
skipped++;
|
|
756
878
|
continue;
|
|
757
879
|
}
|
|
758
|
-
|
|
759
|
-
|
|
880
|
+
const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit[1]);
|
|
881
|
+
// `effortPresentInherit` is guaranteed true here (checked above), so a
|
|
882
|
+
// failed value match means the key is present with an EMPTY value —
|
|
883
|
+
// report `''`, not `null`, so "present-but-empty" is never conflated
|
|
884
|
+
// with "absent" in the sync output.
|
|
760
885
|
if (!dryRun) {
|
|
761
|
-
|
|
886
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
887
|
+
// cmdEffortSyncCodex/cmdEffortSyncOpencode: write to a sibling tmp
|
|
888
|
+
// file, chmod it to match filePath's existing (masked) mode, then
|
|
889
|
+
// retryRenameSync it over the target so filePath is either the old
|
|
890
|
+
// bytes or the new ones, never half-written and never dropped to a
|
|
891
|
+
// default mode. On any failure the tmp file is unlinked (best-effort)
|
|
892
|
+
// and the write is reported (folded into `skipped`/`fileFailureCount`,
|
|
893
|
+
// no new field), not thrown, so the remaining agents still get
|
|
894
|
+
// processed. ONE failure path for this site — no nested try/catch.
|
|
895
|
+
const tmpPathInherit = `${filePath}.tmp.${process.pid}`;
|
|
896
|
+
// Stat filePath BEFORE the write so its mode can be passed at
|
|
897
|
+
// CREATION time — a plain `writeFileSync(tmpPath, data)` creates the
|
|
898
|
+
// tmp file at the default `0666 & ~umask` even when filePath is more
|
|
899
|
+
// restrictive. Best-effort only: a stat failure must not abort the
|
|
900
|
+
// sync, since the content write is what matters, not the mode.
|
|
901
|
+
let originalModeInherit;
|
|
902
|
+
try {
|
|
903
|
+
originalModeInherit = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
904
|
+
}
|
|
905
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
906
|
+
try {
|
|
907
|
+
node_fs_1.default.writeFileSync(tmpPathInherit, removeEffortFrontmatter(content), originalModeInherit !== undefined ? { mode: originalModeInherit } : undefined);
|
|
908
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
909
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
910
|
+
// from an earlier crashed run would be reused (truncated) at its
|
|
911
|
+
// OLD mode instead, and this chmod is what corrects that case.
|
|
912
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
913
|
+
// the content write is what matters, not the mode.
|
|
914
|
+
try {
|
|
915
|
+
if (originalModeInherit !== undefined)
|
|
916
|
+
node_fs_1.default.chmodSync(tmpPathInherit, originalModeInherit);
|
|
917
|
+
}
|
|
918
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
919
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathInherit, filePath);
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
try {
|
|
923
|
+
node_fs_1.default.unlinkSync(tmpPathInherit);
|
|
924
|
+
}
|
|
925
|
+
catch { /* already gone or never created */ }
|
|
926
|
+
skipped++;
|
|
927
|
+
fileFailureCount++;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
762
930
|
}
|
|
931
|
+
changes.push({ agent: agentName, from: effortMatchInherit ? effortMatchInherit[1] : '', to: null });
|
|
932
|
+
synced++;
|
|
763
933
|
continue;
|
|
764
934
|
}
|
|
935
|
+
// `runtime` is guaranteed 'claude' by the guard above (#3007: only
|
|
936
|
+
// codex's 'ultra' rejection can produce a null value).
|
|
765
937
|
const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, universalEffort);
|
|
766
938
|
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
939
|
const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
769
940
|
if (!fmMatch) {
|
|
770
941
|
skipped++;
|
|
771
942
|
continue;
|
|
772
943
|
}
|
|
944
|
+
// Presence and value are distinct questions here too: `currentEffort`
|
|
945
|
+
// reads null both when the key is ABSENT and when it is present with an
|
|
946
|
+
// EMPTY value. `effortPresent` disambiguates those two for the reported
|
|
947
|
+
// `from` below (never `null` when the key is present but empty) — but it
|
|
948
|
+
// has no bearing on the skip check that follows: `newEffortValue` is
|
|
949
|
+
// never null on this path (guarded above), so an absent key already
|
|
950
|
+
// yields `currentEffort === null !== newEffortValue` without consulting
|
|
951
|
+
// presence separately.
|
|
952
|
+
const effortPresent = /^effort:/m.test(fmMatch[1]);
|
|
773
953
|
const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
|
|
774
|
-
|
|
954
|
+
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
955
|
+
// states `effortPresent` deliberately disambiguates — collapsing both to
|
|
956
|
+
// `null` here would make the reported `from` lie about which case fired.
|
|
957
|
+
const currentEffort = effortPresent ? (effortMatch ? effortMatch[1] : '') : null;
|
|
775
958
|
if (currentEffort === newEffortValue) {
|
|
776
959
|
skipped++;
|
|
777
960
|
continue;
|
|
778
961
|
}
|
|
779
|
-
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
780
|
-
synced++;
|
|
781
962
|
if (!dryRun) {
|
|
782
|
-
|
|
963
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
964
|
+
// cmdEffortSyncCodex/cmdEffortSyncOpencode: write to a sibling tmp
|
|
965
|
+
// file, chmod it to match filePath's existing (masked) mode, then
|
|
966
|
+
// retryRenameSync it over the target so filePath is either the old
|
|
967
|
+
// bytes or the new ones, never half-written and never dropped to a
|
|
968
|
+
// default mode. On any failure the tmp file is unlinked (best-effort)
|
|
969
|
+
// and the write is reported (folded into `skipped`/`fileFailureCount`,
|
|
970
|
+
// no new field), not thrown, so the remaining agents still get
|
|
971
|
+
// processed. ONE failure path for this site — no nested try/catch.
|
|
972
|
+
const tmpPathSet = `${filePath}.tmp.${process.pid}`;
|
|
973
|
+
// Stat filePath BEFORE the write so its mode can be passed at CREATION
|
|
974
|
+
// time — a plain `writeFileSync(tmpPath, data)` creates the tmp file
|
|
975
|
+
// at the default `0666 & ~umask` even when filePath is more
|
|
976
|
+
// restrictive. Best-effort only: a stat failure must not abort the
|
|
977
|
+
// sync, since the content write is what matters, not the mode.
|
|
978
|
+
let originalModeSet;
|
|
979
|
+
try {
|
|
980
|
+
originalModeSet = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
981
|
+
}
|
|
982
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
983
|
+
try {
|
|
984
|
+
node_fs_1.default.writeFileSync(tmpPathSet, setEffortFrontmatter(content, newEffortValue), originalModeSet !== undefined ? { mode: originalModeSet } : undefined);
|
|
985
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
986
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
987
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
988
|
+
// mode instead, and this chmod is what corrects that case.
|
|
989
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
990
|
+
// the content write is what matters, not the mode.
|
|
991
|
+
try {
|
|
992
|
+
if (originalModeSet !== undefined)
|
|
993
|
+
node_fs_1.default.chmodSync(tmpPathSet, originalModeSet);
|
|
994
|
+
}
|
|
995
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
996
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathSet, filePath);
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
try {
|
|
1000
|
+
node_fs_1.default.unlinkSync(tmpPathSet);
|
|
1001
|
+
}
|
|
1002
|
+
catch { /* already gone or never created */ }
|
|
1003
|
+
skipped++;
|
|
1004
|
+
fileFailureCount++;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
783
1007
|
}
|
|
1008
|
+
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
1009
|
+
synced++;
|
|
784
1010
|
}
|
|
785
|
-
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
|
|
1011
|
+
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, fileFailureCount > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
786
1012
|
}
|
|
787
1013
|
/**
|
|
788
1014
|
* ADR-2313 D7 (#3243) — the Codex branch of `cmdEffortSync`. Strips a stale
|
|
@@ -793,8 +1019,9 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
793
1019
|
* document is refused and reported, never partially rewritten (40-design.md
|
|
794
1020
|
* "Reconciliation" — parseCodexAgentToml is the STRICT half of the reader/
|
|
795
1021
|
* 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
|
|
1022
|
+
* `{synced, skipped, changes, dry_run, agents_dir}` — `refused`,
|
|
1023
|
+
* `write_failures`, and `read_failures` are new fields, never a reshape of
|
|
1024
|
+
* the existing ones.
|
|
798
1025
|
*/
|
|
799
1026
|
function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
800
1027
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
@@ -822,12 +1049,25 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
822
1049
|
const changes = [];
|
|
823
1050
|
const refused = [];
|
|
824
1051
|
const writeFailures = [];
|
|
1052
|
+
const readFailures = [];
|
|
825
1053
|
let synced = 0;
|
|
826
1054
|
let skipped = 0;
|
|
827
1055
|
for (const file of files) {
|
|
828
1056
|
const agentName = file.replace(/\.toml$/, '');
|
|
829
1057
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
830
|
-
|
|
1058
|
+
let content;
|
|
1059
|
+
try {
|
|
1060
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
1061
|
+
}
|
|
1062
|
+
catch (err) {
|
|
1063
|
+
// An unreadable agent file must not abort the whole sweep — mirrors the
|
|
1064
|
+
// opencode branch's own read guard, reported under its own
|
|
1065
|
+
// `read_failures` key so a caller can tell "never read" apart from
|
|
1066
|
+
// "read but write failed".
|
|
1067
|
+
skipped++;
|
|
1068
|
+
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
831
1071
|
const parsed = (0, codex_agent_toml_cjs_1.parseCodexAgentToml)(content);
|
|
832
1072
|
if (!parsed.ok) {
|
|
833
1073
|
// Never partially rewritten (40-design.md, ADR-2313 reader/writer
|
|
@@ -869,8 +1109,32 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
869
1109
|
// bare fs.renameSync) carries the transient-Windows-lock retry per
|
|
870
1110
|
// DEFECT.WINDOWS-FS-OPS.
|
|
871
1111
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1112
|
+
// Stat filePath BEFORE the write so the original mode is available to
|
|
1113
|
+
// pass at creation time, not just at chmod time afterward — otherwise
|
|
1114
|
+
// the tmp file is briefly created at the default `0666 & ~umask`
|
|
1115
|
+
// (world-readable under a typical 022 umask) even when filePath is
|
|
1116
|
+
// e.g. 0600, exposing its contents for the window between creation and
|
|
1117
|
+
// chmod. Best-effort: a stat failure must not abort the sync, since the
|
|
1118
|
+
// content write is what matters, not the mode.
|
|
1119
|
+
let originalMode;
|
|
872
1120
|
try {
|
|
873
|
-
node_fs_1.default.
|
|
1121
|
+
originalMode = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
1122
|
+
}
|
|
1123
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1124
|
+
try {
|
|
1125
|
+
node_fs_1.default.writeFileSync(tmpPath, (0, codex_agent_toml_cjs_1.renderCodexAgentToml)(doc), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1126
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
1127
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1128
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
1129
|
+
// mode instead, and this chmod is what corrects that case. Mask off
|
|
1130
|
+
// the file-type bits fs.statSync().mode carries (POSIX leaves
|
|
1131
|
+
// chmod's handling of those unspecified); best-effort only, since
|
|
1132
|
+
// the content write is what matters, not the mode.
|
|
1133
|
+
try {
|
|
1134
|
+
if (originalMode !== undefined)
|
|
1135
|
+
node_fs_1.default.chmodSync(tmpPath, originalMode);
|
|
1136
|
+
}
|
|
1137
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
874
1138
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
875
1139
|
}
|
|
876
1140
|
catch (err) {
|
|
@@ -888,7 +1152,177 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
888
1152
|
changes.push(...pendingChanges);
|
|
889
1153
|
synced++;
|
|
890
1154
|
}
|
|
891
|
-
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, refused, write_failures: writeFailures }, raw, synced > 0 ? 'changed' : 'ok');
|
|
1155
|
+
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');
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* #3706 — the OpenCode branch of `cmdEffortSync`. Maintains the `variant:`
|
|
1159
|
+
* frontmatter key install now bakes into every `~/.config/opencode/agents/
|
|
1160
|
+
* gsd-*.md` (or configDir-relative equivalent), mirroring exactly what
|
|
1161
|
+
* install writes: a resolved universal effort clamped through
|
|
1162
|
+
* `clampEffortForHost('opencode', ...)`. Null means the key must be ABSENT —
|
|
1163
|
+
* #3533 (10d): an absent key is the correct state under `inherit`, and a
|
|
1164
|
+
* level OpenCode does not accept must never be written, so both collapse to
|
|
1165
|
+
* the same `target: null` and the same removal path. Result shape is
|
|
1166
|
+
* additive over the claude branch, matching the CODEX branch's
|
|
1167
|
+
* `{synced, skipped, changes, dry_run, agents_dir, write_failures}`.
|
|
1168
|
+
*/
|
|
1169
|
+
function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
1170
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1171
|
+
const { getGlobalConfigDir } = require('./runtime-homes.cjs');
|
|
1172
|
+
const agentsDir = node_path_1.default.join(configDir || getGlobalConfigDir('opencode'), 'agents');
|
|
1173
|
+
if (!node_fs_1.default.existsSync(agentsDir)) {
|
|
1174
|
+
output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
// Skip symlinks — matches the claude branch's existing guard (only write
|
|
1178
|
+
// regular files, never follow a symlink into clobbering its target).
|
|
1179
|
+
const files = node_fs_1.default
|
|
1180
|
+
.readdirSync(agentsDir)
|
|
1181
|
+
.filter(f => {
|
|
1182
|
+
if (!f.startsWith('gsd-') || !f.endsWith('.md'))
|
|
1183
|
+
return false;
|
|
1184
|
+
try {
|
|
1185
|
+
return node_fs_1.default.lstatSync(node_path_1.default.join(agentsDir, f)).isFile();
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
.sort();
|
|
1192
|
+
// Use install-time resolvers: they merge ~/.gsd/defaults.json with project
|
|
1193
|
+
// config, matching the exact logic used when agents were originally
|
|
1194
|
+
// installed. Resolved once, outside the loop, like the claude branch.
|
|
1195
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1196
|
+
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs');
|
|
1197
|
+
const effortCfg = readGsdEffectiveEffortConfig(cwd);
|
|
1198
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
1199
|
+
const { clampEffortForHost } = require('./model-catalog.cjs');
|
|
1200
|
+
const changes = [];
|
|
1201
|
+
const writeFailures = [];
|
|
1202
|
+
const readFailures = [];
|
|
1203
|
+
let synced = 0;
|
|
1204
|
+
let skipped = 0;
|
|
1205
|
+
for (const file of files) {
|
|
1206
|
+
const agentName = file.replace(/\.md$/, '');
|
|
1207
|
+
const filePath = node_path_1.default.join(agentsDir, file);
|
|
1208
|
+
let content;
|
|
1209
|
+
try {
|
|
1210
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
1211
|
+
}
|
|
1212
|
+
catch (err) {
|
|
1213
|
+
// An unreadable agent file must not abort the whole sweep — degrade
|
|
1214
|
+
// like the write path below does, and report it under its own
|
|
1215
|
+
// `read_failures` key so a caller can tell "never read" apart from
|
|
1216
|
+
// "read but write failed".
|
|
1217
|
+
skipped++;
|
|
1218
|
+
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
// `target === null` covers both "no effort configured" (inherit) and "a
|
|
1222
|
+
// level OpenCode does not accept" — both must produce NO `variant:` key,
|
|
1223
|
+
// exactly what install writes.
|
|
1224
|
+
const universal = effortCfg ? resolveInstallTimeEffort(effortCfg, agentName) : null;
|
|
1225
|
+
const target = universal ? clampEffortForHost('opencode', universal) : null;
|
|
1226
|
+
const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
|
|
1227
|
+
if (!fmMatch) {
|
|
1228
|
+
skipped++;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
// Presence and value are distinct questions: `variant:` with an EMPTY
|
|
1232
|
+
// value is a key that IS present but whose captured value is null (the
|
|
1233
|
+
// `(.+?)` group requires at least one char). Deciding "already correct"
|
|
1234
|
+
// from a null-vs-null comparison alone is wrong when target is also
|
|
1235
|
+
// null — it would leave an unresolvable `variant: null` key on disk
|
|
1236
|
+
// forever. Test presence with its own regex, and only compare values
|
|
1237
|
+
// once presence is known.
|
|
1238
|
+
const variantPresent = /^variant:/m.test(fmMatch[1]);
|
|
1239
|
+
const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
|
|
1240
|
+
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
1241
|
+
// states this code deliberately tracks via `variantPresent` above — a
|
|
1242
|
+
// reported `from` that collapses both to `null` would make "no key" and
|
|
1243
|
+
// "empty key" indistinguishable in the sync output, even though only one
|
|
1244
|
+
// of them actually has a `variant:` line to remove.
|
|
1245
|
+
const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
|
|
1246
|
+
if (target === null) {
|
|
1247
|
+
if (!variantPresent) {
|
|
1248
|
+
skipped++;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
else if (variantPresent && currentVariant === target) {
|
|
1253
|
+
skipped++;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
changes.push({ agent: agentName, from: currentVariant, to: target });
|
|
1257
|
+
synced++;
|
|
1258
|
+
if (!dryRun) {
|
|
1259
|
+
// Atomic publish AND mode preservation, same discipline as
|
|
1260
|
+
// cmdEffortSyncCodex above: write to a sibling tmp file, chmod it to
|
|
1261
|
+
// match filePath's existing (masked) mode, then retryRenameSync it over
|
|
1262
|
+
// the target so filePath is either the old bytes or the new ones, never
|
|
1263
|
+
// half-written and never dropped to a default mode. On failure the
|
|
1264
|
+
// write is reported, not thrown, so the remaining agents still get
|
|
1265
|
+
// processed.
|
|
1266
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1267
|
+
// Stat filePath BEFORE the write so its mode can be passed at CREATION
|
|
1268
|
+
// time — a plain `writeFileSync(tmpPath, data)` creates the tmp file at
|
|
1269
|
+
// the default `0666 & ~umask` (world-readable under a typical 022
|
|
1270
|
+
// umask) even when filePath is e.g. 0600, exposing its contents for
|
|
1271
|
+
// the window between creation and the chmod below. Mask off the
|
|
1272
|
+
// file-type bits (e.g. S_IFREG 0o100000) that fs.statSync().mode
|
|
1273
|
+
// carries alongside the permission bits — POSIX leaves chmod's
|
|
1274
|
+
// handling of those bits unspecified, and the remote matrix runs Linux
|
|
1275
|
+
// only (Darwin tolerating the full mode is not evidence it is safe
|
|
1276
|
+
// there). Best-effort only: a stat failure must not abort the sync,
|
|
1277
|
+
// since the content write is what matters, not the mode.
|
|
1278
|
+
let originalMode;
|
|
1279
|
+
try {
|
|
1280
|
+
originalMode = node_fs_1.default.statSync(filePath).mode & 0o7777;
|
|
1281
|
+
}
|
|
1282
|
+
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1283
|
+
try {
|
|
1284
|
+
node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1285
|
+
// Not redundant with the `mode` option above: `mode` only applies
|
|
1286
|
+
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1287
|
+
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
1288
|
+
// mode instead, and this chmod is what corrects that case.
|
|
1289
|
+
// Best-effort only: a chmod failure must not abort the sync, since
|
|
1290
|
+
// the content write is what matters, not the mode.
|
|
1291
|
+
try {
|
|
1292
|
+
if (originalMode !== undefined)
|
|
1293
|
+
node_fs_1.default.chmodSync(tmpPath, originalMode);
|
|
1294
|
+
}
|
|
1295
|
+
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1296
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1297
|
+
}
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
try {
|
|
1300
|
+
node_fs_1.default.unlinkSync(tmpPath);
|
|
1301
|
+
}
|
|
1302
|
+
catch { /* already gone or never created */ }
|
|
1303
|
+
changes.pop();
|
|
1304
|
+
synced--;
|
|
1305
|
+
skipped++;
|
|
1306
|
+
writeFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// Any failure — a write OR a read — must not report 'ok' or 'changed':
|
|
1312
|
+
// either would hide that at least one agent's on-disk state is now unknown
|
|
1313
|
+
// (unread) or unchanged despite being reported as a pending change (write
|
|
1314
|
+
// failed after being pushed onto `changes`/`synced`). `write_failures` and
|
|
1315
|
+
// `read_failures` take priority over the synced-count-derived summary below,
|
|
1316
|
+
// even when other agents in the same run succeeded.
|
|
1317
|
+
//
|
|
1318
|
+
// Known limitation, deliberately not fixed here: `output()` only honors its
|
|
1319
|
+
// third argument when `raw === true`, and this command's process always
|
|
1320
|
+
// exits 0 regardless of the summary string — so `if gsd-tools effort sync;
|
|
1321
|
+
// then` reads success in a shell even on a run where every write failed.
|
|
1322
|
+
// Making the exit code reflect failure would be a CLI-contract change
|
|
1323
|
+
// affecting all three cmdEffortSync* branches (claude, codex, opencode) and
|
|
1324
|
+
// is out of scope for this fix.
|
|
1325
|
+
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
1326
|
}
|
|
893
1327
|
/**
|
|
894
1328
|
* Detect the phase number for a commit from its `--files` path list.
|
|
@@ -1059,7 +1493,10 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1059
1493
|
// — see #2528 for the parallel drift problem in phase-locator/phase),
|
|
1060
1494
|
// so this is the canonical path-segment-bound read, not a fourth copy.
|
|
1061
1495
|
const phaseNum = detectPhaseNumberFromFiles(files);
|
|
1062
|
-
|
|
1496
|
+
// #3734: a 999.x/0.x backlog sentinel is a parking-lot entry, not a real
|
|
1497
|
+
// phase — the phase arm must never branch-mutate for it (isSentinelPhaseId
|
|
1498
|
+
// is the invariant's single owner, src/phase-id.cts).
|
|
1499
|
+
if (phaseNum && !isSentinelPhaseId(phaseNum)) {
|
|
1063
1500
|
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
1064
1501
|
if (phaseInfo) {
|
|
1065
1502
|
branchName = config['phase_branch_template']
|
|
@@ -1223,12 +1660,232 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1223
1660
|
// During a merge, git refuses partial commits — fall back to a bare commit.
|
|
1224
1661
|
// --amend is left without a pathspec: amending with -- <paths> is a different
|
|
1225
1662
|
// operation that rewrites the tip with only those paths.
|
|
1226
|
-
|
|
1663
|
+
const mergeHeadProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd });
|
|
1664
|
+
const isMergeInProgress = mergeHeadProbe.exitCode === 0;
|
|
1665
|
+
// PROVENANCE FOR THIS WHOLE BLOCK: every behavioural claim below was DRIVEN
|
|
1666
|
+
// against git 2.54, not reasoned by analogy. Individual claims state what was
|
|
1667
|
+
// observed and omit the version; where a claim is version-SENSITIVE rather
|
|
1668
|
+
// than merely version-observed, it says so at the claim.
|
|
1669
|
+
//
|
|
1670
|
+
// #3776: git refuses a PARTIAL commit (`git commit -- <paths>`) while a merge
|
|
1671
|
+
// or a cherry-pick is in progress, so in those states the pathspec describes
|
|
1672
|
+
// nothing about what would actually land and the empty-diff decision below
|
|
1673
|
+
// must not be made from it. The three sequencer states do NOT agree:
|
|
1674
|
+
// MERGE_HEAD -> `fatal: cannot do a partial commit during a merge.`
|
|
1675
|
+
// CHERRY_PICK_HEAD -> `fatal: cannot do a partial commit during a cherry-pick.`
|
|
1676
|
+
// REVERT_HEAD -> permitted; behaves like an ordinary commit.
|
|
1677
|
+
// REVERT_HEAD is therefore deliberately absent: including it would suppress
|
|
1678
|
+
// this fix during a revert, reintroducing the very misreport it removes.
|
|
1679
|
+
// `canScope` below keeps its narrower merge-only test on purpose — widening it
|
|
1680
|
+
// would change pre-existing cherry-pick behaviour, which is outside this fix.
|
|
1681
|
+
// Only the scoped, non-amend call can return through the guard below, so the
|
|
1682
|
+
// cherry-pick probe and the guard's own probes are gated on that — an
|
|
1683
|
+
// unscoped commit or an --amend would otherwise pay for git invocations whose
|
|
1684
|
+
// answer it can never use. The MERGE_HEAD probe above predates this fix and
|
|
1685
|
+
// stays unconditional: `canScope` needs it on every path.
|
|
1686
|
+
// A non-zero exit from either sequencer probe means "not in that state" AND
|
|
1687
|
+
// "the probe never answered" — `execGit` surfaces a spawn timeout as
|
|
1688
|
+
// `exitCode: 1` (`_spawnResult`: `result.status ?? 1`), which is the exact
|
|
1689
|
+
// code `rev-parse --verify` returns for a ref that does not exist. Conflating
|
|
1690
|
+
// them is the one path in this fix that does NOT fail toward the old
|
|
1691
|
+
// behaviour: a timeout during a real merge would leave `partialCommitRefused`
|
|
1692
|
+
// false, the guard would decide `nothing_to_commit` from a pathspec git will
|
|
1693
|
+
// not honour, and the merge would be silently abandoned where it previously
|
|
1694
|
+
// reported a loud `commit_failed`. So an unanswered probe is treated as
|
|
1695
|
+
// "assume the partial commit would be refused" — the conservative reading,
|
|
1696
|
+
// which falls through to `git commit` and lets git speak for itself.
|
|
1697
|
+
//
|
|
1698
|
+
// This is deliberately routed into `partialCommitRefused` ONLY, never into
|
|
1699
|
+
// `isMergeInProgress`: that flag also feeds the pre-existing `canScope` below,
|
|
1700
|
+
// where a spurious timeout would convert a scoped commit into a bare one and
|
|
1701
|
+
// record the whole index instead of the named paths. Suppressing a misreport
|
|
1702
|
+
// must not be paid for by committing content the caller never named.
|
|
1703
|
+
const guardApplies = explicitFiles && !amend;
|
|
1704
|
+
const cherryPickProbe = guardApplies
|
|
1705
|
+
? (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'CHERRY_PICK_HEAD'], { cwd })
|
|
1706
|
+
: null;
|
|
1707
|
+
const partialCommitRefused = isMergeInProgress
|
|
1708
|
+
|| (0, shell_command_projection_cjs_1.isSpawnTimeout)(mergeHeadProbe)
|
|
1709
|
+
|| (cherryPickProbe !== null
|
|
1710
|
+
&& (cherryPickProbe.exitCode === 0 || (0, shell_command_projection_cjs_1.isSpawnTimeout)(cherryPickProbe)));
|
|
1711
|
+
// `stagedPaths` records paths whose `git add` exited 0 — that is "did
|
|
1712
|
+
// staging succeed", not "is there anything to commit". Staging an
|
|
1713
|
+
// already-committed, unmodified file succeeds while contributing no diff, so
|
|
1714
|
+
// `length === 0` is reachable only when EVERY named path was missing from
|
|
1715
|
+
// disk. For the ordinary empty-diff case control fell through to `git commit`,
|
|
1716
|
+
// and the only thing converting that back to `nothing_to_commit` was the
|
|
1717
|
+
// string match on git's output below — which a rejecting pre-commit hook
|
|
1718
|
+
// pre-empts, because git runs the hook before it decides there is nothing to
|
|
1719
|
+
// commit. The caller was then handed `commit_failed` carrying a gate message
|
|
1720
|
+
// about a commit that had nothing to gate. Ask git whether the named paths
|
|
1721
|
+
// actually differ instead. Three things about that probe are load-bearing:
|
|
1722
|
+
// - it compares the WORKING TREE to HEAD (`diff HEAD`), not the index
|
|
1723
|
+
// (`diff --cached`). `git commit -- <paths>` is a partial commit: it takes
|
|
1724
|
+
// the working-tree content of those paths and ignores what is staged. A
|
|
1725
|
+
// probe against the index therefore answers a different question than the
|
|
1726
|
+
// commit asks, and a working-tree write landing between the `git add`
|
|
1727
|
+
// above and this line — another process in a shared checkout — would make
|
|
1728
|
+
// the index say "empty" while the commit would still have recorded the new
|
|
1729
|
+
// content. Driven: `diff --cached` rc 0 and `diff HEAD` rc 1 on the same
|
|
1730
|
+
// path, with `git commit -- <path>` then committing it.
|
|
1731
|
+
// - the `length === 0` short-circuit keeps the all-missing-paths case exact.
|
|
1732
|
+
// Spreading an empty array yields a pathspec-less `diff`, which tests the
|
|
1733
|
+
// WHOLE tree — unrelated work elsewhere would then suppress the guard and
|
|
1734
|
+
// regress the skip-missing contract (#2014).
|
|
1735
|
+
// It is deliberately NOT gated on `partialCommitRefused`, and gating it
|
|
1736
|
+
// would be a REGRESSION rather than a hardening. With every named path
|
|
1737
|
+
// missing, `stagedPaths` is empty, so `canScope` is false and the
|
|
1738
|
+
// fall-through reaches a BARE `git commit` — which git PERMITS during a
|
|
1739
|
+
// merge, and which then CONCLUDES that merge: rc 0, a two-parent merge
|
|
1740
|
+
// commit recording the entire index, under a message naming a path that
|
|
1741
|
+
// does not exist, reported to the caller as `committed: true` (driven).
|
|
1742
|
+
// Today's answer writes nothing at all. That is the same trade the timeout
|
|
1743
|
+
// routing above already refuses — a misreport must not be paid for by
|
|
1744
|
+
// committing content the caller never named — which is why the sequencer
|
|
1745
|
+
// states gate the DIFF branch only. The behaviour is also PRE-EXISTING and
|
|
1746
|
+
// unchanged by this fix: before it the identical short-circuit ran ABOVE
|
|
1747
|
+
// the MERGE_HEAD probe, so it never consulted the sequencer either. The
|
|
1748
|
+
// residual it leaves — a merge held open behind a `nothing_to_commit`
|
|
1749
|
+
// report — is offered as a separate issue with the other three, not folded
|
|
1750
|
+
// in here. Both sequencer shapes are pinned in
|
|
1751
|
+
// tests/commit-files-pathspec.test.cjs.
|
|
1752
|
+
// - `!partialCommitRefused`: see above — deciding "nothing to commit" from a
|
|
1753
|
+
// pathspec git will not honour would abandon an in-progress merge, so those
|
|
1754
|
+
// states keep their pre-existing behaviour untouched.
|
|
1755
|
+
// - the probe is pinned against user configuration that would make `git diff`
|
|
1756
|
+
// answer a DIFFERENT question than `git commit -- <paths>` asks. `git diff`
|
|
1757
|
+
// is porcelain and honours settings the commit does not, so without these
|
|
1758
|
+
// flags a caller's config decides whether the guard fires. Each vector
|
|
1759
|
+
// below was driven with the paired `git commit -- <path>` confirmed to
|
|
1760
|
+
// record the change the probe reported as absent:
|
|
1761
|
+
// `diff.ignoreSubmodules=all` -> a gitlink bump is invisible to the probe
|
|
1762
|
+
// `.gitmodules` `ignore = all` -> the same, and it needs NO local config:
|
|
1763
|
+
// it is checked in, so it arrives with a
|
|
1764
|
+
// clone
|
|
1765
|
+
// `diff=<driver>` + `textconv` -> two different blobs converge to one
|
|
1766
|
+
// text, so the probe sees no change at
|
|
1767
|
+
// all; no submodule involved
|
|
1768
|
+
// `--ignore-submodules=dirty` rather than `=none`, because `dirty` is what
|
|
1769
|
+
// a partial commit of a submodule path actually means: it records the
|
|
1770
|
+
// GITLINK, and the gitlink moves only when the submodule's HEAD does. Under
|
|
1771
|
+
// `=none` a merely dirty submodule WORKTREE reports a difference the commit
|
|
1772
|
+
// would not record, sending an empty call back to `git commit` — the #3776
|
|
1773
|
+
// misreport, re-entered from the other side. `dirty` still overrides both
|
|
1774
|
+
// `diff.ignoreSubmodules` and a checked-in `.gitmodules` `ignore`, so the
|
|
1775
|
+
// gitlink vectors above stay closed (driven: rc 1 under every one of them).
|
|
1776
|
+
// `--no-ext-diff` is deliberately absent: `--quiet` short-circuits ahead of
|
|
1777
|
+
// an external diff driver, so an external `diff.<driver>.command` cannot
|
|
1778
|
+
// invert the probe (driven: rc 1 with and without the flag).
|
|
1779
|
+
// Any other non-zero exit from the probe (a genuine git error, or an unborn
|
|
1780
|
+
// HEAD) leaves the guard shut and falls through to the commit — failing toward
|
|
1781
|
+
// today's path rather than manufacturing a no-op.
|
|
1782
|
+
// THE ONE STATE WHERE `git diff` AND `git commit -- <paths>` GENUINELY DISAGREE.
|
|
1783
|
+
// `--assume-unchanged` tells git to skip the worktree stat for a path, so
|
|
1784
|
+
// `git add` stages nothing and BOTH diff forms report no difference — while
|
|
1785
|
+
// `git commit -- <path>` reads the working tree directly and records it
|
|
1786
|
+
// (driven: probe rc 0, commit rc 0, new content in the tree). Left
|
|
1787
|
+
// to the diff probe alone the guard reports `nothing_to_commit` about content
|
|
1788
|
+
// the caller explicitly named in `--files` and git would have written. #3776
|
|
1789
|
+
// is a purely diagnostic bug — nothing is corrupted and no wrong commit is
|
|
1790
|
+
// made — so suppressing its misreport must not be paid for by dropping named
|
|
1791
|
+
// content. The same rule the timeout routing already follows one block up.
|
|
1792
|
+
//
|
|
1793
|
+
// `git ls-files -v` is the discriminator for the STATE: it tags an
|
|
1794
|
+
// assume-unchanged path with a LOWERCASE letter (`h`), where
|
|
1795
|
+
// `--skip-worktree` is an uppercase `S` and never reaches THIS branch:
|
|
1796
|
+
// `git add` exits 1 under it, so a present-but-modified skip-worktree path
|
|
1797
|
+
// fails closed as `staging_failed` above the guard. (An ABSENT one is skipped
|
|
1798
|
+
// before `git add` runs at all per #2014, and is answered by the
|
|
1799
|
+
// `stagedPaths.length === 0` arm above — correctly, and exactly as it was
|
|
1800
|
+
// pre-fix. Both shapes are pinned.)
|
|
1801
|
+
//
|
|
1802
|
+
// Then ASK GIT, rather than reconstructing its answer. `git commit --dry-run`
|
|
1803
|
+
// is the same decision the real commit makes, and `--no-verify` is what keeps
|
|
1804
|
+
// it a DECISION rather than an execution. git 2.54 already declines to run
|
|
1805
|
+
// `pre-commit` on a dry run (driven: a rejecting one neither fires nor writes
|
|
1806
|
+
// its marker), which is the property that matters here, because a firing
|
|
1807
|
+
// `pre-commit` is the whole of #3776 — but that is an observed behaviour of
|
|
1808
|
+
// one version, and the failure it would produce on a version that differs is
|
|
1809
|
+
// SILENT. A `pre-commit` that fires and rejects exits 1, the same code git
|
|
1810
|
+
// returns for `nothing to record`, so the closure below would read it as a
|
|
1811
|
+
// CONFIRMED empty answer, drop the content the caller named, and report
|
|
1812
|
+
// `nothing_to_commit` — #3776's exact shape, in #3776's exact configuration.
|
|
1813
|
+
// `--no-verify` forecloses that structurally instead of resting on the
|
|
1814
|
+
// version, and is behaviour-neutral where the version already agrees (driven:
|
|
1815
|
+
// rc 0 would-record / rc 1 nothing, identical with and without it). This is
|
|
1816
|
+
// VERSION-SENSITIVE reasoning, hence stated at the claim per the provenance
|
|
1817
|
+
// note above.
|
|
1818
|
+
//
|
|
1819
|
+
// It is still NOT hook-free in general, and `--no-verify` does not widen that
|
|
1820
|
+
// claim: `post-index-change` fires on this call with or without the flag
|
|
1821
|
+
// (driven both ways), so a repo using that hook sees TWO extra invocations
|
|
1822
|
+
// for the probe — git fires it twice per `commit --dry-run`, and twice again
|
|
1823
|
+
// for the real commit (driven: 2/2/2 across flagged probe, unflagged probe
|
|
1824
|
+
// and real commit). Stated rather than claimed away; the narrower
|
|
1825
|
+
// claim is the true one. `--porcelain` keeps the output to a couple
|
|
1826
|
+
// of machine-readable lines instead of a full status listing — the rc is
|
|
1827
|
+
// identical either way (driven: 0 would-record / 1 nothing), but the plain
|
|
1828
|
+
// form prints every untracked path, which on a large tree is output this
|
|
1829
|
+
// probe has no use for and `execGit` would have to buffer. rc 0 means the
|
|
1830
|
+
// commit would record something, so the guard must stand aside.
|
|
1831
|
+
//
|
|
1832
|
+
// Reconstructing it was tried and is WRONG in three measured ways, all of
|
|
1833
|
+
// them silent drops of named content. Comparing `git hash-object` against
|
|
1834
|
+
// `HEAD:<path>` misses a mode-only change (`chmod +x` leaves the blob
|
|
1835
|
+
// identical while `git commit -- <path>` records `100755`); it cannot hash a
|
|
1836
|
+
// submodule path at all (`fatal: Unable to hash sub`, while the commit
|
|
1837
|
+
// advances the gitlink); and the path it needs must be parsed out of
|
|
1838
|
+
// `ls-files` output, which `core.quotePath` renders as `"caf\303\251.md"`
|
|
1839
|
+
// by default, so the probe reads a filename that does not exist. Asking git
|
|
1840
|
+
// needs no path parsed and no case enumerated.
|
|
1841
|
+
//
|
|
1842
|
+
// Scoped to this branch on purpose. The diff probe above answers the ordinary
|
|
1843
|
+
// case cheaply and is pinned against the configuration vectors below; the
|
|
1844
|
+
// dry run is the heavier, exact answer, and it runs only when an
|
|
1845
|
+
// assume-unchanged path is actually present.
|
|
1846
|
+
//
|
|
1847
|
+
// The `ls-files` read is an OPTIMISATION, never a gate — so an unreadable one
|
|
1848
|
+
// must not decide anything. It exists only to keep the dry run off the hot
|
|
1849
|
+
// path when no assume-unchanged entry is present; when it cannot answer, the
|
|
1850
|
+
// dry run simply runs, because the dry run needs nothing from it. Both
|
|
1851
|
+
// failing-closed (drop the content) and failing-open (re-enter #3776) are
|
|
1852
|
+
// wrong answers to a question we can just ask directly.
|
|
1853
|
+
const assumeUnchangedWouldRecord = () => {
|
|
1854
|
+
const listed = (0, shell_command_projection_cjs_1.execGit)(['ls-files', '-v', '--', ...stagedPaths], { cwd });
|
|
1855
|
+
// Only the TAG is read; the path is deliberately never parsed out — see the
|
|
1856
|
+
// `core.quotePath` note above, and the dry run below needs no path anyway.
|
|
1857
|
+
if (listed.exitCode === 0
|
|
1858
|
+
&& !listed.stdout.split('\n').some((line) => /^[a-z] /.test(line)))
|
|
1859
|
+
return false;
|
|
1860
|
+
const dryRun = (0, shell_command_projection_cjs_1.execGit)(['commit', '--dry-run', '--porcelain', '--no-verify', '-m', sanitizedMessage, '--', ...stagedPaths], { cwd });
|
|
1861
|
+
// Only a CONFIRMED "nothing to record" closes the path: rc 1 from a git
|
|
1862
|
+
// that actually answered. This is the one probe in the guard whose rc 0
|
|
1863
|
+
// is the REASSURING answer, so it inverts the diff probe's safety: there
|
|
1864
|
+
// a timeout can only yield non-zero and reads as "not clean"; here
|
|
1865
|
+
// `execGit` collapses a spawn timeout (or any spawn error) to
|
|
1866
|
+
// `exitCode: 1` (`_spawnResult`: `result.status ?? 1`), byte-identical to
|
|
1867
|
+
// git's own "nothing to record" — and the guard then reports
|
|
1868
|
+
// `nothing_to_commit` about content it never asked git to write. Same
|
|
1869
|
+
// conflation the sequencer probes above defend against, same remedy: an
|
|
1870
|
+
// unanswered probe falls toward the commit, where git speaks for itself
|
|
1871
|
+
// (and a genuine error there is reported loudly, as it always was). rc 128
|
|
1872
|
+
// is likewise not an answer. Timeout kill of a dry run CAN leave a stale
|
|
1873
|
+
// `index.lock` behind (it refreshes the index); the real commit then
|
|
1874
|
+
// fails on it, loudly — never silently.
|
|
1875
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(dryRun) || dryRun.error !== null)
|
|
1876
|
+
return true;
|
|
1877
|
+
return dryRun.exitCode !== 1;
|
|
1878
|
+
};
|
|
1879
|
+
const nothingToCommit = guardApplies
|
|
1880
|
+
&& (stagedPaths.length === 0
|
|
1881
|
+
|| (!partialCommitRefused
|
|
1882
|
+
&& (0, shell_command_projection_cjs_1.execGit)(['diff', '--quiet', '--ignore-submodules=dirty', '--no-textconv', 'HEAD', '--', ...stagedPaths], { cwd }).exitCode === 0
|
|
1883
|
+
&& !assumeUnchangedWouldRecord()));
|
|
1884
|
+
if (nothingToCommit) {
|
|
1227
1885
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
1228
1886
|
output(result, raw, 'nothing');
|
|
1229
1887
|
return;
|
|
1230
1888
|
}
|
|
1231
|
-
const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
|
|
1232
1889
|
const canScope = explicitFiles && stagedPaths.length > 0 && !amend
|
|
1233
1890
|
&& !isMergeInProgress;
|
|
1234
1891
|
const commitArgs = amend
|
|
@@ -1239,8 +1896,61 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1239
1896
|
if (canScope) {
|
|
1240
1897
|
commitArgs.push('--', ...stagedPaths);
|
|
1241
1898
|
}
|
|
1242
|
-
|
|
1899
|
+
// #3859 follow-up: on git 2.39.5 (confirmed on the CI Linux bench image,
|
|
1900
|
+
// ghcr.io/open-gsd/gsd-tester-linux:v1.8.0-node24; NOT reproducible on git
|
|
1901
|
+
// 2.50.1) `git commit` itself — not just `git diff` — consults
|
|
1902
|
+
// `diff.ignoreSubmodules` when deciding whether there is anything to
|
|
1903
|
+
// record. With a local `diff.ignoreSubmodules=all` and a submodule gitlink
|
|
1904
|
+
// genuinely bumped, that git version silently REFUSES the commit (prints a
|
|
1905
|
+
// `git status`-style "Changes to be committed" dump and exits 1, having
|
|
1906
|
+
// written nothing) even though the diff probe above (already pinned with
|
|
1907
|
+
// its own `--ignore-submodules=dirty`) correctly reported the change as
|
|
1908
|
+
// present. The result was misclassified as generic `commit_failed` because
|
|
1909
|
+
// git's refusal text does not contain "nothing to commit".
|
|
1910
|
+
// Originally scoped to `canScope` on the assumption that only a
|
|
1911
|
+
// PATHSPEC-LIMITED `git commit -- <paths>` exercises this git internal
|
|
1912
|
+
// path. That assumption was wrong: reproduced directly against the pinned
|
|
1913
|
+
// v1.8.0-node24 tester image, a bare WHOLE-INDEX `git commit -m ...` (no
|
|
1914
|
+
// pathspec at all) is refused identically when the only staged change is a
|
|
1915
|
+
// submodule gitlink and `diff.ignoreSubmodules=all` — git's "nothing to
|
|
1916
|
+
// commit" check is a real diff (HEAD vs. index) honouring
|
|
1917
|
+
// `diff.ignoreSubmodules` regardless of whether a pathspec narrows it.
|
|
1918
|
+
// `--amend` is the one shape confirmed NOT to hit this: it always
|
|
1919
|
+
// recreates the commit from the current index and never runs the
|
|
1920
|
+
// empty-diff refusal a plain `git commit` does, override or not. The
|
|
1921
|
+
// override is therefore applied unconditionally here (not gated on
|
|
1922
|
+
// `canScope`) — it is a documented no-op everywhere it is not needed
|
|
1923
|
+
// (dry-run, git 2.50.1, and `--amend` already behave this way with or
|
|
1924
|
+
// without it; see `#3859 follow-up (canScope gap)` regression tests).
|
|
1925
|
+
// The override rides in via `GIT_CONFIG_*` env vars rather than a `-c`
|
|
1926
|
+
// argv flag so `commitArgs[0]` stays `'commit'` — several #3859 regression
|
|
1927
|
+
// tests assert on the raw argv captured at the `execGit` seam (e.g.
|
|
1928
|
+
// `gitCalls.some((a) => a[0] === 'commit')`), and a leading `-c` would shift
|
|
1929
|
+
// every element and break that pinning. Same override the probe already
|
|
1930
|
+
// carries, so the two can never disagree again.
|
|
1931
|
+
const commitEnv = {
|
|
1932
|
+
GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'diff.ignoreSubmodules', GIT_CONFIG_VALUE_0: 'dirty',
|
|
1933
|
+
};
|
|
1934
|
+
// #3886: `git commit` runs pre-commit hooks (husky/lint-staged routinely
|
|
1935
|
+
// idles ~4s on Windows before any task) — 10s is too tight, and a timeout
|
|
1936
|
+
// kill is NOT an ordinary failure. Same band as the push call below.
|
|
1937
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd, env: commitEnv, timeout: COMMIT_TIMEOUT_MS });
|
|
1243
1938
|
if (commitResult.exitCode !== 0) {
|
|
1939
|
+
// #3886: a SIGTERM'd git commit is a timeout, not commit_failed — the
|
|
1940
|
+
// partial stderr it flushed (often incidental CRLF warnings) is noise,
|
|
1941
|
+
// and the kill can leave a stale index.lock that blocks the next
|
|
1942
|
+
// attempt. Report the distinct reason and surface the lock path.
|
|
1943
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
1944
|
+
const result = {
|
|
1945
|
+
committed: false,
|
|
1946
|
+
hash: null,
|
|
1947
|
+
reason: 'commit_timeout',
|
|
1948
|
+
timed_out: true,
|
|
1949
|
+
error: commitTimeoutMessage(cwd, commitResult.stderr, commitResult.stdout),
|
|
1950
|
+
};
|
|
1951
|
+
output(result, raw, 'failed');
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1244
1954
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
1245
1955
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
1246
1956
|
output(result, raw, 'nothing');
|
|
@@ -1380,8 +2090,26 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
1380
2090
|
const commitArgs = canScopeSub
|
|
1381
2091
|
? ['commit', '-m', message, '--', ...stagedRelPaths]
|
|
1382
2092
|
: ['commit', '-m', message];
|
|
1383
|
-
|
|
2093
|
+
// #3859 follow-up fix as cmdCommit above (line ~2081) — git 2.39.5 needs
|
|
2094
|
+
// this override for pathspec-scoped AND whole-index commits alike.
|
|
2095
|
+
const commitEnvSub = {
|
|
2096
|
+
GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'diff.ignoreSubmodules', GIT_CONFIG_VALUE_0: 'dirty',
|
|
2097
|
+
};
|
|
2098
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd, timeout: COMMIT_TIMEOUT_MS, env: commitEnvSub });
|
|
1384
2099
|
if (commitResult.exitCode !== 0) {
|
|
2100
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
2101
|
+
// #3886 (subrepo counterpart): timeout ≠ error; surface the stale-lock
|
|
2102
|
+
// path a killed commit can leave in the subrepo.
|
|
2103
|
+
repos[repo] = {
|
|
2104
|
+
committed: false,
|
|
2105
|
+
hash: null,
|
|
2106
|
+
files: repoFiles,
|
|
2107
|
+
reason: 'commit_timeout',
|
|
2108
|
+
timed_out: true,
|
|
2109
|
+
error: commitTimeoutMessage(repoCwd, commitResult.stderr, commitResult.stdout),
|
|
2110
|
+
};
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
1385
2113
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
1386
2114
|
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
1387
2115
|
continue;
|
|
@@ -1442,7 +2170,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
1442
2170
|
}
|
|
1443
2171
|
// 1. Collect changed files via porcelain status — explicit, never git add -A.
|
|
1444
2172
|
// ?? (untracked) lines are excluded — only stage tracked modifications.
|
|
1445
|
-
|
|
2173
|
+
// #3859 follow-up: `git status --porcelain` honors `diff.ignoreSubmodules`
|
|
2174
|
+
// the same way the empty-diff probe fixed for cmdCommit did — under a local
|
|
2175
|
+
// `diff.ignoreSubmodules=all`, a genuinely bumped submodule gitlink is
|
|
2176
|
+
// invisible here too, so `changedFiles` comes back empty and the function
|
|
2177
|
+
// reports `nothing_to_commit` before ever reaching the (now-fixed) commit
|
|
2178
|
+
// call. `--ignore-submodules=dirty` pins this the same way, reported
|
|
2179
|
+
// verbatim: `git -C repo status --porcelain` (no flag) shows nothing for a
|
|
2180
|
+
// pure gitlink bump under `diff.ignoreSubmodules=all`, while
|
|
2181
|
+
// `--ignore-submodules=dirty` reports ` M nested` (reproduced directly).
|
|
2182
|
+
const statusResult = (0, shell_command_projection_cjs_1.execGit)(['-c', 'core.quotePath=false', 'status', '--porcelain', '--ignore-submodules=dirty'], { cwd: repoCwd });
|
|
1446
2183
|
if (statusResult.exitCode !== 0) {
|
|
1447
2184
|
error(`git status failed in ${repo}: ${statusResult.stderr}`);
|
|
1448
2185
|
}
|
|
@@ -1511,9 +2248,20 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
1511
2248
|
const commitArgs = canScopePr
|
|
1512
2249
|
? ['commit', '-m', commitMessage, '--', ...changedFiles]
|
|
1513
2250
|
: ['commit', '-m', commitMessage];
|
|
1514
|
-
|
|
2251
|
+
// #3859 follow-up fix as cmdCommit above (line ~2081) — git 2.39.5 needs
|
|
2252
|
+
// this override for pathspec-scoped AND whole-index commits alike.
|
|
2253
|
+
const commitEnvPr = {
|
|
2254
|
+
GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'diff.ignoreSubmodules', GIT_CONFIG_VALUE_0: 'dirty',
|
|
2255
|
+
};
|
|
2256
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd, timeout: COMMIT_TIMEOUT_MS, env: commitEnvPr });
|
|
1515
2257
|
if (commitResult.exitCode !== 0) {
|
|
1516
2258
|
rollback();
|
|
2259
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
2260
|
+
// #3886 (PR-subrepo counterpart): name the timeout and the stale lock
|
|
2261
|
+
// instead of echoing the killed hook's partial stderr.
|
|
2262
|
+
error(`git commit timed out after ${COMMIT_TIMEOUT_MS / 1000}s in ${repo} (killed mid-hook; ` +
|
|
2263
|
+
`a stale lock may remain at ${resolveIndexLockPath(repoCwd)} — remove it if no git process is running)`);
|
|
2264
|
+
}
|
|
1517
2265
|
error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
|
|
1518
2266
|
}
|
|
1519
2267
|
// 6. Capture commit hash
|
|
@@ -1906,7 +2654,34 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
|
|
|
1906
2654
|
matches.sort((a, b) => b.score - a.score);
|
|
1907
2655
|
output({ phase, matches, todo_count: todos.length }, raw, undefined);
|
|
1908
2656
|
}
|
|
1909
|
-
|
|
2657
|
+
// #4096: upsert completion keys INSIDE the leading frontmatter block. Never a
|
|
2658
|
+
// bare prefix line above the opening `---` (that displaces the fence to line 2
|
|
2659
|
+
// and breaks every fence-locating reader). A file with no well-formed block
|
|
2660
|
+
// (absent, or an unterminated opening fence) gains a complete block.
|
|
2661
|
+
function upsertTodoCompletionFields(content, today) {
|
|
2662
|
+
const lines = content.split('\n');
|
|
2663
|
+
const fields = [`completed: ${today}`, 'status: completed'];
|
|
2664
|
+
const hasOpeningFence = lines[0] !== undefined && lines[0].trim() === '---';
|
|
2665
|
+
const closeIdx = hasOpeningFence ? lines.findIndex((l, i) => i > 0 && l.trim() === '---') : -1;
|
|
2666
|
+
if (!hasOpeningFence || closeIdx === -1) {
|
|
2667
|
+
// No parseable frontmatter: wrap the whole content in a complete block
|
|
2668
|
+
// rather than prefixing bare keys (#4096 fix 2).
|
|
2669
|
+
return `---\n${fields.join('\n')}\n---\n\n${content}`;
|
|
2670
|
+
}
|
|
2671
|
+
const block = lines.slice(1, closeIdx);
|
|
2672
|
+
for (const field of fields) {
|
|
2673
|
+
const key = `${field.slice(0, field.indexOf(':'))}:`;
|
|
2674
|
+
const idx = block.findIndex(l => l.startsWith(key));
|
|
2675
|
+
if (idx === -1) {
|
|
2676
|
+
block.push(field);
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
block[idx] = field;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return [...lines.slice(0, 1), ...block, ...lines.slice(closeIdx)].join('\n');
|
|
2683
|
+
}
|
|
2684
|
+
function cmdTodoComplete(cwd, filename, options, raw) {
|
|
1910
2685
|
if (!filename) {
|
|
1911
2686
|
error('filename required for todo complete');
|
|
1912
2687
|
}
|
|
@@ -1916,13 +2691,30 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
1916
2691
|
if (!node_fs_1.default.existsSync(sourcePath)) {
|
|
1917
2692
|
error(`Todo not found: ${filename}`);
|
|
1918
2693
|
}
|
|
1919
|
-
|
|
1920
|
-
(0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
|
|
1921
|
-
// Read, add completion timestamp, move
|
|
1922
|
-
let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
2694
|
+
const content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
1923
2695
|
const today = clock_cjs_1.realClock.localToday();
|
|
1924
|
-
|
|
1925
|
-
|
|
2696
|
+
// #4096: --dry-run mirrors `milestone complete --dry-run` (#2118) — every
|
|
2697
|
+
// existence check above still runs, nothing below mutates, and the payload
|
|
2698
|
+
// is preview-shaped (`dry_run`/`would_*`), never `completed: true`.
|
|
2699
|
+
if (options.dryRun) {
|
|
2700
|
+
output({
|
|
2701
|
+
dry_run: true,
|
|
2702
|
+
would_complete: true,
|
|
2703
|
+
file: filename,
|
|
2704
|
+
date: today,
|
|
2705
|
+
would_move: {
|
|
2706
|
+
source: node_path_1.default.relative(cwd, sourcePath).split(node_path_1.default.sep).join('/'),
|
|
2707
|
+
target: node_path_1.default.relative(cwd, node_path_1.default.join(completedDir, filename)).split(node_path_1.default.sep).join('/'),
|
|
2708
|
+
},
|
|
2709
|
+
would_set: { completed: today, status: 'completed' },
|
|
2710
|
+
}, raw);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
// Ensure completed directory exists (only on the real run — a dry run
|
|
2714
|
+
// creates nothing).
|
|
2715
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
|
|
2716
|
+
const completedContent = upsertTodoCompletionFields(content, today);
|
|
2717
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), completedContent);
|
|
1926
2718
|
node_fs_1.default.unlinkSync(sourcePath);
|
|
1927
2719
|
output({ completed: true, file: filename, date: today }, raw, 'completed');
|
|
1928
2720
|
}
|
|
@@ -2275,6 +3067,36 @@ function buildCommitDocsGuardHookScript() {
|
|
|
2275
3067
|
];
|
|
2276
3068
|
return lines.join('\n') + '\n';
|
|
2277
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* #3886: the timeout band for `git commit` — pre-commit hooks (husky +
|
|
3072
|
+
* lint-staged idles ~4s on Windows before any task) routinely exceed the 10s
|
|
3073
|
+
* plumbing default; 30s is the same band the push call uses. Shared by all
|
|
3074
|
+
* three commit sites AND their timeout messages, so the number and the text
|
|
3075
|
+
* cannot drift apart.
|
|
3076
|
+
*/
|
|
3077
|
+
const COMMIT_TIMEOUT_MS = 30_000;
|
|
3078
|
+
/**
|
|
3079
|
+
* #3886: resolve where a killed `git commit` would leave its stale
|
|
3080
|
+
* index.lock — via `git rev-parse --git-path index.lock`, never a literal
|
|
3081
|
+
* `.git/index.lock` join (#3588 row 8's class: a linked worktree's `.git` is
|
|
3082
|
+
* a FILE pointing at `<gitdir>/worktrees/<name>/`, so the literal path
|
|
3083
|
+
* cannot exist there while the real lock blocks the next commit). Best
|
|
3084
|
+
* effort: any resolution failure falls back to the literal join, and the
|
|
3085
|
+
* message already hedges with "may remain".
|
|
3086
|
+
*/
|
|
3087
|
+
function resolveIndexLockPath(cwd) {
|
|
3088
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--git-path', 'index.lock'], { cwd });
|
|
3089
|
+
if (result.exitCode !== 0)
|
|
3090
|
+
return node_path_1.default.join(cwd, '.git', 'index.lock');
|
|
3091
|
+
const raw = result.stdout.trim();
|
|
3092
|
+
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');
|
|
3093
|
+
}
|
|
3094
|
+
/** #3886: shared timeout message shape for all three commit sites. */
|
|
3095
|
+
function commitTimeoutMessage(cwd, stderr, stdout) {
|
|
3096
|
+
return (`git commit timed out after ${COMMIT_TIMEOUT_MS / 1000}s (killed mid-hook; a stale lock may remain at ` +
|
|
3097
|
+
`${resolveIndexLockPath(cwd)} — remove it if no git process is running). ` +
|
|
3098
|
+
`Partial stderr: ${stderr || stdout || '(none)'}`);
|
|
3099
|
+
}
|
|
2278
3100
|
/**
|
|
2279
3101
|
* Resolve the real git hooks directory for `cwd` via `git rev-parse
|
|
2280
3102
|
* --git-path hooks` — never a literal `.git/hooks` join (#3588 row 8: a
|
|
@@ -2366,6 +3188,7 @@ function cmdCommitDocsGuardDisable(cwd, raw) {
|
|
|
2366
3188
|
output({ disabled: true, action: 'removed', path: hookPath }, raw, 'disabled');
|
|
2367
3189
|
}
|
|
2368
3190
|
module.exports = {
|
|
3191
|
+
effortSurfaceForHost,
|
|
2369
3192
|
groupFilesBySubrepo,
|
|
2370
3193
|
determinePhaseStatus,
|
|
2371
3194
|
foldPhaseStatus,
|