@opengsd/gsd-core 1.18.0 → 1.20.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 +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -1264,10 +1264,10 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
|
|
|
1264
1264
|
const bash = resolveBashExecutable(opts);
|
|
1265
1265
|
if (isShellHook) {
|
|
1266
1266
|
// An unresolved bash must still surface as an interpreterCandidates entry
|
|
1267
|
-
// (the
|
|
1267
|
+
// (the sentinel token, same as the portableHooks runner below) so
|
|
1268
1268
|
// validateConfiguredEntrypoints reports 'unresolved-interpreter' instead
|
|
1269
1269
|
// of silently skipping the check because the field is absent.
|
|
1270
|
-
return [{ ...target, interpreterCandidates: [bash === null ?
|
|
1270
|
+
return [{ ...target, interpreterCandidates: [bash === null ? UNRESOLVED_BASH_SENTINEL : bash] }];
|
|
1271
1271
|
}
|
|
1272
1272
|
// #4249: check the SAME stable alias buildNodeRunnerChainToken bakes as its
|
|
1273
1273
|
// first candidate (normalizeNodePath rewrites a version-manager shim like
|
|
@@ -1287,7 +1287,7 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
|
|
|
1287
1287
|
runtime,
|
|
1288
1288
|
configPath,
|
|
1289
1289
|
scriptPath: node_path_1.default.join(configDir, 'hooks', NODE_RUNNER_RESOLVER_HOOK),
|
|
1290
|
-
interpreterCandidates: bash === null ? [
|
|
1290
|
+
interpreterCandidates: bash === null ? [UNRESOLVED_BASH_SENTINEL] : [bash],
|
|
1291
1291
|
platform,
|
|
1292
1292
|
};
|
|
1293
1293
|
return [runner, { ...target, interpreterCandidates: nodeCandidates }];
|
|
@@ -1298,6 +1298,15 @@ function recordConfiguredHookCommand(command, configDir, hookName, opts) {
|
|
|
1298
1298
|
}
|
|
1299
1299
|
return command;
|
|
1300
1300
|
}
|
|
1301
|
+
// #5100: the two null-runner branches of buildHookCommand share this. No
|
|
1302
|
+
// command was ever built, so track() (which needs one) cannot record the
|
|
1303
|
+
// hook — push the entry directly instead, so the gate sees it rather than
|
|
1304
|
+
// the hook going silently unregistered.
|
|
1305
|
+
function recordUnresolvedBashEntrypoint(configDir, hookName, opts) {
|
|
1306
|
+
if (opts.configuredEntrypoints) {
|
|
1307
|
+
opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1301
1310
|
function buildHookCommand(configDir, hookName, opts) {
|
|
1302
1311
|
if (!opts)
|
|
1303
1312
|
opts = {};
|
|
@@ -1323,15 +1332,9 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1323
1332
|
const runner = resolveBashRunner(opts);
|
|
1324
1333
|
if (runner === null) {
|
|
1325
1334
|
// #4249 (antigravity review): this early return skips `track()` below,
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
|
|
1329
|
-
// own 'unresolved bash must still surface' comment describes intent this
|
|
1330
|
-
// return never reached. Push the entry directly (no `command`, since
|
|
1331
|
-
// none was ever built) so the gate actually sees it.
|
|
1332
|
-
if (opts.configuredEntrypoints) {
|
|
1333
|
-
opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
|
|
1334
|
-
}
|
|
1335
|
+
// which needs a command that was never built — record the entry
|
|
1336
|
+
// directly so the gate can reject it.
|
|
1337
|
+
recordUnresolvedBashEntrypoint(configDir, hookName, opts);
|
|
1335
1338
|
return null;
|
|
1336
1339
|
}
|
|
1337
1340
|
if (opts.portableHooks) {
|
|
@@ -1374,9 +1377,13 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1374
1377
|
configDir,
|
|
1375
1378
|
homeDir: node_os_1.default.homedir(),
|
|
1376
1379
|
});
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
const resolverRunner = resolveBashRunner(opts)
|
|
1380
|
+
// #5100: same contract as the .sh early return. A bare `bash` is how
|
|
1381
|
+
// win32 reaches WSL's System32 launcher, and the gate used to accept it.
|
|
1382
|
+
const resolverRunner = resolveBashRunner(opts);
|
|
1383
|
+
if (resolverRunner === null) {
|
|
1384
|
+
recordUnresolvedBashEntrypoint(configDir, hookName, opts);
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1380
1387
|
return track(shellCmdProjection.projectShellCommandText({
|
|
1381
1388
|
runnerToken: resolverRunner,
|
|
1382
1389
|
argTokens: [
|
|
@@ -2158,6 +2165,9 @@ const SETTINGS_JSON_HOOK_ROWS = deepFreeze([
|
|
|
2158
2165
|
// Guard: only register if the hook file was actually installed (#1754, #1817).
|
|
2159
2166
|
{ file: 'gsd-check-update.js', event: 'SessionStart', command: { opts: 'updateCheckCommand' },
|
|
2160
2167
|
configuredMessage: 'Configured update check hook', skipLabel: 'update check hook' },
|
|
2168
|
+
// #5465: persists the gsd-core bin directory on PATH through $CLAUDE_ENV_FILE (SessionStart-only).
|
|
2169
|
+
{ file: 'gsd-path-export.js', event: 'SessionStart', timeout: 5, command: { build: 'js' },
|
|
2170
|
+
configuredMessage: 'Configured PATH persistence hook', skipLabel: 'PATH persistence hook' },
|
|
2161
2171
|
{ file: 'gsd-context-monitor.js', event: 'post', matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task', timeout: 10,
|
|
2162
2172
|
command: { opts: 'contextMonitorCommand' },
|
|
2163
2173
|
configuredMessage: 'Configured context window monitor hook', skipLabel: 'context monitor hook',
|
|
@@ -2710,6 +2720,30 @@ function referencesHook(h, hookName) {
|
|
|
2710
2720
|
(Array.isArray(args) && args.some(a => typeof a === 'string' && a.includes(hookName))) ||
|
|
2711
2721
|
(typeof url === 'string' && url.includes(hookName));
|
|
2712
2722
|
}
|
|
2723
|
+
// #5100: the writer emits the bare `bash` token when the Git Bash policy
|
|
2724
|
+
// finds nothing, and the gate resolves that token through the same policy —
|
|
2725
|
+
// never a PATH scan. One shared definition so the two sides cannot drift.
|
|
2726
|
+
const UNRESOLVED_BASH_SENTINEL = 'bash';
|
|
2727
|
+
const WIN32_BARE_BASH_TOKENS = [UNRESOLVED_BASH_SENTINEL, 'bash.exe'];
|
|
2728
|
+
function isWin32BareBashToken(candidate, platform) {
|
|
2729
|
+
if ((platform || process.platform) !== 'win32')
|
|
2730
|
+
return false;
|
|
2731
|
+
// Equality already rejects any path: a string holding a separator can never
|
|
2732
|
+
// equal a bare token, so no explicit separator guard is needed here.
|
|
2733
|
+
return WIN32_BARE_BASH_TOKENS.includes(candidate.trim().toLowerCase());
|
|
2734
|
+
}
|
|
2735
|
+
// #5100: the literal `bash` sentinel means "Git Bash policy found nothing"
|
|
2736
|
+
// (#4249). Resolve that token through resolveBashExecutable, never a PATH
|
|
2737
|
+
// scan — WSL's System32\bash.exe is on PATH and cannot see Windows paths.
|
|
2738
|
+
function win32BareBashResolves(candidate, platform, deps) {
|
|
2739
|
+
if (!isWin32BareBashToken(candidate, platform))
|
|
2740
|
+
return null;
|
|
2741
|
+
return resolveBashExecutable({
|
|
2742
|
+
platform: 'win32',
|
|
2743
|
+
env: deps.env,
|
|
2744
|
+
existsSync: deps.existsSync,
|
|
2745
|
+
}) !== null;
|
|
2746
|
+
}
|
|
2713
2747
|
function validateConfiguredEntrypoints(entries, deps = {}) {
|
|
2714
2748
|
const statSync = deps.statSync ?? node_fs_1.default.statSync;
|
|
2715
2749
|
const accessSync = deps.accessSync ?? node_fs_1.default.accessSync;
|
|
@@ -2766,7 +2800,12 @@ function validateConfiguredEntrypoints(entries, deps = {}) {
|
|
|
2766
2800
|
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'not-executable' });
|
|
2767
2801
|
}
|
|
2768
2802
|
}
|
|
2769
|
-
if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate =>
|
|
2803
|
+
if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate => {
|
|
2804
|
+
const bareBash = win32BareBashResolves(candidate, entry.platform, deps);
|
|
2805
|
+
if (bareBash !== null)
|
|
2806
|
+
return bareBash;
|
|
2807
|
+
return resolve(candidate, { platform: entry.platform, requireExecutable: true }) !== null;
|
|
2808
|
+
})) {
|
|
2770
2809
|
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'interpreter', path: entry.interpreterCandidates.join(' | '), reason: 'unresolved-interpreter' });
|
|
2771
2810
|
}
|
|
2772
2811
|
}
|
|
@@ -2856,6 +2895,11 @@ module.exports = {
|
|
|
2856
2895
|
resolveBashExecutable,
|
|
2857
2896
|
resolveBashRunner,
|
|
2858
2897
|
NODE_RUNNER_RESOLVER_HOOK,
|
|
2898
|
+
// #5100: the literal the writer records for an unresolved Git Bash and the
|
|
2899
|
+
// gate resolves through the Git Bash policy. Exported so a consumer that
|
|
2900
|
+
// needs to recognise that entry imports it rather than respelling it;
|
|
2901
|
+
// bin/install.js keys on the entry having no command instead.
|
|
2902
|
+
UNRESOLVED_BASH_SENTINEL,
|
|
2859
2903
|
// Atomic write seam (shared with bin/install.js so all writes participate
|
|
2860
2904
|
// in install.js's _cleanTmpFiles() scoped temp-cleanup).
|
|
2861
2905
|
atomicWriteFileSync,
|
|
@@ -15,7 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.PATH_ACTION_REASON = void 0;
|
|
18
|
+
exports.scrubGitLocationEnv = exports.GIT_LOCATION_ENV_VARS = exports.PATH_ACTION_REASON = void 0;
|
|
19
19
|
exports.toPosixPath = toPosixPath;
|
|
20
20
|
exports.toNativePath = toNativePath;
|
|
21
21
|
exports.posixNormalize = posixNormalize;
|
|
@@ -37,6 +37,7 @@ exports.projectCodexHookTomlCommand = projectCodexHookTomlCommand;
|
|
|
37
37
|
exports.escapePowerShellSingleQuoted = escapePowerShellSingleQuoted;
|
|
38
38
|
exports.escapePosixDoubleQuoted = escapePosixDoubleQuoted;
|
|
39
39
|
exports.escapeSingleQuotedShellLiteral = escapeSingleQuotedShellLiteral;
|
|
40
|
+
exports.shellSingleQuote = shellSingleQuote;
|
|
40
41
|
exports.projectPathExportLine = projectPathExportLine;
|
|
41
42
|
exports.renderShellActionLines = renderShellActionLines;
|
|
42
43
|
exports.projectPathActionProjection = projectPathActionProjection;
|
|
@@ -64,8 +65,11 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
64
65
|
// at load time and become un-mockable.
|
|
65
66
|
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
66
67
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
68
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
67
69
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
70
|
+
const markdown_line_diff_cjs_1 = require("./markdown-line-diff.cjs");
|
|
68
71
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
72
|
+
const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
|
|
69
73
|
/**
|
|
70
74
|
* Convert a filesystem path to POSIX form (forward slashes) by translating the
|
|
71
75
|
* platform-native separator. Single seam for native→POSIX conversion.
|
|
@@ -249,6 +253,8 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
|
249
253
|
'gsd-worktree-path-guard.js',
|
|
250
254
|
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
251
255
|
'gsd-secret-read-guard.js',
|
|
256
|
+
// #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
|
|
257
|
+
'gsd-path-export.js',
|
|
252
258
|
]),
|
|
253
259
|
'codex-toml': new Set([
|
|
254
260
|
'gsd-check-update.js',
|
|
@@ -279,6 +285,8 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
|
|
|
279
285
|
'gsd-worktree-path-guard.js',
|
|
280
286
|
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
281
287
|
'gsd-secret-read-guard.js',
|
|
288
|
+
// #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
|
|
289
|
+
'gsd-path-export.js',
|
|
282
290
|
]),
|
|
283
291
|
'codex-toml': new Set([
|
|
284
292
|
'gsd-check-update.js',
|
|
@@ -452,6 +460,18 @@ function escapePosixDoubleQuoted(value) {
|
|
|
452
460
|
function escapeSingleQuotedShellLiteral(value) {
|
|
453
461
|
return String(value).replace(/'/g, "'\\''");
|
|
454
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* #1460 / #4039: POSIX single-quote an arbitrary string as ONE shell token. A path such as a
|
|
465
|
+
* hook script's absolute install location commonly sits under a home directory containing
|
|
466
|
+
* spaces or other metacharacters (e.g. "/Users/Bob Smith/.claude/..."); written unquoted it
|
|
467
|
+
* would word-split and, with a hostile prefix, could inject. Wrapping in single quotes — with
|
|
468
|
+
* each embedded `'` escaped as `'\''` by escapeSingleQuotedShellLiteral — makes the whole value a
|
|
469
|
+
* single token no metacharacter inside it can break. This is the one implementation of that rule;
|
|
470
|
+
* do not re-implement it in a caller.
|
|
471
|
+
*/
|
|
472
|
+
function shellSingleQuote(value) {
|
|
473
|
+
return "'" + escapeSingleQuotedShellLiteral(value) + "'";
|
|
474
|
+
}
|
|
455
475
|
/**
|
|
456
476
|
* The `export PATH="<dir>:$PATH"` line every persistence lane appends, plus the escaped directory
|
|
457
477
|
* token it embeds. One builder because three lanes emit this line: a lane that re-escapes it
|
|
@@ -609,12 +629,28 @@ function _spawnResult(result, program) {
|
|
|
609
629
|
timedOut: isSpawnTimeout({ error }),
|
|
610
630
|
};
|
|
611
631
|
}
|
|
632
|
+
// #5548: owned by read-only-git-env.cts (its default base is scrubbed); re-exported for callers.
|
|
633
|
+
var read_only_git_env_cjs_2 = require("./read-only-git-env.cjs");
|
|
634
|
+
Object.defineProperty(exports, "GIT_LOCATION_ENV_VARS", { enumerable: true, get: function () { return read_only_git_env_cjs_2.GIT_LOCATION_ENV_VARS; } });
|
|
635
|
+
Object.defineProperty(exports, "scrubGitLocationEnv", { enumerable: true, get: function () { return read_only_git_env_cjs_2.scrubGitLocationEnv; } });
|
|
612
636
|
function execGit(args, opts = {}) {
|
|
613
637
|
// Non-interactive defaults: a hung credential prompt or terminal-input
|
|
614
638
|
// probe must surface as a timeout, not block the tool forever. Callers
|
|
615
639
|
// can override via opts.env.
|
|
640
|
+
//
|
|
641
|
+
// #5048: readOnlyGitEnv() as well. execGit is the shared seam behind every
|
|
642
|
+
// git call the tool makes, including the read-only ones (`status`,
|
|
643
|
+
// `diff --cached`, `rev-parse`, `log`, …). By default git refreshes the index
|
|
644
|
+
// and takes an *optional* `.git/index.lock` to write the refreshed copy back,
|
|
645
|
+
// so a read can lose a race against a real `git add` / `git commit` and fail
|
|
646
|
+
// it with `Unable to create '.git/index.lock': File exists`. The owner is
|
|
647
|
+
// src/read-only-git-env.cts, shared with every other read-only seam, so this
|
|
648
|
+
// call site cannot drift from them. The variable disables only those optional
|
|
649
|
+
// index operations; a caller that genuinely needs one (a writing command, or a
|
|
650
|
+
// status whose freshness it wants persisted) can still set it back through
|
|
651
|
+
// opts.env, which is spread last on purpose.
|
|
616
652
|
const env = {
|
|
617
|
-
...
|
|
653
|
+
...(0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
|
|
618
654
|
GIT_TERMINAL_PROMPT: '0',
|
|
619
655
|
GCM_INTERACTIVE: 'never',
|
|
620
656
|
...(opts.env || {}),
|
|
@@ -1037,7 +1073,6 @@ function probeTty(opts = {}) {
|
|
|
1037
1073
|
return null;
|
|
1038
1074
|
}
|
|
1039
1075
|
}
|
|
1040
|
-
// ─── Platform file I/O ────────────────────────────────────────────────────────
|
|
1041
1076
|
/**
|
|
1042
1077
|
* How many leading lines of LF-only `text` are its closed YAML frontmatter block (opening
|
|
1043
1078
|
* fence through closing fence), or 0 when there is none (an unterminated block is not
|
|
@@ -1053,18 +1088,43 @@ function leadingFrontmatterLineCount(text) {
|
|
|
1053
1088
|
// The lines before the closing fence, plus the closing fence line itself.
|
|
1054
1089
|
return text.slice(0, fence.closingStart).split('\n').length;
|
|
1055
1090
|
}
|
|
1056
|
-
|
|
1091
|
+
/** A leading BOM is not part of the first line's content when aligning two documents. */
|
|
1092
|
+
function withoutBom(line) {
|
|
1093
|
+
return line !== undefined && line.charCodeAt(0) === 0xFEFF ? line.slice(1) : (line ?? '');
|
|
1094
|
+
}
|
|
1095
|
+
function _normalizeMd(content, prior, preserveFencedStructure = false) {
|
|
1057
1096
|
if (!content || typeof content !== 'string')
|
|
1058
1097
|
return content;
|
|
1059
1098
|
let text = content.replace(/\r\n/g, '\n');
|
|
1060
1099
|
const lines = text.split('\n');
|
|
1100
|
+
// #5165: when the document being replaced is known, only the junctions the
|
|
1101
|
+
// write touched are normalized; every other region stays byte-identical.
|
|
1102
|
+
let scope = null;
|
|
1103
|
+
let priorLines = [];
|
|
1104
|
+
if (typeof prior === 'string' && prior !== '') {
|
|
1105
|
+
priorLines = prior.replace(/\r\n/g, '\n').split('\n');
|
|
1106
|
+
// A BOM on either first line must not make an unchanged line look rewritten.
|
|
1107
|
+
const alignPrior = [withoutBom(priorLines[0]), ...priorLines.slice(1)];
|
|
1108
|
+
const alignNext = [withoutBom(lines[0]), ...lines.slice(1)];
|
|
1109
|
+
const match = (0, markdown_line_diff_cjs_1.matchLines)(alignPrior, alignNext);
|
|
1110
|
+
if (match)
|
|
1111
|
+
scope = (0, markdown_line_diff_cjs_1.writeScope)(match, priorLines.length);
|
|
1112
|
+
}
|
|
1061
1113
|
// The frontmatter block is YAML, not markdown: its lines are published exactly as written.
|
|
1062
1114
|
// A `# comment` is not a heading, and a blank line inserted into a multi-line quoted scalar
|
|
1063
1115
|
// or removed from a block scalar changes the value (found while implementing #5105).
|
|
1064
1116
|
const frontmatterLines = leadingFrontmatterLineCount(text);
|
|
1065
1117
|
const result = [];
|
|
1118
|
+
// A separating blank the rules add. In scoped mode (#5165) two rules that fire on
|
|
1119
|
+
// the same junction add ONE blank: the whole-document pass collapses them in its
|
|
1120
|
+
// final blank-run regex, which scoped mode must not apply to runs the write kept.
|
|
1121
|
+
const blank = () => {
|
|
1122
|
+
if (scope === null || result[result.length - 1] !== '')
|
|
1123
|
+
result.push('');
|
|
1124
|
+
};
|
|
1066
1125
|
const fenceRegex = /^```/;
|
|
1067
1126
|
const insideFence = new Array(lines.length);
|
|
1127
|
+
const insideSharedFence = new Array(lines.length).fill(false);
|
|
1068
1128
|
let fenceOpen = false;
|
|
1069
1129
|
for (let i = 0; i < lines.length; i++) {
|
|
1070
1130
|
if (i < frontmatterLines) {
|
|
@@ -1085,6 +1145,13 @@ function _normalizeMd(content) {
|
|
|
1085
1145
|
insideFence[i] = fenceOpen;
|
|
1086
1146
|
}
|
|
1087
1147
|
}
|
|
1148
|
+
if (preserveFencedStructure) {
|
|
1149
|
+
for (const block of (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines)) {
|
|
1150
|
+
const end = block.closeLineIdx === -1 ? lines.length : block.closeLineIdx;
|
|
1151
|
+
for (let i = block.openLineIdx + 1; i < end; i++)
|
|
1152
|
+
insideSharedFence[i] = true;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1088
1155
|
for (let i = 0; i < lines.length; i++) {
|
|
1089
1156
|
const line = lines[i];
|
|
1090
1157
|
if (i < frontmatterLines) {
|
|
@@ -1095,11 +1162,25 @@ function _normalizeMd(content) {
|
|
|
1095
1162
|
const prevTrimmed = prev.trimEnd();
|
|
1096
1163
|
const trimmed = line.trimEnd();
|
|
1097
1164
|
const isFenceLine = fenceRegex.test(trimmed);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1165
|
+
// Bracket writers preserve literal headings and lists inside fenced examples.
|
|
1166
|
+
const normalizeStructuralLine = !preserveFencedStructure || !insideSharedFence[i];
|
|
1167
|
+
// A heading or fence line the write authored brings its own separation, even
|
|
1168
|
+
// where it replaced a prose line in place (#5165). One that replaced a heading
|
|
1169
|
+
// or fence (a rename, a renumber) inherits the spacing around it instead.
|
|
1170
|
+
const isStructural = (text) => /^#{1,6}\s/.test(text.trimEnd()) || fenceRegex.test(text.trimEnd());
|
|
1171
|
+
const replacedPrior = scope !== null ? scope.replacedPriorIndex(i) : -1;
|
|
1172
|
+
const authoredStructure = scope !== null && scope.lineWritten(i) && isStructural(line)
|
|
1173
|
+
&& !(replacedPrior >= 0 && isStructural(priorLines[replacedPrior] ?? ''));
|
|
1174
|
+
const keptBefore = scope !== null && !authoredStructure && i > 0 && scope.junctionKept(i - 1);
|
|
1175
|
+
const keptAfter = scope !== null && !authoredStructure && i < lines.length - 1 && scope.junctionKept(i);
|
|
1176
|
+
// A blank-line run is collapsed only where the write made it (#5165).
|
|
1177
|
+
if (scope !== null && line === '' && result.length > 0 && result[result.length - 1] === '' && !keptBefore)
|
|
1178
|
+
continue;
|
|
1179
|
+
if (normalizeStructuralLine && /^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---' && !keptBefore)
|
|
1180
|
+
blank();
|
|
1181
|
+
if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine) && !keptBefore) {
|
|
1101
1182
|
if (i === 0 || !insideFence[i - 1])
|
|
1102
|
-
|
|
1183
|
+
blank();
|
|
1103
1184
|
}
|
|
1104
1185
|
// No "separate a list from a preceding paragraph" rule (#3854, #4725).
|
|
1105
1186
|
// This pass re-normalizes the ENTIRE document on every .md write, so an
|
|
@@ -1111,21 +1192,21 @@ function _normalizeMd(content) {
|
|
|
1111
1192
|
// from the after-heading rule below. The after-a-bullet rule at the end
|
|
1112
1193
|
// of this loop is a different transition (list→prose) and is unaffected.
|
|
1113
1194
|
result.push(line);
|
|
1114
|
-
if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '')
|
|
1115
|
-
|
|
1116
|
-
if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '')
|
|
1117
|
-
|
|
1118
|
-
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1) {
|
|
1195
|
+
if (normalizeStructuralLine && /^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
|
|
1196
|
+
blank();
|
|
1197
|
+
if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
|
|
1198
|
+
blank();
|
|
1199
|
+
if (normalizeStructuralLine && /^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1 && !keptAfter) {
|
|
1119
1200
|
const next = lines[i + 1];
|
|
1120
1201
|
if (next !== undefined && next.trimEnd() !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(next) && !/^\s/.test(next))
|
|
1121
|
-
|
|
1202
|
+
blank();
|
|
1122
1203
|
}
|
|
1123
1204
|
}
|
|
1124
1205
|
// The frontmatter lines are `result`'s first entries, unchanged; the blank-run collapse
|
|
1125
1206
|
// applies from the line ending that closes the block onward.
|
|
1126
1207
|
const head = result.slice(0, frontmatterLines).join('\n');
|
|
1127
1208
|
text = result.join('\n');
|
|
1128
|
-
text = head + text.slice(head.length).replace(/\n{3,}/g, '\n\n');
|
|
1209
|
+
text = scope === null ? head + text.slice(head.length).replace(/\n{3,}/g, '\n\n') : text;
|
|
1129
1210
|
text = text.replace(/\n*$/, '\n');
|
|
1130
1211
|
return text;
|
|
1131
1212
|
}
|
|
@@ -1136,7 +1217,7 @@ function normalizeContent(filePath, content, opts = {}) {
|
|
|
1136
1217
|
const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
|
|
1137
1218
|
let normalized;
|
|
1138
1219
|
if (isMd) {
|
|
1139
|
-
normalized = _normalizeMd(content);
|
|
1220
|
+
normalized = _normalizeMd(content, opts.prior, opts.preserveFencedMarkdownStructure === true);
|
|
1140
1221
|
}
|
|
1141
1222
|
else {
|
|
1142
1223
|
normalized = (content ?? '').replace(/\r\n/g, '\n').replace(/\n*$/, '\n');
|
|
@@ -1157,8 +1238,8 @@ function normalizeContent(filePath, content, opts = {}) {
|
|
|
1157
1238
|
* post-write re-read of the actual on-disk bytes) instead of a raw `!==` —
|
|
1158
1239
|
* do not simplify this back to a direct string comparison.
|
|
1159
1240
|
*/
|
|
1160
|
-
function contentChangedAfterNormalize(filePath, before, after) {
|
|
1161
|
-
return normalizeContent(filePath, after).content !== normalizeContent(filePath, before).content;
|
|
1241
|
+
function contentChangedAfterNormalize(filePath, before, after, opts = {}) {
|
|
1242
|
+
return normalizeContent(filePath, after, { ...opts, prior: before }).content !== normalizeContent(filePath, before, { ...opts, prior: before }).content;
|
|
1162
1243
|
}
|
|
1163
1244
|
// Rename errnos that are transient on Windows: a concurrent reader (or an AV
|
|
1164
1245
|
// scanner / indexer) holding the target open makes renameSync fail briefly.
|
|
@@ -1210,8 +1291,59 @@ function retryRenameSync(fromPath, toPath) {
|
|
|
1210
1291
|
if (err !== null)
|
|
1211
1292
|
throw err;
|
|
1212
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Largest on-disk Markdown document read back as the `prior` of a scoped write.
|
|
1296
|
+
* A larger file is normalized whole (the pre-#5165 behavior) rather than read and
|
|
1297
|
+
* diffed on every write.
|
|
1298
|
+
*/
|
|
1299
|
+
const MAX_PRIOR_BYTES = 2 * 1024 * 1024;
|
|
1300
|
+
/**
|
|
1301
|
+
* The Markdown document a write replaces, or undefined when there is none worth
|
|
1302
|
+
* diffing against: no file, not a regular file (a FIFO or device reports size 0
|
|
1303
|
+
* and would block or never end), unreadable, or larger than MAX_PRIOR_BYTES. The
|
|
1304
|
+
* bound is enforced on the bytes actually read from one open descriptor, so a
|
|
1305
|
+
* file that grows after the check cannot exceed it.
|
|
1306
|
+
*/
|
|
1307
|
+
function readPriorMarkdown(filePath, encoding) {
|
|
1308
|
+
let fd;
|
|
1309
|
+
try {
|
|
1310
|
+
// O_NONBLOCK keeps opening a FIFO from waiting for a writer; it is absent on Windows.
|
|
1311
|
+
fd = node_fs_1.default.openSync(filePath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NONBLOCK ?? 0));
|
|
1312
|
+
const stat = node_fs_1.default.fstatSync(fd);
|
|
1313
|
+
if (!stat.isFile() || stat.size > MAX_PRIOR_BYTES)
|
|
1314
|
+
return undefined;
|
|
1315
|
+
// One byte of headroom: reading it means the file grew past what was measured.
|
|
1316
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
1317
|
+
let length = 0;
|
|
1318
|
+
while (length < buffer.length) {
|
|
1319
|
+
const read = node_fs_1.default.readSync(fd, buffer, length, buffer.length - length, null);
|
|
1320
|
+
if (read === 0)
|
|
1321
|
+
break;
|
|
1322
|
+
length += read;
|
|
1323
|
+
}
|
|
1324
|
+
if (length > stat.size)
|
|
1325
|
+
return undefined;
|
|
1326
|
+
return buffer.toString(encoding, 0, length);
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return undefined; // no prior document (new file or unreadable): normalize it whole
|
|
1330
|
+
}
|
|
1331
|
+
finally {
|
|
1332
|
+
if (fd !== undefined) {
|
|
1333
|
+
try {
|
|
1334
|
+
node_fs_1.default.closeSync(fd);
|
|
1335
|
+
}
|
|
1336
|
+
catch { /* already closed */ }
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1213
1340
|
function platformWriteSync(filePath, content, opts = {}) {
|
|
1214
|
-
|
|
1341
|
+
// #5165: a Markdown write is scoped to what it changed relative to the file it replaces.
|
|
1342
|
+
let prior = opts.prior;
|
|
1343
|
+
if (prior === undefined && opts.verbatim !== true && node_path_1.default.extname(filePath).toLowerCase() === '.md') {
|
|
1344
|
+
prior = readPriorMarkdown(filePath, opts.encoding ?? 'utf-8');
|
|
1345
|
+
}
|
|
1346
|
+
const { content: normalized, encoding } = normalizeContent(filePath, content, { ...opts, prior });
|
|
1215
1347
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
1216
1348
|
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
1217
1349
|
// Step 1: write the sibling tmp file. If THIS fails, nothing was published, so a
|
|
@@ -43,6 +43,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
43
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
46
|
+
const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
|
|
46
47
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
47
48
|
// #3696: the calendar-validity predicate moved to the STATE.md document module
|
|
48
49
|
// so `state validate` can assert the same `last_activity` invariant this reader
|
|
@@ -68,11 +69,14 @@ const stateDocument = require("./state-document.cjs");
|
|
|
68
69
|
const { stateFieldValue } = stateDocument;
|
|
69
70
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
70
71
|
const phaseId = require("./phase-id.cjs");
|
|
71
|
-
const { comparePhaseNum, extractPhaseToken,
|
|
72
|
+
const { comparePhaseNum, extractPhaseToken, customPhaseQuery, customDirectoryOwner, parsePhaseFromProse, stripProjectCodePrefix } = phaseId;
|
|
72
73
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
73
74
|
const stateMod = require("./state.cjs");
|
|
74
75
|
const { readStateHeadFreshness } = stateMod;
|
|
75
76
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
77
|
+
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
78
|
+
const { phaseDirectoryOptions, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
76
80
|
const unusableInput = require("./unusable-input.cjs");
|
|
77
81
|
const { warnUnusableInput, UNUSABLE_REASON } = unusableInput;
|
|
78
82
|
// #5118: the verification-status owner's frontmatter-only report check.
|
|
@@ -119,8 +123,8 @@ function parseIntOrNull(s) {
|
|
|
119
123
|
const n = Number.parseInt(cleaned, 10);
|
|
120
124
|
return Number.isFinite(n) ? n : null;
|
|
121
125
|
}
|
|
122
|
-
function phaseTokenFromDirName(name) {
|
|
123
|
-
const token = extractPhaseToken(name);
|
|
126
|
+
function phaseTokenFromDirName(name, convention) {
|
|
127
|
+
const token = extractPhaseToken(name, convention);
|
|
124
128
|
// #2528: the shape probe runs on the PROJECT-CODE-STRIPPED token. A prefixed
|
|
125
129
|
// directory tokenizes to `MEM-05-80-20`, which does not start with a digit,
|
|
126
130
|
// so the unstripped probe rejected it and the entry was dropped before any
|
|
@@ -222,6 +226,18 @@ function readGitSignals(cwd) {
|
|
|
222
226
|
windowsHide: true,
|
|
223
227
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
224
228
|
timeout: 10_000,
|
|
229
|
+
// #5048: readGitSignals runs `git status`, which by default refreshes
|
|
230
|
+
// the index and takes an *optional* `.git/index.lock` to write the
|
|
231
|
+
// refreshed copy back. That lock is the one a real `git add` / `git
|
|
232
|
+
// commit` needs, so a routine "which phase am I in" probe can fail
|
|
233
|
+
// someone else's commit with `Unable to create '.git/index.lock': File
|
|
234
|
+
// exists`. GIT_OPTIONAL_LOCKS=0 turns off exactly that optional write.
|
|
235
|
+
// The `rev-parse` / `log` calls sharing this helper never open the
|
|
236
|
+
// index, so the variable is inert for them. The block itself is owned by
|
|
237
|
+
// src/read-only-git-env.cts and shared with every other read-only seam;
|
|
238
|
+
// parity across every read-only surface is asserted in
|
|
239
|
+
// tests/git-optional-locks-parity.test.cjs.
|
|
240
|
+
env: (0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
|
|
225
241
|
});
|
|
226
242
|
}
|
|
227
243
|
catch {
|
|
@@ -239,9 +255,9 @@ function readGitSignals(cwd) {
|
|
|
239
255
|
const unpushed = run(['log', '@{u}..HEAD', '--oneline']).trim().length > 0;
|
|
240
256
|
return { has_git: true, dirty, unpushed };
|
|
241
257
|
}
|
|
242
|
-
/**
|
|
243
|
-
function phaseTokenFromState(raw) {
|
|
244
|
-
return parsePhaseFromProse(raw).phase;
|
|
258
|
+
/** Phase identity from a STATE.md scalar or body `Phase:` value, honoring custom naming when a context is supplied. */
|
|
259
|
+
function phaseTokenFromState(raw, context) {
|
|
260
|
+
return parsePhaseFromProse(raw, undefined, context).phase;
|
|
245
261
|
}
|
|
246
262
|
/**
|
|
247
263
|
* Detect whether the current phase's verify report indicates failure. The
|
|
@@ -251,12 +267,20 @@ function phaseTokenFromState(raw) {
|
|
|
251
267
|
*/
|
|
252
268
|
function detectVerifyFailed(cwd, currentPhaseRaw) {
|
|
253
269
|
const phasesDir = node_path_1.default.join(planningPaths(cwd).phases);
|
|
270
|
+
// #4304: both the directory admission scan and the later selector must read
|
|
271
|
+
// the same current-checkout grammar. Bracket is the only opt-in widening.
|
|
272
|
+
const convention = resolvePhaseIdConvention(cwd) === 'bracket' ? 'bracket' : undefined;
|
|
273
|
+
const directoryContext = phaseDirectoryOptions(cwd);
|
|
254
274
|
let entries = [];
|
|
255
275
|
try {
|
|
256
276
|
entries = node_fs_1.default.readdirSync(phasesDir)
|
|
257
|
-
.map((name) =>
|
|
277
|
+
.map((name) => {
|
|
278
|
+
const custom = customPhaseQuery(name, directoryContext);
|
|
279
|
+
const parts = custom && customDirectoryOwner(name, custom);
|
|
280
|
+
return { name, phaseToken: parts ? parts.id : phaseTokenFromDirName(name, convention) };
|
|
281
|
+
})
|
|
258
282
|
.filter((entry) => entry.phaseToken !== null)
|
|
259
|
-
.sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken) || a.name.localeCompare(b.name))
|
|
283
|
+
.sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken, directoryContext) || a.name.localeCompare(b.name))
|
|
260
284
|
.map((entry) => entry.name);
|
|
261
285
|
}
|
|
262
286
|
catch {
|
|
@@ -264,10 +288,10 @@ function detectVerifyFailed(cwd, currentPhaseRaw) {
|
|
|
264
288
|
}
|
|
265
289
|
if (entries.length === 0)
|
|
266
290
|
return false;
|
|
267
|
-
const phaseToken = phaseTokenFromState(currentPhaseRaw);
|
|
291
|
+
const phaseToken = phaseTokenFromState(currentPhaseRaw, directoryContext);
|
|
268
292
|
let targetDir;
|
|
269
293
|
if (phaseToken) {
|
|
270
|
-
const
|
|
294
|
+
const lookup = resolvePhaseDirectoryLookup(cwd, phaseToken, undefined, directoryContext);
|
|
271
295
|
// #2528: the fourth directory-resolution site, and the one where a miss is
|
|
272
296
|
// silent — a phase whose directory cannot be found reports "not failed",
|
|
273
297
|
// which reads identically to a healthy phase. It must therefore apply the
|
|
@@ -276,7 +300,7 @@ function detectVerifyFailed(cwd, currentPhaseRaw) {
|
|
|
276
300
|
// surfaces its own failed verification. `entries` is already sorted, and
|
|
277
301
|
// `matchPhaseDirs` filters without reordering, so taking the first match
|
|
278
302
|
// preserves the previous `.find()` selection exactly.
|
|
279
|
-
const { matches } =
|
|
303
|
+
const { matches } = matchPhaseDirsForLookup(entries, lookup, undefined, directoryContext);
|
|
280
304
|
targetDir = matches[0];
|
|
281
305
|
if (!targetDir)
|
|
282
306
|
return false;
|