@opengsd/gsd-core 1.14.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -1947,7 +1947,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1947
1947
|
'skillPriorityFrontmatter',
|
|
1948
1948
|
'skillsGlobalOnboarding',
|
|
1949
1949
|
'skillsManifestPrefix',
|
|
1950
|
-
'skipCodexSkillsManifest',
|
|
1951
1950
|
'skipHomePrefixSubstitution',
|
|
1952
1951
|
'skipSettingsUi',
|
|
1953
1952
|
'skipSharedHooksInstall',
|
|
@@ -281,6 +281,7 @@ function loadDecisionExtraction(contextPath) {
|
|
|
281
281
|
return {
|
|
282
282
|
trackable: extraction.decisions.filter((d) => d.trackable),
|
|
283
283
|
outcome: extraction.outcome,
|
|
284
|
+
unreadableIds: extraction.unreadableIds ?? [],
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
/**
|
|
@@ -329,21 +330,48 @@ function cmdDecisionCoveragePlan(projectDir, args, raw) {
|
|
|
329
330
|
output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
|
|
330
331
|
return;
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
// #4794: a NON-FILE path (a directory — the adjacent same-looking positional
|
|
334
|
+
// swapped, the issue's repro 2) is a caller error like #2770's empty argument:
|
|
335
|
+
// fs.existsSync is true, the read yields nothing, and the gate used to
|
|
336
|
+
// certify passed:true on a phase full of decisions. Fail closed, naming it.
|
|
337
|
+
// The stat is wrapped: a path that vanishes between existsSync and statSync
|
|
338
|
+
// (or any stat failure) must answer the SAME fail-closed JSON, never a throw.
|
|
339
|
+
let contextIsFile = false;
|
|
340
|
+
let contextKind = 'non-file entry';
|
|
341
|
+
try {
|
|
342
|
+
const st = node_fs_1.default.statSync(contextPath);
|
|
343
|
+
contextIsFile = st.isFile();
|
|
344
|
+
if (st.isDirectory())
|
|
345
|
+
contextKind = 'directory';
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
contextIsFile = false;
|
|
349
|
+
contextKind = 'unreadable path';
|
|
350
|
+
}
|
|
351
|
+
if (!contextIsFile) {
|
|
352
|
+
output({ passed: false, skipped: false, reason: 'context path is not a file', total: null, covered: null, message: `Decision coverage gate: the context path "${contextArg}" is not a readable file (${contextKind}). Swap the adjacent positionals or pass --context <path-to-CONTEXT.md>.` }, raw, undefined);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const { trackable: decisions, outcome, unreadableIds } = loadDecisionExtraction(contextPath);
|
|
333
356
|
// #1365 fail-loud gate: any could-not-parse outcome must NOT silently pass —
|
|
334
357
|
// even when some decisions were extracted (e.g. D-01 valid but D-02 malformed).
|
|
335
358
|
// A parse-miss on ANY bullet means the gate cannot certify full coverage.
|
|
336
359
|
// Fire independent of decisions.length so a partial-parse still blocks.
|
|
337
360
|
if (outcome === 'could-not-parse') {
|
|
361
|
+
// #4794: nothing was measured — the answer must not carry the fields of a
|
|
362
|
+
// gate that did. total/covered are null (a type change is the point:
|
|
363
|
+
// 0 reads as data, null does not), `uncovered` is OMITTED (the list was
|
|
364
|
+
// never built), and the ids that failed to parse are carried so a caller
|
|
365
|
+
// capturing stdout knows which decision to fix.
|
|
338
366
|
const partialParse = decisions.length > 0;
|
|
339
367
|
output({
|
|
340
368
|
passed: false,
|
|
341
369
|
skipped: false,
|
|
342
370
|
reason: 'could-not-parse',
|
|
343
|
-
total:
|
|
344
|
-
covered:
|
|
345
|
-
|
|
346
|
-
message: partialParse
|
|
371
|
+
total: null,
|
|
372
|
+
covered: null,
|
|
373
|
+
unreadable: unreadableIds,
|
|
374
|
+
message: (partialParse
|
|
347
375
|
? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
|
|
348
376
|
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
|
|
349
377
|
'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
|
|
@@ -353,7 +381,8 @@ function cmdDecisionCoveragePlan(projectDir, args, raw) {
|
|
|
353
381
|
'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
|
|
354
382
|
'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
|
|
355
383
|
'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
|
|
356
|
-
'also lands here.'
|
|
384
|
+
'also lands here.')
|
|
385
|
+
+ (unreadableIds.length > 0 ? ' Unreadable ids: ' + unreadableIds.join(', ') + '.' : ''),
|
|
357
386
|
}, raw, undefined);
|
|
358
387
|
return;
|
|
359
388
|
}
|
|
@@ -546,8 +575,10 @@ function findUiSpecInDir(phaseDir) {
|
|
|
546
575
|
* matches the token `dashboard` exactly like the real compound `micro-frontend`
|
|
547
576
|
* (the boundary rule of #3718 is intentional and untouched). The gate therefore
|
|
548
577
|
* blocks only when the token match is corroborated by static frontend evidence
|
|
549
|
-
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep
|
|
550
|
-
*
|
|
578
|
+
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep, a
|
|
579
|
+
* component-framework file, or native UI evidence — a `.xaml` file or a
|
|
580
|
+
* `.swift`/`.kt`/`.dart` file carrying its ecosystem's UI import marker,
|
|
581
|
+
* #4658). This mirrors the sibling post-wave gate
|
|
551
582
|
* computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
|
|
552
583
|
* matchedToken/matchedLine surface what tripped the sniffer so an operator can
|
|
553
584
|
* judge the flag in one second instead of reaching for --skip-ui.
|
|
@@ -969,8 +1000,22 @@ function resolvePhaseDirOrEmpty(projectDir, phase) {
|
|
|
969
1000
|
* phase's `-PLAN.md` files against the filesystem WITHOUT executing anything —
|
|
970
1001
|
* see verify-command-grounding.cjs for the recognizer contract.
|
|
971
1002
|
*
|
|
972
|
-
* Args: check verify-command-paths <phase>
|
|
1003
|
+
* Args: check verify-command-paths <phase> | check verify-command-paths --dir <plan-dir>
|
|
973
1004
|
* Invocable as: gsd_run check verify-command-paths <phase>
|
|
1005
|
+
* gsd_run check verify-command-paths --dir <plan-dir>
|
|
1006
|
+
*
|
|
1007
|
+
* `--dir` (#4767) names a directory holding `-PLAN.md` files directly, for
|
|
1008
|
+
* plans that live outside `.planning/phases/` — quick mode's
|
|
1009
|
+
* `.planning/quick/<id>/` is the motivating caller, which until #4767 never ran
|
|
1010
|
+
* this probe at all. The directory is resolved against the project root AND
|
|
1011
|
+
* CONTAINED WITHIN IT — an absolute or climbing `--dir` that lands outside the
|
|
1012
|
+
* root is `unresolvable`, never read — then probed exactly as a phase directory
|
|
1013
|
+
* is; `projectRoot` stays the project root in both forms. `--dir <value>` is the
|
|
1014
|
+
* only accepted spelling: `--dir=<value>` yields no `dir` flag and falls through to
|
|
1015
|
+
* the no-argument arm, as does an empty value. Both are `partitionPredicateArgs`
|
|
1016
|
+
* behaviour, inherited and unchanged. (How that parser resolves a REPEATED `--dir`
|
|
1017
|
+
* is deliberately not characterised here — a malformed later occurrence does not
|
|
1018
|
+
* displace an earlier valid one, so the obvious "last one wins" gloss is wrong.)
|
|
974
1019
|
*
|
|
975
1020
|
* When the phase cannot be resolved to a directory, this emits a non-throwing
|
|
976
1021
|
* degraded JSON payload (status/commands/counts all zeroed, `readError`
|
|
@@ -979,18 +1024,60 @@ function resolvePhaseDirOrEmpty(projectDir, phase) {
|
|
|
979
1024
|
* look", which a non-zero exit / thrown error would collapse.
|
|
980
1025
|
*/
|
|
981
1026
|
function cmdVerifyCommandPaths(projectDir, args, raw) {
|
|
982
|
-
// args[0] = 'check', args[1] = 'verify-command-paths',
|
|
983
|
-
|
|
984
|
-
|
|
1027
|
+
// args[0] = 'check', args[1] = 'verify-command-paths', then either a phase
|
|
1028
|
+
// positional or `--dir <plan-dir>` (#4767).
|
|
1029
|
+
const { flags, positionals } = partitionPredicateArgs(args.slice(2));
|
|
1030
|
+
const dirFlag = typeof flags['dir'] === 'string' ? flags['dir'] : '';
|
|
1031
|
+
// First non-flag positional: `--raw` (valueless) lands in positionals too, and its position
|
|
1032
|
+
// relative to the phase argument is the caller's choice.
|
|
1033
|
+
const phase = positionals.find(p => !p.startsWith('--')) ?? '';
|
|
1034
|
+
if (!phase && !dirFlag) {
|
|
985
1035
|
output({
|
|
986
1036
|
status: 'unresolvable',
|
|
987
1037
|
commands: [],
|
|
988
1038
|
counts: { blocker: 0, warning: 0, total: 0 },
|
|
989
|
-
readError: 'verify-command-paths requires a phase argument: check verify-command-paths <phase>',
|
|
1039
|
+
readError: 'verify-command-paths requires a phase argument or --dir: check verify-command-paths <phase> | --dir <plan-dir>',
|
|
990
1040
|
}, raw, undefined);
|
|
991
1041
|
return;
|
|
992
1042
|
}
|
|
993
|
-
|
|
1043
|
+
// `--dir` is CALLER-SUPPLIED, so it is contained before it reaches the
|
|
1044
|
+
// `readdirSync`/`readFileSync` calls in probePhaseVerifyCommands (#4785 review).
|
|
1045
|
+
// Same predicate and policy as `resolvePath` above, and for the reason ADR-4650
|
|
1046
|
+
// gives at the other read site: the reads below FOLLOW SYMLINKS, so containment
|
|
1047
|
+
// must be decided on the resolved target, not a lexical prefix — a link inside
|
|
1048
|
+
// the root pointing outside it passes `tryWithinRootLexical` and is then read.
|
|
1049
|
+
// Read the value the predicate RETURNED; never re-derive the path. An escape
|
|
1050
|
+
// degrades to the same non-throwing payload the unresolvable-phase arm emits,
|
|
1051
|
+
// because a consumer must be able to tell "could not look" from "nothing to
|
|
1052
|
+
// report" (and `error()` would collapse them).
|
|
1053
|
+
//
|
|
1054
|
+
// RESIDUAL, stated rather than left to be rediscovered: this is check-then-use, so
|
|
1055
|
+
// a symlink planted at the resolved path BETWEEN this call and the reads inside
|
|
1056
|
+
// probePhaseVerifyCommands would be followed. A link already in place when the
|
|
1057
|
+
// command runs IS refused — the predicate resolves it and returns null (driven) —
|
|
1058
|
+
// so the window is the in-process gap, not the ordinary case. It is a property of
|
|
1059
|
+
// every `tryWithinRoot` call site in this repo, including `resolvePath` above and
|
|
1060
|
+
// the artifact scan below, not of this arm; closing it needs O_NOFOLLOW/dirfd
|
|
1061
|
+
// semantics inside the ADR-4650 predicate, which is a wider change than the bug
|
|
1062
|
+
// this fixes.
|
|
1063
|
+
let phaseDir;
|
|
1064
|
+
if (dirFlag) {
|
|
1065
|
+
const candidate = node_path_1.default.isAbsolute(dirFlag) ? dirFlag : node_path_1.default.join(projectDir, dirFlag);
|
|
1066
|
+
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1067
|
+
if (contained === null) {
|
|
1068
|
+
output({
|
|
1069
|
+
status: 'unresolvable',
|
|
1070
|
+
commands: [],
|
|
1071
|
+
counts: { blocker: 0, warning: 0, total: 0 },
|
|
1072
|
+
readError: `--dir resolves outside the project root: ${dirFlag}`,
|
|
1073
|
+
}, raw, undefined);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
phaseDir = contained;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
|
|
1080
|
+
}
|
|
994
1081
|
if (!phaseDir) {
|
|
995
1082
|
output({
|
|
996
1083
|
status: 'unresolvable',
|
|
@@ -382,9 +382,9 @@ function stripReasoningEffort(doc) {
|
|
|
382
382
|
/**
|
|
383
383
|
* The 17 roles measured as widening under derivation (declare Write/Edit,
|
|
384
384
|
* never in the pre-#3897 `CODEX_AGENT_SANDBOX` map, so the old
|
|
385
|
-
* `|| 'read-only'` fallback silently under-granted them)
|
|
386
|
-
* `read-only`
|
|
387
|
-
*
|
|
385
|
+
* `|| 'read-only'` fallback silently under-granted them) — pinned to
|
|
386
|
+
* `read-only` from 2026-09-08 (HALT.md) until #4770 lifted the hold on
|
|
387
|
+
* 2026-09-21. This list is CLOSED and
|
|
388
388
|
* SHRINK-ONLY: a new writing role never lands here (S6, T26); it is validated
|
|
389
389
|
* against the live tool contract every time it is consulted
|
|
390
390
|
* ({@link _deriveCodexSandboxModeFromTools}) and against the real
|
|
@@ -401,29 +401,25 @@ function stripReasoningEffort(doc) {
|
|
|
401
401
|
* parses the list correctly, so this role genuinely derives
|
|
402
402
|
* `workspace-write` from its tool contract — HALT.md's original 16-role
|
|
403
403
|
* count measured against the pre-fix (single-line) readers and undercounted
|
|
404
|
-
* this role.
|
|
405
|
-
*
|
|
406
|
-
*
|
|
404
|
+
* this role.
|
|
405
|
+
*
|
|
406
|
+
* **LIFTED 2026-09-21 (#4770, maintainer decision: documented enforcement
|
|
407
|
+
* suffices).** The map is empty: the rung-3 hold's recorded reopen condition
|
|
408
|
+
* — official OpenAI documentation establishing Codex `sandbox_mode` as an
|
|
409
|
+
* enforced technical boundary that custom subagent TOML files honor — is
|
|
410
|
+
* satisfied, so every role now derives `sandbox_mode` purely from its own
|
|
411
|
+
* `tools:` frontmatter (`workspace-write` iff Write/Edit is declared). The
|
|
412
|
+
* shrink-to-zero invariant (ADR-3473 §8.3) is satisfied by reaching zero;
|
|
413
|
+
* the map is kept as an empty frozen structure so a future re-hold has a
|
|
414
|
+
* shape to land in, and {@link validateCodexSandboxHolds} keeps failing if
|
|
415
|
+
* the list ever grows a role that no longer exists in `agents/`. The
|
|
416
|
+
* #3897 security-review F1/F3 fail-closed pins are unchanged and
|
|
417
|
+
* map-independent: `suspicious` identities (non-ASCII after
|
|
418
|
+
* normalization) still pin `read-only`, and post-lift the sandbox derives
|
|
419
|
+
* from an artifact's own CONTENT, so the F1 identity-confusion attack no
|
|
420
|
+
* longer has a hold to ride.
|
|
407
421
|
*/
|
|
408
|
-
exports.CODEX_SANDBOX_HOLDS = Object.freeze({
|
|
409
|
-
'gsd-ai-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
410
|
-
'gsd-code-fixer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
411
|
-
'gsd-code-reviewer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
412
|
-
'gsd-debug-session-manager': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
413
|
-
'gsd-doc-classifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
414
|
-
'gsd-doc-synthesizer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
415
|
-
'gsd-doc-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
416
|
-
'gsd-doc-writer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
417
|
-
'gsd-dom-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
418
|
-
'gsd-domain-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
419
|
-
'gsd-eval-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
420
|
-
'gsd-eval-planner': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
421
|
-
'gsd-intel-updater': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
422
|
-
'gsd-pattern-mapper': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
423
|
-
'gsd-ui-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
424
|
-
'gsd-ui-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
425
|
-
'gsd-nyquist-auditor': 'declares Write/Edit (YAML list-form tools:, surfaced by the list-form parse fix); pending Codex sandbox_mode enforcement decision',
|
|
426
|
-
});
|
|
422
|
+
exports.CODEX_SANDBOX_HOLDS = Object.freeze({});
|
|
427
423
|
// True iff a `tools:` frontmatter value declares Write or Edit as a whole
|
|
428
424
|
// token (never a substring match, so a hypothetical "Edith"-named tool could
|
|
429
425
|
// never collide). Single predicate owner for both the emitter
|
|
@@ -30,7 +30,10 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
30
30
|
const { toPosixPath, generateSlugInternal, extractOneLinerFromBody } = coreUtilsMod;
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const phaseIdMod = require("./phase-id.cjs");
|
|
33
|
-
const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName } = phaseIdMod;
|
|
33
|
+
const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, } = phaseIdMod;
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const phaseIdDisplayMod = require("./phase-id-display.cjs");
|
|
36
|
+
const { renderBracketMilestoneDisplay } = phaseIdDisplayMod;
|
|
34
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
38
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
36
39
|
const { getArchivedPhaseDirs, findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
@@ -56,7 +59,7 @@ const codex_agent_toml_cjs_1 = require("./codex-agent-toml.cjs");
|
|
|
56
59
|
const hostIntegrationMod = require("./host-integration.cjs");
|
|
57
60
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
61
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
59
|
-
const { planningDir, planningPaths, todosDir } = planningWorkspace;
|
|
62
|
+
const { planningDir, planningPaths, todosDir, resolvePhaseIdConvention } = planningWorkspace;
|
|
60
63
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
61
64
|
const frontmatter = require("./frontmatter.cjs");
|
|
62
65
|
const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar } = frontmatter;
|
|
@@ -72,6 +75,32 @@ const { scanPhasePlans } = planScanMod;
|
|
|
72
75
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
73
76
|
const verificationMod = require("./verification.cjs");
|
|
74
77
|
const { resolveVerificationFile } = verificationMod;
|
|
78
|
+
/**
|
|
79
|
+
* Project one canonical bracket directory onto its display identity and slug.
|
|
80
|
+
*
|
|
81
|
+
* The identity is deliberately obtained only through parsePhaseId/renderPhaseId;
|
|
82
|
+
* this helper owns no second bracket grammar. Callers invoke it only after the
|
|
83
|
+
* project's resolved convention is exactly `bracket`.
|
|
84
|
+
*/
|
|
85
|
+
function bracketPhaseDirProjection(dir) {
|
|
86
|
+
const id = parsePhaseId(dir);
|
|
87
|
+
const number = id.subphase ? `${id.phase}.${id.subphase}` : id.phase;
|
|
88
|
+
const identityPrefix = `${id.project}.${id.milestone}-${number}`;
|
|
89
|
+
const slug = dir.slice(identityPrefix.length).replace(/^-/, '');
|
|
90
|
+
return {
|
|
91
|
+
number,
|
|
92
|
+
display_id: renderPhaseId(id),
|
|
93
|
+
name: slug ? slug.replace(/-/g, ' ') : '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function recoverBracketPhaseName(dir, phaseToken) {
|
|
97
|
+
const tokenBoundary = `-${phaseToken}`;
|
|
98
|
+
const tokenOffset = dir.indexOf(tokenBoundary);
|
|
99
|
+
if (tokenOffset === -1)
|
|
100
|
+
return '';
|
|
101
|
+
const afterToken = dir.slice(tokenOffset + tokenBoundary.length).replace(/^-/, '');
|
|
102
|
+
return afterToken ? afterToken.replace(/-/g, ' ') : '';
|
|
103
|
+
}
|
|
75
104
|
// ─── Phase Status ─────────────────────────────────────────────────────────────
|
|
76
105
|
/**
|
|
77
106
|
* Phase-status precedence ladder — furthest-along wins (#2408).
|
|
@@ -237,26 +266,43 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
237
266
|
* displayed field is passed through sanitizeForDisplay and each file path is
|
|
238
267
|
* validated with requireSafePath before reading. Read-only — never mutates.
|
|
239
268
|
*/
|
|
269
|
+
/**
|
|
270
|
+
* Seed id grammars. `SEED-YYMMDD-xxx` (date + 3 base36 chars, the shape
|
|
271
|
+
* `.planning/quick/` uses) is what plant-seed has minted since #4378 removed
|
|
272
|
+
* the shared `wc -l` counter; `SEED-NNN` is the legacy counter form, which
|
|
273
|
+
* keeps parsing forever — existing seeds must never lose their identity.
|
|
274
|
+
*
|
|
275
|
+
* Known (theoretical, documented-not-fixed per #4378 review): a frontmatter-less
|
|
276
|
+
* legacy file whose counter is exactly 6 digits and whose slug opens with
|
|
277
|
+
* exactly 3 base36 chars parses as new-format. Requires a counter >= 100000 AND
|
|
278
|
+
* a missing frontmatter id; with frontmatter the legacy id always wins.
|
|
279
|
+
*/
|
|
280
|
+
const CANONICAL_SEED_ID_RE = /^SEED-(?:\d{6}-[a-z0-9]{3}|\d+)$/i;
|
|
281
|
+
const SEED_ID_PREFIX_RE = /^(SEED-(?:\d{6}-[a-z0-9]{3}|\d+))/i;
|
|
282
|
+
const SEED_SLUG_RE = /^SEED-(?:\d{6}-[a-z0-9]{3}|\d+)-(.+)$/i;
|
|
240
283
|
/**
|
|
241
284
|
* Derive the canonical `{ seed_id, slug }` from a seed filename stem and the
|
|
242
285
|
* frontmatter `id:` value. Pure (no I/O) so it can be property-tested directly.
|
|
243
286
|
*
|
|
244
|
-
* seed_id: frontmatter `id:` when it matches
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
287
|
+
* seed_id: frontmatter `id:` when it matches a seed id grammar (`SEED-YYMMDD-xxx`
|
|
288
|
+
* or legacy `SEED-NNN`), else the id prefix of the filename (`SEED-…-<slug>`),
|
|
289
|
+
* else the whole stem. The prefix fallback must keep the FULL new-format id —
|
|
290
|
+
* truncating at the date gives every same-day seed the same id (#4378).
|
|
291
|
+
* slug: the descriptive remainder after the id, else the stem with a leading
|
|
292
|
+
* `SEED-` stripped. `rawFmId` is `unknown` because frontmatter values are not
|
|
293
|
+
* guaranteed strings.
|
|
248
294
|
*/
|
|
249
295
|
function deriveSeedIdentity(stem, rawFmId) {
|
|
250
296
|
const fmId = typeof rawFmId === 'string' ? rawFmId.trim() : '';
|
|
251
297
|
let seedId;
|
|
252
|
-
if (
|
|
298
|
+
if (CANONICAL_SEED_ID_RE.test(fmId)) {
|
|
253
299
|
seedId = fmId;
|
|
254
300
|
}
|
|
255
301
|
else {
|
|
256
|
-
const
|
|
257
|
-
seedId =
|
|
302
|
+
const prefixMatch = stem.match(SEED_ID_PREFIX_RE);
|
|
303
|
+
seedId = prefixMatch ? prefixMatch[1] : stem;
|
|
258
304
|
}
|
|
259
|
-
const slugMatch = stem.match(
|
|
305
|
+
const slugMatch = stem.match(SEED_SLUG_RE);
|
|
260
306
|
const slug = slugMatch ? slugMatch[1] : stem.replace(/^SEED-/i, '');
|
|
261
307
|
return { seed_id: seedId, slug };
|
|
262
308
|
}
|
|
@@ -303,9 +349,11 @@ function cmdListSeeds(cwd, statusFilter, raw) {
|
|
|
303
349
|
// sanitizeForDisplay is for output, not comparison.
|
|
304
350
|
if (wantStatus && status !== wantStatus)
|
|
305
351
|
continue;
|
|
306
|
-
// Canonical seed
|
|
307
|
-
//
|
|
308
|
-
//
|
|
352
|
+
// Canonical seed ids are `SEED-YYMMDD-xxx` (frontmatter `id:`, what
|
|
353
|
+
// plant-seed has minted since #4378) or legacy `SEED-NNN`; deriveSeedIdentity
|
|
354
|
+
// owns that grammar. Fall back to the id prefix of the filename, then to the
|
|
355
|
+
// whole stem. The descriptive remainder of the filename (`SEED-…-<slug>.md`)
|
|
356
|
+
// is the slug.
|
|
309
357
|
const stem = node_path_1.default.basename(entry.name, '.md');
|
|
310
358
|
const { seed_id: seedId, slug } = deriveSeedIdentity(stem, fm.id);
|
|
311
359
|
let title = (0, security_cjs_1.sanitizeForDisplay)(fmStr(fm.title).slice(0, 100));
|
|
@@ -503,13 +551,10 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
503
551
|
opts = opts || {};
|
|
504
552
|
const config = loadConfig(cwd);
|
|
505
553
|
const profile = config['model_profile'] || 'balanced';
|
|
506
|
-
// #2068: resolve the model per-attempt so dynamic_routing
|
|
507
|
-
// (heavy tier) alongside effort.
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// including dynamic_routing-enabled users who don't pass --attempt), and only an
|
|
511
|
-
// explicit attempt routes through the tier ladder. resolveModelForTier itself
|
|
512
|
-
// still falls back to resolveModelInternal when dynamic_routing is off.
|
|
554
|
+
// #2068: resolve the model per-attempt so dynamic_routing ESCALATES the MODEL
|
|
555
|
+
// (heavy tier) alongside effort. The FIRST-spawn tier now comes from
|
|
556
|
+
// resolveModelInternal's own dynamic_routing step (#4505), so the absent-attempt
|
|
557
|
+
// branch below reaches it too — this gate is only about escalation.
|
|
513
558
|
let model = (opts.attempt !== undefined && opts.attempt !== null)
|
|
514
559
|
? resolveModelForTier(cwd, agentType, opts.attempt)
|
|
515
560
|
: resolveModelInternal(cwd, agentType);
|
|
@@ -3006,6 +3051,11 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
3006
3051
|
function cmdProgressRender(cwd, format, raw) {
|
|
3007
3052
|
const phasesDir = planningPaths(cwd).phases;
|
|
3008
3053
|
const milestone = getMilestoneInfo(cwd).value;
|
|
3054
|
+
const phaseIdConvention = resolvePhaseIdConvention(cwd);
|
|
3055
|
+
const milestoneDisplay = phaseIdConvention === 'bracket'
|
|
3056
|
+
? renderBracketMilestoneDisplay(milestone?.version, loadConfig(cwd).project_code)
|
|
3057
|
+
: null;
|
|
3058
|
+
const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
|
|
3009
3059
|
const phases = [];
|
|
3010
3060
|
let totalPlans = 0;
|
|
3011
3061
|
let totalSummaries = 0;
|
|
@@ -3016,12 +3066,36 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3016
3066
|
// comparePhaseNum. This command previously read the phases directory
|
|
3017
3067
|
// directly with neither, which is why `query progress` listed 999.*
|
|
3018
3068
|
// backlog directories as current-milestone phases (#3167).
|
|
3019
|
-
const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
|
|
3069
|
+
const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
|
|
3070
|
+
cwd,
|
|
3071
|
+
phaseIdConvention,
|
|
3072
|
+
});
|
|
3020
3073
|
phaseScope = scope;
|
|
3021
3074
|
for (const dir of dirs) {
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3075
|
+
let phaseNum;
|
|
3076
|
+
let phaseName;
|
|
3077
|
+
let displayId;
|
|
3078
|
+
if (phaseIdConvention === 'bracket') {
|
|
3079
|
+
try {
|
|
3080
|
+
const projection = bracketPhaseDirProjection(dir);
|
|
3081
|
+
phaseNum = projection.number;
|
|
3082
|
+
phaseName = projection.name;
|
|
3083
|
+
displayId = projection.display_id;
|
|
3084
|
+
}
|
|
3085
|
+
catch {
|
|
3086
|
+
// A malformed/tolerated directory must not suppress every later row.
|
|
3087
|
+
// It cannot receive a canonical display_id because parsePhaseId
|
|
3088
|
+
// rejected it, but the convention-aware token keeps it observable.
|
|
3089
|
+
const phaseToken = extractPhaseToken(dir, phaseIdConvention);
|
|
3090
|
+
phaseNum = phaseToken || dir;
|
|
3091
|
+
phaseName = recoverBracketPhaseName(dir, phaseToken);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
3096
|
+
phaseNum = dm ? dm[1] : dir;
|
|
3097
|
+
phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
3098
|
+
}
|
|
3025
3099
|
// #3183: canonical plan/summary counts (root+nested, superseded-excluded,
|
|
3026
3100
|
// canonical pairing) from the single owner.
|
|
3027
3101
|
const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
|
|
@@ -3030,7 +3104,14 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3030
3104
|
totalPlans += plans;
|
|
3031
3105
|
totalSummaries += summaries;
|
|
3032
3106
|
const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Pending');
|
|
3033
|
-
phases.push({
|
|
3107
|
+
phases.push({
|
|
3108
|
+
number: phaseNum,
|
|
3109
|
+
...(displayId ? { display_id: displayId } : {}),
|
|
3110
|
+
name: phaseName,
|
|
3111
|
+
plans,
|
|
3112
|
+
summaries,
|
|
3113
|
+
status,
|
|
3114
|
+
});
|
|
3034
3115
|
}
|
|
3035
3116
|
}
|
|
3036
3117
|
catch { /* intentionally empty */ }
|
|
@@ -3048,12 +3129,12 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3048
3129
|
// Render markdown table
|
|
3049
3130
|
const bar = (0, phase_lifecycle_cjs_1.renderProgressBar)(percent, 10);
|
|
3050
3131
|
const percentSuffix = percent === null ? '' : ` (${percent}%)`;
|
|
3051
|
-
let out = `# ${
|
|
3132
|
+
let out = `# ${milestoneVersion ?? ''} ${milestone?.name ?? ''}\n\n`;
|
|
3052
3133
|
out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans${percentSuffix}\n\n`;
|
|
3053
3134
|
out += `| Phase | Name | Plans | Status |\n`;
|
|
3054
3135
|
out += `|-------|------|-------|--------|\n`;
|
|
3055
3136
|
for (const p of phases) {
|
|
3056
|
-
out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
|
|
3137
|
+
out += `| ${p.display_id ?? p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
|
|
3057
3138
|
}
|
|
3058
3139
|
output({ rendered: out }, raw, out);
|
|
3059
3140
|
}
|
|
@@ -3066,7 +3147,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3066
3147
|
else {
|
|
3067
3148
|
// JSON format
|
|
3068
3149
|
output({
|
|
3069
|
-
milestone_version:
|
|
3150
|
+
milestone_version: milestoneVersion,
|
|
3070
3151
|
milestone_name: milestone?.name ?? null,
|
|
3071
3152
|
phases,
|
|
3072
3153
|
total_plans: totalPlans,
|
|
@@ -3361,6 +3442,11 @@ function cmdStats(cwd, format, raw) {
|
|
|
3361
3442
|
const reqPath = planningPaths(cwd).requirements;
|
|
3362
3443
|
const statePath = planningPaths(cwd).state;
|
|
3363
3444
|
const milestone = getMilestoneInfo(cwd).value;
|
|
3445
|
+
const phaseIdConvention = resolvePhaseIdConvention(cwd);
|
|
3446
|
+
const milestoneDisplay = phaseIdConvention === 'bracket'
|
|
3447
|
+
? renderBracketMilestoneDisplay(milestone?.version, loadConfig(cwd).project_code)
|
|
3448
|
+
: null;
|
|
3449
|
+
const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
|
|
3364
3450
|
// Phase & plan stats (reuse progress pattern)
|
|
3365
3451
|
const phasesByNumber = new Map();
|
|
3366
3452
|
let totalPlans = 0;
|
|
@@ -3380,20 +3466,43 @@ function cmdStats(cwd, format, raw) {
|
|
|
3380
3466
|
// prose mentioning `### Phase N:` inside an inline code span produced a phantom
|
|
3381
3467
|
// Not-Started row and made phases_total disagree with roadmap analyze.
|
|
3382
3468
|
// 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.
|
|
3383
|
-
const
|
|
3469
|
+
const capturesBracketId = phaseIdConvention === 'bracket';
|
|
3470
|
+
const headingPrefix = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, phaseIdConvention, capturesBracketId);
|
|
3471
|
+
// phase-id-owner: this preserves cmdStats's shipped dot-or-dash heading
|
|
3472
|
+
// token variant; PHASE_NUMBER_TOKEN_SOURCE is dot-only and would drop M-NN.
|
|
3473
|
+
const headingPattern = new RegExp(
|
|
3474
|
+
// phase-id-owner: cmdStats's shipped [.-] token variant; the canonical token is dot-only.
|
|
3475
|
+
`#{2,4}\\s*${headingPrefix}([A-Za-z]?\\d+[A-Z]?(?:[.-]\\d+)*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
3384
3476
|
let match;
|
|
3385
3477
|
while ((match = headingPattern.exec(roadmapContent)) !== null) {
|
|
3478
|
+
const bracketId = capturesBracketId ? match[1] : undefined;
|
|
3479
|
+
const phaseToken = capturesBracketId ? match[2] : match[1];
|
|
3480
|
+
const phaseName = capturesBracketId ? match[3] : match[2];
|
|
3386
3481
|
// #3185: the heading seed carried no sentinel filter, so a
|
|
3387
3482
|
// `### Phase 999.1:` backlog heading produced a stats row even with no
|
|
3388
3483
|
// directory on disk. Uses the canonical predicate (phase-id.cts), not a
|
|
3389
3484
|
// local literal — the rule had five copies and three regex variants
|
|
3390
3485
|
// before this phase, disagreeing about Phase 0.
|
|
3391
|
-
|
|
3486
|
+
const sentinelId = bracketId ? `${bracketId}-${phaseToken}` : phaseToken;
|
|
3487
|
+
if (capturesBracketId
|
|
3488
|
+
? isSentinelPhaseId(sentinelId, phaseIdConvention)
|
|
3489
|
+
: isSentinelPhaseId(sentinelId))
|
|
3392
3490
|
continue;
|
|
3393
|
-
const key = normalizePhaseName(
|
|
3491
|
+
const key = normalizePhaseName(phaseToken);
|
|
3492
|
+
let displayId;
|
|
3493
|
+
if (bracketId) {
|
|
3494
|
+
try {
|
|
3495
|
+
displayId = renderPhaseId(parsePhaseId(`${bracketId}-${phaseToken}`));
|
|
3496
|
+
}
|
|
3497
|
+
catch {
|
|
3498
|
+
// Read tolerance can admit a non-canonical heading spelling; keep
|
|
3499
|
+
// the stats row but do not invent a canonical identity for it.
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3394
3502
|
phasesByNumber.set(key, {
|
|
3395
3503
|
number: key,
|
|
3396
|
-
|
|
3504
|
+
...(displayId ? { display_id: displayId } : {}),
|
|
3505
|
+
name: phaseName.replace(/\(INSERTED\)/i, '').trim(),
|
|
3397
3506
|
plans: 0,
|
|
3398
3507
|
summaries: 0,
|
|
3399
3508
|
status: 'Not Started',
|
|
@@ -3407,15 +3516,36 @@ function cmdStats(cwd, format, raw) {
|
|
|
3407
3516
|
// sentinel filter — and getMilestonePhaseFilter degrades to a pass-all
|
|
3408
3517
|
// predicate when its heading set is empty, at which point every directory
|
|
3409
3518
|
// on disk passed, backlog included (#3167).
|
|
3410
|
-
const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
|
|
3519
|
+
const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
|
|
3520
|
+
cwd,
|
|
3521
|
+
phaseIdConvention,
|
|
3522
|
+
});
|
|
3411
3523
|
phaseScope = scope;
|
|
3412
3524
|
for (const dir of dirs) {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3525
|
+
let phaseNum;
|
|
3526
|
+
let phaseName;
|
|
3527
|
+
let displayId;
|
|
3528
|
+
if (phaseIdConvention === 'bracket') {
|
|
3529
|
+
try {
|
|
3530
|
+
const projection = bracketPhaseDirProjection(dir);
|
|
3531
|
+
phaseNum = projection.number;
|
|
3532
|
+
phaseName = projection.name;
|
|
3533
|
+
displayId = projection.display_id;
|
|
3534
|
+
}
|
|
3535
|
+
catch {
|
|
3536
|
+
const phaseToken = extractPhaseToken(dir, phaseIdConvention);
|
|
3537
|
+
phaseNum = phaseToken || dir;
|
|
3538
|
+
phaseName = recoverBracketPhaseName(dir, phaseToken);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
else {
|
|
3542
|
+
// Use extractPhaseToken to correctly parse M-NN-style and code-prefixed dir names.
|
|
3543
|
+
const phaseToken = extractPhaseToken(dir);
|
|
3544
|
+
phaseNum = phaseToken || dir;
|
|
3545
|
+
// phaseName is everything after the token (strip leading '-')
|
|
3546
|
+
const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|
|
3547
|
+
phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
|
|
3548
|
+
}
|
|
3419
3549
|
// #3183: canonical plan/summary counts (root+nested, superseded-excluded,
|
|
3420
3550
|
// canonical pairing) from the single owner.
|
|
3421
3551
|
const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
|
|
@@ -3428,6 +3558,9 @@ function cmdStats(cwd, format, raw) {
|
|
|
3428
3558
|
const existing = phasesByNumber.get(normalizedNum);
|
|
3429
3559
|
phasesByNumber.set(normalizedNum, {
|
|
3430
3560
|
number: normalizedNum,
|
|
3561
|
+
...(existing?.display_id || displayId
|
|
3562
|
+
? { display_id: existing?.display_id || displayId }
|
|
3563
|
+
: {}),
|
|
3431
3564
|
name: existing?.name || phaseName,
|
|
3432
3565
|
plans: (existing?.plans || 0) + plans,
|
|
3433
3566
|
summaries: (existing?.summaries || 0) + summaries,
|
|
@@ -3486,7 +3619,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
3486
3619
|
}
|
|
3487
3620
|
}
|
|
3488
3621
|
const result = {
|
|
3489
|
-
milestone_version:
|
|
3622
|
+
milestone_version: milestoneVersion,
|
|
3490
3623
|
milestone_name: milestone?.name ?? null,
|
|
3491
3624
|
phases,
|
|
3492
3625
|
phases_completed: completedPhases,
|
|
@@ -3506,7 +3639,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
3506
3639
|
};
|
|
3507
3640
|
if (format === 'table') {
|
|
3508
3641
|
const bar = (0, phase_lifecycle_cjs_1.renderProgressBar)(percent, 10);
|
|
3509
|
-
let out = `# ${
|
|
3642
|
+
let out = `# ${milestoneVersion ?? ''} ${milestone?.name ?? ''} — Statistics\n\n`;
|
|
3510
3643
|
const percentSuffix = percent === null ? '' : ` (${percent}%)`;
|
|
3511
3644
|
out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases${percentSuffix}\n`;
|
|
3512
3645
|
if (totalPlans > 0 && planPercent !== null) {
|
|
@@ -3520,7 +3653,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
3520
3653
|
out += `| Phase | Name | Plans | Completed | Status |\n`;
|
|
3521
3654
|
out += `|-------|------|-------|-----------|--------|\n`;
|
|
3522
3655
|
for (const p of phases) {
|
|
3523
|
-
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
3656
|
+
out += `| ${p.display_id ?? p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
3524
3657
|
}
|
|
3525
3658
|
if (gitCommits > 0) {
|
|
3526
3659
|
out += `\n**Git:** ${gitCommits} commits`;
|