@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -89,7 +89,7 @@ function coerceTruthToString(t) {
|
|
|
89
89
|
return '';
|
|
90
90
|
}
|
|
91
91
|
// ─── countPhasePlansAndSummaries ──────────────────────────────────────────────
|
|
92
|
-
function countPhasePlansAndSummaries(phaseDir) {
|
|
92
|
+
function countPhasePlansAndSummaries(phaseDir, convention) {
|
|
93
93
|
const { planCount, summaryCount } = scanPhasePlans(phaseDir);
|
|
94
94
|
// hasContext and hasResearch are not plan-scan concerns — read the directory
|
|
95
95
|
// once and share the listing for all non-plan metadata that cmdRoadmapAnalyze needs.
|
|
@@ -112,7 +112,9 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
112
112
|
// summaryCount above stay on scanPhasePlans's own unscoped listing since a
|
|
113
113
|
// PLAN/SUMMARY leading number is a plan sequence number, not a phase
|
|
114
114
|
// number. Mirrors core-utils.cts's getPhaseFileStats.
|
|
115
|
-
|
|
115
|
+
// #612: `convention` threaded from the one caller (which already threads it
|
|
116
|
+
// into matchPhaseDirs) so a bracket dir scopes by its real token.
|
|
117
|
+
const scopedFiles = scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir), convention);
|
|
116
118
|
return {
|
|
117
119
|
planCount,
|
|
118
120
|
summaryCount,
|
|
@@ -349,8 +351,17 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames, convention) {
|
|
|
349
351
|
// #3036: widen the id capture to accept non-numeric-leading ids (e.g. B7, P0.3-2)
|
|
350
352
|
// that get-phase/execute-phase already resolve. An optional leading letter prefix
|
|
351
353
|
// ([A-Za-z]?) covers letter-prefixed ids without breaking numeric-leading ones.
|
|
354
|
+
// #4478: line-anchored (`^ {0,3}`, `/m`) — unanchored, `#{2,4}` matched a
|
|
355
|
+
// `### Phase N:`-shaped mention ANYWHERE `exec()`'s scan reached: mid-sentence
|
|
356
|
+
// prose, inside a blockquote, inside an inline code span (backtick-quoted on
|
|
357
|
+
// the same line, not a fenced block `tokenizeHeadings` would exclude). Any
|
|
358
|
+
// such line minted a phantom phase entry, inflating phase_count and able to
|
|
359
|
+
// collide on a phase NUMBER with a real heading nearby. `{0,3}` leading
|
|
360
|
+
// spaces mirrors `tokenizeHeadings`'s own CommonMark ATX-heading tolerance
|
|
361
|
+
// (src/markdown-sectionizer.cts:453) so a legitimately-indented heading that
|
|
362
|
+
// matched before this fix still matches after it.
|
|
352
363
|
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
353
|
-
const phasePattern = new RegExp(
|
|
364
|
+
const phasePattern = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention, true)}([A-Za-z]?\\d+[A-Z]?(?:[.-]\\d+)*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gim');
|
|
354
365
|
// The capturing intro inserts the bracket id at group 1 only under the
|
|
355
366
|
// bracket convention; the token and name shift by the same offset.
|
|
356
367
|
const G = convention === 'bracket' ? 1 : 0;
|
|
@@ -372,7 +383,13 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames, convention) {
|
|
|
372
383
|
// #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
|
|
373
384
|
// recognised as section boundaries. #3036: `[A-Za-z]?\d` so non-numeric-leading ids
|
|
374
385
|
// (e.g. B7) are also recognised.
|
|
375
|
-
|
|
386
|
+
// #4478 follow-up (independent code review on this same fix): ` {0,3}` after
|
|
387
|
+
// the literal `\n` mirrors phasePattern's own new leading-space tolerance
|
|
388
|
+
// above -- without it, a legitimately-indented (1-3 space) NEXT phase
|
|
389
|
+
// heading was invisible to this boundary lookup, letting the PRIOR phase's
|
|
390
|
+
// goal/mode/depends_on extraction bleed across the section boundary into
|
|
391
|
+
// the next phase's own body.
|
|
392
|
+
const nextHeader = restOfContent.match(new RegExp(`\\n {0,3}#{2,4}\\s+${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention)}[A-Za-z]?\\d[\\d.-]*`, 'i'));
|
|
376
393
|
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
377
394
|
const section = content.slice(sectionStart, sectionEnd);
|
|
378
395
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -420,7 +437,7 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames, convention) {
|
|
|
420
437
|
// convention into that owner rather than reviving the primitive `.find()`.
|
|
421
438
|
const dirMatch = matchPhaseDirs(phaseDirNames, normalized, convention).matches[0];
|
|
422
439
|
if (dirMatch) {
|
|
423
|
-
const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatch));
|
|
440
|
+
const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatch), convention);
|
|
424
441
|
planCount = counts.planCount;
|
|
425
442
|
summaryCount = counts.summaryCount;
|
|
426
443
|
hasContext = counts.hasContext;
|
|
@@ -433,7 +450,10 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames, convention) {
|
|
|
433
450
|
// NOT a precondition, so a zero-plan phase with a passing
|
|
434
451
|
// `*-VERIFICATION.md` reports complete here too, not just via
|
|
435
452
|
// `phase.complete`.
|
|
436
|
-
|
|
453
|
+
// #612: `convention` (a parameter of this function, same thread as
|
|
454
|
+
// matchPhaseDirs above) rides into completion so a bracket phase dir
|
|
455
|
+
// resolves and scopes its verification report like its legacy twin.
|
|
456
|
+
const completionResult = isPhaseComplete(node_path_1.default.join(phasesDir, dirMatch), { convention });
|
|
437
457
|
if (completionResult.value.complete)
|
|
438
458
|
diskStatus = 'complete';
|
|
439
459
|
else if (summaryCount > 0)
|
|
@@ -841,7 +861,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
841
861
|
// the same phase (ADR-3180 §7.4's headline: one predicate for the read
|
|
842
862
|
// path and the write path).
|
|
843
863
|
const phaseDir = node_path_1.default.join(cwd, phaseInfo.directory);
|
|
844
|
-
|
|
864
|
+
// ADR-3180 §7.4 read/write-path symmetry with the threaded site at ~583:
|
|
865
|
+
// thread convention here too, so this write path's completion reading
|
|
866
|
+
// agrees with the read path's under the bracket convention.
|
|
867
|
+
const convention = resolvePhaseIdConvention(cwd);
|
|
868
|
+
const completionResult = isPhaseComplete(phaseDir, { convention });
|
|
845
869
|
const verificationResult = completionResult.value.verification;
|
|
846
870
|
// #2648 precedent, applied at this write site (ADR-3180 §7.4 / #3186):
|
|
847
871
|
// `isPhaseComplete` deliberately carries NO plan-count precondition — the
|
|
@@ -872,6 +896,23 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
872
896
|
}
|
|
873
897
|
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
|
874
898
|
let updated = false;
|
|
899
|
+
// #4247: the refusal flag. The write/report decision below must be keyed to
|
|
900
|
+
// "a writable roadmap representation of THIS phase was found", never to "any
|
|
901
|
+
// byte moved". On a checklist-form ROADMAP (`- [ ] **Phase N: …**`, the
|
|
902
|
+
// roadmapper's own summary-checklist form) every phase-targeted grammar
|
|
903
|
+
// below requires an ATX `#{2,4} Phase N` heading and therefore finds
|
|
904
|
+
// nothing; the Progress-table row is the only other writable target, and
|
|
905
|
+
// when its Phase cell does not match `phaseCellRe` (e.g. a word-prefixed
|
|
906
|
+
// `Phase 68` cell — deliberately unrecognized on the read side too,
|
|
907
|
+
// `deriveProgressFromRoadmap`'s `/^\d/` data-row filter) the command used
|
|
908
|
+
// to fall through to unrelated byte deltas (an UN-scoped plan-checkbox mark
|
|
909
|
+
// anywhere in the document) and report `updated: true` while the phase's
|
|
910
|
+
// own row stayed untouched — with the file-global write then letting the
|
|
911
|
+
// platform write seam's markdown normalization inject blank lines around
|
|
912
|
+
// other phases' bullets, splitting hand-wrapped sentences mid-entry. The
|
|
913
|
+
// refusal below declines with the analyzer's own `missing_phase_details`
|
|
914
|
+
// vocabulary and leaves ROADMAP.md byte-identical.
|
|
915
|
+
let missingPhaseDetails = false;
|
|
875
916
|
withPlanningLock(cwd, () => {
|
|
876
917
|
// #3957 (B9.4): captured BEFORE any transform runs, so the write/report
|
|
877
918
|
// decision below reflects whether the transforms actually changed
|
|
@@ -881,6 +922,32 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
881
922
|
const originalContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
882
923
|
let roadmapContent = originalContent;
|
|
883
924
|
const phasePattern = phaseMarkdownRegexSource(phaseNum);
|
|
925
|
+
// #4247: ONE local source for the ATX phase-heading anchor that every
|
|
926
|
+
// section-scoped writer below (`planCountPattern`,
|
|
927
|
+
// `insertRowsPatternA|B`) starts with — extracted so the target-detection
|
|
928
|
+
// gate below reads the SAME grammar the writers anchor on, and a future
|
|
929
|
+
// edit to one cannot drift from the other three copies.
|
|
930
|
+
const phaseHeadingAnchor = `#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])`;
|
|
931
|
+
// #4247: target detection runs against the ORIGINAL content's active
|
|
932
|
+
// (post-</details>) region — the same milestone scoping every writer
|
|
933
|
+
// below applies — so the gate asks "does the file carry a writable phase
|
|
934
|
+
// representation" rather than "did some regex fire mid-transform".
|
|
935
|
+
const gateDetailsClose = originalContent.lastIndexOf('</details>');
|
|
936
|
+
const gateActiveRegion = gateDetailsClose === -1
|
|
937
|
+
? originalContent
|
|
938
|
+
: originalContent.slice(gateDetailsClose + '</details>'.length);
|
|
939
|
+
// Heading target: the exact grammar `planCountPattern` /
|
|
940
|
+
// `insertRowsPatternA|B` anchor on (an ATX phase heading for this phase).
|
|
941
|
+
const headingTargetFound = new RegExp(phaseHeadingAnchor, 'i').test(gateActiveRegion);
|
|
942
|
+
// Checklist target: when the phase is complete, its own checklist bullet
|
|
943
|
+
// (`- [ ] **Phase N: …**`) IS a writable phase row — the completion
|
|
944
|
+
// checkbox stamp below updates it. Same grammar as that writer, widened
|
|
945
|
+
// one notch to `[ x]` so an ALREADY-checked bullet still counts as a
|
|
946
|
+
// found target: an idempotent re-run then takes the honest
|
|
947
|
+
// "no changes were needed" decline instead of this refusal.
|
|
948
|
+
const checklistTargetFound = isComplete && new RegExp(`-\\s*\\[[ x]\\]\\s*.*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i').test(gateActiveRegion);
|
|
949
|
+
// Table-row target: set by the row-scoped cell updates below.
|
|
950
|
+
let tableRowFound = false;
|
|
884
951
|
// Progress table row: update Plans Complete/Status/Completed columns BY
|
|
885
952
|
// COLUMN NAME (handles 4- or 5-column RoadmapProgress tables regardless of
|
|
886
953
|
// Milestone-column presence) via the markdown-table seam (ADR-2143 §7) —
|
|
@@ -899,11 +966,15 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
899
966
|
roadmapContent = editProgressTableSlice(roadmapContent, (scoped) => {
|
|
900
967
|
let text = scoped;
|
|
901
968
|
const plansResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Plans Complete', ` ${summaryCount}/${planCount} `);
|
|
902
|
-
if (plansResult.ok)
|
|
969
|
+
if (plansResult.ok) {
|
|
903
970
|
text = plansResult.value;
|
|
971
|
+
tableRowFound = true;
|
|
972
|
+
}
|
|
904
973
|
const statusResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Status', ` ${status.padEnd(11)}`);
|
|
905
|
-
if (statusResult.ok)
|
|
974
|
+
if (statusResult.ok) {
|
|
906
975
|
text = statusResult.value;
|
|
976
|
+
tableRowFound = true;
|
|
977
|
+
}
|
|
907
978
|
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage).
|
|
908
979
|
// Ragged-tolerant (#2245 Blocker 2): probe the CURRENT Completed cell via
|
|
909
980
|
// a no-op updateTableCell write (its own tolerant row scan) rather than
|
|
@@ -918,8 +989,10 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
918
989
|
}
|
|
919
990
|
return ' ';
|
|
920
991
|
});
|
|
921
|
-
if (completedResult.ok)
|
|
992
|
+
if (completedResult.ok) {
|
|
922
993
|
text = completedResult.value;
|
|
994
|
+
tableRowFound = true;
|
|
995
|
+
}
|
|
923
996
|
return text;
|
|
924
997
|
});
|
|
925
998
|
// Update plan count in phase detail section.
|
|
@@ -960,7 +1033,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
960
1033
|
// `_match` unchanged. An untouched first line cannot orphan its own
|
|
961
1034
|
// continuation on the next line, since the pattern never spans past
|
|
962
1035
|
// `\n` in the first place.
|
|
963
|
-
const planCountPattern = new RegExp(`(
|
|
1036
|
+
const planCountPattern = new RegExp(`(${phaseHeadingAnchor}(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)(\\d+\\s*\\/\\s*\\d+\\s+plans(?:\\s+(?:complete|executed))?|\\d+\\s+plans?)?([^\\r\\n]*)`, 'i');
|
|
964
1037
|
const planCountText = isComplete
|
|
965
1038
|
? `${summaryCount}/${planCount} plans complete`
|
|
966
1039
|
: `${summaryCount}/${planCount} plans executed`;
|
|
@@ -1040,8 +1113,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1040
1113
|
//
|
|
1041
1114
|
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
1042
1115
|
// Pattern B: fallback to bold summary when no bare header exists.
|
|
1043
|
-
const insertRowsPatternA = new RegExp(`(
|
|
1044
|
-
const insertRowsPatternB = new RegExp(`(
|
|
1116
|
+
const insertRowsPatternA = new RegExp(`(${phaseHeadingAnchor}(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
1117
|
+
const insertRowsPatternB = new RegExp(`(${phaseHeadingAnchor}(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
1045
1118
|
const sortedMissing = [...missingPlans].sort();
|
|
1046
1119
|
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
1047
1120
|
const inserter = (match) => `${match}\n${newRows}`;
|
|
@@ -1081,10 +1154,24 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1081
1154
|
// `cmdRoadmapAnnotateDependencies`'s existing `nextContent !== content`
|
|
1082
1155
|
// gate. Previously this wrote and reported `updated: true`
|
|
1083
1156
|
// unconditionally, even on an idempotent re-run that changed nothing.
|
|
1084
|
-
|
|
1157
|
+
//
|
|
1158
|
+
// #4247: ...but ONLY when a writable representation of THIS phase was
|
|
1159
|
+
// found. Without the target gate, a byte delta from an unrelated
|
|
1160
|
+
// transform (the un-scoped plan-checkbox mark) satisfied the #3957 gate
|
|
1161
|
+
// and produced a success-shaped `updated: true` while the phase's own
|
|
1162
|
+
// row stayed untouched — and the file-global write let the platform
|
|
1163
|
+
// write seam's markdown normalization reflow unrelated entries. When no
|
|
1164
|
+
// target exists the command refuses: no write at all, so ROADMAP.md is
|
|
1165
|
+
// left byte-identical, and the caller gets a typed
|
|
1166
|
+
// `missing_phase_details` decline instead of a false green.
|
|
1167
|
+
const phaseRepresentationFound = tableRowFound || headingTargetFound || checklistTargetFound;
|
|
1168
|
+
if (phaseRepresentationFound && roadmapContent !== originalContent) {
|
|
1085
1169
|
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, roadmapContent);
|
|
1086
1170
|
updated = true;
|
|
1087
1171
|
}
|
|
1172
|
+
if (!phaseRepresentationFound) {
|
|
1173
|
+
missingPhaseDetails = true;
|
|
1174
|
+
}
|
|
1088
1175
|
});
|
|
1089
1176
|
const computed = {
|
|
1090
1177
|
phase: phaseNum,
|
|
@@ -1097,6 +1184,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1097
1184
|
if (updated) {
|
|
1098
1185
|
output({ updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
|
|
1099
1186
|
}
|
|
1187
|
+
else if (missingPhaseDetails) {
|
|
1188
|
+
// #4247: honest refusal — the reason names the real condition (the
|
|
1189
|
+
// analyzer's `missing_phase_details` vocabulary), never "already
|
|
1190
|
+
// reflects", which was false: the ROADMAP was never able to record this
|
|
1191
|
+
// phase's progress in the first place.
|
|
1192
|
+
declineNoOp(raw, 'updated', 'missing_phase_details', `roadmap update-plan-progress skipped — ROADMAP.md has no writable entry for phase ${formatDiagnosticToken(String(phaseNum))} (no matching Progress-table row, no phase detail section, and no checklist entry this command can update). ROADMAP.md was left unchanged.`, computed);
|
|
1193
|
+
}
|
|
1100
1194
|
else {
|
|
1101
1195
|
declineNoOp(raw, 'updated', "no changes were needed — ROADMAP.md already reflects this phase's plan/summary counts and status", "roadmap update-plan-progress skipped — no changes were needed; ROADMAP.md already reflects this phase's plan/summary counts and status.", computed);
|
|
1102
1196
|
}
|
|
@@ -1011,7 +1011,7 @@ function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], requestedSuba
|
|
|
1011
1011
|
* @param {boolean} [isGlobal=false] - Whether this is a global install
|
|
1012
1012
|
*/
|
|
1013
1013
|
function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
|
1014
|
-
let c = content;
|
|
1014
|
+
let c = filterRuntimeNotesForTarget(content, 'antigravity');
|
|
1015
1015
|
if (isGlobal) {
|
|
1016
1016
|
// #3738: global skills install under ~/.gemini/config/skills (the dir AGY
|
|
1017
1017
|
// scans for global discovery), so skills-path references must divert there
|
|
@@ -1103,7 +1103,7 @@ function convertSlashCommandsToCursorSkillMentions(content) {
|
|
|
1103
1103
|
return content.replace(/gsd:/gi, 'gsd-');
|
|
1104
1104
|
}
|
|
1105
1105
|
function convertClaudeToCursorMarkdown(content) {
|
|
1106
|
-
let converted = convertSlashCommandsToCursorSkillMentions(content);
|
|
1106
|
+
let converted = convertSlashCommandsToCursorSkillMentions(filterRuntimeNotesForTarget(content, 'cursor'));
|
|
1107
1107
|
// Replace tool name references in body text
|
|
1108
1108
|
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
1109
1109
|
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
@@ -1203,7 +1203,7 @@ function convertSlashCommandsToWindsurfSkillMentions(content) {
|
|
|
1203
1203
|
return content.replace(/gsd:/gi, 'gsd-');
|
|
1204
1204
|
}
|
|
1205
1205
|
function convertClaudeToWindsurfMarkdown(content) {
|
|
1206
|
-
let converted = convertSlashCommandsToWindsurfSkillMentions(content);
|
|
1206
|
+
let converted = convertSlashCommandsToWindsurfSkillMentions(filterRuntimeNotesForTarget(content, 'windsurf'));
|
|
1207
1207
|
// Replace tool name references in body text
|
|
1208
1208
|
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
1209
1209
|
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
@@ -1362,7 +1362,7 @@ function convertSlashCommandsToAugmentSkillMentions(content) {
|
|
|
1362
1362
|
return content.replace(/gsd:/gi, 'gsd-');
|
|
1363
1363
|
}
|
|
1364
1364
|
function convertClaudeToAugmentMarkdown(content) {
|
|
1365
|
-
let converted = convertSlashCommandsToAugmentSkillMentions(content);
|
|
1365
|
+
let converted = convertSlashCommandsToAugmentSkillMentions(filterRuntimeNotesForTarget(content, 'augment'));
|
|
1366
1366
|
converted = converted.replace(/\bBash\(/g, 'launch-process(');
|
|
1367
1367
|
converted = converted.replace(/\bEdit\(/g, 'str-replace-editor(');
|
|
1368
1368
|
converted = converted.replace(/\bRead\(/g, 'view(');
|
|
@@ -1440,7 +1440,7 @@ function convertSlashCommandsToTraeSkillMentions(content) {
|
|
|
1440
1440
|
});
|
|
1441
1441
|
}
|
|
1442
1442
|
function convertClaudeToTraeMarkdown(content) {
|
|
1443
|
-
let converted = convertSlashCommandsToTraeSkillMentions(content);
|
|
1443
|
+
let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae'));
|
|
1444
1444
|
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
1445
1445
|
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
1446
1446
|
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
|
@@ -1548,7 +1548,7 @@ function convertSlashCommandsToCodebuddySkillMentions(content) {
|
|
|
1548
1548
|
});
|
|
1549
1549
|
}
|
|
1550
1550
|
function convertClaudeToCodebuddyMarkdown(content) {
|
|
1551
|
-
let converted = convertSlashCommandsToCodebuddySkillMentions(content);
|
|
1551
|
+
let converted = convertSlashCommandsToCodebuddySkillMentions(filterRuntimeNotesForTarget(content, 'codebuddy'));
|
|
1552
1552
|
// CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
|
|
1553
1553
|
// No tool name conversion needed
|
|
1554
1554
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
@@ -1625,7 +1625,7 @@ function convertClaudeCommandToCodebuddyCommand(content, commandName) {
|
|
|
1625
1625
|
}
|
|
1626
1626
|
// ── Cline converters ────────────────────────────────────────────────────────
|
|
1627
1627
|
function convertClaudeToCliineMarkdown(content) {
|
|
1628
|
-
let converted = content;
|
|
1628
|
+
let converted = filterRuntimeNotesForTarget(content, 'cline');
|
|
1629
1629
|
// Cline uses the same tool names as Claude Code — no tool name conversion needed
|
|
1630
1630
|
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
|
|
1631
1631
|
converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
|
|
@@ -1712,7 +1712,7 @@ function rewriteBareGsdToolsCommandsForCodex(content) {
|
|
|
1712
1712
|
.replace(/((?:&&|\|\||[;|])\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`);
|
|
1713
1713
|
}
|
|
1714
1714
|
function convertClaudeToCodexMarkdown(content) {
|
|
1715
|
-
let converted = convertSlashCommandsToCodexSkillMentions(content);
|
|
1715
|
+
let converted = convertSlashCommandsToCodexSkillMentions(filterRuntimeNotesForTarget(content, 'codex'));
|
|
1716
1716
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
1717
1717
|
// Remove /clear references — Codex has no equivalent command
|
|
1718
1718
|
// Handle backtick-wrapped: `\/clear` then: → (removed)
|
|
@@ -1901,9 +1901,24 @@ function frontmatterScalar(key, value) {
|
|
|
1901
1901
|
? `${key} "${frontmatterModule.escapeDoubleQuotedScalar(value)}"`
|
|
1902
1902
|
: `${key} ${value}`;
|
|
1903
1903
|
}
|
|
1904
|
+
const RUNTIME_NOTE_AUDIENCE_BY_HEADING = new Map([
|
|
1905
|
+
['copilot (vs code)', 'copilot'],
|
|
1906
|
+
]);
|
|
1907
|
+
function filterRuntimeNotesForTarget(content, targetRuntime) {
|
|
1908
|
+
return content.replace(/<runtime_note(?:\s+runtime=["']([^"']+)["'])?>([\s\S]*?)<\/runtime_note>/g, (whole, declaredAudience, inner) => {
|
|
1909
|
+
if (declaredAudience && declaredAudience.toLowerCase() !== targetRuntime)
|
|
1910
|
+
return '';
|
|
1911
|
+
const remaining = inner.replace(/(?:^|\n)[ \t]*\*\*([^*\n]+):\*\*[^\n]*(?:\n(?![ \t]*\n)[^\n]*)*/g, (section, heading) => {
|
|
1912
|
+
const audience = RUNTIME_NOTE_AUDIENCE_BY_HEADING.get(heading.trim().toLowerCase());
|
|
1913
|
+
return audience && audience !== targetRuntime ? '' : section;
|
|
1914
|
+
});
|
|
1915
|
+
const body = remaining.trim();
|
|
1916
|
+
return body ? `<runtime_note>\n${body}\n</runtime_note>` : '';
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1904
1919
|
function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null, variant = null } = {}) {
|
|
1905
1920
|
// Replace tool name references in content (applies to all files)
|
|
1906
|
-
let convertedContent = content;
|
|
1921
|
+
let convertedContent = filterRuntimeNotesForTarget(content, 'opencode');
|
|
1907
1922
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
1908
1923
|
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
1909
1924
|
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
@@ -2064,7 +2079,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
2064
2079
|
// (#2093).
|
|
2065
2080
|
function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
|
|
2066
2081
|
// Replace tool name references in content (applies to all files)
|
|
2067
|
-
let convertedContent = content;
|
|
2082
|
+
let convertedContent = filterRuntimeNotesForTarget(content, 'kilo');
|
|
2068
2083
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
2069
2084
|
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
2070
2085
|
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
@@ -2945,6 +2960,7 @@ function restoreClaudeGlobalAtRefTilde(content, pathPrefix) {
|
|
|
2945
2960
|
function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
|
|
2946
2961
|
const dirName = getDirName(runtime);
|
|
2947
2962
|
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
2963
|
+
content = filterRuntimeNotesForTarget(content, runtime);
|
|
2948
2964
|
// #1521: stamp runtime identity + use_worktrees=false for every non-Claude runtime
|
|
2949
2965
|
// before brand-specific path rewrites, so the replace operates on the pristine
|
|
2950
2966
|
// source line and is idempotent regardless of subsequent path substitutions.
|
|
@@ -3613,6 +3629,7 @@ module.exports = {
|
|
|
3613
3629
|
neutralizeAgentReferences,
|
|
3614
3630
|
convertClaudeCommandToOpencodeSkill,
|
|
3615
3631
|
convertClaudeCommandToKiloSkill,
|
|
3632
|
+
filterRuntimeNotesForTarget,
|
|
3616
3633
|
// #2087 — opencode/kilo command-frontmatter converters, exported so the
|
|
3617
3634
|
// layout-driven `convertedCommandsKind` can resolve them by name (routes the
|
|
3618
3635
|
// opencode/kilo command install through the engine instead of the bespoke path).
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// In .cts (CommonJS output) files, `require` is available as a global.
|
|
10
10
|
const _require = require;
|
|
11
11
|
const path = _require('node:path');
|
|
12
|
+
const { tryWithinRootLexical } = _require('./security.cjs');
|
|
12
13
|
// #2870: InstallScope is owned by install-scope.cts, not re-declared here.
|
|
13
14
|
// `isGlobalScope` centralizes the `scope === 'global'` boolean projection
|
|
14
15
|
// this module needs at `_computePathPrefix`'s `isGlobal: boolean` boundary
|
|
@@ -33,11 +34,19 @@ function assertDestWithinConfigHome(configDir, destSubpath) {
|
|
|
33
34
|
throw new Error(`destSubpath "${destSubpath}" contains a NUL byte and is not valid`);
|
|
34
35
|
}
|
|
35
36
|
const root = path.resolve(configDir);
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// `resolved === root` is a DELIBERATE ADDITIONAL rejection, separate from
|
|
38
|
+
// the containment decision: `tryWithinRootLexical` treats target === root
|
|
39
|
+
// as CONTAINED, but a destSubpath of "" (or one that resolves to configDir
|
|
40
|
+
// itself) must never be accepted here — this is the strict-subpath
|
|
41
|
+
// requirement Phase B of ADR-1239 imposes on third-party descriptors, and
|
|
42
|
+
// it prevents a descriptor from writing at configHome itself. Kept as its
|
|
43
|
+
// own check per ADR-4650 decision 6 (a wrapper may add its own conditions
|
|
44
|
+
// on top of the canonical predicate, never invert it).
|
|
45
|
+
const contained = tryWithinRootLexical(destSubpath, configDir);
|
|
46
|
+
if (contained === null || contained === root) {
|
|
38
47
|
throw new Error(`destSubpath "${destSubpath}" must be a strict subpath of configHome "${configDir}" — not configHome itself or outside it (escapes configHome)`);
|
|
39
48
|
}
|
|
40
|
-
return
|
|
49
|
+
return contained;
|
|
41
50
|
}
|
|
42
51
|
function errorMessage(err) {
|
|
43
52
|
if (err instanceof Error)
|
|
@@ -24,6 +24,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
24
24
|
// unless the top-level installRuntimeArtifacts call injected a `deps.fs`.
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
26
|
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
27
|
+
const security_cjs_1 = require("./security.cjs");
|
|
27
28
|
const { installFs, mkInstallTempDir } = installFsAdapter;
|
|
28
29
|
// Reuse the install manifest's existing parser and streamed SHA-256
|
|
29
30
|
// classification instead of deriving a second integrity implementation here.
|
|
@@ -101,10 +102,15 @@ function isReadableDirectory(candidate, routed) {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
function isPhysicallyConfinedTo(root, candidate) {
|
|
105
|
+
// ADR-4650 decision 6: lexical family on already-realpath'd operands — the
|
|
106
|
+
// surrounding try/catch must survive verbatim, since a non-existent
|
|
107
|
+
// candidate throwing out of realpathSync (not `tryWithinRootLexical`, which
|
|
108
|
+
// would accept it) is exactly the "incomplete manifest" signal this
|
|
109
|
+
// function's callers depend on.
|
|
104
110
|
try {
|
|
105
111
|
const physicalRoot = installFs().realpathSync(root);
|
|
106
112
|
const physicalCandidate = installFs().realpathSync(candidate);
|
|
107
|
-
return
|
|
113
|
+
return (0, security_cjs_1.tryWithinRootLexical)(physicalCandidate, physicalRoot) !== null;
|
|
108
114
|
}
|
|
109
115
|
catch {
|
|
110
116
|
return false;
|
|
@@ -167,9 +173,11 @@ function installedManifestIsComplete(runtimeConfigDir, required) {
|
|
|
167
173
|
const parts = key.split('/');
|
|
168
174
|
if (parts.some((part) => part === '' || part === '.' || part === '..'))
|
|
169
175
|
return false;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
// ADR-4650 decision 6: lexical family — the object is lstat'd (never
|
|
177
|
+
// stat'd) and refused if it is a symlink just below, so this gate must
|
|
178
|
+
// refuse rather than resolve.
|
|
179
|
+
const candidate = (0, security_cjs_1.tryWithinRootLexical)(parts.join('/'), runtimeConfigDir);
|
|
180
|
+
if (candidate === null || candidate === node_path_1.default.resolve(runtimeConfigDir))
|
|
173
181
|
return false;
|
|
174
182
|
const stat = io.lstatSync(candidate);
|
|
175
183
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
@@ -217,7 +225,7 @@ function providersShareRequiredRoots(left, right, required) {
|
|
|
217
225
|
const overlap = (leftPath, rightPath) => {
|
|
218
226
|
const relative = node_path_1.default.relative(leftPath, rightPath);
|
|
219
227
|
return relative === '' ||
|
|
220
|
-
(relative !== '..' && !relative.startsWith(`..${node_path_1.default.sep}`) && !node_path_1.default.isAbsolute(relative));
|
|
228
|
+
(relative !== '..' && !relative.startsWith(`..${node_path_1.default.sep}`) && !node_path_1.default.isAbsolute(relative)); // allow-handrolled-containment: bidirectional physical-root overlap/identity check between two providers for dedup detection — not a security confinement gate on untrusted input
|
|
221
229
|
};
|
|
222
230
|
const physicalLeft = canonicalize(leftFs, leftRoot);
|
|
223
231
|
const physicalRight = canonicalize(rightFs, rightRoot);
|