@opengsd/gsd-core 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -149,6 +149,81 @@ function atomicWriteFileSync(target, data, options) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
|
+
// CommonJS package.json marker for staged .js hook scripts (#2717)
|
|
153
|
+
//
|
|
154
|
+
// Node resolves the nearest package.json walking up from a .js file. When a
|
|
155
|
+
// runtime's config root (e.g. ~/.cursor, ~/.codeium/windsurf, ~/.codex) — or any
|
|
156
|
+
// parent — declares {"type":"module"}, Node loads GSD's staged CommonJS hook
|
|
157
|
+
// scripts as ESM and every require() fails with "require is not defined",
|
|
158
|
+
// silently disabling that runtime's lifecycle hooks.
|
|
159
|
+
//
|
|
160
|
+
// installSharedHooksBundle writes this marker for the 12 runtimes that go
|
|
161
|
+
// through the shared hooks bundle, but cursor/windsurf (skipSharedHooksInstall)
|
|
162
|
+
// and codex (the !isCodex gate) stage their .js hooks via the dedicated paths
|
|
163
|
+
// below and never reached it. These helpers decouple the marker write from the
|
|
164
|
+
// shared bundle so any code path that stages .js hooks can ensure the marker
|
|
165
|
+
// lands in the SAME directory as the scripts (#2717).
|
|
166
|
+
//
|
|
167
|
+
// The marker content is byte-identical to installSharedHooksBundle's
|
|
168
|
+
// (bin/install.js installSharedHooksBundle): {"type":"commonjs"}\n.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** The exact marker content GSD writes, matching installSharedHooksBundle. */
|
|
171
|
+
const COMMONJS_MARKER_CONTENT = '{"type":"commonjs"}\n';
|
|
172
|
+
/**
|
|
173
|
+
* Ensure a `package.json` forcing CommonJS mode exists in `dir` (the directory
|
|
174
|
+
* holding GSD-staged `.js` hook scripts). Idempotent: a no-op if the marker is
|
|
175
|
+
* already present with GSD's content. Overwrites only when the file is absent
|
|
176
|
+
* or already carries GSD's exact marker — it never clobbers a distinct
|
|
177
|
+
* user-authored package.json (it leaves such a file in place; the user owns it).
|
|
178
|
+
*
|
|
179
|
+
* @param dir - absolute path to the directory holding the staged .js hooks
|
|
180
|
+
* @returns `true` if the marker is present after the call (written or already there)
|
|
181
|
+
*/
|
|
182
|
+
function ensureCommonJsMarker(dir) {
|
|
183
|
+
const markerPath = node_path_1.default.join(dir, 'package.json');
|
|
184
|
+
try {
|
|
185
|
+
if (node_fs_1.default.existsSync(markerPath)) {
|
|
186
|
+
const existing = node_fs_1.default.readFileSync(markerPath, 'utf8');
|
|
187
|
+
// Already GSD's marker (tolerant of trailing-whitespace variants) — done.
|
|
188
|
+
if (existing.trim() === '{"type":"commonjs"}')
|
|
189
|
+
return true;
|
|
190
|
+
// A distinct package.json the user owns — do NOT clobber. The hook will
|
|
191
|
+
// load as whatever type the user declared; that is the user's choice.
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
node_fs_1.default.writeFileSync(markerPath, COMMONJS_MARKER_CONTENT);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Best-effort: a marker write failure must not fail the whole install.
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove the CommonJS marker from `dir` on uninstall — but ONLY if it carries
|
|
204
|
+
* GSD's exact marker content. A user-authored package.json is never deleted.
|
|
205
|
+
* Mirrors the kimi uninstall guard in bin/install.js.
|
|
206
|
+
*
|
|
207
|
+
* @param dir - absolute path to the directory that held the staged .js hooks
|
|
208
|
+
* @returns `true` if a GSD-owned marker was removed
|
|
209
|
+
*/
|
|
210
|
+
function removeCommonJsMarkerIfGsdOwned(dir) {
|
|
211
|
+
const markerPath = node_path_1.default.join(dir, 'package.json');
|
|
212
|
+
try {
|
|
213
|
+
if (!node_fs_1.default.existsSync(markerPath))
|
|
214
|
+
return false;
|
|
215
|
+
const content = node_fs_1.default.readFileSync(markerPath, 'utf8').trim();
|
|
216
|
+
if (content === '{"type":"commonjs"}') {
|
|
217
|
+
node_fs_1.default.unlinkSync(markerPath);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
152
227
|
// parseTomlValue + findMultilineBasicStringClose
|
|
153
228
|
// (needed by rewriteLegacyCodexHookBlock — pure TOML helpers, no state)
|
|
154
229
|
// ---------------------------------------------------------------------------
|
|
@@ -309,6 +384,22 @@ function normalizeNodePath(execPath, opts) {
|
|
|
309
384
|
if (existsSync(shim))
|
|
310
385
|
return shim;
|
|
311
386
|
}
|
|
387
|
+
// volta pins a concrete node image at <VOLTA_HOME>/tools/image/node/<ver>/bin/node
|
|
388
|
+
// (Windows: <VOLTA_HOME>/tools/image/node/<ver>/node.exe — volta's own layout
|
|
389
|
+
// puts node.exe at the image root, no bin/). `volta uninstall node@<ver>` prunes
|
|
390
|
+
// that image, so a baked hook command 404s — the same ephemeral-path failure
|
|
391
|
+
// #977 fixed for fnm and #1619 for mise. The stable alias is the shim
|
|
392
|
+
// <VOLTA_HOME>/bin/node, a symlink to volta-shim that always resolves to the
|
|
393
|
+
// active pin. Derive <VOLTA_HOME> from execPath rather than the env so a custom
|
|
394
|
+
// VOLTA_HOME and the Windows %LOCALAPPDATA%\Volta default both work (#2185's
|
|
395
|
+
// reasoning), and only rewrite when the shim exists — otherwise fall back to
|
|
396
|
+
// the raw execPath unchanged.
|
|
397
|
+
const voltaMatch = normalizedForMatch.match(/^(.*)\/tools\/image\/node\/[^/]+\/(?:bin\/)?node(\.exe)?$/);
|
|
398
|
+
if (voltaMatch) {
|
|
399
|
+
const shim = `${voltaMatch[1]}/bin/node${voltaMatch[2] || ''}`;
|
|
400
|
+
if (existsSync(shim))
|
|
401
|
+
return shim;
|
|
402
|
+
}
|
|
312
403
|
return execPath;
|
|
313
404
|
}
|
|
314
405
|
function resolveNodeRunner(opts) {
|
|
@@ -940,6 +1031,48 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
940
1031
|
installedScripts.add(script);
|
|
941
1032
|
}
|
|
942
1033
|
}
|
|
1034
|
+
// Stage the hooks/lib/ helpers the staged scripts require (#2587). Cursor sets
|
|
1035
|
+
// hostBehaviors.skipSharedHooksInstall, so it never reaches the installer's
|
|
1036
|
+
// bulk hooks/lib copy — without this, a script requiring './lib/…' would throw
|
|
1037
|
+
// MODULE_NOT_FOUND at load, BEFORE its own try/catch, and wedge every Cursor
|
|
1038
|
+
// session on the one runtime these hooks exist for. Driven off what the staged
|
|
1039
|
+
// scripts actually require so a future helper cannot be silently omitted.
|
|
1040
|
+
const requiredLibFiles = new Set();
|
|
1041
|
+
for (const script of installedScripts) {
|
|
1042
|
+
const staged = node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8');
|
|
1043
|
+
// Tolerant of interior whitespace and either quote style: a hook author
|
|
1044
|
+
// writing `require( "./lib/x.js" )` must still get its helper staged, since
|
|
1045
|
+
// a miss here surfaces as MODULE_NOT_FOUND at hook load, not at install.
|
|
1046
|
+
const re = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1047
|
+
let m;
|
|
1048
|
+
while ((m = re.exec(staged)) !== null)
|
|
1049
|
+
requiredLibFiles.add(m[1]);
|
|
1050
|
+
}
|
|
1051
|
+
if (requiredLibFiles.size > 0) {
|
|
1052
|
+
const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
|
|
1053
|
+
const destLibDir = node_path_1.default.join(hooksDir, 'lib');
|
|
1054
|
+
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1055
|
+
for (const libFile of requiredLibFiles) {
|
|
1056
|
+
const libSrc = node_path_1.default.join(srcLibDir, libFile);
|
|
1057
|
+
if (!node_fs_1.default.existsSync(libSrc)) {
|
|
1058
|
+
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1059
|
+
// require() throws before their own try/catch, wedging every session —
|
|
1060
|
+
// and the install would still exit 0, so nobody would know until a user
|
|
1061
|
+
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1062
|
+
throw new Error(`hooks/lib/${libFile} is required by a staged Cursor hook but is missing from ${srcLibDir}. `
|
|
1063
|
+
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1064
|
+
}
|
|
1065
|
+
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1066
|
+
libContent = libContent.replace(/gsd:/gi, 'gsd-');
|
|
1067
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1071
|
+
// scripts. Cursor sets skipSharedHooksInstall, so it never reaches
|
|
1072
|
+
// installSharedHooksBundle (the only other writer of this marker); without
|
|
1073
|
+
// it, a ~/.cursor/package.json declaring {"type":"module"} makes Node load
|
|
1074
|
+
// these require()-using scripts as ESM and every Cursor hook fails silently.
|
|
1075
|
+
ensureCommonJsMarker(hooksDir);
|
|
943
1076
|
const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform };
|
|
944
1077
|
const commands = {};
|
|
945
1078
|
for (const ev of events) {
|
|
@@ -971,6 +1104,14 @@ function removeCursorHooksJson(targetDir) {
|
|
|
971
1104
|
const hasAnyEvents = Object.keys(hookTable).some((k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0);
|
|
972
1105
|
if (!hasAnyEvents) {
|
|
973
1106
|
node_fs_1.default.unlinkSync(hooksJsonPath);
|
|
1107
|
+
// #2717: also remove the CommonJS marker GSD wrote into hooks/ — but
|
|
1108
|
+
// only if it still carries GSD's exact content (a user-authored
|
|
1109
|
+
// package.json is never deleted). Best-effort: a failure here must not
|
|
1110
|
+
// mask the hooks.json removal above.
|
|
1111
|
+
try {
|
|
1112
|
+
removeCommonJsMarkerIfGsdOwned(node_path_1.default.join(targetDir, 'hooks'));
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* leave it */ }
|
|
974
1115
|
return { changed: true };
|
|
975
1116
|
}
|
|
976
1117
|
}
|
|
@@ -1120,6 +1261,12 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1120
1261
|
installedScripts.add(script);
|
|
1121
1262
|
}
|
|
1122
1263
|
}
|
|
1264
|
+
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1265
|
+
// scripts. Windsurf sets skipSharedHooksInstall, so it never reaches
|
|
1266
|
+
// installSharedHooksBundle (the only other writer of this marker); without
|
|
1267
|
+
// it, a config-root package.json declaring {"type":"module"} makes Node load
|
|
1268
|
+
// these require()-using scripts as ESM and the Windsurf hooks fail silently.
|
|
1269
|
+
ensureCommonJsMarker(hooksDir);
|
|
1123
1270
|
const hookOpts = { runtime: 'windsurf', platform: opts.platform || process.platform };
|
|
1124
1271
|
const commands = {};
|
|
1125
1272
|
for (const ev of WINDSURF_HOOK_EVENTS) {
|
|
@@ -1158,6 +1305,13 @@ function removeWindsurfHooksJson(targetDir) {
|
|
|
1158
1305
|
const hasAnyEvents = Object.keys(hookTable).some((k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0);
|
|
1159
1306
|
if (!hasAnyEvents) {
|
|
1160
1307
|
node_fs_1.default.unlinkSync(hooksJsonPath);
|
|
1308
|
+
// #2717: also remove the CommonJS marker GSD wrote into hooks/ — but
|
|
1309
|
+
// only if it still carries GSD's exact content (a user-authored
|
|
1310
|
+
// package.json is never deleted). Best-effort.
|
|
1311
|
+
try {
|
|
1312
|
+
removeCommonJsMarkerIfGsdOwned(node_path_1.default.join(targetDir, 'hooks'));
|
|
1313
|
+
}
|
|
1314
|
+
catch { /* leave it */ }
|
|
1161
1315
|
return { changed: true };
|
|
1162
1316
|
}
|
|
1163
1317
|
}
|
|
@@ -1947,6 +2101,8 @@ module.exports = {
|
|
|
1947
2101
|
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
1948
2102
|
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
1949
2103
|
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
2104
|
+
ensureCommonJsMarker,
|
|
2105
|
+
removeCommonJsMarkerIfGsdOwned,
|
|
1950
2106
|
GSD_WINDSURF_HOOK_MARKER,
|
|
1951
2107
|
// Copilot
|
|
1952
2108
|
buildCopilotHookConfig,
|
|
@@ -40,6 +40,7 @@ const FALLBACK_ALIASES = {
|
|
|
40
40
|
qwen: ['qwen', 'qwen-code', 'qwen-cli'],
|
|
41
41
|
hermes: ['hermes', 'hermes-agent', 'hermes-cli'],
|
|
42
42
|
kimi: ['kimi'],
|
|
43
|
+
'kimi-code': ['kimi-code', 'kimicode', 'kimi_code'],
|
|
43
44
|
codebuddy: ['codebuddy', 'codebuddy-cli'],
|
|
44
45
|
cline: ['cline', 'cline-cli'],
|
|
45
46
|
};
|
|
@@ -248,6 +249,7 @@ const RUNTIME_LABELS = {
|
|
|
248
249
|
qwen: 'Qwen Code',
|
|
249
250
|
hermes: 'Hermes Agent',
|
|
250
251
|
kimi: 'Kimi CLI',
|
|
252
|
+
'kimi-code': 'Kimi Code',
|
|
251
253
|
codebuddy: 'CodeBuddy',
|
|
252
254
|
cline: 'Cline',
|
|
253
255
|
zcode: 'ZCode',
|
|
@@ -303,6 +305,7 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS = {
|
|
|
303
305
|
codebuddy: "'.codebuddy'",
|
|
304
306
|
cline: "'.cline'",
|
|
305
307
|
kimi: "'.config', 'agents'",
|
|
308
|
+
'kimi-code': "'.kimi-code'",
|
|
306
309
|
zcode: "'.zcode'",
|
|
307
310
|
// pi's global config home is ~/.pi/agent (configHome: dot-home-nested,
|
|
308
311
|
// parent '.pi', name 'agent' — capabilities/pi/capability.json), matching
|
|
@@ -337,8 +340,18 @@ function getGlobalConfigHomeFragment(runtime) {
|
|
|
337
340
|
// folds the shared-hooks-install skip).
|
|
338
341
|
const RUNTIME_FLAG_IDS = Object.freeze([
|
|
339
342
|
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
|
|
340
|
-
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode', 'pi',
|
|
343
|
+
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'kimi-code', 'zcode', 'pi',
|
|
341
344
|
]);
|
|
345
|
+
/**
|
|
346
|
+
* Convert a runtime id (kebab-case, e.g. 'kimi-code') to its `is<Foo>` flag
|
|
347
|
+
* name in PascalCase (e.g. 'isKimiCode'). The first letter is capitalised and
|
|
348
|
+
* every `-[a-z]` boundary is folded to its uppercase twin. Single-word ids
|
|
349
|
+
* (the prior 16: opencode, kilo, codex, …) are unaffected — only hyphenated
|
|
350
|
+
* ids like 'kimi-code' (#2454) hit the folding branch.
|
|
351
|
+
*/
|
|
352
|
+
function runtimeIdToFlagName(id) {
|
|
353
|
+
return 'is' + id.charAt(0).toUpperCase() + id.slice(1).replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
354
|
+
}
|
|
342
355
|
/**
|
|
343
356
|
* Return a frozen map of `is<Runtime>` boolean predicates for the given runtime
|
|
344
357
|
* id (e.g. `flags.isOpencode`). Collapses the four duplicated `const isX =
|
|
@@ -349,7 +362,7 @@ const RUNTIME_FLAG_IDS = Object.freeze([
|
|
|
349
362
|
function runtimeFlags(runtime) {
|
|
350
363
|
const flags = {};
|
|
351
364
|
for (const id of RUNTIME_FLAG_IDS) {
|
|
352
|
-
flags[
|
|
365
|
+
flags[runtimeIdToFlagName(id)] = runtime === id;
|
|
353
366
|
}
|
|
354
367
|
return Object.freeze(flags);
|
|
355
368
|
}
|
|
@@ -52,6 +52,9 @@ const { planningPaths } = planningWorkspace;
|
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
53
|
const frontmatter = require("./frontmatter.cjs");
|
|
54
54
|
const { extractFrontmatter } = frontmatter;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
|
|
56
|
+
const phaseLifecycle = require("./phase-lifecycle.cjs");
|
|
57
|
+
const { deriveProgressFromRoadmap } = phaseLifecycle;
|
|
55
58
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
59
|
const stateDocument = require("./state-document.cjs");
|
|
57
60
|
const { stateExtractField } = stateDocument;
|
|
@@ -221,7 +224,7 @@ function readStateFile(statePath) {
|
|
|
221
224
|
catch {
|
|
222
225
|
return null;
|
|
223
226
|
}
|
|
224
|
-
const fm = extractFrontmatter(content);
|
|
227
|
+
const fm = extractFrontmatter(content, statePath);
|
|
225
228
|
const body = content.replace(/^---[\s\S]*?---\s*/, '');
|
|
226
229
|
return { fm, body };
|
|
227
230
|
}
|
|
@@ -245,6 +248,8 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
245
248
|
has_git: git.has_git,
|
|
246
249
|
verify_failed: false,
|
|
247
250
|
stale_activity: false,
|
|
251
|
+
roadmap_total_phases: null,
|
|
252
|
+
roadmap_completed_phases: null,
|
|
248
253
|
};
|
|
249
254
|
if (!hasPlanning)
|
|
250
255
|
return empty;
|
|
@@ -291,6 +296,24 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
291
296
|
// STATUS: marker on the current phase's summary/verify artifact.
|
|
292
297
|
const verifyFailed = /\bverify-fail(ed)?|verification-fail|uat-fail\b/i.test(statusRaw || '') ||
|
|
293
298
|
detectVerifyFailed(cwd, currentPhaseRaw);
|
|
299
|
+
// #2427: derive global phase counts from ROADMAP.md's Progress table. These
|
|
300
|
+
// are preferred over STATE.md's cached milestone-scoped `total_phases` (which
|
|
301
|
+
// goes stale when phases are appended after a milestone switch) for the
|
|
302
|
+
// completion check. Null when ROADMAP.md is absent or has no parseable
|
|
303
|
+
// Progress table — isComplete falls back to the legacy comparison in that case.
|
|
304
|
+
let roadmapTotalPhases = null;
|
|
305
|
+
let roadmapCompletedPhases = null;
|
|
306
|
+
if (hasRoadmap) {
|
|
307
|
+
try {
|
|
308
|
+
const roadmapContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf8');
|
|
309
|
+
const derived = deriveProgressFromRoadmap(roadmapContent);
|
|
310
|
+
roadmapTotalPhases = derived.totalPhases;
|
|
311
|
+
roadmapCompletedPhases = derived.completedPhases;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
/* ROADMAP.md unreadable — leave null; isComplete falls back to legacy. */
|
|
315
|
+
}
|
|
316
|
+
}
|
|
294
317
|
return {
|
|
295
318
|
current_phase: parseIntOrNull(currentPhaseRaw),
|
|
296
319
|
total_phases: parseIntOrNull(totalPhasesRaw),
|
|
@@ -305,14 +328,56 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
305
328
|
has_git: git.has_git,
|
|
306
329
|
verify_failed: verifyFailed,
|
|
307
330
|
stale_activity: staleActivity,
|
|
331
|
+
roadmap_total_phases: roadmapTotalPhases,
|
|
332
|
+
roadmap_completed_phases: roadmapCompletedPhases,
|
|
308
333
|
};
|
|
309
334
|
}
|
|
310
335
|
// ─── Situation classification ─────────────────────────────────────────────────
|
|
311
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* True when the workflow has fully completed all phases.
|
|
338
|
+
*
|
|
339
|
+
* #2427: completion is grounded in ROADMAP.md's Progress table (global,
|
|
340
|
+
* authoritative, never stale) when available, with a legacy fallback to
|
|
341
|
+
* STATE.md's cached `total_phases` when the roadmap has no parseable Progress
|
|
342
|
+
* table (e.g. a fresh project or a non-standard roadmap layout). The status
|
|
343
|
+
* regex was tightened to require milestone-level completion language
|
|
344
|
+
* (`milestone complete` / `all phases complete` / `complete(d)`) and no longer
|
|
345
|
+
* matches per-phase messages like "Phase X shipped — PR #N" that falsely
|
|
346
|
+
* satisfied the pre-fix alternation (`\bcomplete(d)?|done|shipped\b`).
|
|
347
|
+
*/
|
|
312
348
|
function isComplete(s) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
// Prefer ROADMAP-derived counts (global, authoritative) over STATE.md's
|
|
350
|
+
// cached milestone-scoped total_phases (stale-prone). Fall back to legacy
|
|
351
|
+
// when the roadmap has no Progress table.
|
|
352
|
+
if (s.roadmap_total_phases !== null && s.roadmap_completed_phases !== null) {
|
|
353
|
+
if (s.roadmap_total_phases === 0)
|
|
354
|
+
return false;
|
|
355
|
+
if (s.roadmap_completed_phases < s.roadmap_total_phases)
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Legacy path: STATE.md comparison. Still subject to the two-scale bug,
|
|
360
|
+
// but only fires when ROADMAP.md is absent or has no Progress table.
|
|
361
|
+
if (s.total_phases === null || s.current_phase === null)
|
|
362
|
+
return false;
|
|
363
|
+
if (s.current_phase < s.total_phases)
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
// Status regex: require milestone-level completion language. The pre-fix
|
|
367
|
+
// regex matched any "shipped" / "done" substring (per-phase language).
|
|
368
|
+
// Tightened to match:
|
|
369
|
+
// - "milestone complete" (ADR-2207 terminal status — usually written as
|
|
370
|
+
// "<version> milestone complete", e.g. "v1.0 milestone complete"; the
|
|
371
|
+
// substring match handles both forms)
|
|
372
|
+
// - "all phases complete" (ADR-2207 intermediate terminal)
|
|
373
|
+
// - "complete" / "completed" (legacy short form — STATE.md milestone
|
|
374
|
+
// status is a single value, not a per-phase log)
|
|
375
|
+
// Intentionally does NOT match "done" alone even though normalizeStateStatus
|
|
376
|
+
// (state-document.cts) treats "done" as "completed" — in the milestone
|
|
377
|
+
// status field, "done" is per-phase noise (e.g. "Phase X done"), not a
|
|
378
|
+
// milestone-completion signal. Mirrors workstream-inventory-builder.cts's
|
|
379
|
+
// terminal pattern \bmilestone\s+complete\b.
|
|
380
|
+
return /\b(milestone\s+complete|all\s+phases\s+complete|complete(d)?)\b/i.test(s.status);
|
|
316
381
|
}
|
|
317
382
|
/** Idle-stranded: clean tree, committed work not shipped, optionally stale. */
|
|
318
383
|
function isIdleStranded(s) {
|
|
@@ -18,6 +18,7 @@ exports.shouldPreserveExistingProgress = shouldPreserveExistingProgress;
|
|
|
18
18
|
exports.normalizeProgressNumbers = normalizeProgressNumbers;
|
|
19
19
|
exports.isStateTemplateDefault = isStateTemplateDefault;
|
|
20
20
|
exports.stateReplaceFieldIfTemplate = stateReplaceFieldIfTemplate;
|
|
21
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
21
22
|
// Internal helpers
|
|
22
23
|
function escapeRegex(str) {
|
|
23
24
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -40,22 +41,167 @@ function isTableSeparatorRow(firstCell) {
|
|
|
40
41
|
// A separator cell contains only dashes, colons (alignment hints), and whitespace.
|
|
41
42
|
return /^[\s\-:]+$/.test(firstCell.trim());
|
|
42
43
|
}
|
|
44
|
+
function countLeading(str) {
|
|
45
|
+
const match = /^[ \t]*/.exec(str);
|
|
46
|
+
return match ? match[0].length : 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Canonicalize one UTF-16 code unit per the ECMAScript non-unicode
|
|
50
|
+
* `Canonicalize` abstract operation, which governs how a case-insensitive
|
|
51
|
+
* (`/i`, no `u` flag) RegExp compares characters: take `ch.toUpperCase()`.
|
|
52
|
+
* The uppercasing is REJECTED (the original character is kept as-is) in
|
|
53
|
+
* either of two cases: (1) `ch.toUpperCase()` does not produce exactly one
|
|
54
|
+
* character (e.g. "ß" -> "SS" — a multi-character case-fold can never be a
|
|
55
|
+
* per-character regex match, so Canonicalize leaves it alone), or (2) it
|
|
56
|
+
* produces exactly one character but the original character's code point is
|
|
57
|
+
* >= 128 while the uppercased character's code point is < 128 (this is what
|
|
58
|
+
* stops a non-ASCII character from folding onto an ASCII one under `/i` —
|
|
59
|
+
* e.g. KELVIN SIGN U+212A uppercases to ASCII "K" (U+004B), so this rule
|
|
60
|
+
* rejects the fold and keeps U+212A, meaning `/k/i`/`/K/i` do NOT match
|
|
61
|
+
* U+212A). Otherwise, the uppercased character is used. Plain
|
|
62
|
+
* `.toLowerCase()`/`.toUpperCase()` folds both of these cases, which is
|
|
63
|
+
* exactly why they diverge from real regex `/i` semantics.
|
|
64
|
+
*/
|
|
65
|
+
function canonicalizeCharForCaselessCompare(ch) {
|
|
66
|
+
const upper = ch.toUpperCase();
|
|
67
|
+
if (upper.length !== 1) {
|
|
68
|
+
return ch;
|
|
69
|
+
}
|
|
70
|
+
if (ch.charCodeAt(0) >= 128 && upper.charCodeAt(0) < 128) {
|
|
71
|
+
return ch;
|
|
72
|
+
}
|
|
73
|
+
return upper;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Canonicalize a whole string, one UTF-16 code unit at a time, per the
|
|
77
|
+
* ECMAScript non-unicode `Canonicalize` rule (see
|
|
78
|
+
* canonicalizeCharForCaselessCompare) so that two strings compare equal
|
|
79
|
+
* under this function iff a non-`u`-flag `/i` RegExp would treat them as
|
|
80
|
+
* the same literal text. This is the correct replacement for
|
|
81
|
+
* `.toLowerCase()` when replicating a non-`u` `/i` regex: `.toLowerCase()`
|
|
82
|
+
* folds some non-ASCII characters (e.g. KELVIN SIGN U+212A) onto their
|
|
83
|
+
* ASCII counterparts, which real `/i` regex semantics do not. Iteration is
|
|
84
|
+
* by UTF-16 code unit (not code point) to match how a non-`u` regex engine
|
|
85
|
+
* itself operates on surrogate halves individually.
|
|
86
|
+
*/
|
|
87
|
+
function canonicalizeForCaselessCompare(str) {
|
|
88
|
+
let result = '';
|
|
89
|
+
for (let i = 0; i < str.length; i++) {
|
|
90
|
+
result += canonicalizeCharForCaselessCompare(str[i]);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
43
94
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
95
|
+
* Return true when the caller's raw (untrimmed) `fieldName` may be considered
|
|
96
|
+
* to match a row's raw (untrimmed) field cell text. Faithfully replicates the
|
|
97
|
+
* backtracking of the regex this function replaced: `^(\|[ \t]*)(FieldName)
|
|
98
|
+
* ([ \t]*\|...)`. Group 1 (`\|[ \t]*`, greedy but backtrackable) can hand any
|
|
99
|
+
* PREFIX of the cell's leading `[ \t]` run over to group 2 (the literal,
|
|
100
|
+
* case-insensitive `fieldName` text) — so `fieldName` is tried at every offset
|
|
101
|
+
* `j` from 0 up to the length of that leading run. For a given `j` to be a
|
|
102
|
+
* genuine match, two things must hold: `rawCell.slice(j, j + fieldName.length)`
|
|
103
|
+
* must equal `fieldName` case-insensitively (group 2), AND everything left
|
|
104
|
+
* over after it — `rawCell.slice(j + fieldName.length)` — must be entirely
|
|
105
|
+
* `[ \t]` characters, because group 3 (`[ \t]*\|`) must consume that leftover
|
|
106
|
+
* as whitespace before it can reach the delimiter pipe.
|
|
47
107
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
108
|
+
* A simple count-of-leading/trailing-whitespace comparison is NOT equivalent:
|
|
109
|
+
* it ignores that group 2 is a literal-character match, not a whitespace-
|
|
110
|
+
* class match, so it can produce false positives whenever `fieldName`'s own
|
|
111
|
+
* padding is a different run of `[ \t]` characters than the cell's (e.g.
|
|
112
|
+
* `fieldName` padded with spaces against a cell padded with tabs) — caught by
|
|
113
|
+
* differential fuzzing against the regex this replaces.
|
|
53
114
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
115
|
+
* The case-insensitive comparison itself is done via
|
|
116
|
+
* canonicalizeForCaselessCompare, NOT `.toLowerCase()`: the replaced regex
|
|
117
|
+
* used `/i` WITHOUT the `u` flag, whose case-folding is the ECMAScript
|
|
118
|
+
* non-unicode `Canonicalize` operation. `.toLowerCase()` folds some non-ASCII
|
|
119
|
+
* characters onto ASCII ones (e.g. KELVIN SIGN U+212A -> "k") that `/i`
|
|
120
|
+
* (no `u`) does NOT fold, so `.toLowerCase()` alone would NOT faithfully
|
|
121
|
+
* replicate the old regex's semantics; canonicalizeForCaselessCompare does.
|
|
56
122
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
123
|
+
function fieldNameMatchesRawCell(fieldName, rawCell) {
|
|
124
|
+
const n = fieldName.length;
|
|
125
|
+
const cellLength = rawCell.length;
|
|
126
|
+
if (n > cellLength)
|
|
127
|
+
return false;
|
|
128
|
+
const leadingRun = countLeading(rawCell);
|
|
129
|
+
const maxOffset = Math.min(leadingRun, cellLength - n);
|
|
130
|
+
const canonicalFieldName = canonicalizeForCaselessCompare(fieldName);
|
|
131
|
+
for (let j = 0; j <= maxOffset; j++) {
|
|
132
|
+
if (canonicalizeForCaselessCompare(rawCell.slice(j, j + n)) !== canonicalFieldName)
|
|
133
|
+
continue;
|
|
134
|
+
if (/^[ \t]*$/.test(rawCell.slice(j + n)))
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Locate the value cell of a pipe-table row `| FieldName | value |` for the
|
|
141
|
+
* given field name, by scanning `content` line by line (no whole-document
|
|
142
|
+
* regex). Only a strict two-column row (exactly 3 `|` chars, starting the
|
|
143
|
+
* line, ending the line after trailing space/tab is stripped) is considered;
|
|
144
|
+
* this is what makes a 3-column row or an unescaped-pipe-bearing value cell
|
|
145
|
+
* fail to match, mirroring the previous regex's behaviour. Separator rows
|
|
146
|
+
* (`| --- | --- |`) are skipped, not matched. The match is case-insensitive.
|
|
147
|
+
* A line terminator is `\r\n`, a lone `\r`, or a lone `\n` — matching the `m`
|
|
148
|
+
* flag semantics of the regex this function replaced. Returns the byte range
|
|
149
|
+
* of the value cell (after trimming surrounding space/tab) so the caller can
|
|
150
|
+
* splice it directly.
|
|
151
|
+
*/
|
|
152
|
+
function locateFieldRow(content, fieldName) {
|
|
153
|
+
let lineStart = 0;
|
|
154
|
+
while (lineStart <= content.length) {
|
|
155
|
+
// A line terminator is `\r\n`, a lone `\r`, or a lone `\n` (JS treats a
|
|
156
|
+
// bare `\r` as a line terminator too — the regex this replaced used the
|
|
157
|
+
// `m` flag, which honors all three). Scan for whichever of `\r`/`\n`
|
|
158
|
+
// occurs first; if it's `\r` immediately followed by `\n`, the terminator
|
|
159
|
+
// is 2 chars wide, otherwise 1.
|
|
160
|
+
let terminatorIndex = -1;
|
|
161
|
+
let terminatorLength = 0;
|
|
162
|
+
for (let i = lineStart; i < content.length; i++) {
|
|
163
|
+
const ch = content[i];
|
|
164
|
+
if (ch === '\n') {
|
|
165
|
+
terminatorIndex = i;
|
|
166
|
+
terminatorLength = 1;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (ch === '\r') {
|
|
170
|
+
terminatorIndex = i;
|
|
171
|
+
terminatorLength = content[i + 1] === '\n' ? 2 : 1;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const lineEnd = terminatorIndex === -1 ? content.length : terminatorIndex;
|
|
176
|
+
const line = content.slice(lineStart, lineEnd);
|
|
177
|
+
if (line.startsWith('|')) {
|
|
178
|
+
const pipeCount = (line.match(/\|/g) || []).length;
|
|
179
|
+
const trimmedEnd = line.replace(/[ \t]+$/, '');
|
|
180
|
+
if (pipeCount === 3 && trimmedEnd.endsWith('|')) {
|
|
181
|
+
const cells = (0, markdown_table_cjs_1.splitTableRow)(line);
|
|
182
|
+
if (cells.length === 2 && !isTableSeparatorRow(cells[0])) {
|
|
183
|
+
// Line has exactly 3 pipes (enforced above): opening pipe, the
|
|
184
|
+
// field/value separator pipe, and the row-closing pipe.
|
|
185
|
+
const fieldValueSeparatorPipe = line.indexOf('|', line.indexOf('|') + 1);
|
|
186
|
+
const rawCell = line.slice(1, fieldValueSeparatorPipe);
|
|
187
|
+
if (fieldNameMatchesRawCell(fieldName, rawCell)) {
|
|
188
|
+
const rowClosingPipe = line.indexOf('|', fieldValueSeparatorPipe + 1);
|
|
189
|
+
let valueStart = lineStart + fieldValueSeparatorPipe + 1;
|
|
190
|
+
while (content[valueStart] === ' ' || content[valueStart] === '\t')
|
|
191
|
+
valueStart++;
|
|
192
|
+
let valueEnd = lineStart + rowClosingPipe;
|
|
193
|
+
while (valueEnd - 1 >= valueStart && (content[valueEnd - 1] === ' ' || content[valueEnd - 1] === '\t'))
|
|
194
|
+
valueEnd--;
|
|
195
|
+
return { valueStart, valueEnd, rawValue: content.slice(valueStart, valueEnd) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (terminatorIndex === -1)
|
|
201
|
+
break;
|
|
202
|
+
lineStart = terminatorIndex + terminatorLength;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
59
205
|
}
|
|
60
206
|
function stateExtractField(content, fieldName) {
|
|
61
207
|
const escaped = escapeRegex(fieldName);
|
|
@@ -71,9 +217,9 @@ function stateExtractField(content, fieldName) {
|
|
|
71
217
|
return plainMatch[1].trim();
|
|
72
218
|
// Pipe-table format: | FieldName | value |
|
|
73
219
|
// (Separator rows such as `| --- | --- |` are excluded.)
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
220
|
+
const hit = locateFieldRow(content, fieldName);
|
|
221
|
+
if (hit)
|
|
222
|
+
return hit.rawValue.trim();
|
|
77
223
|
return null;
|
|
78
224
|
}
|
|
79
225
|
function stateReplaceField(content, fieldName, newValue) {
|
|
@@ -90,11 +236,9 @@ function stateReplaceField(content, fieldName, newValue) {
|
|
|
90
236
|
}
|
|
91
237
|
// Pipe-table format: | FieldName | value |
|
|
92
238
|
// Preserve the surrounding pipe/whitespace structure; only swap the value cell.
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Reconstruct the row, preserving the original surrounding whitespace/pipes.
|
|
97
|
-
return content.replace(tblPat, (_m, leadPipe, fieldCell, midPipe, _oldVal, trailPipe) => `${leadPipe}${fieldCell}${midPipe}${newValue}${trailPipe}`);
|
|
239
|
+
const hit = locateFieldRow(content, fieldName);
|
|
240
|
+
if (hit) {
|
|
241
|
+
return content.slice(0, hit.valueStart) + newValue + content.slice(hit.valueEnd);
|
|
98
242
|
}
|
|
99
243
|
return null;
|
|
100
244
|
}
|
|
@@ -153,11 +297,14 @@ function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
|
|
153
297
|
return false;
|
|
154
298
|
const existing = existingProgress;
|
|
155
299
|
const derived = derivedProgress;
|
|
156
|
-
// total_phases
|
|
157
|
-
// take the freshly derived value so
|
|
158
|
-
//
|
|
300
|
+
// total_phases (#1446) and total_plans (#2440) are intentionally excluded
|
|
301
|
+
// from the ratchet: both must always take the freshly derived value so they
|
|
302
|
+
// can correct in BOTH directions. total_plans legitimately moves up (a new
|
|
303
|
+
// phase adds plans) and down (milestone reorganization removes phases).
|
|
304
|
+
// Ratcheting it freezes stale values. Only completed_phases and
|
|
305
|
+
// completed_plans keep ratchet behaviour — they are monotonic (once a
|
|
306
|
+
// phase/plan is complete, it stays complete).
|
|
159
307
|
return (existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
|
|
160
|
-
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
|
|
161
308
|
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
|
|
162
309
|
}
|
|
163
310
|
function normalizeProgressNumbers(progress) {
|