@opengsd/gsd-core 1.12.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-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.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/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- 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 +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- 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/review-lane-descriptor.cjs +53 -5
- 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-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- 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-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- 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 +2 -0
- 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 +2 -0
- 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.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- 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 +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- 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 +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.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 +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- 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.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/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-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -625,6 +625,11 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
625
625
|
* applies here exactly as everywhere else: an unknown host, a missing axis, or the
|
|
626
626
|
* `undocumented` sentinel all degrade to the safe floor rather than being trusted.
|
|
627
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.
|
|
628
633
|
*/
|
|
629
634
|
function effortSurfaceForHost(cwd, host) {
|
|
630
635
|
void cwd;
|
|
@@ -1655,12 +1660,232 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1655
1660
|
// During a merge, git refuses partial commits — fall back to a bare commit.
|
|
1656
1661
|
// --amend is left without a pathspec: amending with -- <paths> is a different
|
|
1657
1662
|
// operation that rewrites the tip with only those paths.
|
|
1658
|
-
|
|
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) {
|
|
1659
1885
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
1660
1886
|
output(result, raw, 'nothing');
|
|
1661
1887
|
return;
|
|
1662
1888
|
}
|
|
1663
|
-
const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
|
|
1664
1889
|
const canScope = explicitFiles && stagedPaths.length > 0 && !amend
|
|
1665
1890
|
&& !isMergeInProgress;
|
|
1666
1891
|
const commitArgs = amend
|
|
@@ -1671,10 +1896,45 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
1671
1896
|
if (canScope) {
|
|
1672
1897
|
commitArgs.push('--', ...stagedPaths);
|
|
1673
1898
|
}
|
|
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
|
+
};
|
|
1674
1934
|
// #3886: `git commit` runs pre-commit hooks (husky/lint-staged routinely
|
|
1675
1935
|
// idles ~4s on Windows before any task) — 10s is too tight, and a timeout
|
|
1676
1936
|
// kill is NOT an ordinary failure. Same band as the push call below.
|
|
1677
|
-
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd, timeout: COMMIT_TIMEOUT_MS });
|
|
1937
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd, env: commitEnv, timeout: COMMIT_TIMEOUT_MS });
|
|
1678
1938
|
if (commitResult.exitCode !== 0) {
|
|
1679
1939
|
// #3886: a SIGTERM'd git commit is a timeout, not commit_failed — the
|
|
1680
1940
|
// partial stderr it flushed (often incidental CRLF warnings) is noise,
|
|
@@ -1830,7 +2090,12 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
1830
2090
|
const commitArgs = canScopeSub
|
|
1831
2091
|
? ['commit', '-m', message, '--', ...stagedRelPaths]
|
|
1832
2092
|
: ['commit', '-m', message];
|
|
1833
|
-
|
|
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 });
|
|
1834
2099
|
if (commitResult.exitCode !== 0) {
|
|
1835
2100
|
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
1836
2101
|
// #3886 (subrepo counterpart): timeout ≠ error; surface the stale-lock
|
|
@@ -1905,7 +2170,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
1905
2170
|
}
|
|
1906
2171
|
// 1. Collect changed files via porcelain status — explicit, never git add -A.
|
|
1907
2172
|
// ?? (untracked) lines are excluded — only stage tracked modifications.
|
|
1908
|
-
|
|
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 });
|
|
1909
2183
|
if (statusResult.exitCode !== 0) {
|
|
1910
2184
|
error(`git status failed in ${repo}: ${statusResult.stderr}`);
|
|
1911
2185
|
}
|
|
@@ -1974,7 +2248,12 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
1974
2248
|
const commitArgs = canScopePr
|
|
1975
2249
|
? ['commit', '-m', commitMessage, '--', ...changedFiles]
|
|
1976
2250
|
: ['commit', '-m', commitMessage];
|
|
1977
|
-
|
|
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 });
|
|
1978
2257
|
if (commitResult.exitCode !== 0) {
|
|
1979
2258
|
rollback();
|
|
1980
2259
|
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(commitResult)) {
|
|
@@ -2375,7 +2654,34 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
|
|
|
2375
2654
|
matches.sort((a, b) => b.score - a.score);
|
|
2376
2655
|
output({ phase, matches, todo_count: todos.length }, raw, undefined);
|
|
2377
2656
|
}
|
|
2378
|
-
|
|
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) {
|
|
2379
2685
|
if (!filename) {
|
|
2380
2686
|
error('filename required for todo complete');
|
|
2381
2687
|
}
|
|
@@ -2385,13 +2691,30 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
2385
2691
|
if (!node_fs_1.default.existsSync(sourcePath)) {
|
|
2386
2692
|
error(`Todo not found: ${filename}`);
|
|
2387
2693
|
}
|
|
2388
|
-
|
|
2389
|
-
(0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
|
|
2390
|
-
// Read, add completion timestamp, move
|
|
2391
|
-
let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
2694
|
+
const content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
2392
2695
|
const today = clock_cjs_1.realClock.localToday();
|
|
2393
|
-
|
|
2394
|
-
|
|
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);
|
|
2395
2718
|
node_fs_1.default.unlinkSync(sourcePath);
|
|
2396
2719
|
output({ completed: true, file: filename, date: today }, raw, 'completed');
|
|
2397
2720
|
}
|
|
@@ -2865,6 +3188,7 @@ function cmdCommitDocsGuardDisable(cwd, raw) {
|
|
|
2865
3188
|
output({ disabled: true, action: 'removed', path: hookPath }, raw, 'disabled');
|
|
2866
3189
|
}
|
|
2867
3190
|
module.exports = {
|
|
3191
|
+
effortSurfaceForHost,
|
|
2868
3192
|
groupFilesBySubrepo,
|
|
2869
3193
|
determinePhaseStatus,
|
|
2870
3194
|
foldPhaseStatus,
|
|
@@ -831,6 +831,7 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
831
831
|
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
|
832
832
|
base_branch: get('base_branch', { section: 'git', field: 'base_branch' }),
|
|
833
833
|
protected_branches: getNested('git', 'protected_branches'),
|
|
834
|
+
allow_default_branch_commits: getNested('git', 'allow_default_branch_commits'),
|
|
834
835
|
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
835
836
|
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
836
837
|
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
|
|
@@ -844,6 +845,7 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
844
845
|
firecrawl: get('firecrawl') ?? defaults.firecrawl,
|
|
845
846
|
exa_search: get('exa_search') ?? defaults.exa_search,
|
|
846
847
|
mvp_mode: get('mvp_mode', { section: 'workflow', field: 'mvp_mode' }) ?? false,
|
|
848
|
+
tdd_mode: getNested('workflow', 'tdd_mode') ?? false,
|
|
847
849
|
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
|
|
848
850
|
auto_advance: get('auto_advance', { section: 'workflow', field: 'auto_advance' }) ?? false,
|
|
849
851
|
_auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
|
|
@@ -856,6 +858,7 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
856
858
|
project_code: get('project_code') ?? defaults.project_code,
|
|
857
859
|
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
|
858
860
|
model_overrides: (parsed['model_overrides']) || null,
|
|
861
|
+
agent_tools: (parsed['agent_tools']) || null,
|
|
859
862
|
models: (parsed['models']) || null,
|
|
860
863
|
granularity: parsed['granularity'] !== undefined ? parsed['granularity'] : null,
|
|
861
864
|
granularities: (parsed['granularities']) || null,
|
|
@@ -43,6 +43,11 @@ const phaseIdModule = require("./phase-id.cjs");
|
|
|
43
43
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const shellCommandProjection = require("./shell-command-projection.cjs");
|
|
46
|
+
// planning-scope.cjs is a leaf module (no imports of its own — see its file
|
|
47
|
+
// header), so importing it here directly cannot introduce a cycle.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
50
|
+
const { SCOPE } = planningScopeMod;
|
|
46
51
|
// ─── Line-ending normalization ─────────────────────────────────────────────────
|
|
47
52
|
/**
|
|
48
53
|
* Normalize every line ending in `content` to a bare `\n`, ONCE — the shared
|
|
@@ -271,16 +276,38 @@ function transliterateForSlug(text) {
|
|
|
271
276
|
* Degrades on an unreadable directory instead of throwing: empty arrays,
|
|
272
277
|
* every flag false, scope UNREADABLE (mirroring scanPhasePlans's own
|
|
273
278
|
* degrade path).
|
|
279
|
+
*
|
|
280
|
+
* #4014 (epic #3473 B4-unreadable): this function's OWN readdirSync used to
|
|
281
|
+
* catch its own failure and then return `scope: scan.scope` — the scope of
|
|
282
|
+
* the UNRELATED, already-successful scanPhasePlans call — silently dropping
|
|
283
|
+
* that THIS read failed. The own-readdirSync is now routed through
|
|
284
|
+
* findContextMdIn's directory-string form (which never throws and reports
|
|
285
|
+
* its own SCOPE.UNREADABLE), and the two independently-scoped answers are
|
|
286
|
+
* combined into the worse of the two below — never letting a successful
|
|
287
|
+
* scan.scope mask this function's own failed read.
|
|
288
|
+
*
|
|
289
|
+
* The combination is written out as a two-value comparison rather than
|
|
290
|
+
* calling planning-snapshot.cts's `worstScope` directly: that module
|
|
291
|
+
* requires core-utils.cjs (for `findOrphanSummaries`) at its own top level,
|
|
292
|
+
* so importing it back here would create a tight, direct
|
|
293
|
+
* core-utils.cjs <-> planning-snapshot.cjs cycle — a materially different
|
|
294
|
+
* (and riskier) shape than this file's existing, documented lazy-access
|
|
295
|
+
* cyclic partners (planning-workspace.cjs, phase-id.cjs), which
|
|
296
|
+
* planning-snapshot.cts sits *above* in the dependency graph, not beside.
|
|
297
|
+
* `findContextMdIn`'s directory-string form can only ever report
|
|
298
|
+
* SCOPE.COMPLETE or SCOPE.UNREADABLE (a single readdirSync is binary — see
|
|
299
|
+
* its own doc comment), so per planning-snapshot.cts's SCOPE_SEVERITY
|
|
300
|
+
* ordering (UNREADABLE is maximal), `worstScope(scan.scope, ownScope)` is
|
|
301
|
+
* exactly `ownScope === UNREADABLE ? UNREADABLE : scan.scope` — the
|
|
302
|
+
* expression below is that identity, not a re-derivation of the ordering.
|
|
274
303
|
*/
|
|
275
304
|
function getPhaseFileStats(phaseDir) {
|
|
276
305
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
277
306
|
const scanPhasePlans = require('./plan-scan.cjs');
|
|
278
307
|
const scan = scanPhasePlans(phaseDir);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
308
|
+
const { files, scope: ownScope } = planningWorkspace.findContextMdIn(phaseDir);
|
|
309
|
+
const scope = ownScope === SCOPE.UNREADABLE ? SCOPE.UNREADABLE : scan.scope;
|
|
310
|
+
if (ownScope === SCOPE.UNREADABLE) {
|
|
284
311
|
return {
|
|
285
312
|
plans: scan.planFiles,
|
|
286
313
|
summaries: scan.summaryFiles,
|
|
@@ -288,7 +315,7 @@ function getPhaseFileStats(phaseDir) {
|
|
|
288
315
|
hasContext: false,
|
|
289
316
|
hasVerification: false,
|
|
290
317
|
hasReviews: false,
|
|
291
|
-
scope
|
|
318
|
+
scope,
|
|
292
319
|
};
|
|
293
320
|
}
|
|
294
321
|
const scopedFiles = phaseIdModule.scopeToPhase(files, node_path_1.default.basename(phaseDir));
|
|
@@ -299,7 +326,7 @@ function getPhaseFileStats(phaseDir) {
|
|
|
299
326
|
hasContext: planningWorkspace.findContextMdIn(scopedFiles) !== null,
|
|
300
327
|
hasVerification: scopedFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
|
|
301
328
|
hasReviews: scopedFiles.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
|
|
302
|
-
scope
|
|
329
|
+
scope,
|
|
303
330
|
};
|
|
304
331
|
}
|
|
305
332
|
/**
|