@opengsd/gsd-core 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +29 -2
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1152 -80
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/gsd-core/bin/gsd-tools.cjs +1878 -2507
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +338 -45
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-registry.cjs +155 -86
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +128 -25
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +81 -4
- package/gsd-core/bin/lib/config-loader.cjs +14 -2
- package/gsd-core/bin/lib/config.cjs +69 -18
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/init.cjs +111 -47
- package/gsd-core/bin/lib/install-engine.cjs +298 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +246 -12
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase.cjs +201 -12
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
- package/gsd-core/bin/lib/roadmap.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
- package/gsd-core/bin/lib/smart-entry.cjs +69 -4
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +22 -1
- package/gsd-core/bin/lib/state.cjs +65 -11
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat.cjs +420 -5
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +220 -22
- package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +25 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +25 -34
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +19 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +89 -73
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -0
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +50 -3
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +88 -3
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +88 -3
- package/package.json +6 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
- package/vscode/package.json +1 -1
|
@@ -12,10 +12,62 @@ const node_path_1 = require("node:path");
|
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
13
|
const coreUtils = require("./core-utils.cjs");
|
|
14
14
|
const { countMatchedSummaries } = coreUtils;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
17
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
15
18
|
// Excluded derivative files
|
|
16
19
|
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
|
17
20
|
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
|
18
21
|
const PLAN_REVIEW_RE = /-PLAN-REVIEW\.md$/i;
|
|
22
|
+
// #2349: a plan's frontmatter always sits at byte 0 and closes well before the
|
|
23
|
+
// body, so only a bounded prefix is ever needed to read the `status` marker.
|
|
24
|
+
// Capping the read keeps scanPhasePlans — which loops over every phase directory
|
|
25
|
+
// on hot paths (state sync/validate, roadmap progress) — from slurping a
|
|
26
|
+
// pathologically large committed plan file into memory just to inspect one key.
|
|
27
|
+
const PLAN_FRONTMATTER_READ_CAP = 64 * 1024;
|
|
28
|
+
/**
|
|
29
|
+
* #2349: a plan whose frontmatter declares `status: superseded` was deliberately
|
|
30
|
+
* reassigned or never executed — its work moved to a later plan, so it can never
|
|
31
|
+
* gain a matching `*-SUMMARY.md`. Like a retired phase (#1514, one level up), such
|
|
32
|
+
* a plan must be excluded from BOTH the plan and summary counts; otherwise a phase
|
|
33
|
+
* with a deliberately-unexecuted plan reads `completed: false` forever, pinning the
|
|
34
|
+
* milestone below 100%. Reading only the frontmatter `status` key is the same seam
|
|
35
|
+
* verify.cts / phase.cts already use for plan metadata; a plan without the marker is
|
|
36
|
+
* counted exactly as before.
|
|
37
|
+
*
|
|
38
|
+
* This is the only path in scanPhasePlans that opens file *contents* (the rest is
|
|
39
|
+
* filename matching), so it is hardened accordingly: `statSync().isFile()` rejects
|
|
40
|
+
* anything that is not a regular file — a directory, socket, or a symlink resolving
|
|
41
|
+
* to a device such as `/dev/zero` (a git-committable DoS vector; cf. #2378/#2383) —
|
|
42
|
+
* BEFORE any open, and the read is bounded to a fixed prefix. Fail-safe throughout:
|
|
43
|
+
* a non-regular or unreadable plan is treated as a normal (counted) plan, never
|
|
44
|
+
* silently dropped.
|
|
45
|
+
*/
|
|
46
|
+
function isPlanSuperseded(planFullPath) {
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
const st = (0, node_fs_1.statSync)(planFullPath); // follows symlinks → resolves to the target's real type
|
|
50
|
+
if (!st.isFile())
|
|
51
|
+
return false;
|
|
52
|
+
const length = Math.min(st.size, PLAN_FRONTMATTER_READ_CAP);
|
|
53
|
+
if (length === 0)
|
|
54
|
+
return false;
|
|
55
|
+
const fd = (0, node_fs_1.openSync)(planFullPath, 'r');
|
|
56
|
+
try {
|
|
57
|
+
const buf = Buffer.allocUnsafe(length);
|
|
58
|
+
const bytesRead = (0, node_fs_1.readSync)(fd, buf, 0, length, 0);
|
|
59
|
+
content = buf.toString('utf8', 0, bytesRead);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
(0, node_fs_1.closeSync)(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const status = extractFrontmatter(content)['status'];
|
|
69
|
+
return typeof status === 'string' && status.trim().toLowerCase() === 'superseded';
|
|
70
|
+
}
|
|
19
71
|
function isRootPlanFile(fileName) {
|
|
20
72
|
if (PLAN_OUTLINE_RE.test(fileName))
|
|
21
73
|
return false;
|
|
@@ -75,7 +127,16 @@ function scanPhasePlans(phaseDir) {
|
|
|
75
127
|
}
|
|
76
128
|
catch { /* ignore unreadable nested layout */ }
|
|
77
129
|
}
|
|
78
|
-
const
|
|
130
|
+
const allPlanFiles = rootPlanFiles.concat(nestedPlanFiles);
|
|
131
|
+
// #2349: drop plans explicitly marked `status: superseded` from the plan set
|
|
132
|
+
// BEFORE counting, so they inflate neither the denominator (planCount) nor,
|
|
133
|
+
// via countMatchedSummaries below, the numerator (summaryCount). Plans without
|
|
134
|
+
// the marker are untouched, so behaviour is byte-for-behaviour identical for
|
|
135
|
+
// every existing phase — only a phase carrying the new marker changes.
|
|
136
|
+
const supersededPlanFiles = allPlanFiles.filter((f) => isPlanSuperseded((0, node_path_1.join)(phaseDir, f)));
|
|
137
|
+
const planFiles = supersededPlanFiles.length === 0
|
|
138
|
+
? allPlanFiles
|
|
139
|
+
: allPlanFiles.filter((f) => !supersededPlanFiles.includes(f));
|
|
79
140
|
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
|
80
141
|
const planCount = planFiles.length;
|
|
81
142
|
// Count only summaries that are the PLAN→SUMMARY partner of an existing plan
|
|
@@ -87,7 +148,14 @@ function scanPhasePlans(phaseDir) {
|
|
|
87
148
|
return {
|
|
88
149
|
planCount,
|
|
89
150
|
summaryCount,
|
|
90
|
-
|
|
151
|
+
// #2349: gate completion on whether the phase had ANY plans on disk
|
|
152
|
+
// (allPlanFiles), NOT on the post-exclusion planCount. A phase whose plans
|
|
153
|
+
// were ALL marked superseded has planCount 0, but it is NOT an unplanned
|
|
154
|
+
// empty phase — there is simply no remaining work, so it must read complete
|
|
155
|
+
// (0 >= 0) rather than being pinned below 100% forever, which is the very
|
|
156
|
+
// failure this fix removes. A genuinely empty phase (no plans authored)
|
|
157
|
+
// still has allPlanFiles.length 0 and stays not-completed, exactly as before.
|
|
158
|
+
completed: allPlanFiles.length > 0 && summaryCount >= planCount,
|
|
91
159
|
hasNestedPlans,
|
|
92
160
|
planFiles,
|
|
93
161
|
summaryFiles,
|
|
@@ -511,12 +511,15 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
511
511
|
return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
|
|
512
512
|
}
|
|
513
513
|
const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
|
|
514
|
-
// #2043: milestone-prefixed sub-phase components must be zero-padded
|
|
515
|
-
//
|
|
514
|
+
// #2043: milestone-prefixed sub-phase components must be zero-padded — so a
|
|
515
|
+
// single-digit slug word after the phase
|
|
516
516
|
// number (e.g. dir "46-6-rs-…") captures "46" and is not silently excluded from
|
|
517
|
-
// the milestone as a bogus "46-6" id.
|
|
517
|
+
// the milestone as a bogus "46-6" id. #2232: the continuation width is exactly 2
|
|
518
|
+
// (PHASE_CONTINUATION_SEGMENT_SOURCE), so a year-leading slug word (dir
|
|
519
|
+
// "14-2026-photos-…") captures "14" and is not excluded as a bogus "14-2026" id.
|
|
520
|
+
// Built via new RegExp (no /i — the [A-Za-z] letter class does real case handling).
|
|
518
521
|
const numericRe = roadmapUsesHyphenedIds
|
|
519
|
-
?
|
|
522
|
+
? new RegExp(`^0*(\\d+(?:-${phaseIdModule.PHASE_CONTINUATION_SEGMENT_SOURCE})*[A-Za-z]?(?:\\.\\d+)*)`)
|
|
520
523
|
// phase-id-owner: the [A-Za-z] letter class does real case handling here — this regex carries NO /i flag; kept literal, not source-byte-equal to the canonical PHASE_NUMBER_TOKEN_SOURCE.
|
|
521
524
|
: /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
|
|
522
525
|
function isDirInMilestone(dirName) {
|
|
@@ -170,9 +170,19 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
170
170
|
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
|
|
171
171
|
return null;
|
|
172
172
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
// Read directly rather than gating on fs.existsSync: existsSync returns false
|
|
174
|
+
// on EACCES/EIO too, which would mask an UNREADABLE roadmap as "missing" and
|
|
175
|
+
// let a blocking gate certify empty scope (#2365 review). Honor the documented
|
|
176
|
+
// contract — null only when genuinely absent (ENOENT), otherwise throw.
|
|
177
|
+
let rawContent;
|
|
178
|
+
try {
|
|
179
|
+
rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err?.code === 'ENOENT')
|
|
183
|
+
return null;
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
176
186
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
177
187
|
const fullContent = stripShippedMilestones(rawContent);
|
|
178
188
|
// #2121/#2114: iterate the shared lookup-source list (exact → numeric →
|
|
@@ -868,7 +868,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
|
|
868
868
|
description = toSingleLine(description);
|
|
869
869
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
870
870
|
const adapter = getCursorSkillAdapterHeader(skillName);
|
|
871
|
-
|
|
871
|
+
// #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
|
|
872
|
+
// menu (it defaults to true). Cursor also writes a commands/ surface (#785),
|
|
873
|
+
// and surfacing both duplicated every /gsd-* entry. This mirrors the #789
|
|
874
|
+
// CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
|
|
875
|
+
// stay model-invocable background knowledge. (user-invocable:false hides from
|
|
876
|
+
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
|
877
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
872
878
|
}
|
|
873
879
|
/**
|
|
874
880
|
* Convert a Claude Code command to a Cursor 1.6 slash command (#785).
|
|
@@ -227,8 +227,12 @@ function kimiAgentsKind(destSubpath, prefix, configDir) {
|
|
|
227
227
|
* arg so scope-aware converters (antigravity, copilot) can choose
|
|
228
228
|
* between global home paths and workspace-relative paths without
|
|
229
229
|
* colliding with the `runtime` string at position 3.
|
|
230
|
+
* @param capabilityRegistry #2322: optional capability registry — captured in the
|
|
231
|
+
* stage() closure so third-party capability skills are bound to
|
|
232
|
+
* their declaring capId at staging time. Absent -> stage() stages
|
|
233
|
+
* nothing third-party (fail closed).
|
|
230
234
|
*/
|
|
231
|
-
function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false, scope = 'global') {
|
|
235
|
+
function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false, scope = 'global', capabilityRegistry) {
|
|
232
236
|
return {
|
|
233
237
|
kind: 'skills',
|
|
234
238
|
destSubpath,
|
|
@@ -247,7 +251,7 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nest
|
|
|
247
251
|
: [];
|
|
248
252
|
const isGlobal = scope === 'global';
|
|
249
253
|
const wrappedConverter = (content, skillName) => realConverter(content, skillName, runtime, cmdNames, isGlobal);
|
|
250
|
-
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested);
|
|
254
|
+
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
|
|
251
255
|
},
|
|
252
256
|
};
|
|
253
257
|
}
|
|
@@ -285,7 +289,7 @@ function getRegistry() {
|
|
|
285
289
|
* Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
|
|
286
290
|
* matching builder function. Mirrors the hand-built calls in the old switch.
|
|
287
291
|
*/
|
|
288
|
-
function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
292
|
+
function dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry) {
|
|
289
293
|
const { kind, destSubpath, prefix, nesting, converter } = entry;
|
|
290
294
|
const nested = nesting === 'nested';
|
|
291
295
|
let result;
|
|
@@ -304,7 +308,7 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
|
304
308
|
if (converter == null) {
|
|
305
309
|
throw new TypeError(`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`);
|
|
306
310
|
}
|
|
307
|
-
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope);
|
|
311
|
+
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, capabilityRegistry);
|
|
308
312
|
break;
|
|
309
313
|
case 'kimi-agents':
|
|
310
314
|
result = kimiAgentsKind(destSubpath, prefix, configDir);
|
|
@@ -322,11 +326,21 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
|
322
326
|
*
|
|
323
327
|
* ADR-857 phase 5d: driven by the capability-registry artifactLayout descriptor
|
|
324
328
|
* instead of a hardcoded switch statement.
|
|
329
|
+
*
|
|
330
|
+
* @param capabilityRegistry #2322: optional — when the caller has a composed
|
|
331
|
+
* capability registry in scope (e.g. capability-writer.cts's `capability set`
|
|
332
|
+
* path, or a fresh install's registry-aware profile resolution), pass it here
|
|
333
|
+
* so the skills kind's stage() closure can materialize installed third-party
|
|
334
|
+
* capability skills bound to their declaring capId. Both call paths (surface
|
|
335
|
+
* apply AND the installer) must pass their registry here — resolveProfile's
|
|
336
|
+
* own `'*'` (full profile) short-circuit never carries a registry, so if it
|
|
337
|
+
* is not threaded in at layout-build time a `full`-profile install stages no
|
|
338
|
+
* third-party capability skills regardless of registration (#2322 blocker 2).
|
|
325
339
|
*/
|
|
326
|
-
function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
327
|
-
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope);
|
|
340
|
+
function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global', capabilityRegistry) {
|
|
341
|
+
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope, capabilityRegistry);
|
|
328
342
|
}
|
|
329
|
-
function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir, scope = 'global') {
|
|
343
|
+
function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir, scope = 'global', capabilityRegistry) {
|
|
330
344
|
if (typeof configDir !== 'string' || configDir === '') {
|
|
331
345
|
throw new TypeError('configDir must be a non-empty string');
|
|
332
346
|
}
|
|
@@ -338,7 +352,7 @@ function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir,
|
|
|
338
352
|
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
|
339
353
|
}
|
|
340
354
|
const entries = desc[scope] ?? [];
|
|
341
|
-
const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope));
|
|
355
|
+
const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry));
|
|
342
356
|
return { runtime, configDir, scope, kinds };
|
|
343
357
|
}
|
|
344
358
|
module.exports = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot };
|
|
@@ -309,6 +309,22 @@ function normalizeNodePath(execPath, opts) {
|
|
|
309
309
|
if (existsSync(shim))
|
|
310
310
|
return shim;
|
|
311
311
|
}
|
|
312
|
+
// volta pins a concrete node image at <VOLTA_HOME>/tools/image/node/<ver>/bin/node
|
|
313
|
+
// (Windows: <VOLTA_HOME>/tools/image/node/<ver>/node.exe — volta's own layout
|
|
314
|
+
// puts node.exe at the image root, no bin/). `volta uninstall node@<ver>` prunes
|
|
315
|
+
// that image, so a baked hook command 404s — the same ephemeral-path failure
|
|
316
|
+
// #977 fixed for fnm and #1619 for mise. The stable alias is the shim
|
|
317
|
+
// <VOLTA_HOME>/bin/node, a symlink to volta-shim that always resolves to the
|
|
318
|
+
// active pin. Derive <VOLTA_HOME> from execPath rather than the env so a custom
|
|
319
|
+
// VOLTA_HOME and the Windows %LOCALAPPDATA%\Volta default both work (#2185's
|
|
320
|
+
// reasoning), and only rewrite when the shim exists — otherwise fall back to
|
|
321
|
+
// the raw execPath unchanged.
|
|
322
|
+
const voltaMatch = normalizedForMatch.match(/^(.*)\/tools\/image\/node\/[^/]+\/(?:bin\/)?node(\.exe)?$/);
|
|
323
|
+
if (voltaMatch) {
|
|
324
|
+
const shim = `${voltaMatch[1]}/bin/node${voltaMatch[2] || ''}`;
|
|
325
|
+
if (existsSync(shim))
|
|
326
|
+
return shim;
|
|
327
|
+
}
|
|
312
328
|
return execPath;
|
|
313
329
|
}
|
|
314
330
|
function resolveNodeRunner(opts) {
|
|
@@ -52,6 +52,9 @@ const { planningPaths } = planningWorkspace;
|
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
53
|
const frontmatter = require("./frontmatter.cjs");
|
|
54
54
|
const { extractFrontmatter } = frontmatter;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
|
|
56
|
+
const phaseLifecycle = require("./phase-lifecycle.cjs");
|
|
57
|
+
const { deriveProgressFromRoadmap } = phaseLifecycle;
|
|
55
58
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
59
|
const stateDocument = require("./state-document.cjs");
|
|
57
60
|
const { stateExtractField } = stateDocument;
|
|
@@ -245,6 +248,8 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
245
248
|
has_git: git.has_git,
|
|
246
249
|
verify_failed: false,
|
|
247
250
|
stale_activity: false,
|
|
251
|
+
roadmap_total_phases: null,
|
|
252
|
+
roadmap_completed_phases: null,
|
|
248
253
|
};
|
|
249
254
|
if (!hasPlanning)
|
|
250
255
|
return empty;
|
|
@@ -291,6 +296,24 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
291
296
|
// STATUS: marker on the current phase's summary/verify artifact.
|
|
292
297
|
const verifyFailed = /\bverify-fail(ed)?|verification-fail|uat-fail\b/i.test(statusRaw || '') ||
|
|
293
298
|
detectVerifyFailed(cwd, currentPhaseRaw);
|
|
299
|
+
// #2427: derive global phase counts from ROADMAP.md's Progress table. These
|
|
300
|
+
// are preferred over STATE.md's cached milestone-scoped `total_phases` (which
|
|
301
|
+
// goes stale when phases are appended after a milestone switch) for the
|
|
302
|
+
// completion check. Null when ROADMAP.md is absent or has no parseable
|
|
303
|
+
// Progress table — isComplete falls back to the legacy comparison in that case.
|
|
304
|
+
let roadmapTotalPhases = null;
|
|
305
|
+
let roadmapCompletedPhases = null;
|
|
306
|
+
if (hasRoadmap) {
|
|
307
|
+
try {
|
|
308
|
+
const roadmapContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf8');
|
|
309
|
+
const derived = deriveProgressFromRoadmap(roadmapContent);
|
|
310
|
+
roadmapTotalPhases = derived.totalPhases;
|
|
311
|
+
roadmapCompletedPhases = derived.completedPhases;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
/* ROADMAP.md unreadable — leave null; isComplete falls back to legacy. */
|
|
315
|
+
}
|
|
316
|
+
}
|
|
294
317
|
return {
|
|
295
318
|
current_phase: parseIntOrNull(currentPhaseRaw),
|
|
296
319
|
total_phases: parseIntOrNull(totalPhasesRaw),
|
|
@@ -305,14 +328,56 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
305
328
|
has_git: git.has_git,
|
|
306
329
|
verify_failed: verifyFailed,
|
|
307
330
|
stale_activity: staleActivity,
|
|
331
|
+
roadmap_total_phases: roadmapTotalPhases,
|
|
332
|
+
roadmap_completed_phases: roadmapCompletedPhases,
|
|
308
333
|
};
|
|
309
334
|
}
|
|
310
335
|
// ─── Situation classification ─────────────────────────────────────────────────
|
|
311
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* True when the workflow has fully completed all phases.
|
|
338
|
+
*
|
|
339
|
+
* #2427: completion is grounded in ROADMAP.md's Progress table (global,
|
|
340
|
+
* authoritative, never stale) when available, with a legacy fallback to
|
|
341
|
+
* STATE.md's cached `total_phases` when the roadmap has no parseable Progress
|
|
342
|
+
* table (e.g. a fresh project or a non-standard roadmap layout). The status
|
|
343
|
+
* regex was tightened to require milestone-level completion language
|
|
344
|
+
* (`milestone complete` / `all phases complete` / `complete(d)`) and no longer
|
|
345
|
+
* matches per-phase messages like "Phase X shipped — PR #N" that falsely
|
|
346
|
+
* satisfied the pre-fix alternation (`\bcomplete(d)?|done|shipped\b`).
|
|
347
|
+
*/
|
|
312
348
|
function isComplete(s) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
// Prefer ROADMAP-derived counts (global, authoritative) over STATE.md's
|
|
350
|
+
// cached milestone-scoped total_phases (stale-prone). Fall back to legacy
|
|
351
|
+
// when the roadmap has no Progress table.
|
|
352
|
+
if (s.roadmap_total_phases !== null && s.roadmap_completed_phases !== null) {
|
|
353
|
+
if (s.roadmap_total_phases === 0)
|
|
354
|
+
return false;
|
|
355
|
+
if (s.roadmap_completed_phases < s.roadmap_total_phases)
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Legacy path: STATE.md comparison. Still subject to the two-scale bug,
|
|
360
|
+
// but only fires when ROADMAP.md is absent or has no Progress table.
|
|
361
|
+
if (s.total_phases === null || s.current_phase === null)
|
|
362
|
+
return false;
|
|
363
|
+
if (s.current_phase < s.total_phases)
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
// Status regex: require milestone-level completion language. The pre-fix
|
|
367
|
+
// regex matched any "shipped" / "done" substring (per-phase language).
|
|
368
|
+
// Tightened to match:
|
|
369
|
+
// - "milestone complete" (ADR-2207 terminal status — usually written as
|
|
370
|
+
// "<version> milestone complete", e.g. "v1.0 milestone complete"; the
|
|
371
|
+
// substring match handles both forms)
|
|
372
|
+
// - "all phases complete" (ADR-2207 intermediate terminal)
|
|
373
|
+
// - "complete" / "completed" (legacy short form — STATE.md milestone
|
|
374
|
+
// status is a single value, not a per-phase log)
|
|
375
|
+
// Intentionally does NOT match "done" alone even though normalizeStateStatus
|
|
376
|
+
// (state-document.cts) treats "done" as "completed" — in the milestone
|
|
377
|
+
// status field, "done" is per-phase noise (e.g. "Phase X done"), not a
|
|
378
|
+
// milestone-completion signal. Mirrors workstream-inventory-builder.cts's
|
|
379
|
+
// terminal pattern \bmilestone\s+complete\b.
|
|
380
|
+
return /\b(milestone\s+complete|all\s+phases\s+complete|complete(d)?)\b/i.test(s.status);
|
|
316
381
|
}
|
|
317
382
|
/** Idle-stranded: clean tree, committed work not shipped, optionally stale. */
|
|
318
383
|
function isIdleStranded(s) {
|
|
@@ -153,11 +153,14 @@ function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
|
|
153
153
|
return false;
|
|
154
154
|
const existing = existingProgress;
|
|
155
155
|
const derived = derivedProgress;
|
|
156
|
-
// total_phases
|
|
157
|
-
// take the freshly derived value so
|
|
158
|
-
//
|
|
156
|
+
// total_phases (#1446) and total_plans (#2440) are intentionally excluded
|
|
157
|
+
// from the ratchet: both must always take the freshly derived value so they
|
|
158
|
+
// can correct in BOTH directions. total_plans legitimately moves up (a new
|
|
159
|
+
// phase adds plans) and down (milestone reorganization removes phases).
|
|
160
|
+
// Ratcheting it freezes stale values. Only completed_phases and
|
|
161
|
+
// completed_plans keep ratchet behaviour — they are monotonic (once a
|
|
162
|
+
// phase/plan is complete, it stays complete).
|
|
159
163
|
return (existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
|
|
160
|
-
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
|
|
161
164
|
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
|
|
162
165
|
}
|
|
163
166
|
function normalizeProgressNumbers(progress) {
|
|
@@ -100,7 +100,28 @@ function applyStatePreservation(input) {
|
|
|
100
100
|
!resync &&
|
|
101
101
|
preFm &&
|
|
102
102
|
preFm['progress']) {
|
|
103
|
-
|
|
103
|
+
// #2440: when the caller opts in (deriveProgressKeys), total_plans and
|
|
104
|
+
// total_phases always take the derived (post-sync) value even under !resync.
|
|
105
|
+
// This is used by cmdStatePlannedPhase where total_plans must correct upward
|
|
106
|
+
// after plans are added. For body-only writes (state.update/patch without
|
|
107
|
+
// the flag), the wholesale restore preserves everything as before — the
|
|
108
|
+
// #3242 Bug A protection stays fully in force.
|
|
109
|
+
if (input.deriveProgressKeys && postFm['progress']) {
|
|
110
|
+
const curated = preFm['progress'];
|
|
111
|
+
const derived = (postFm['progress'] ?? {});
|
|
112
|
+
const merged = { ...derived };
|
|
113
|
+
if (curated) {
|
|
114
|
+
for (const [key, value] of Object.entries(curated)) {
|
|
115
|
+
if (key !== 'total_plans' && key !== 'total_phases') {
|
|
116
|
+
merged[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
postFm['progress'] = merged;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
postFm['progress'] = preFm['progress'];
|
|
124
|
+
}
|
|
104
125
|
mutated = true;
|
|
105
126
|
}
|
|
106
127
|
// status — #1230 body-delta heuristic. Table: preserve-when-unchanged.
|
|
@@ -170,6 +170,12 @@ function cmdStateLoad(cwd, raw) {
|
|
|
170
170
|
state_exists: stateExists,
|
|
171
171
|
roadmap_exists: roadmapExists,
|
|
172
172
|
config_exists: configExists,
|
|
173
|
+
// #2376: absolute (anchored on cwd), not orchestrator-cwd-relative — a
|
|
174
|
+
// spawned subagent's own cwd may differ from the orchestrator's.
|
|
175
|
+
// debug.md has no init.* call of its own; it reads this field from
|
|
176
|
+
// `state load` to build debug_file_path for its gsd-debug-session-manager
|
|
177
|
+
// spawns instead of hardcoding '.planning/debug/{slug}.md'.
|
|
178
|
+
debug_dir: (0, shell_command_projection_cjs_1.toPosixPath)(node_path_1.default.join(planDir, 'debug')),
|
|
173
179
|
};
|
|
174
180
|
// For --raw, output a condensed key=value format
|
|
175
181
|
if (raw) {
|
|
@@ -1003,6 +1009,15 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1003
1009
|
// duplicate section alongside an existing one.
|
|
1004
1010
|
const existingCanonicalSession = /^## Session[ \t]*$/im.test(content);
|
|
1005
1011
|
const existingSessionContinuity = /^## Session Continuity[ \t]*$/im.test(content);
|
|
1012
|
+
// Track whether the chosen branch's rewrite actually matched. The detector
|
|
1013
|
+
// regexes (existingCanonicalSession/existingSessionContinuity) are CRLF-
|
|
1014
|
+
// tolerant ($ under /m treats \r as a line terminator); the writer regexes
|
|
1015
|
+
// below must be too. If a writer regex silently fails to match (line-ending
|
|
1016
|
+
// mismatch, unexpected heading shape, ...), do NOT report success — the
|
|
1017
|
+
// caller would believe fields were persisted that were silently dropped
|
|
1018
|
+
// (#2450). The append branch always sets rewriteMatched=true (it always
|
|
1019
|
+
// mutates content).
|
|
1020
|
+
let rewriteMatched = false;
|
|
1006
1021
|
if (existingCanonicalSession) {
|
|
1007
1022
|
// Normalize in place: replace the ENTIRE BODY of the existing ## Session
|
|
1008
1023
|
// section (heading + all content up to the next ## heading or EOF) with
|
|
@@ -1010,7 +1025,13 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1010
1025
|
// `(?!^## )[\s\S]` consumes every line that doesn't start with "## ",
|
|
1011
1026
|
// which correctly stops at the next section boundary without consuming it.
|
|
1012
1027
|
// A trailing blank line is added so the next ## heading keeps its spacing.
|
|
1013
|
-
|
|
1028
|
+
//
|
|
1029
|
+
// CRLF-tolerant (`\r?\n` after `[ \t]*`): the prior literal `\n` could not
|
|
1030
|
+
// match a CRLF STATE.md (`---\r\n`), silently no-op'ing the replace while
|
|
1031
|
+
// updated.push(...) reported success — #2450. The detector regex on the
|
|
1032
|
+
// line above (`/^## Session[ \t]*$/im`) was already CRLF-tolerant, so the
|
|
1033
|
+
// asymmetry armed the bug.
|
|
1034
|
+
const canonicalReplacement = [
|
|
1014
1035
|
'## Session',
|
|
1015
1036
|
'',
|
|
1016
1037
|
`**Last session:** ${now}`,
|
|
@@ -1018,7 +1039,11 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1018
1039
|
`**Resume file:** ${resumeValue}`,
|
|
1019
1040
|
'',
|
|
1020
1041
|
'',
|
|
1021
|
-
].join('\n')
|
|
1042
|
+
].join('\n');
|
|
1043
|
+
content = content.replace(/^(## Session[ \t]*\r?\n(?:(?!^## )[\s\S])*)/m, () => {
|
|
1044
|
+
rewriteMatched = true;
|
|
1045
|
+
return canonicalReplacement;
|
|
1046
|
+
});
|
|
1022
1047
|
}
|
|
1023
1048
|
else if (existingSessionContinuity) {
|
|
1024
1049
|
// #1101: a `## Session Continuity` section already exists (bootstrap
|
|
@@ -1029,6 +1054,8 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1029
1054
|
// (e.g. prose like "Next recommended action"). Fields already updated in
|
|
1030
1055
|
// place above (needs* false) are not re-inserted. A function replacement
|
|
1031
1056
|
// is used so `$`-bearing caller values are inserted literally (#3454).
|
|
1057
|
+
//
|
|
1058
|
+
// CRLF-tolerant (`\r?\n`): same #2450 fix as the canonical branch above.
|
|
1032
1059
|
const linesToInsert = [];
|
|
1033
1060
|
if (needsLastSession)
|
|
1034
1061
|
linesToInsert.push(`**Last session:** ${now}`);
|
|
@@ -1040,8 +1067,18 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1040
1067
|
// Case-insensitive to match the `existingSessionContinuity` detection
|
|
1041
1068
|
// above (#1101 review F3) — otherwise a lowercase heading would detect
|
|
1042
1069
|
// but no-op the insert while still reporting the fields as updated.
|
|
1043
|
-
content = content.replace(/^(## Session Continuity[ \t]*\n)/im, (_m, heading) =>
|
|
1070
|
+
content = content.replace(/^(## Session Continuity[ \t]*\r?\n)/im, (_m, heading) => {
|
|
1071
|
+
rewriteMatched = true;
|
|
1072
|
+
return heading + linesToInsert.join('\n') + '\n';
|
|
1073
|
+
});
|
|
1044
1074
|
}
|
|
1075
|
+
// No `else` branch: if linesToInsert.length === 0 the outer guard at
|
|
1076
|
+
// :1144 (callerSuppliedValues && (needsStoppedAt || needsResumeFile
|
|
1077
|
+
// || needsLastSession)) could not have fired, so this whole block is
|
|
1078
|
+
// unreachable. Leaving `rewriteMatched = false` here is the fail-loud
|
|
1079
|
+
// posture — a future change to the outer guard or needs* computation
|
|
1080
|
+
// that makes this branch reachable will surface as a missing
|
|
1081
|
+
// updated[] entry (silent recorded:false) rather than re-arming #2450.
|
|
1045
1082
|
}
|
|
1046
1083
|
else {
|
|
1047
1084
|
// No session heading exists at all — append a new canonical section.
|
|
@@ -1055,14 +1092,30 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
1055
1092
|
'',
|
|
1056
1093
|
].join('\n');
|
|
1057
1094
|
content = content.trimEnd() + '\n' + scaffold;
|
|
1095
|
+
rewriteMatched = true;
|
|
1096
|
+
}
|
|
1097
|
+
// #2450 defensive invariant: only report sessionCreated/updated when the
|
|
1098
|
+
// chosen branch's rewrite actually mutated content. Unreachable when the
|
|
1099
|
+
// writer regexes above stay in sync with the CRLF-tolerant detector —
|
|
1100
|
+
// but unreachable-defensive is the right posture for a silent-success
|
|
1101
|
+
// gate. A no-op replace here means a future line-ending or shape drift
|
|
1102
|
+
// between detector and writer; fail to record rather than claim success.
|
|
1103
|
+
//
|
|
1104
|
+
// Scope limitation (not a regression of this fix): the gate covers only
|
|
1105
|
+
// the section-rewrite block. The earlier in-place stateReplaceField
|
|
1106
|
+
// successes at :1081/:1083/:1089/:1101/:1108/:1114 push to `updated`
|
|
1107
|
+
// unconditionally — those represent fields that DID land on disk via
|
|
1108
|
+
// same-line replace (CRLF-agnostic seam), so unconditional push is
|
|
1109
|
+
// correct. The class-defect防御 here is for the INSERT path only.
|
|
1110
|
+
if (rewriteMatched) {
|
|
1111
|
+
sessionCreated = true;
|
|
1112
|
+
if (needsLastSession)
|
|
1113
|
+
updated.push('Last session');
|
|
1114
|
+
if (needsStoppedAt)
|
|
1115
|
+
updated.push('Stopped At');
|
|
1116
|
+
if (needsResumeFile)
|
|
1117
|
+
updated.push('Resume File');
|
|
1058
1118
|
}
|
|
1059
|
-
sessionCreated = true;
|
|
1060
|
-
if (needsLastSession)
|
|
1061
|
-
updated.push('Last session');
|
|
1062
|
-
if (needsStoppedAt)
|
|
1063
|
-
updated.push('Stopped At');
|
|
1064
|
-
if (needsResumeFile)
|
|
1065
|
-
updated.push('Resume File');
|
|
1066
1119
|
}
|
|
1067
1120
|
return content;
|
|
1068
1121
|
}, cwd);
|
|
@@ -1970,6 +2023,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1970
2023
|
const postFm = extractFrontmatter(synced);
|
|
1971
2024
|
const preservation = applyStatePreservation({
|
|
1972
2025
|
preFm, postFm, preFmSnapshot, resync,
|
|
2026
|
+
deriveProgressKeys: options?.deriveProgressKeys === true,
|
|
1973
2027
|
preBodyStatus, postBodyStatus,
|
|
1974
2028
|
preBodyStoppedAt, postBodyStoppedAt,
|
|
1975
2029
|
preBodyPhaseSource, postBodyPhaseSource,
|
|
@@ -2313,7 +2367,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
2313
2367
|
const result = transitionCore(content, intent, deps);
|
|
2314
2368
|
updated = result.updated;
|
|
2315
2369
|
return result.content;
|
|
2316
|
-
}, cwd, { resync: false });
|
|
2370
|
+
}, cwd, { resync: false, deriveProgressKeys: true });
|
|
2317
2371
|
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
|
2318
2372
|
}
|
|
2319
2373
|
/**
|