@opengsd/gsd-core 1.18.0 → 1.19.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 +2 -2
- package/agents/gsd-code-fixer.md +4 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/gsd-core/bin/gsd-tools.cjs +106 -7
- 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-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +156 -74
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +17 -0
- package/gsd-core/bin/lib/config-loader.cjs +10 -0
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +34 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
- 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-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 +57 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -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 +65 -1
- 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/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
- 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 +43 -1
- 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-id.cjs +21 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
- package/gsd-core/bin/lib/phase.cjs +115 -26
- package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -25
- 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 +121 -16
- package/gsd-core/bin/lib/smart-entry.cjs +13 -0
- package/gsd-core/bin/lib/state-contract.cjs +29 -9
- package/gsd-core/bin/lib/state-document.cjs +12 -4
- package/gsd-core/bin/lib/state-transition.cjs +119 -24
- package/gsd-core/bin/lib/state.cjs +50 -28
- 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/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +2 -0
- 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/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- 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.md +61 -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 +9 -5
- 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.md +13 -13
- 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/review.md +16 -13
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +3 -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-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +35 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -1
- package/hooks/dist/gsd-worktree-path-guard.js +28 -1
- 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-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +35 -2
- package/hooks/gsd-windsurf-pre-write.js +28 -1
- package/hooks/gsd-workflow-guard.js +1 -1
- package/hooks/gsd-worktree-path-guard.js +28 -1
- package/hooks/hooks.json +1 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -6
- 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 +769 -0
- package/pi/package-agents/gsd-code-reviewer.md +402 -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 +894 -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 +51 -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/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +20 -7
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- 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/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,
|
|
@@ -65,7 +65,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
65
65
|
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
66
66
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
67
67
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
68
|
+
const markdown_line_diff_cjs_1 = require("./markdown-line-diff.cjs");
|
|
68
69
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
70
|
+
const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
|
|
69
71
|
/**
|
|
70
72
|
* Convert a filesystem path to POSIX form (forward slashes) by translating the
|
|
71
73
|
* platform-native separator. Single seam for native→POSIX conversion.
|
|
@@ -249,6 +251,8 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
|
249
251
|
'gsd-worktree-path-guard.js',
|
|
250
252
|
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
251
253
|
'gsd-secret-read-guard.js',
|
|
254
|
+
// #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
|
|
255
|
+
'gsd-path-export.js',
|
|
252
256
|
]),
|
|
253
257
|
'codex-toml': new Set([
|
|
254
258
|
'gsd-check-update.js',
|
|
@@ -279,6 +283,8 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
|
|
|
279
283
|
'gsd-worktree-path-guard.js',
|
|
280
284
|
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
281
285
|
'gsd-secret-read-guard.js',
|
|
286
|
+
// #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
|
|
287
|
+
'gsd-path-export.js',
|
|
282
288
|
]),
|
|
283
289
|
'codex-toml': new Set([
|
|
284
290
|
'gsd-check-update.js',
|
|
@@ -613,8 +619,20 @@ function execGit(args, opts = {}) {
|
|
|
613
619
|
// Non-interactive defaults: a hung credential prompt or terminal-input
|
|
614
620
|
// probe must surface as a timeout, not block the tool forever. Callers
|
|
615
621
|
// can override via opts.env.
|
|
622
|
+
//
|
|
623
|
+
// #5048: readOnlyGitEnv() as well. execGit is the shared seam behind every
|
|
624
|
+
// git call the tool makes, including the read-only ones (`status`,
|
|
625
|
+
// `diff --cached`, `rev-parse`, `log`, …). By default git refreshes the index
|
|
626
|
+
// and takes an *optional* `.git/index.lock` to write the refreshed copy back,
|
|
627
|
+
// so a read can lose a race against a real `git add` / `git commit` and fail
|
|
628
|
+
// it with `Unable to create '.git/index.lock': File exists`. The owner is
|
|
629
|
+
// src/read-only-git-env.cts, shared with every other read-only seam, so this
|
|
630
|
+
// call site cannot drift from them. The variable disables only those optional
|
|
631
|
+
// index operations; a caller that genuinely needs one (a writing command, or a
|
|
632
|
+
// status whose freshness it wants persisted) can still set it back through
|
|
633
|
+
// opts.env, which is spread last on purpose.
|
|
616
634
|
const env = {
|
|
617
|
-
...
|
|
635
|
+
...(0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
|
|
618
636
|
GIT_TERMINAL_PROMPT: '0',
|
|
619
637
|
GCM_INTERACTIVE: 'never',
|
|
620
638
|
...(opts.env || {}),
|
|
@@ -1053,16 +1071,40 @@ function leadingFrontmatterLineCount(text) {
|
|
|
1053
1071
|
// The lines before the closing fence, plus the closing fence line itself.
|
|
1054
1072
|
return text.slice(0, fence.closingStart).split('\n').length;
|
|
1055
1073
|
}
|
|
1056
|
-
|
|
1074
|
+
/** A leading BOM is not part of the first line's content when aligning two documents. */
|
|
1075
|
+
function withoutBom(line) {
|
|
1076
|
+
return line !== undefined && line.charCodeAt(0) === 0xFEFF ? line.slice(1) : (line ?? '');
|
|
1077
|
+
}
|
|
1078
|
+
function _normalizeMd(content, prior) {
|
|
1057
1079
|
if (!content || typeof content !== 'string')
|
|
1058
1080
|
return content;
|
|
1059
1081
|
let text = content.replace(/\r\n/g, '\n');
|
|
1060
1082
|
const lines = text.split('\n');
|
|
1083
|
+
// #5165: when the document being replaced is known, only the junctions the
|
|
1084
|
+
// write touched are normalized; every other region stays byte-identical.
|
|
1085
|
+
let scope = null;
|
|
1086
|
+
let priorLines = [];
|
|
1087
|
+
if (typeof prior === 'string' && prior !== '') {
|
|
1088
|
+
priorLines = prior.replace(/\r\n/g, '\n').split('\n');
|
|
1089
|
+
// A BOM on either first line must not make an unchanged line look rewritten.
|
|
1090
|
+
const alignPrior = [withoutBom(priorLines[0]), ...priorLines.slice(1)];
|
|
1091
|
+
const alignNext = [withoutBom(lines[0]), ...lines.slice(1)];
|
|
1092
|
+
const match = (0, markdown_line_diff_cjs_1.matchLines)(alignPrior, alignNext);
|
|
1093
|
+
if (match)
|
|
1094
|
+
scope = (0, markdown_line_diff_cjs_1.writeScope)(match, priorLines.length);
|
|
1095
|
+
}
|
|
1061
1096
|
// The frontmatter block is YAML, not markdown: its lines are published exactly as written.
|
|
1062
1097
|
// A `# comment` is not a heading, and a blank line inserted into a multi-line quoted scalar
|
|
1063
1098
|
// or removed from a block scalar changes the value (found while implementing #5105).
|
|
1064
1099
|
const frontmatterLines = leadingFrontmatterLineCount(text);
|
|
1065
1100
|
const result = [];
|
|
1101
|
+
// A separating blank the rules add. In scoped mode (#5165) two rules that fire on
|
|
1102
|
+
// the same junction add ONE blank: the whole-document pass collapses them in its
|
|
1103
|
+
// final blank-run regex, which scoped mode must not apply to runs the write kept.
|
|
1104
|
+
const blank = () => {
|
|
1105
|
+
if (scope === null || result[result.length - 1] !== '')
|
|
1106
|
+
result.push('');
|
|
1107
|
+
};
|
|
1066
1108
|
const fenceRegex = /^```/;
|
|
1067
1109
|
const insideFence = new Array(lines.length);
|
|
1068
1110
|
let fenceOpen = false;
|
|
@@ -1095,11 +1137,23 @@ function _normalizeMd(content) {
|
|
|
1095
1137
|
const prevTrimmed = prev.trimEnd();
|
|
1096
1138
|
const trimmed = line.trimEnd();
|
|
1097
1139
|
const isFenceLine = fenceRegex.test(trimmed);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1140
|
+
// A heading or fence line the write authored brings its own separation, even
|
|
1141
|
+
// where it replaced a prose line in place (#5165). One that replaced a heading
|
|
1142
|
+
// or fence (a rename, a renumber) inherits the spacing around it instead.
|
|
1143
|
+
const isStructural = (text) => /^#{1,6}\s/.test(text.trimEnd()) || fenceRegex.test(text.trimEnd());
|
|
1144
|
+
const replacedPrior = scope !== null ? scope.replacedPriorIndex(i) : -1;
|
|
1145
|
+
const authoredStructure = scope !== null && scope.lineWritten(i) && isStructural(line)
|
|
1146
|
+
&& !(replacedPrior >= 0 && isStructural(priorLines[replacedPrior] ?? ''));
|
|
1147
|
+
const keptBefore = scope !== null && !authoredStructure && i > 0 && scope.junctionKept(i - 1);
|
|
1148
|
+
const keptAfter = scope !== null && !authoredStructure && i < lines.length - 1 && scope.junctionKept(i);
|
|
1149
|
+
// A blank-line run is collapsed only where the write made it (#5165).
|
|
1150
|
+
if (scope !== null && line === '' && result.length > 0 && result[result.length - 1] === '' && !keptBefore)
|
|
1151
|
+
continue;
|
|
1152
|
+
if (/^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---' && !keptBefore)
|
|
1153
|
+
blank();
|
|
1154
|
+
if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine) && !keptBefore) {
|
|
1101
1155
|
if (i === 0 || !insideFence[i - 1])
|
|
1102
|
-
|
|
1156
|
+
blank();
|
|
1103
1157
|
}
|
|
1104
1158
|
// No "separate a list from a preceding paragraph" rule (#3854, #4725).
|
|
1105
1159
|
// This pass re-normalizes the ENTIRE document on every .md write, so an
|
|
@@ -1111,21 +1165,21 @@ function _normalizeMd(content) {
|
|
|
1111
1165
|
// from the after-heading rule below. The after-a-bullet rule at the end
|
|
1112
1166
|
// of this loop is a different transition (list→prose) and is unaffected.
|
|
1113
1167
|
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) {
|
|
1168
|
+
if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
|
|
1169
|
+
blank();
|
|
1170
|
+
if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
|
|
1171
|
+
blank();
|
|
1172
|
+
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1 && !keptAfter) {
|
|
1119
1173
|
const next = lines[i + 1];
|
|
1120
1174
|
if (next !== undefined && next.trimEnd() !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(next) && !/^\s/.test(next))
|
|
1121
|
-
|
|
1175
|
+
blank();
|
|
1122
1176
|
}
|
|
1123
1177
|
}
|
|
1124
1178
|
// The frontmatter lines are `result`'s first entries, unchanged; the blank-run collapse
|
|
1125
1179
|
// applies from the line ending that closes the block onward.
|
|
1126
1180
|
const head = result.slice(0, frontmatterLines).join('\n');
|
|
1127
1181
|
text = result.join('\n');
|
|
1128
|
-
text = head + text.slice(head.length).replace(/\n{3,}/g, '\n\n');
|
|
1182
|
+
text = scope === null ? head + text.slice(head.length).replace(/\n{3,}/g, '\n\n') : text;
|
|
1129
1183
|
text = text.replace(/\n*$/, '\n');
|
|
1130
1184
|
return text;
|
|
1131
1185
|
}
|
|
@@ -1136,7 +1190,7 @@ function normalizeContent(filePath, content, opts = {}) {
|
|
|
1136
1190
|
const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
|
|
1137
1191
|
let normalized;
|
|
1138
1192
|
if (isMd) {
|
|
1139
|
-
normalized = _normalizeMd(content);
|
|
1193
|
+
normalized = _normalizeMd(content, opts.prior);
|
|
1140
1194
|
}
|
|
1141
1195
|
else {
|
|
1142
1196
|
normalized = (content ?? '').replace(/\r\n/g, '\n').replace(/\n*$/, '\n');
|
|
@@ -1158,7 +1212,7 @@ function normalizeContent(filePath, content, opts = {}) {
|
|
|
1158
1212
|
* do not simplify this back to a direct string comparison.
|
|
1159
1213
|
*/
|
|
1160
1214
|
function contentChangedAfterNormalize(filePath, before, after) {
|
|
1161
|
-
return normalizeContent(filePath, after).content !== normalizeContent(filePath, before).content;
|
|
1215
|
+
return normalizeContent(filePath, after, { prior: before }).content !== normalizeContent(filePath, before, { prior: before }).content;
|
|
1162
1216
|
}
|
|
1163
1217
|
// Rename errnos that are transient on Windows: a concurrent reader (or an AV
|
|
1164
1218
|
// scanner / indexer) holding the target open makes renameSync fail briefly.
|
|
@@ -1210,8 +1264,59 @@ function retryRenameSync(fromPath, toPath) {
|
|
|
1210
1264
|
if (err !== null)
|
|
1211
1265
|
throw err;
|
|
1212
1266
|
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Largest on-disk Markdown document read back as the `prior` of a scoped write.
|
|
1269
|
+
* A larger file is normalized whole (the pre-#5165 behavior) rather than read and
|
|
1270
|
+
* diffed on every write.
|
|
1271
|
+
*/
|
|
1272
|
+
const MAX_PRIOR_BYTES = 2 * 1024 * 1024;
|
|
1273
|
+
/**
|
|
1274
|
+
* The Markdown document a write replaces, or undefined when there is none worth
|
|
1275
|
+
* diffing against: no file, not a regular file (a FIFO or device reports size 0
|
|
1276
|
+
* and would block or never end), unreadable, or larger than MAX_PRIOR_BYTES. The
|
|
1277
|
+
* bound is enforced on the bytes actually read from one open descriptor, so a
|
|
1278
|
+
* file that grows after the check cannot exceed it.
|
|
1279
|
+
*/
|
|
1280
|
+
function readPriorMarkdown(filePath, encoding) {
|
|
1281
|
+
let fd;
|
|
1282
|
+
try {
|
|
1283
|
+
// O_NONBLOCK keeps opening a FIFO from waiting for a writer; it is absent on Windows.
|
|
1284
|
+
fd = node_fs_1.default.openSync(filePath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NONBLOCK ?? 0));
|
|
1285
|
+
const stat = node_fs_1.default.fstatSync(fd);
|
|
1286
|
+
if (!stat.isFile() || stat.size > MAX_PRIOR_BYTES)
|
|
1287
|
+
return undefined;
|
|
1288
|
+
// One byte of headroom: reading it means the file grew past what was measured.
|
|
1289
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
1290
|
+
let length = 0;
|
|
1291
|
+
while (length < buffer.length) {
|
|
1292
|
+
const read = node_fs_1.default.readSync(fd, buffer, length, buffer.length - length, null);
|
|
1293
|
+
if (read === 0)
|
|
1294
|
+
break;
|
|
1295
|
+
length += read;
|
|
1296
|
+
}
|
|
1297
|
+
if (length > stat.size)
|
|
1298
|
+
return undefined;
|
|
1299
|
+
return buffer.toString(encoding, 0, length);
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
return undefined; // no prior document (new file or unreadable): normalize it whole
|
|
1303
|
+
}
|
|
1304
|
+
finally {
|
|
1305
|
+
if (fd !== undefined) {
|
|
1306
|
+
try {
|
|
1307
|
+
node_fs_1.default.closeSync(fd);
|
|
1308
|
+
}
|
|
1309
|
+
catch { /* already closed */ }
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1213
1313
|
function platformWriteSync(filePath, content, opts = {}) {
|
|
1214
|
-
|
|
1314
|
+
// #5165: a Markdown write is scoped to what it changed relative to the file it replaces.
|
|
1315
|
+
let prior = opts.prior;
|
|
1316
|
+
if (prior === undefined && opts.verbatim !== true && node_path_1.default.extname(filePath).toLowerCase() === '.md') {
|
|
1317
|
+
prior = readPriorMarkdown(filePath, opts.encoding ?? 'utf-8');
|
|
1318
|
+
}
|
|
1319
|
+
const { content: normalized, encoding } = normalizeContent(filePath, content, { ...opts, prior });
|
|
1215
1320
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
1216
1321
|
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
1217
1322
|
// 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
|
|
@@ -222,6 +223,18 @@ function readGitSignals(cwd) {
|
|
|
222
223
|
windowsHide: true,
|
|
223
224
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
224
225
|
timeout: 10_000,
|
|
226
|
+
// #5048: readGitSignals runs `git status`, which by default refreshes
|
|
227
|
+
// the index and takes an *optional* `.git/index.lock` to write the
|
|
228
|
+
// refreshed copy back. That lock is the one a real `git add` / `git
|
|
229
|
+
// commit` needs, so a routine "which phase am I in" probe can fail
|
|
230
|
+
// someone else's commit with `Unable to create '.git/index.lock': File
|
|
231
|
+
// exists`. GIT_OPTIONAL_LOCKS=0 turns off exactly that optional write.
|
|
232
|
+
// The `rev-parse` / `log` calls sharing this helper never open the
|
|
233
|
+
// index, so the variable is inert for them. The block itself is owned by
|
|
234
|
+
// src/read-only-git-env.cts and shared with every other read-only seam;
|
|
235
|
+
// parity across every read-only surface is asserted in
|
|
236
|
+
// tests/git-optional-locks-parity.test.cjs.
|
|
237
|
+
env: (0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
|
|
225
238
|
});
|
|
226
239
|
}
|
|
227
240
|
catch {
|
|
@@ -129,6 +129,7 @@ function owners() {
|
|
|
129
129
|
locateProgressTable: phaseLifecycleMod.locateProgressTable,
|
|
130
130
|
isSentinelPhaseId: phaseIdMod.isSentinelPhaseId,
|
|
131
131
|
parsePhaseFromProse: phaseIdMod.parsePhaseFromProse,
|
|
132
|
+
resolvePhaseIdConvention: planningWorkspaceMod.resolvePhaseIdConvention,
|
|
132
133
|
getMilestoneInfo: roadmapParserMod.getMilestoneInfo,
|
|
133
134
|
collectSection: markdownSectionizerMod.collectSection,
|
|
134
135
|
stateFieldValue: stateDocumentMod.stateFieldValue,
|
|
@@ -154,16 +155,31 @@ function buildMilestone(cwd) {
|
|
|
154
155
|
}
|
|
155
156
|
// ─── phases: Progress-table (primary) enumerator ───────────────────────────
|
|
156
157
|
const PHASE_CELL_RE = /^(\d+(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
|
|
157
|
-
|
|
158
|
+
// #5626: under `milestone-prefixed` the phase id is the whole `M-NN` (`3-01`), so the number capture takes
|
|
159
|
+
// one optional `-NN` segment. Chosen by the convention value alone: every other convention keeps the
|
|
160
|
+
// dotted-only capture above, and the `\d+-\d+` shape stays an ordinary leading integer there.
|
|
161
|
+
const MILESTONE_PREFIXED = 'milestone-prefixed';
|
|
162
|
+
const PHASE_CELL_MILESTONE_RE = /^(\d+(?:-\d+)?(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
|
|
163
|
+
/** The project's `phase_id_convention` (null when unset or unreadable), through its one owner. */
|
|
164
|
+
function phaseIdConventionOf(cwd) {
|
|
165
|
+
try {
|
|
166
|
+
return owners().resolvePhaseIdConvention(cwd);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function phasesFromProgressTable(table, convention) {
|
|
158
173
|
const { isSentinelPhaseId } = owners();
|
|
159
174
|
const phases = [];
|
|
175
|
+
const cellRe = convention === MILESTONE_PREFIXED ? PHASE_CELL_MILESTONE_RE : PHASE_CELL_RE;
|
|
160
176
|
for (const row of table.rows) {
|
|
161
177
|
const cell = (row['Phase'] ?? '').trim();
|
|
162
178
|
if (!/^\d/.test(cell))
|
|
163
179
|
continue; // stray note row, no leading digit
|
|
164
180
|
if (isSentinelPhaseId(cell))
|
|
165
181
|
continue; // phase 0 / 999.x sentinels
|
|
166
|
-
const m =
|
|
182
|
+
const m = cellRe.exec(cell);
|
|
167
183
|
const number = m ? m[1] : cell;
|
|
168
184
|
const name = m && m[2].trim().length > 0 ? m[2].trim() : null;
|
|
169
185
|
phases.push({ number, name, status: (0, phase_status_cjs_1.parseRoadmapStatusCell)(row['Status']) });
|
|
@@ -173,6 +189,8 @@ function phasesFromProgressTable(table) {
|
|
|
173
189
|
// ─── phases: `## Phases` checkbox fallback ─────────────────────────────────
|
|
174
190
|
// `- [x] **Phase 1: Foundation**` / `- [ ] **Phase 2.1: Hardening**`.
|
|
175
191
|
const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
|
|
192
|
+
// #5626: the same bullet under `milestone-prefixed`: the number capture takes one optional `-NN` segment.
|
|
193
|
+
const PHASE_BULLET_MILESTONE_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:-\d+)?(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
|
|
176
194
|
/**
|
|
177
195
|
* STATE.md's current phase, via the same frontmatter-then-body chain
|
|
178
196
|
* `cmdStateCompletePhase` uses (`stateFieldValue` owns the #1760 fallback
|
|
@@ -180,7 +198,7 @@ const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?
|
|
|
180
198
|
* phase as unknown so every unchecked bullet is PENDING rather than
|
|
181
199
|
* IN_PROGRESS by a guess.
|
|
182
200
|
*/
|
|
183
|
-
function currentPhaseFromState(statePath) {
|
|
201
|
+
function currentPhaseFromState(statePath, convention) {
|
|
184
202
|
const { platformReadSync, extractFrontmatter, stripFrontmatter, stateFieldValue, parsePhaseFromProse } = owners();
|
|
185
203
|
try {
|
|
186
204
|
const raw = platformReadSync(statePath);
|
|
@@ -191,7 +209,7 @@ function currentPhaseFromState(statePath) {
|
|
|
191
209
|
const { value } = stateFieldValue(fm, body, 'current_phase', 'Current Phase');
|
|
192
210
|
if (!value)
|
|
193
211
|
return null;
|
|
194
|
-
return parsePhaseFromProse(value).phase;
|
|
212
|
+
return parsePhaseFromProse(value, convention).phase;
|
|
195
213
|
}
|
|
196
214
|
catch {
|
|
197
215
|
return null;
|
|
@@ -207,15 +225,16 @@ function currentPhaseFromState(statePath) {
|
|
|
207
225
|
* `**Phase N` anchor inside `PHASE_BULLET_RE` is the second layer of that
|
|
208
226
|
* defence — a plan bullet's text never starts with `**Phase`.
|
|
209
227
|
*/
|
|
210
|
-
function phasesFromChecklist(roadmapContent, cwd, statePath) {
|
|
228
|
+
function phasesFromChecklist(roadmapContent, cwd, statePath, convention) {
|
|
211
229
|
const { collectSection, isSentinelPhaseId } = owners();
|
|
212
230
|
const section = collectSection(roadmapContent, (h) => /^phases$/i.test(h.text.trim()));
|
|
213
231
|
if (!section)
|
|
214
232
|
return [];
|
|
215
|
-
const currentPhase = currentPhaseFromState(statePath);
|
|
233
|
+
const currentPhase = currentPhaseFromState(statePath, convention);
|
|
216
234
|
const phases = [];
|
|
217
235
|
let match;
|
|
218
|
-
const
|
|
236
|
+
const bulletRe = convention === MILESTONE_PREFIXED ? PHASE_BULLET_MILESTONE_RE : PHASE_BULLET_RE;
|
|
237
|
+
const pattern = new RegExp(bulletRe.source, bulletRe.flags);
|
|
219
238
|
while ((match = pattern.exec(section.body)) !== null) {
|
|
220
239
|
const checked = match[1] === 'x' || match[1] === 'X';
|
|
221
240
|
const number = match[2];
|
|
@@ -237,10 +256,11 @@ function buildPhases(cwd, paths) {
|
|
|
237
256
|
return [];
|
|
238
257
|
if (content.charCodeAt(0) === 0xfeff)
|
|
239
258
|
content = content.slice(1); // strip leading BOM
|
|
259
|
+
const convention = phaseIdConventionOf(cwd);
|
|
240
260
|
const table = locateProgressTable(content);
|
|
241
261
|
if (table)
|
|
242
|
-
return phasesFromProgressTable(table);
|
|
243
|
-
return phasesFromChecklist(content, cwd, paths.state);
|
|
262
|
+
return phasesFromProgressTable(table, convention);
|
|
263
|
+
return phasesFromChecklist(content, cwd, paths.state, convention);
|
|
244
264
|
}
|
|
245
265
|
catch {
|
|
246
266
|
return [];
|
|
@@ -360,6 +360,8 @@ const STATE_SIBLING_FIELD_LINE_RE = /^\*{0,2}[A-Za-z][A-Za-z0-9 _-]*\*{0,2}:{1,2
|
|
|
360
360
|
* cannot wrap), or is followed by end-of-file, a blank line, Markdown
|
|
361
361
|
* structure, or a sibling field.
|
|
362
362
|
*/
|
|
363
|
+
/** The line terminators the STATE.md readers (JS `m`-flag `^`/`$`) treat as a line boundary. */
|
|
364
|
+
const LINE_SEPARATOR_RE = /[\n\u2028\u2029]/;
|
|
363
365
|
function stateFieldContinuation(content, fieldName) {
|
|
364
366
|
const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
|
|
365
367
|
// Same two single-line grammars stateExtractField uses, in the same order, so
|
|
@@ -376,9 +378,11 @@ function stateFieldContinuation(content, fieldName) {
|
|
|
376
378
|
// before testing for the newline.
|
|
377
379
|
const afterValue = match.index + match[0].length;
|
|
378
380
|
const rest = content.slice(afterValue).replace(/^\r/, '');
|
|
379
|
-
|
|
381
|
+
// #5460: U+2028 / U+2029 are line starts to the `m`-flag readers (and stop `(.+)`), so they end
|
|
382
|
+
// the value's line exactly as `\n` does.
|
|
383
|
+
if (!/^[\n\u2028\u2029]/.test(rest))
|
|
380
384
|
return null; // end of file: nothing follows
|
|
381
|
-
const lines = rest.slice(1).split(
|
|
385
|
+
const lines = rest.slice(1).split(LINE_SEPARATOR_RE).map((line) => line.replace(/\r$/, ''));
|
|
382
386
|
const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
|
|
383
387
|
return continuation.length ? continuation.join(' ') : null;
|
|
384
388
|
}
|
|
@@ -414,10 +418,14 @@ function continuationLineCount(lines) {
|
|
|
414
418
|
*/
|
|
415
419
|
function fieldContinuationEnd(content, valueEnd) {
|
|
416
420
|
const tail = content.slice(valueEnd);
|
|
417
|
-
|
|
421
|
+
// #5460: the readers' `m`-flag `^` also starts a line after U+2028 / U+2029, so a continuation
|
|
422
|
+
// scan that split on `\n` alone swallowed `...tail<U+2028>Stopped at: x` as ONE continuation line
|
|
423
|
+
// and deleted the sibling field with it. Each separator is one UTF-16 unit, like `\n`, so the
|
|
424
|
+
// removal arithmetic below is unchanged.
|
|
425
|
+
const first = /^(?:\r?\n|[\u2028\u2029])/.exec(tail);
|
|
418
426
|
if (!first)
|
|
419
427
|
return valueEnd;
|
|
420
|
-
const rawLines = tail.slice(first[0].length).split(
|
|
428
|
+
const rawLines = tail.slice(first[0].length).split(LINE_SEPARATOR_RE);
|
|
421
429
|
const count = continuationLineCount(rawLines.map((line) => line.replace(/\r$/, '')));
|
|
422
430
|
if (count === 0)
|
|
423
431
|
return valueEnd;
|