@opengsd/gsd-core 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +38 -2
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +18 -11
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +70 -64
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +6 -4
- package/gsd-core/bin/lib/commands.cjs +5 -1
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/init.cjs +11 -4
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/phase.cjs +120 -11
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +47 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +24 -16
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +31 -10
- package/gsd-core/bin/lib/state.cjs +90 -60
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +5 -1
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +37 -6
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +5 -1
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +72 -14
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +72 -14
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +9 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/release-tarball-smoke.cjs +1 -0
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -1067,6 +1067,62 @@ function describeGoalShapedTitle(description) {
|
|
|
1067
1067
|
return (`description looks goal-shaped, not title-shaped (${reasons}). It was written verbatim ` +
|
|
1068
1068
|
`as the phase title; consider a short title with the detail moved to **Goal:**.`);
|
|
1069
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Phase numbers the roadmap itself declares — a section header or a roadmap bullet — sentinels
|
|
1072
|
+
* excluded. This is deliberately NOT the allocation set: allocation also counts on-disk directories
|
|
1073
|
+
* and sibling-worktree phases so a new number cannot collide, but a `Depends on:` line has to point
|
|
1074
|
+
* at something a reader of THIS roadmap can find (#5113).
|
|
1075
|
+
*/
|
|
1076
|
+
function roadmapDeclaredPhaseNums(rawContent) {
|
|
1077
|
+
const { regex: headerPattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
|
|
1078
|
+
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
1079
|
+
const nums = new Set();
|
|
1080
|
+
let m;
|
|
1081
|
+
while ((m = headerPattern.exec(rawContent)) !== null) {
|
|
1082
|
+
const num = parseInt(m[phaseNumGroup], 10);
|
|
1083
|
+
if (!isSentinelPhaseId(num))
|
|
1084
|
+
nums.add(num);
|
|
1085
|
+
}
|
|
1086
|
+
while ((m = bulletPattern.exec(rawContent)) !== null) {
|
|
1087
|
+
const num = parseInt(m[1], 10);
|
|
1088
|
+
if (!isSentinelPhaseId(num))
|
|
1089
|
+
nums.add(num);
|
|
1090
|
+
}
|
|
1091
|
+
return [...nums];
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* The `**Depends on:**` line for a freshly added numeric phase: the nearest lower phase the roadmap
|
|
1095
|
+
* declares, or `Nothing` when it declares none. It was `newId - 1` regardless, which named a phase
|
|
1096
|
+
* that exists only on a sibling branch, as an orphan directory, or not at all when numbering has a
|
|
1097
|
+
* gap (#5113). Custom-named phases carry no dependency line.
|
|
1098
|
+
*/
|
|
1099
|
+
function dependsOnLine(newPhaseId, declared) {
|
|
1100
|
+
if (typeof newPhaseId !== 'number')
|
|
1101
|
+
return '';
|
|
1102
|
+
let nearest = null;
|
|
1103
|
+
for (const n of declared)
|
|
1104
|
+
if (n < newPhaseId && (nearest === null || n > nearest))
|
|
1105
|
+
nearest = n;
|
|
1106
|
+
return `\n**Depends on:** ${nearest === null ? 'Nothing' : `Phase ${nearest}`}`;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Splice a phase entry into `rawContent` at `at`, changing nothing else: the blank lines around the
|
|
1110
|
+
* block and its line endings are the block's own, so the surrounding document is published byte for
|
|
1111
|
+
* byte (#5113). `block` is LF text that starts and ends with a newline.
|
|
1112
|
+
*/
|
|
1113
|
+
function spliceRoadmapBlock(rawContent, atOffset, block) {
|
|
1114
|
+
const eol = rawContent.includes('\r\n') ? '\r\n' : '\n';
|
|
1115
|
+
// The insertion offset is found by searching for '\n---', which lands between the CR and the LF of
|
|
1116
|
+
// a CRLF pair; splice before the pair instead of through it.
|
|
1117
|
+
const at = rawContent[atOffset - 1] === '\r' && rawContent[atOffset] === '\n' ? atOffset - 1 : atOffset;
|
|
1118
|
+
const before = rawContent.slice(0, at);
|
|
1119
|
+
const after = rawContent.slice(at);
|
|
1120
|
+
const body = block.replace(/^\n+/, '').replace(/\n+$/, '\n').replace(/\n/g, eol);
|
|
1121
|
+
const blankBefore = before === '' || /(?:\r?\n){2}$/.test(before);
|
|
1122
|
+
const lead = blankBefore ? '' : /\n$/.test(before) ? eol : eol + eol;
|
|
1123
|
+
const tail = after === '' || /^\r?\n/.test(after) ? '' : eol;
|
|
1124
|
+
return before + lead + body + tail + after;
|
|
1125
|
+
}
|
|
1070
1126
|
/**
|
|
1071
1127
|
* #3163: compute the byte offset in `rawContent` where a new `### Phase N:`
|
|
1072
1128
|
* entry should be inserted — at the end of the active phase list, scoped to the
|
|
@@ -1310,13 +1366,11 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1310
1366
|
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', _dirName);
|
|
1311
1367
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
1312
1368
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
1313
|
-
const dependsOn = config.phase_naming === 'custom'
|
|
1314
|
-
? ''
|
|
1315
|
-
: `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
|
|
1369
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(_newPhaseId, roadmapDeclaredPhaseNums(rawContent));
|
|
1316
1370
|
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_newPhaseId} to break down)\n`;
|
|
1317
1371
|
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
|
1318
|
-
const updatedContent = rawContent
|
|
1319
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
|
|
1372
|
+
const updatedContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
|
|
1373
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent, { verbatim: true });
|
|
1320
1374
|
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
|
1321
1375
|
});
|
|
1322
1376
|
const titleWarning = describeGoalShapedTitle(description);
|
|
@@ -1432,12 +1486,10 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1432
1486
|
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', dirName);
|
|
1433
1487
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
1434
1488
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
1435
|
-
const dependsOn = config.phase_naming === 'custom'
|
|
1436
|
-
? ''
|
|
1437
|
-
: `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
|
1489
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(newPhaseId, roadmapDeclaredPhaseNums(rawContent));
|
|
1438
1490
|
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${newPhaseId} to break down)\n`;
|
|
1439
1491
|
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
|
1440
|
-
rawContent = rawContent
|
|
1492
|
+
rawContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
|
|
1441
1493
|
added.push({
|
|
1442
1494
|
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
1443
1495
|
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
@@ -1447,7 +1499,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1447
1499
|
naming_mode: config.phase_naming,
|
|
1448
1500
|
});
|
|
1449
1501
|
}
|
|
1450
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent);
|
|
1502
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent, { verbatim: true });
|
|
1451
1503
|
return added;
|
|
1452
1504
|
});
|
|
1453
1505
|
output({ phases: results, count: results.length }, raw);
|
|
@@ -3061,6 +3113,42 @@ function formatRequirementsLineWarning(phaseNum, rawLine, analysis) {
|
|
|
3061
3113
|
oversized,
|
|
3062
3114
|
};
|
|
3063
3115
|
}
|
|
3116
|
+
/**
|
|
3117
|
+
* #4972: advisory gaps for the phase artifacts that active `verify:post` steps
|
|
3118
|
+
* declared `onError: "halt"` and `produces` (SECURITY.md, VALIDATION.md).
|
|
3119
|
+
* Missing file, an unresolved `status: draft`, or (SECURITY.md) `threats_open`
|
|
3120
|
+
* above zero. Any other `status` spelling counts as closed — the audit skills
|
|
3121
|
+
* do not pin one value, so this does not invent a vocabulary.
|
|
3122
|
+
*/
|
|
3123
|
+
function auditArtifactWarnings(cwd, phaseFullDir) {
|
|
3124
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy: loop-resolver pulls in the capability registry
|
|
3125
|
+
const loopResolver = require('./loop-resolver.cjs');
|
|
3126
|
+
const out = [];
|
|
3127
|
+
const files = node_fs_1.default.readdirSync(phaseFullDir);
|
|
3128
|
+
for (const hook of loopResolver.resolveActiveHooksForPoint(cwd, 'verify:post').activeHooks) {
|
|
3129
|
+
if (hook.kind !== 'step' || hook.onError !== 'halt')
|
|
3130
|
+
continue;
|
|
3131
|
+
for (const artifact of hook.produces ?? []) {
|
|
3132
|
+
if (!/^[A-Z][A-Z0-9-]*\.md$/.test(artifact))
|
|
3133
|
+
continue;
|
|
3134
|
+
const skill = hook.ref?.skill ?? hook.capId;
|
|
3135
|
+
const found = files.filter((f) => f === artifact || f.endsWith(`-${artifact}`)).sort()[0];
|
|
3136
|
+
if (!found) {
|
|
3137
|
+
out.push(`${artifact}: missing — the ${hook.capId} audit (/gsd:${skill}) never ran for this phase, and a verify:post step cannot block the close (#4972)`);
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const fm = frontmatterMod.extractFrontmatter(node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, found), 'utf-8'));
|
|
3141
|
+
if (typeof fm['status'] === 'string' && fm['status'].trim().toLowerCase() === 'draft') {
|
|
3142
|
+
out.push(`${found}: status is still draft — the ${hook.capId} audit (/gsd:${skill}) was not completed (#4972)`);
|
|
3143
|
+
}
|
|
3144
|
+
const open = Number(fm['threats_open']);
|
|
3145
|
+
if (artifact === 'SECURITY.md' && Number.isFinite(open) && open > 0) {
|
|
3146
|
+
out.push(`${found}: ${open} threat(s) still open (#4972)`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
return out;
|
|
3151
|
+
}
|
|
3064
3152
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
3065
3153
|
if (!phaseNum) {
|
|
3066
3154
|
error('phase number required for phase complete');
|
|
@@ -3274,6 +3362,23 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3274
3362
|
/* best-effort, same posture as the #2245 pre-scan above: an unreadable
|
|
3275
3363
|
* SUMMARY means one fewer advisory this run, never a blocked completion. */
|
|
3276
3364
|
}
|
|
3365
|
+
// #4972: a `verify:post` STEP declared `onError: "halt"` that produces a
|
|
3366
|
+
// phase artifact (security -> SECURITY.md, nyquist -> VALIDATION.md) reads as
|
|
3367
|
+
// a blocking precondition, but a step is advisory by construction (ADR-894
|
|
3368
|
+
// "steps are additive, gates block") and the workflow only prints a line the
|
|
3369
|
+
// operator can ignore — so a phase could close with the audit never run. This
|
|
3370
|
+
// verb is the one place every close passes through, so it names the gap on
|
|
3371
|
+
// the same ADVISORY `warnings[]` channel as the #2572 check above. Advisory,
|
|
3372
|
+
// not a refusal: both capabilities are on by default, so a hard block here
|
|
3373
|
+
// would stop every existing project's next phase close; the security
|
|
3374
|
+
// threats-open gate at `ship:pre` is the enforced one and is untouched.
|
|
3375
|
+
try {
|
|
3376
|
+
warnings.push(...auditArtifactWarnings(cwd, phaseFullDir));
|
|
3377
|
+
}
|
|
3378
|
+
catch {
|
|
3379
|
+
/* best-effort, same posture as the advisories above: an unresolvable hook
|
|
3380
|
+
* set or unreadable artifact means one fewer advisory, never a blocked close. */
|
|
3381
|
+
}
|
|
3277
3382
|
let nextPhaseNum = null;
|
|
3278
3383
|
let nextPhaseName = null;
|
|
3279
3384
|
let isLastPhase = true;
|
|
@@ -4230,7 +4335,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4230
4335
|
nextPhaseDisk,
|
|
4231
4336
|
}, {
|
|
4232
4337
|
clock: clock_cjs_1.realClock,
|
|
4233
|
-
|
|
4338
|
+
// #4901: the body counter derives from the same current-milestone scope
|
|
4339
|
+
// as its frontmatter twin below (postCompletionRoadmapScope), not the
|
|
4340
|
+
// whole file, so the two writers cannot disagree on which
|
|
4341
|
+
// `## Progress` table they count.
|
|
4342
|
+
roadmapProvider: () => (roadmapContent !== null ? extractCurrentMilestone(roadmapContent, cwd) : null),
|
|
4234
4343
|
sourcePath: statePath,
|
|
4235
4344
|
});
|
|
4236
4345
|
stateContent = completeResult.content;
|
|
@@ -6,7 +6,7 @@ exports.parseTestReport = parseTestReport;
|
|
|
6
6
|
const tap_parser_cjs_1 = require("./vendor/tap-parser.cjs");
|
|
7
7
|
const saxes_cjs_1 = require("./vendor/saxes.cjs");
|
|
8
8
|
exports.ReportFormat = {
|
|
9
|
-
Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', Unknown: 'unknown',
|
|
9
|
+
Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', GoTest: 'go-test', Unknown: 'unknown',
|
|
10
10
|
};
|
|
11
11
|
exports.TestStatus = { Passed: 'passed', Failed: 'failed', Skipped: 'skipped', Todo: 'todo' };
|
|
12
12
|
function parseTap(output) {
|
|
@@ -247,7 +247,63 @@ function parseUnittest(output) {
|
|
|
247
247
|
report.valid = report.issues.length === 0;
|
|
248
248
|
return report;
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* `go test` text output (#5106). Plain console text, so the shape is matched by line:
|
|
252
|
+
* --- FAIL: TestName (0.00s) one per failed test (every test with -v; subtests indented)
|
|
253
|
+
* FAIL\texample.com/pkg\t0.004s one package summary per package, after that package's tests
|
|
254
|
+
* FAIL\texample.com/pkg [build failed] the package did not compile — nothing asserted anything
|
|
255
|
+
* A test's name carries no package, so each test is also known as `<package>.<name>`, attributed to
|
|
256
|
+
* the next package summary line. The same name failing in two packages is ambiguous until qualified.
|
|
257
|
+
* `go test -json` is not text and stays unsupported.
|
|
258
|
+
*/
|
|
259
|
+
const GO_TEST_RESULT = /^[ \t]*-{3} (PASS|FAIL|SKIP): (\S+) \(\d+(?:\.\d+)?s\)$/; // -{3}: not a literal fence, see frontmatter-fence-drift
|
|
260
|
+
const GO_PACKAGE_LINE = /^(ok|FAIL|\?)[ \t]+(\S+)[ \t]+(?:\d+(?:\.\d+)?s\b|\(cached\)|\[([a-z ]+)\])/;
|
|
261
|
+
function isGoTestOutput(output) {
|
|
262
|
+
if (/^TAP version \d+/.test(output.trimStart()))
|
|
263
|
+
return false;
|
|
264
|
+
return output.split(/\r?\n/).some((line) => GO_TEST_RESULT.test(line) || GO_PACKAGE_LINE.test(line));
|
|
265
|
+
}
|
|
266
|
+
function parseGoTest(output) {
|
|
267
|
+
const report = { format: exports.ReportFormat.GoTest, valid: true, tests: [], issues: [] };
|
|
268
|
+
let segment = [];
|
|
269
|
+
let packages = 0;
|
|
270
|
+
for (const line of output.split(/\r?\n/)) {
|
|
271
|
+
const result = GO_TEST_RESULT.exec(line);
|
|
272
|
+
if (result) {
|
|
273
|
+
const name = result[2];
|
|
274
|
+
const status = result[1] === 'FAIL' ? exports.TestStatus.Failed : result[1] === 'SKIP' ? exports.TestStatus.Skipped : exports.TestStatus.Passed;
|
|
275
|
+
if (!segment.some((test) => test.name === name && test.status === status)) {
|
|
276
|
+
segment.push({ name, identities: [name], group: null, groupIdentities: [], status });
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const pkg = GO_PACKAGE_LINE.exec(line);
|
|
281
|
+
if (!pkg)
|
|
282
|
+
continue;
|
|
283
|
+
packages++;
|
|
284
|
+
const [, outcome, importPath, bracket] = pkg;
|
|
285
|
+
if (outcome === 'FAIL' && (bracket === 'build failed' || bracket === 'setup failed')) {
|
|
286
|
+
report.buildFailed = true;
|
|
287
|
+
report.issues.push(`go test package ${importPath} ${bracket}`);
|
|
288
|
+
}
|
|
289
|
+
else if (outcome === 'ok' && segment.some((test) => test.status === exports.TestStatus.Failed)) {
|
|
290
|
+
report.issues.push(`go test package ${importPath} is ok but its output reports a failed test`);
|
|
291
|
+
}
|
|
292
|
+
for (const test of segment)
|
|
293
|
+
test.identities.push(`${importPath}.${test.name}`);
|
|
294
|
+
report.tests.push(...segment);
|
|
295
|
+
segment = [];
|
|
296
|
+
}
|
|
297
|
+
if (packages === 0)
|
|
298
|
+
report.issues.push('Missing go test package summary');
|
|
299
|
+
if (segment.length > 0)
|
|
300
|
+
report.issues.push('go test results without a following package summary');
|
|
301
|
+
report.valid = report.issues.length === 0;
|
|
302
|
+
return report;
|
|
303
|
+
}
|
|
250
304
|
const adapters = [
|
|
305
|
+
// Before TAP: a run whose first package is fine begins `ok <TAB>pkg`, which the TAP matcher reads as a point.
|
|
306
|
+
{ matches: isGoTestOutput, parse: parseGoTest },
|
|
251
307
|
{ matches: (output) => /^(?:TAP version \d+|(?:not )?ok\b|1\.\.\d+|#)/.test(output.trimStart()), parse: parseTap },
|
|
252
308
|
{ matches: (output) => output.trimStart().startsWith('<'), parse: parseJunit },
|
|
253
309
|
{ matches: (output) => new RegExp(SWIFT_AGGREGATE.source, 'm').test(output), parse: parseSwiftTesting },
|
|
@@ -31,6 +31,7 @@ exports.resolveTimeoutMs = resolveTimeoutMs;
|
|
|
31
31
|
exports.nativeTimeoutToken = nativeTimeoutToken;
|
|
32
32
|
exports.isEmptyReview = isEmptyReview;
|
|
33
33
|
exports.fileRefPrompt = fileRefPrompt;
|
|
34
|
+
exports.applyPromptOverride = applyPromptOverride;
|
|
34
35
|
exports.artifactPaths = artifactPaths;
|
|
35
36
|
exports.resolveLaneBudget = resolveLaneBudget;
|
|
36
37
|
exports.resolveLanePlan = resolveLanePlan;
|
|
@@ -229,6 +230,45 @@ function fileRefPrompt(promptPath, repoRoot) {
|
|
|
229
230
|
`review request against that absolute root. Output only the resulting markdown review. ` +
|
|
230
231
|
`Do not edit any files.`);
|
|
231
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Re-point a resolved plan at a different prompt file (`review-lane invoke --prompt-file`, #5049).
|
|
235
|
+
*
|
|
236
|
+
* The override carries a budget-trimmed prompt, so it has to land wherever THIS lane actually
|
|
237
|
+
* reads its prompt, and that differs by channel:
|
|
238
|
+
* - `stdin`: the runner streams the file `plan.stdin` names.
|
|
239
|
+
* - `argv-file-ref`: the prompt PATH is embedded in a sentence in argv (`fileRefPrompt`), already
|
|
240
|
+
* expanded by the time an override can run. Swapping `plan.promptPath` alone changes nothing
|
|
241
|
+
* the CLI reads, which is how the override was dropped silently for cursor, antigravity and
|
|
242
|
+
* kimi-code.
|
|
243
|
+
* - `argv`: the bare path is an argv element.
|
|
244
|
+
* - `openai-http`: the runner reads `plan.promptPath`.
|
|
245
|
+
* Argv elements are matched whole (the file-ref sentence, or the bare path), never by substring,
|
|
246
|
+
* so an unrelated argument that merely contains the path is left alone. `promptPath` moves with
|
|
247
|
+
* argv: the antigravity handler re-derives the sentence it swaps from `plan.promptPath` and would
|
|
248
|
+
* otherwise stop recognising it. A plan that takes no prompt (`none`) is returned unchanged.
|
|
249
|
+
* Pure: returns a new plan and never mutates the one it is given.
|
|
250
|
+
*/
|
|
251
|
+
function applyPromptOverride(plan, promptPath, repoRoot) {
|
|
252
|
+
if (plan.transport !== 'spawn')
|
|
253
|
+
return { ...plan, promptPath };
|
|
254
|
+
if (plan.stdin)
|
|
255
|
+
return { ...plan, stdin: promptPath, promptPath };
|
|
256
|
+
const oldRef = fileRefPrompt(plan.promptPath, repoRoot);
|
|
257
|
+
const newRef = fileRefPrompt(promptPath, repoRoot);
|
|
258
|
+
let replaced = false;
|
|
259
|
+
const argv = plan.argv.map((a) => {
|
|
260
|
+
if (a === oldRef) {
|
|
261
|
+
replaced = true;
|
|
262
|
+
return newRef;
|
|
263
|
+
}
|
|
264
|
+
if (a === plan.promptPath) {
|
|
265
|
+
replaced = true;
|
|
266
|
+
return promptPath;
|
|
267
|
+
}
|
|
268
|
+
return a;
|
|
269
|
+
});
|
|
270
|
+
return replaced ? { ...plan, argv, promptPath } : plan;
|
|
271
|
+
}
|
|
232
272
|
/** Run-dir artifact paths. POSIX-joined: these are workflow-visible strings, not OS paths. */
|
|
233
273
|
function artifactPaths(runDir, slug) {
|
|
234
274
|
const base = String(runDir ?? '').replace(/\/+$/, '');
|
|
@@ -113,6 +113,22 @@ exports.KNOWN_REVIEWER_SLUGS = (() => {
|
|
|
113
113
|
return [];
|
|
114
114
|
}
|
|
115
115
|
})();
|
|
116
|
+
/**
|
|
117
|
+
* #5359: map a lane flag alias (`agy` for `--agy`) to its canonical slug via the
|
|
118
|
+
* descriptor roster's declared `flags` (same source as `review-lane explicit-from-argv`).
|
|
119
|
+
* Guarded like KNOWN_REVIEWER_SLUGS: a load failure degrades to the plain error.
|
|
120
|
+
*/
|
|
121
|
+
function laneSlugForAlias(name, lanes) {
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
124
|
+
const roster = lanes ?? require('./review-lane-descriptor.cjs').REVIEWER_LANES;
|
|
125
|
+
// `l.slug !== name`: an undetected canonical slug (`--codex` flag) is not an alias.
|
|
126
|
+
return roster.find((l) => l.slug !== name && l.flags.includes(`--${name}`))?.slug;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
116
132
|
/** Instance names are lowercase slugs that must not shadow a built-in slug. */
|
|
117
133
|
exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
118
134
|
function normalizeConfiguredDefaultReviewers(rawValue) {
|
|
@@ -239,7 +255,10 @@ function resolveReviewerSelection(input) {
|
|
|
239
255
|
// Sorted so the error order does not depend on flag order on the command line.
|
|
240
256
|
const missing = [...explicitFlags].filter((slug) => !detected.has(slug)).sort();
|
|
241
257
|
for (const slug of missing) {
|
|
242
|
-
|
|
258
|
+
const canonical = laneSlugForAlias(slug, input.lanes);
|
|
259
|
+
errors.push(canonical
|
|
260
|
+
? `explicit reviewer '${slug}' is a flag alias; pass its canonical slug '${canonical}'`
|
|
261
|
+
: `explicit reviewer '${slug}' is not available on this host`);
|
|
243
262
|
}
|
|
244
263
|
// Guarded on the error count as it stood BEFORE this branch, so the
|
|
245
264
|
// aggregate message fires under exactly the conditions it did previously.
|
|
@@ -1075,6 +1075,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1075
1075
|
// refusal below declines with the analyzer's own `missing_phase_details`
|
|
1076
1076
|
// vocabulary and leaves ROADMAP.md byte-identical.
|
|
1077
1077
|
let missingPhaseDetails = false;
|
|
1078
|
+
const warnings = [];
|
|
1078
1079
|
withPlanningLock(cwd, () => {
|
|
1079
1080
|
// #3957 (B9.4): captured BEFORE any transform runs, so the write/report
|
|
1080
1081
|
// decision below reflects whether the transforms actually changed
|
|
@@ -1190,6 +1191,16 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1190
1191
|
}
|
|
1191
1192
|
return text;
|
|
1192
1193
|
});
|
|
1194
|
+
// #4947: a word-prefixed Phase cell (`Phase 68`) is deliberately not a
|
|
1195
|
+
// writable table target (#4247 — the read side ignores it too), but when the
|
|
1196
|
+
// phase's heading section is still written the command reports
|
|
1197
|
+
// `updated: true` and the unwritten Status cell looked like a silent bug.
|
|
1198
|
+
// The row stays byte-identical; the output now says so.
|
|
1199
|
+
const wordPrefixedCellRe = new RegExp(`^Phase\\s+${phasePattern}\\.?(?:\\s|$)`, 'i');
|
|
1200
|
+
const progressTable = tableRowFound ? null : (0, markdown_table_cjs_1.findTableWithColumns)(gateActiveRegion, ['Phase', 'Status']);
|
|
1201
|
+
if (progressTable && progressTable.rows.some((row) => wordPrefixedCellRe.test((row['Phase'] ?? '').trim()))) {
|
|
1202
|
+
warnings.push(`Progress table row for phase ${formatDiagnosticToken(String(phaseNum))} was not updated: its Phase cell is word-prefixed ("Phase N"), and only a bare phase number (N or N.) is recognized. Edit that row by hand or rewrite the cell as the bare number.`);
|
|
1203
|
+
}
|
|
1193
1204
|
// Update plan count in phase detail section.
|
|
1194
1205
|
// Three recognised forms (all tolerated; canonical template uses the first):
|
|
1195
1206
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
@@ -1247,6 +1258,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1247
1258
|
// wording, so it correctly falls through to arm 3 untouched. Kept as a
|
|
1248
1259
|
// caller-side classifier (NOT seam grammar) reused below against the
|
|
1249
1260
|
// `boldField` node's own parsed `value`.
|
|
1261
|
+
// #4947: a value that is exactly `TBD` is the planner's own "no plans yet"
|
|
1262
|
+
// marker, not an operator annotation — once plans exist it contradicts the
|
|
1263
|
+
// checklist this verb inserts right under it. `TBD — note` keeps its note
|
|
1264
|
+
// (row 5): on the bold path the note is the node's trailing span, which the
|
|
1265
|
+
// caller checks is empty; on the plain path the note is part of the value,
|
|
1266
|
+
// so the whole-value comparison already excludes it.
|
|
1267
|
+
const isBareTbd = (value) => value.trim() === 'TBD';
|
|
1250
1268
|
const isTemplatePlaceholder = (value) => {
|
|
1251
1269
|
const trimmed = value.trim();
|
|
1252
1270
|
return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
|
|
@@ -1272,6 +1290,35 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1272
1290
|
const m = value.match(/^\d+\s+plans?/i);
|
|
1273
1291
|
return m ? m[0].length : -1;
|
|
1274
1292
|
};
|
|
1293
|
+
// #4947: the pending-plan clause a human or planner glues after the count
|
|
1294
|
+
// (`; 2 gap-closure plans pending (48-18, 48-19)`) is DERIVED from the same
|
|
1295
|
+
// plan/summary facts this verb already computes, so a completed plan left
|
|
1296
|
+
// listed as pending contradicts the count the verb just wrote. The verb
|
|
1297
|
+
// owns that one clause: ids that now have a SUMMARY.md are removed, the
|
|
1298
|
+
// number is recounted, and an emptied clause is dropped. Everything else in
|
|
1299
|
+
// the tail (`in 11 waves`, free prose) stays the operator's — a wave total
|
|
1300
|
+
// does not change when a plan completes. A clause whose ids are not all
|
|
1301
|
+
// plan ids (`(see backlog)`) is not provably derived, so it is left alone.
|
|
1302
|
+
const summarizedPlanIds = new Set(phaseInfo.summaries
|
|
1303
|
+
.map((summaryFile) => summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
|
1304
|
+
.filter((id) => id !== ''));
|
|
1305
|
+
const pendingClauseRe = /;[ \t]*(\d+)[ \t]+((?:[\w-]+[ \t]+)*?)plans?[ \t]+pending[ \t]*\(([^()\r\n]*)\)/i;
|
|
1306
|
+
const planIdShape = /^\d+(?:\.\d+)?-\d+(?:\.\d+)?$/;
|
|
1307
|
+
const reconcilePendingClause = (tail) => {
|
|
1308
|
+
const m = pendingClauseRe.exec(tail);
|
|
1309
|
+
if (!m)
|
|
1310
|
+
return tail;
|
|
1311
|
+
const ids = m[3].split(',').map((id) => id.trim());
|
|
1312
|
+
if (ids.length === 0 || !ids.every((id) => planIdShape.test(id)))
|
|
1313
|
+
return tail;
|
|
1314
|
+
const stillPending = ids.filter((id) => !summarizedPlanIds.has(id));
|
|
1315
|
+
if (stillPending.length === ids.length)
|
|
1316
|
+
return tail;
|
|
1317
|
+
const replacement = stillPending.length === 0
|
|
1318
|
+
? ''
|
|
1319
|
+
: `; ${stillPending.length} ${m[2]}plan${stillPending.length === 1 ? '' : 's'} pending (${stillPending.join(', ')})`;
|
|
1320
|
+
return tail.slice(0, m.index) + replacement + tail.slice(m.index + m[0].length);
|
|
1321
|
+
};
|
|
1275
1322
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, planSectionPattern, (sectionText) => {
|
|
1276
1323
|
const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(sectionText, 'ROADMAP.md');
|
|
1277
1324
|
if (!parsed.ok) {
|
|
@@ -1302,12 +1349,12 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1302
1349
|
const plainFractionLen = fractionCountPrefixLength(plainValue);
|
|
1303
1350
|
const plainBareLen = bareCountPrefixLength(plainValue);
|
|
1304
1351
|
const plainCountPrefixLen = plainFractionLen >= 0 ? plainFractionLen : plainBareLen;
|
|
1305
|
-
if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue)) {
|
|
1352
|
+
if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue) && !isBareTbd(plainValue)) {
|
|
1306
1353
|
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
1307
1354
|
// empty value — leave the section exactly as it was.
|
|
1308
1355
|
return sectionText;
|
|
1309
1356
|
}
|
|
1310
|
-
const plainSuffix = plainCountPrefixLen >= 0 ? plainValue.slice(plainCountPrefixLen) : '';
|
|
1357
|
+
const plainSuffix = plainCountPrefixLen >= 0 ? reconcilePendingClause(plainValue.slice(plainCountPrefixLen)) : '';
|
|
1311
1358
|
const newPlainLine = `${indent}Plans:${spacing}${planCountText}${plainSuffix}`;
|
|
1312
1359
|
const start = plainMatch.index ?? sectionText.indexOf(whole);
|
|
1313
1360
|
return sectionText.slice(0, start) + newPlainLine + sectionText.slice(start + whole.length);
|
|
@@ -1320,7 +1367,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1320
1367
|
const fractionPrefixLen = fractionCountPrefixLength(currentValue);
|
|
1321
1368
|
const barePrefixLen = bareCountPrefixLength(currentValue);
|
|
1322
1369
|
const countPrefixLen = fractionPrefixLen >= 0 ? fractionPrefixLen : barePrefixLen;
|
|
1323
|
-
|
|
1370
|
+
const fieldNode = parsed.value.nodes.find((n) => n.id === fieldId);
|
|
1371
|
+
const trailingText = fieldNode && fieldNode.kind === 'boldField'
|
|
1372
|
+
? parsed.value.source.slice(fieldNode.trailingSpan.start, fieldNode.trailingSpan.end)
|
|
1373
|
+
: '';
|
|
1374
|
+
if (countPrefixLen < 0 && !isTemplatePlaceholder(currentValue) && !(isBareTbd(currentValue) && trailingText.trim() === '')) {
|
|
1324
1375
|
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
1325
1376
|
// empty value — leave the section exactly as it was.
|
|
1326
1377
|
return sectionText;
|
|
@@ -1331,7 +1382,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1331
1382
|
// `setFieldValue`'s valueSpan/trailingSpan split additionally preserves
|
|
1332
1383
|
// any EM-DASH-separated trailing annotation (#2853) automatically — no
|
|
1333
1384
|
// separate "preserve trailing" branch needed for that shape.
|
|
1334
|
-
const suffix = countPrefixLen >= 0 ? currentValue.slice(countPrefixLen) : '';
|
|
1385
|
+
const suffix = countPrefixLen >= 0 ? reconcilePendingClause(currentValue.slice(countPrefixLen)) : '';
|
|
1335
1386
|
const newValueToWrite = planCountText + suffix;
|
|
1336
1387
|
const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
|
|
1337
1388
|
if (!staged.ok) {
|
|
@@ -1490,7 +1541,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1490
1541
|
verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
|
|
1491
1542
|
};
|
|
1492
1543
|
if (updated) {
|
|
1493
|
-
output({ updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
|
|
1544
|
+
output(warnings.length > 0 ? { updated: true, ...computed, warnings } : { updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
|
|
1494
1545
|
}
|
|
1495
1546
|
else if (missingPhaseDetails) {
|
|
1496
1547
|
// #4247: honest refusal — the reason names the real condition (the
|
|
@@ -2923,8 +2923,13 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
|
|
|
2923
2923
|
// See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md.
|
|
2924
2924
|
const posixTarget = (0, shell_command_projection_cjs_1.posixNormalize)(String(resolvedTarget));
|
|
2925
2925
|
const posixHome = homeDir ? (0, shell_command_projection_cjs_1.posixNormalize)(String(homeDir)) : homeDir;
|
|
2926
|
-
|
|
2927
|
-
|
|
2926
|
+
// #5123: `startsWith` alone treats a SIBLING directory whose name begins with
|
|
2927
|
+
// the home path (`/home/u2/cfg` under `/home/u`) as under home and yields the
|
|
2928
|
+
// broken `$HOMEu2/cfg/`. Require a path boundary.
|
|
2929
|
+
const underHome = typeof posixHome === 'string' && posixHome.length > 0
|
|
2930
|
+
&& (posixTarget === posixHome || posixTarget.startsWith(posixHome.endsWith('/') ? posixHome : `${posixHome}/`));
|
|
2931
|
+
if (isGlobal && underHome && !isOpencode) {
|
|
2932
|
+
return '$HOME' + posixTarget.slice(posixHome.length).replace(/^(?!\/)(?=.)/, '/') + '/';
|
|
2928
2933
|
}
|
|
2929
2934
|
if (!isGlobal && projectRelative) {
|
|
2930
2935
|
// An explicit local config dir need not be the descriptor's conventional
|
|
@@ -2938,6 +2943,44 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
|
|
|
2938
2943
|
}
|
|
2939
2944
|
return `${posixTarget}/`;
|
|
2940
2945
|
}
|
|
2946
|
+
/**
|
|
2947
|
+
* #5123: retarget an installed artifact's `@` includes to the REAL install root.
|
|
2948
|
+
* Driven by the runtime descriptor's hostBehaviors, never by a runtime name:
|
|
2949
|
+
*
|
|
2950
|
+
* - `defaultHomeIncludeRoot` (Copilot, Antigravity): their converters hard-code
|
|
2951
|
+
* the DEFAULT home into includes, so with `--config-dir` the payload lives
|
|
2952
|
+
* elsewhere and `@~/<root>/gsd-core/…` points at nothing. `@~/<root>/` and
|
|
2953
|
+
* `@$HOME/<root>/` become `@<pathPrefix>`. A default install (pathPrefix is
|
|
2954
|
+
* already `$HOME/<root>/`) is returned unchanged, so it stays byte-identical.
|
|
2955
|
+
* - `retargetsGsdCoreProsePath` (Kimi): Kimi has no `@` include, so its prompt
|
|
2956
|
+
* converter turns `~/.claude/gsd-core` into the prose "GSD core", leaving
|
|
2957
|
+
* `@GSD core/x.md` pointing nowhere. A `GSD core/<path>` reference followed
|
|
2958
|
+
* by a path segment becomes the real `<pathPrefix>gsd-core/<path>`, with the
|
|
2959
|
+
* unsupported `@` dropped. Bare prose "GSD core" is not followed by `/`.
|
|
2960
|
+
*
|
|
2961
|
+
* `pathPrefix` is the install root as computePathPrefix derives it
|
|
2962
|
+
* (`$HOME/…/` under HOME, otherwise absolute), with a trailing slash. Pure and
|
|
2963
|
+
* idempotent: a second pass finds none of the literals it rewrites.
|
|
2964
|
+
*/
|
|
2965
|
+
function retargetInstalledIncludes(content, behaviors, pathPrefix) {
|
|
2966
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
2967
|
+
return content;
|
|
2968
|
+
if (typeof pathPrefix !== 'string' || !pathPrefix.endsWith('/'))
|
|
2969
|
+
return content;
|
|
2970
|
+
if (!behaviors)
|
|
2971
|
+
return content;
|
|
2972
|
+
let result = content;
|
|
2973
|
+
if (behaviors.retargetsGsdCoreProsePath === true) {
|
|
2974
|
+
result = result.replace(/@?GSD core\/(?=[a-z])/g, `${pathPrefix}gsd-core/`);
|
|
2975
|
+
}
|
|
2976
|
+
const home = behaviors.defaultHomeIncludeRoot;
|
|
2977
|
+
if (typeof home === 'string' && home.length > 0 && pathPrefix !== `$HOME/${home}/`) {
|
|
2978
|
+
result = result
|
|
2979
|
+
.split(`@~/${home}/`).join(`@${pathPrefix}`)
|
|
2980
|
+
.split(`@$HOME/${home}/`).join(`@${pathPrefix}`);
|
|
2981
|
+
}
|
|
2982
|
+
return result;
|
|
2983
|
+
}
|
|
2941
2984
|
/** Return a safe project-relative prefix for a resolved install target. */
|
|
2942
2985
|
function projectRelativePrefixFromProjectRoot(projectRoot, resolvedTarget) {
|
|
2943
2986
|
if (typeof projectRoot !== 'string' || typeof resolvedTarget !== 'string')
|
|
@@ -3974,4 +4017,6 @@ module.exports = {
|
|
|
3974
4017
|
_negotiatedDispatchIsolation,
|
|
3975
4018
|
// #1521: canonical non-Claude runtime list for test files and tooling
|
|
3976
4019
|
NON_CLAUDE_RUNTIMES,
|
|
4020
|
+
// #5123: post-install include retarget (config-dir aware; Kimi prose path)
|
|
4021
|
+
retargetInstalledIncludes,
|
|
3977
4022
|
};
|
|
@@ -40,6 +40,7 @@ exports.resolveConfigHomeFromDescriptor = resolveConfigHomeFromDescriptor;
|
|
|
40
40
|
exports.resolveAntigravityGlobalDir = resolveAntigravityGlobalDir;
|
|
41
41
|
exports.detectAntigravityDirAmbiguity = detectAntigravityDirAmbiguity;
|
|
42
42
|
exports.resolveKimiGlobalDir = resolveKimiGlobalDir;
|
|
43
|
+
exports.resolveGsdHome = resolveGsdHome;
|
|
43
44
|
exports.resolveKimiHooksTomlDir = resolveKimiHooksTomlDir;
|
|
44
45
|
exports.getGlobalConfigDir = getGlobalConfigDir;
|
|
45
46
|
exports.resolveSkillsBaseFromDescriptor = resolveSkillsBaseFromDescriptor;
|
|
@@ -403,6 +404,23 @@ exports.NON_REGISTRY_CONFIG_HOME_DESCRIPTORS = [
|
|
|
403
404
|
* through resolveConfigHomeFromDescriptor and have no `kind`/`name` shape.
|
|
404
405
|
*/
|
|
405
406
|
exports.GSD_LOCATION_ENV_KEYS = ['GSD_HOME', 'GSD_AGENTS_DIR'];
|
|
407
|
+
/**
|
|
408
|
+
* #5017: the ONE rule for the root of the GSD-owned store (`$GSD_HOME/.gsd/`:
|
|
409
|
+
* defaults.json, consent.json, capability overlays) — `GSD_HOME` when set and
|
|
410
|
+
* non-blank, otherwise the home directory. The config loader, the install-time
|
|
411
|
+
* resolvers, the installer's defaults writer and capability staging all call
|
|
412
|
+
* this, so one configuration gives one answer at install time, at dispatch time
|
|
413
|
+
* and in the staged capabilities. The hooks (cold-tree, no lib import) and the
|
|
414
|
+
* settings / new-project workflows (shell) carry the same rule inline; a test
|
|
415
|
+
* pins each of them to it.
|
|
416
|
+
*
|
|
417
|
+
* `fallbackHome` lets a caller that already holds an injected home directory
|
|
418
|
+
* (the install-time resolvers' test seam) keep using it when GSD_HOME is unset.
|
|
419
|
+
* A blank `GSD_HOME` is "unset": `''` must never become a relative store path.
|
|
420
|
+
*/
|
|
421
|
+
function resolveGsdHome(fallbackHome = node_os_1.default.homedir()) {
|
|
422
|
+
return process.env['GSD_HOME'] || fallbackHome;
|
|
423
|
+
}
|
|
406
424
|
/**
|
|
407
425
|
* Resolve the directory holding the Kimi product's OWN native config.toml —
|
|
408
426
|
* the file that product itself reads for providers/models/hooks/etc, and the
|
|
@@ -1131,6 +1131,8 @@ function _normalizeMd(content) {
|
|
|
1131
1131
|
}
|
|
1132
1132
|
function normalizeContent(filePath, content, opts = {}) {
|
|
1133
1133
|
const encoding = opts.encoding ?? 'utf-8';
|
|
1134
|
+
if (opts.verbatim === true)
|
|
1135
|
+
return { content: content ?? '', encoding };
|
|
1134
1136
|
const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
|
|
1135
1137
|
let normalized;
|
|
1136
1138
|
if (isMd) {
|
|
@@ -675,6 +675,28 @@ function actionsFor(situation, s, runtime = 'claude') {
|
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
677
|
// ─── Summary line ─────────────────────────────────────────────────────────────
|
|
678
|
+
function phaseSummary(s) {
|
|
679
|
+
const phase = s.current_phase;
|
|
680
|
+
// Preserve the milestone ordinal introduced by #5228 when it is known.
|
|
681
|
+
const at = phase === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, phase) === 0);
|
|
682
|
+
if (at >= 0)
|
|
683
|
+
return `Phase ${at + 1} of ${s.milestone_phases.length}`;
|
|
684
|
+
const total = s.total_phases;
|
|
685
|
+
const legacy = `Phase ${phase ?? '?'} of ${total ?? '?'}`;
|
|
686
|
+
const roadmapTotal = s.roadmap_total_phases;
|
|
687
|
+
// Keep the recorded pair when roadmap counts are unavailable, or when the
|
|
688
|
+
// pair can describe a single milestone without changing its existing text.
|
|
689
|
+
if (phase === null || total === null || roadmapTotal === null || roadmapTotal <= 0
|
|
690
|
+
|| (total === roadmapTotal && comparePhaseNum(phase, total) <= 0)) {
|
|
691
|
+
return legacy;
|
|
692
|
+
}
|
|
693
|
+
// When the recorded pair cannot safely describe this roadmap, label its
|
|
694
|
+
// counts separately rather than printing a mixed-scope ratio.
|
|
695
|
+
const roadmapDone = s.roadmap_completed_phases;
|
|
696
|
+
return roadmapDone === null
|
|
697
|
+
? `Phase ${phase} (${roadmapTotal} roadmap phases)`
|
|
698
|
+
: `Phase ${phase} (${roadmapDone} of ${roadmapTotal} roadmap phases complete)`;
|
|
699
|
+
}
|
|
678
700
|
function buildSummary(situation, s) {
|
|
679
701
|
switch (situation) {
|
|
680
702
|
case 'no-project':
|
|
@@ -688,7 +710,7 @@ function buildSummary(situation, s) {
|
|
|
688
710
|
case 'needs-first-phase':
|
|
689
711
|
return 'Project initialized — plan your first phase';
|
|
690
712
|
case 'planning':
|
|
691
|
-
return `${
|
|
713
|
+
return `${phaseSummary(s)} — needs a plan`;
|
|
692
714
|
case 'executing':
|
|
693
715
|
return progressLine('executing', s);
|
|
694
716
|
case 'verify-pending':
|
|
@@ -702,24 +724,10 @@ function buildSummary(situation, s) {
|
|
|
702
724
|
return 'Unsure of state — showing progress';
|
|
703
725
|
}
|
|
704
726
|
}
|
|
705
|
-
/**
|
|
706
|
-
* "Phase N of M" for the current phase. N is the phase's 1-based position in
|
|
707
|
-
* the current milestone's ordered phase list (#5228), so a milestone that
|
|
708
|
-
* continues the numbering of an earlier one (phases 9-17) reads "Phase 1 of 9",
|
|
709
|
-
* not "Phase 9 of 9". When the phase is not in that list (or the roadmap lists
|
|
710
|
-
* none) the STATE.md figures are printed as recorded.
|
|
711
|
-
*/
|
|
712
|
-
function phasePosition(s) {
|
|
713
|
-
const current = s.current_phase;
|
|
714
|
-
const at = current === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, current) === 0);
|
|
715
|
-
if (at >= 0)
|
|
716
|
-
return `Phase ${at + 1} of ${s.milestone_phases.length}`;
|
|
717
|
-
return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'}`;
|
|
718
|
-
}
|
|
719
727
|
function progressLine(tail, s) {
|
|
720
728
|
const parts = [];
|
|
721
729
|
if (s.current_phase !== null && s.total_phases !== null) {
|
|
722
|
-
parts.push(
|
|
730
|
+
parts.push(phaseSummary(s));
|
|
723
731
|
}
|
|
724
732
|
else if (s.current_phase !== null) {
|
|
725
733
|
parts.push(`Phase ${s.current_phase}`);
|