@opengsd/gsd-core 1.2.0 → 1.3.0-rc.1
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/README.ja-JP.md +6 -5
- package/README.ko-KR.md +6 -5
- package/README.md +10 -45
- package/README.pt-BR.md +5 -4
- package/README.zh-CN.md +6 -5
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-doc-writer.md +7 -6
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-planner.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +1 -1
- package/assets/gsd-logo-2000-transparent.png +0 -0
- package/assets/gsd-logo-2000-transparent.svg +17 -0
- package/assets/gsd-logo-2000.png +0 -0
- package/assets/gsd-logo-2000.svg +21 -0
- package/assets/terminal.svg +68 -0
- package/bin/install.js +86 -44
- package/commands/gsd/review.md +2 -1
- package/get-shit-done/bin/gsd-tools.cjs +37 -1
- package/get-shit-done/bin/lib/command-aliases.cjs +16 -0
- package/get-shit-done/bin/lib/commands.cjs +109 -5
- package/get-shit-done/bin/lib/config-types.cjs +19 -0
- package/get-shit-done/bin/lib/core.cjs +455 -63
- package/get-shit-done/bin/lib/gap-checker.cjs +56 -7
- package/get-shit-done/bin/lib/installer-migration-report.cjs +1 -0
- package/get-shit-done/bin/lib/milestone.cjs +59 -1
- package/get-shit-done/bin/lib/model-catalog.cjs +17 -0
- package/get-shit-done/bin/lib/phase-lifecycle.cjs +0 -1
- package/get-shit-done/bin/lib/review-reviewer-selection.cjs +1 -0
- package/get-shit-done/bin/lib/roadmap-command-router.cjs +134 -0
- package/get-shit-done/bin/lib/roadmap-upgrade.cjs +569 -0
- package/get-shit-done/bin/lib/roadmap.cjs +6 -5
- package/get-shit-done/bin/lib/semver-compare.cjs +34 -27
- package/get-shit-done/bin/lib/shell-command-projection.cjs +35 -0
- package/get-shit-done/bin/lib/state-document.cjs +0 -4
- package/get-shit-done/bin/lib/state.cjs +41 -6
- package/get-shit-done/bin/lib/ui-safety-gate.cjs +111 -0
- package/get-shit-done/bin/lib/validate.cjs +43 -17
- package/get-shit-done/bin/lib/verify.cjs +107 -4
- package/get-shit-done/bin/shared/config-defaults.manifest.json +1 -0
- package/get-shit-done/bin/shared/config-schema.manifest.json +12 -1
- package/get-shit-done/bin/shared/model-catalog.json +27 -0
- package/get-shit-done/references/checkpoints.md +1 -1
- package/get-shit-done/references/planner-human-verify-mode.md +1 -1
- package/get-shit-done/references/ui-brand.md +4 -2
- package/get-shit-done/workflows/audit-fix.md +1 -1
- package/get-shit-done/workflows/audit-milestone.md +2 -0
- package/get-shit-done/workflows/autonomous.md +7 -5
- package/get-shit-done/workflows/cleanup.md +43 -3
- package/get-shit-done/workflows/code-review-fix.md +3 -3
- package/get-shit-done/workflows/code-review.md +2 -0
- package/get-shit-done/workflows/debug.md +6 -1
- package/get-shit-done/workflows/diagnose-issues.md +2 -0
- package/get-shit-done/workflows/discuss-phase/modes/advisor.md +1 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/docs-update.md +18 -4
- package/get-shit-done/workflows/eval-review.md +1 -1
- package/get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/get-shit-done/workflows/execute-phase.md +21 -11
- package/get-shit-done/workflows/execute-plan.md +1 -1
- package/get-shit-done/workflows/explore.md +2 -0
- package/get-shit-done/workflows/help/modes/default.md +3 -3
- package/get-shit-done/workflows/help/modes/full.md +5 -5
- package/get-shit-done/workflows/import.md +2 -0
- package/get-shit-done/workflows/ingest-docs.md +2 -2
- package/get-shit-done/workflows/manager.md +2 -2
- package/get-shit-done/workflows/map-codebase.md +2 -0
- package/get-shit-done/workflows/new-milestone.md +2 -2
- package/get-shit-done/workflows/new-project.md +2 -2
- package/get-shit-done/workflows/plan-phase.md +16 -15
- package/get-shit-done/workflows/plan-review-convergence.md +3 -3
- package/get-shit-done/workflows/quick.md +5 -3
- package/get-shit-done/workflows/review.md +109 -1
- package/get-shit-done/workflows/scan.md +2 -0
- package/get-shit-done/workflows/secure-phase.md +2 -0
- package/get-shit-done/workflows/settings-advanced.md +195 -5
- package/get-shit-done/workflows/settings.md +15 -1
- package/get-shit-done/workflows/ui-phase.md +2 -2
- package/get-shit-done/workflows/ui-review.md +1 -1
- package/get-shit-done/workflows/validate-phase.md +2 -0
- package/get-shit-done/workflows/verify-work.md +2 -2
- package/hooks/dist/gsd-worktree-path-guard.js +169 -0
- package/hooks/gsd-worktree-path-guard.js +169 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +9 -6
- package/scripts/build-hooks.js +1 -0
- package/scripts/ci-test-scope.cjs +10 -0
- package/scripts/run-tests.cjs +25 -1
- package/scripts/lint-shared-module-handsync.cjs +0 -388
- package/scripts/shared-module-handsync-allowlist.json +0 -183
|
@@ -41,6 +41,39 @@ function formatHookCommandForRuntime(command, opts = {}) {
|
|
|
41
41
|
return hookCommandNeedsPowerShellCallOperator(opts) ? `& ${command}` : command;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// #166/#580: Claude Code on Windows executes hook command strings inside Git
|
|
45
|
+
// Bash. A `.sh` hook wrapped with an explicit bash.exe path makes bash try to
|
|
46
|
+
// exec bash itself ("C:/.../bash.exe: cannot execute binary file"). Both install
|
|
47
|
+
// paths — global (buildHookCommand) and local (buildLocalShellHookCommand) — must
|
|
48
|
+
// drop the bash runner in this case and emit only the anchored script path.
|
|
49
|
+
// Centralized here so the two paths cannot silently drift apart again: the local
|
|
50
|
+
// path missed this guard and reintroduced the #166/#377 failure (#580).
|
|
51
|
+
function shellHookOmitsBashRunner({ platform = process.platform, runtime = 'generic', isShellHook = false } = {}) {
|
|
52
|
+
return platform === 'win32' && runtime === 'claude' && isShellHook;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Builds the command string for a local-install managed `.sh` hook. Mirrors the
|
|
56
|
+
// global buildHookCommand path but uses the $CLAUDE_PROJECT_DIR-anchored prefix
|
|
57
|
+
// instead of an absolute configDir. On Claude/Windows the bash runner is dropped
|
|
58
|
+
// (see shellHookOmitsBashRunner) and the anchored script path is emitted alone —
|
|
59
|
+
// matching the global path. Elsewhere the resolved bash runner is required; a
|
|
60
|
+
// null runner yields null so callers skip registration instead of emitting a
|
|
61
|
+
// broken hook (#3393).
|
|
62
|
+
function buildLocalShellHookCommand({ localPrefix, hookFile, bashRunner, runtime = 'generic', platform = process.platform }) {
|
|
63
|
+
if (!localPrefix || !hookFile) return null;
|
|
64
|
+
const scriptPath = `${localPrefix}/hooks/${hookFile}`;
|
|
65
|
+
if (shellHookOmitsBashRunner({ platform, runtime, isShellHook: true })) {
|
|
66
|
+
return formatHookCommandForRuntime(scriptPath, { platform, runtime });
|
|
67
|
+
}
|
|
68
|
+
if (!bashRunner) return null;
|
|
69
|
+
return projectShellCommandText({
|
|
70
|
+
runnerToken: bashRunner,
|
|
71
|
+
argTokens: [scriptPath],
|
|
72
|
+
runtime,
|
|
73
|
+
platform,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
/**
|
|
45
78
|
* Project a managed hook script path token for serialized shell commands.
|
|
46
79
|
* Windows managed hook commands normalize to forward slashes so the same path
|
|
@@ -476,6 +509,8 @@ function platformEnsureDir(dirPath) {
|
|
|
476
509
|
module.exports = {
|
|
477
510
|
hookCommandNeedsPowerShellCallOperator,
|
|
478
511
|
formatHookCommandForRuntime,
|
|
512
|
+
shellHookOmitsBashRunner,
|
|
513
|
+
buildLocalShellHookCommand,
|
|
479
514
|
formatManagedHookScriptToken,
|
|
480
515
|
projectLocalHookPrefix,
|
|
481
516
|
projectPortableHookBaseDir,
|
|
@@ -238,10 +238,6 @@ function isStateTemplateDefault(field, value) {
|
|
|
238
238
|
function stateReplaceFieldIfTemplate(content, field, knownDefaults, newValue) {
|
|
239
239
|
if (newValue === null || newValue === undefined) return content;
|
|
240
240
|
const existing = stateExtractField(content, field);
|
|
241
|
-
// Build a temporary KNOWN_TEMPLATE_DEFAULTS-compatible lookup so we can reuse
|
|
242
|
-
// the isStateTemplateDefault logic for the provided knownDefaults array.
|
|
243
|
-
const tempField = '__tmp__';
|
|
244
|
-
const tempDefaults = { [tempField]: knownDefaults || [] };
|
|
245
241
|
// Inline check: absent/blank → always write; in list → write; else → skip.
|
|
246
242
|
if (existing === null || existing === undefined || existing.trim() === '') {
|
|
247
243
|
return stateReplaceField(content, field, newValue) || content;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, extractCurrentMilestone, output, error } = require('./core.cjs');
|
|
8
8
|
const { platformWriteSync, platformReadSync, platformEnsureDir } = require('./shell-command-projection.cjs');
|
|
9
9
|
const { planningDir, planningPaths } = require('./planning-workspace.cjs');
|
|
10
10
|
const { realClock } = require('./clock.cjs');
|
|
@@ -886,9 +886,31 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
886
886
|
diskTotalSummaries += summaryCount;
|
|
887
887
|
if (completed) diskCompletedPhases++;
|
|
888
888
|
}
|
|
889
|
+
// Count phase headings from ROADMAP using a digit-containing pattern
|
|
890
|
+
// that matches both numeric phases (01, 05.1) and project-code phases
|
|
891
|
+
// (PROJ-42, CK-05) but excludes pure-word section headers like
|
|
892
|
+
// `## Phase Overview:` or `## Phase Details:` — single source of
|
|
893
|
+
// truth for total_phases (#549).
|
|
894
|
+
let roadmapPhaseCount = 0;
|
|
895
|
+
try {
|
|
896
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
897
|
+
const roadmapRaw = platformReadSync(roadmapPath);
|
|
898
|
+
if (roadmapRaw !== null) {
|
|
899
|
+
const roadmapScope = extractCurrentMilestone(roadmapRaw, cwd);
|
|
900
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
|
901
|
+
let m;
|
|
902
|
+
while ((m = phaseHeadingPattern.exec(roadmapScope)) !== null) {
|
|
903
|
+
// Only count tokens that contain at least one digit — excludes
|
|
904
|
+
// pure-word section headings (Overview, Details) while keeping
|
|
905
|
+
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
|
|
906
|
+
if (/\d/.test(m[1])) roadmapPhaseCount++;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch { /* fall through: phaseDirs.length used as sole count */ }
|
|
910
|
+
|
|
889
911
|
cached = {
|
|
890
|
-
totalPhases:
|
|
891
|
-
? Math.max(phaseDirs.length,
|
|
912
|
+
totalPhases: roadmapPhaseCount > 0
|
|
913
|
+
? Math.max(phaseDirs.length, roadmapPhaseCount)
|
|
892
914
|
: phaseDirs.length,
|
|
893
915
|
completedPhases: diskCompletedPhases,
|
|
894
916
|
totalPlans: diskTotalPlans,
|
|
@@ -1651,9 +1673,22 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
1651
1673
|
// Mirrors the logic in buildStateFrontmatter so both report consistent percents (#3242 Bug B).
|
|
1652
1674
|
let syncTotalPhases = null;
|
|
1653
1675
|
try {
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1676
|
+
let roadmapPhaseCount = 0;
|
|
1677
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
1678
|
+
const roadmapRaw = platformReadSync(roadmapPath);
|
|
1679
|
+
if (roadmapRaw !== null) {
|
|
1680
|
+
const roadmapScope = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1681
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
|
1682
|
+
let m;
|
|
1683
|
+
while ((m = phaseHeadingPattern.exec(roadmapScope)) !== null) {
|
|
1684
|
+
// Only count tokens that contain at least one digit — excludes
|
|
1685
|
+
// pure-word section headings (Overview, Details) while keeping
|
|
1686
|
+
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
|
|
1687
|
+
if (/\d/.test(m[1])) roadmapPhaseCount++;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (roadmapPhaseCount > 0) {
|
|
1691
|
+
syncTotalPhases = Math.max(entries.length, roadmapPhaseCount);
|
|
1657
1692
|
} else {
|
|
1658
1693
|
syncTotalPhases = entries.length;
|
|
1659
1694
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UI Safety Gate — shell-free implementation (#3706, #3718)
|
|
5
|
+
*
|
|
6
|
+
* Replaces the bash shell-based one-liner that silently degraded on Windows
|
|
7
|
+
* PowerShell / cmd.exe because the locale env-var prefix was not recognised.
|
|
8
|
+
* This module runs inside Node.js — no shell dependency, works identically
|
|
9
|
+
* on bash, Git-Bash, PowerShell, and cmd.exe.
|
|
10
|
+
*
|
|
11
|
+
* Word-boundary anchoring:
|
|
12
|
+
* (^|[^a-zA-Z0-9])(TOKEN)([^a-zA-Z0-9]|$)
|
|
13
|
+
* Equivalent to POSIX ERE [^[:alnum:]] — matches tokens only when they are not
|
|
14
|
+
* interior substrings of alphanumeric compound words (e.g. "microfrontend" is NOT
|
|
15
|
+
* matched; "micro-frontend" and "micro frontend" ARE matched).
|
|
16
|
+
*
|
|
17
|
+
* Public API:
|
|
18
|
+
* checkUiPresence(text: string): { hasUI: boolean, tokens: string[] }
|
|
19
|
+
*
|
|
20
|
+
* CLI usage — reads phase-section text from STDIN to avoid ARG_MAX limits:
|
|
21
|
+
* echo "$PHASE_SECTION" | node get-shit-done/bin/lib/ui-safety-gate.cjs
|
|
22
|
+
* echo $? → 0 if UI tokens found, 1 if not, 2 on usage error
|
|
23
|
+
*
|
|
24
|
+
* Exit codes mirror grep: 0 = match found, 1 = no match, 2 = usage error.
|
|
25
|
+
*
|
|
26
|
+
* Canonical location: get-shit-done/bin/lib/ui-safety-gate.cjs (#448)
|
|
27
|
+
* This path is deployed by the GSD installer to $RUNTIME_DIR/get-shit-done/bin/lib/.
|
|
28
|
+
* bin/lib/ui-safety-gate.cjs (root) is retained for source-repo and npm usage.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const UI_TOKENS = [
|
|
32
|
+
'UI',
|
|
33
|
+
'interface',
|
|
34
|
+
'frontend',
|
|
35
|
+
'component',
|
|
36
|
+
'layout',
|
|
37
|
+
'page',
|
|
38
|
+
'screen',
|
|
39
|
+
'view',
|
|
40
|
+
'form',
|
|
41
|
+
'dashboard',
|
|
42
|
+
'widget',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Built once at module load — no per-call compilation overhead.
|
|
47
|
+
* ASCII word boundaries — matches the original ASCII-grep intent of #3706.
|
|
48
|
+
* Note: JS [a-zA-Z0-9] is ASCII-only and NOT equivalent to POSIX [[:alnum:]],
|
|
49
|
+
* which is locale-sensitive and includes accented characters.
|
|
50
|
+
*/
|
|
51
|
+
const UI_GATE_PATTERN = new RegExp(
|
|
52
|
+
'(^|[^a-zA-Z0-9])(' + UI_TOKENS.join('|') + ')([^a-zA-Z0-9]|$)',
|
|
53
|
+
'i'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Global-flagged variant for extracting ALL matches per line (matchAll).
|
|
57
|
+
const UI_GATE_PATTERN_GLOBAL = new RegExp(UI_GATE_PATTERN.source, 'gi');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check a roadmap phase section string for frontend UI indicators.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} text - The roadmap phase section content (may be multi-line, CRLF or LF).
|
|
63
|
+
* @returns {{ hasUI: boolean, tokens: string[] }}
|
|
64
|
+
* hasUI — true if any UI token was matched as a standalone word.
|
|
65
|
+
* tokens — matched token strings (lowercased), deduplicated.
|
|
66
|
+
*/
|
|
67
|
+
function checkUiPresence(text) {
|
|
68
|
+
if (typeof text !== 'string') {
|
|
69
|
+
return { hasUI: false, tokens: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Normalise CRLF so the pattern sees consistent line boundaries.
|
|
73
|
+
const normalised = text.replace(/\r\n/g, '\n');
|
|
74
|
+
|
|
75
|
+
const found = new Set();
|
|
76
|
+
for (const line of normalised.split('\n')) {
|
|
77
|
+
// Reset lastIndex before each line so the global pattern restarts from 0.
|
|
78
|
+
UI_GATE_PATTERN_GLOBAL.lastIndex = 0;
|
|
79
|
+
for (const m of line.matchAll(UI_GATE_PATTERN_GLOBAL)) {
|
|
80
|
+
found.add(m[2].toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { hasUI: found.size > 0, tokens: [...found] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { checkUiPresence, UI_TOKENS };
|
|
88
|
+
|
|
89
|
+
// ── CLI entry point ─────────────────────────────────────────────────────────
|
|
90
|
+
// Reads phase-section text from STDIN (not argv) to avoid OS ARG_MAX limits.
|
|
91
|
+
// Invoked by workflow .md bash blocks as: echo "$PHASE_SECTION" | node .../ui-safety-gate.cjs
|
|
92
|
+
// Exit 0 = UI found, 1 = no UI, 2 = startup error.
|
|
93
|
+
|
|
94
|
+
if (require.main === module) {
|
|
95
|
+
// Collect stdin chunks asynchronously.
|
|
96
|
+
const chunks = [];
|
|
97
|
+
process.stdin.setEncoding('utf-8');
|
|
98
|
+
|
|
99
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
100
|
+
|
|
101
|
+
process.stdin.on('end', () => {
|
|
102
|
+
const input = chunks.join('');
|
|
103
|
+
const result = checkUiPresence(input);
|
|
104
|
+
process.exit(result.hasUI ? 0 : 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
process.stdin.on('error', (err) => {
|
|
108
|
+
process.stderr.write(`ERROR: ui-safety-gate.cjs stdin read failed: ${err.message}\n`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -29,8 +29,13 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
// ── Issue #26: regex constants (W005, W006-archived) ────────────────────────
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Matches legacy numeric dirs (01-setup), milestone-prefixed dirs (02-01-setup),
|
|
33
|
+
// deep dirs (02-04-01-deep), and project-code-prefixed variants (GSD-02-01-setup).
|
|
34
|
+
const phaseDirNameRe = /^(?:[A-Z]{1,6}-)?\d{2,}(?:-\d+)*(?:\.\d+)*-[\w-]+$/i;
|
|
35
|
+
// Extracts the full phase token from a directory name, including milestone-prefixed
|
|
36
|
+
// multi-segment tokens like "02-01" from "02-01-setup" or "GSD-02-01-setup".
|
|
37
|
+
// Greedily captures all leading all-digit segments before the first letter-start segment.
|
|
38
|
+
const PHASE_TOKEN_FROM_DIR_RE = /^(?:[A-Z]{1,6}-)?(\d+(?:-\d+)*[A-Z]?(?:\.\d+)*)(?:-[a-z]|$)/i;
|
|
34
39
|
const MILESTONE_ARCHIVE_DIR_RE = /^v\d+.*-phases$/i;
|
|
35
40
|
|
|
36
41
|
// ── Issue #26: I001 canonicalization ────────────────────────────────────────
|
|
@@ -41,26 +46,46 @@ function canonicalPlanStem(stem) {
|
|
|
41
46
|
|
|
42
47
|
// ── Issue #6: phase variant helpers (W006/W007) ──────────────────────────────
|
|
43
48
|
function phaseVariants(phase) {
|
|
49
|
+
const variants = new Set([phase]);
|
|
50
|
+
const dotIdx = phase.indexOf('.');
|
|
51
|
+
const head = dotIdx === -1 ? phase : phase.slice(0, dotIdx);
|
|
52
|
+
const tail = dotIdx === -1 ? '' : phase.slice(dotIdx);
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// Milestone-prefixed IDs: M-NN or M-N-N. Add padding-normalized variant.
|
|
55
|
+
// e.g. "2-01" → also "02-01"; "02-01" → also "2-01"
|
|
56
|
+
const milestoneHeadMatch = head.match(/^(\d+)((?:-\d+)+)([A-Z]?)$/i);
|
|
57
|
+
if (milestoneHeadMatch) {
|
|
58
|
+
const major = milestoneHeadMatch[1];
|
|
59
|
+
const subSegs = milestoneHeadMatch[2]; // e.g. "-01" or "-04-01"
|
|
60
|
+
const letter = milestoneHeadMatch[3] || '';
|
|
61
|
+
const paddedMajor = major.padStart(2, '0');
|
|
62
|
+
const unpaddedMajor = String(parseInt(major, 10));
|
|
63
|
+
// Pad/unpad sub-segments individually
|
|
64
|
+
const paddedSubs = subSegs.slice(1).split('-').map(s => s.padStart(2, '0')).join('-');
|
|
65
|
+
const unpaddedSubs = subSegs.slice(1).split('-').map(s => String(parseInt(s, 10))).join('-');
|
|
66
|
+
variants.add(`${paddedMajor}-${paddedSubs}${letter}${tail}`);
|
|
67
|
+
variants.add(`${unpaddedMajor}-${unpaddedSubs}${letter}${tail}`);
|
|
68
|
+
variants.add(`${unpaddedMajor}-${paddedSubs}${letter}${tail}`);
|
|
69
|
+
variants.add(`${paddedMajor}-${unpaddedSubs}${letter}${tail}`);
|
|
70
|
+
return variants;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Plain numeric/decimal IDs: "1", "01", "12A", "12.1"
|
|
74
|
+
const headMatch = head.match(/^(\d+)([A-Z]?)$/i);
|
|
75
|
+
if (!headMatch) return variants;
|
|
76
|
+
const numericHead = headMatch[1];
|
|
77
|
+
const letterSuffix = headMatch[2] || '';
|
|
78
|
+
variants.add(`${String(parseInt(numericHead, 10))}${letterSuffix}${tail}`);
|
|
79
|
+
variants.add(`${numericHead.padStart(2, '0')}${letterSuffix}${tail}`);
|
|
80
|
+
return variants;
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
function buildRoadmapPhaseVariants(roadmapContent) {
|
|
61
84
|
const roadmapPhases = new Set();
|
|
62
85
|
const roadmapPhaseVariants = new Set();
|
|
63
|
-
|
|
86
|
+
// Matches both legacy numeric (Phase 1:), decimal (Phase 2.1:), milestone-prefixed (Phase 2-01:),
|
|
87
|
+
// and bracket-prefixed (### [GSD] Phase 2-01:) headings.
|
|
88
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
|
|
64
89
|
let m;
|
|
65
90
|
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
|
66
91
|
roadmapPhases.add(m[1]);
|
|
@@ -71,7 +96,8 @@ function buildRoadmapPhaseVariants(roadmapContent) {
|
|
|
71
96
|
|
|
72
97
|
function buildNotStartedPhaseVariants(roadmapContent) {
|
|
73
98
|
const notStartedPhases = new Set();
|
|
74
|
-
|
|
99
|
+
// Also matches milestone-prefixed and bracket-prefixed checklist items.
|
|
100
|
+
const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)[:\s*]/gi;
|
|
75
101
|
let um;
|
|
76
102
|
while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
|
|
77
103
|
for (const variant of phaseVariants(um[1])) notStartedPhases.add(variant);
|
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const os = require('os');
|
|
15
|
-
const { loadConfig, normalizePhaseName, escapeRegex, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error, checkAgentsInstalled, CONFIG_DEFAULTS, inspectWorktreeHealth } = require('./core.cjs');
|
|
15
|
+
const { loadConfig, normalizePhaseName, phaseTokenMatches, escapeRegex, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error, checkAgentsInstalled, CONFIG_DEFAULTS, inspectWorktreeHealth } = require('./core.cjs');
|
|
16
16
|
const { execGit, platformReadSync: safeReadFile, platformWriteSync } = require('./shell-command-projection.cjs');
|
|
17
17
|
const { PACKAGE_NAME } = require('./package-identity.cjs');
|
|
18
18
|
const { planningDir } = require('./planning-workspace.cjs');
|
|
@@ -507,6 +507,34 @@ function collectDiskPhases(planBase) {
|
|
|
507
507
|
return diskPhases;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
// W021: phase ID integer prefix doesn't match its enclosing milestone section
|
|
511
|
+
// Only fires when phase_id_convention is 'milestone-prefixed' (opt-in).
|
|
512
|
+
// Returns array of mismatch objects: { phaseId, foundInMilestone, expectedMilestone }
|
|
513
|
+
function checkMilestonePrefixMismatches(roadmapContent, { getMilestoneFromPhaseId }) {
|
|
514
|
+
const mismatches = [];
|
|
515
|
+
// Find all milestone sections (## vN.N or ## [code] vN.N)
|
|
516
|
+
const sections = [];
|
|
517
|
+
const sectionRx = /^#{1,3}\s+(?:\[[^\]]+\]\s*)?.*v(\d+\.\d+)/gim;
|
|
518
|
+
let m;
|
|
519
|
+
while ((m = sectionRx.exec(roadmapContent)) !== null) {
|
|
520
|
+
if (sections.length > 0) sections[sections.length - 1].end = m.index;
|
|
521
|
+
sections.push({ version: `v${m[1]}`, start: m.index, end: roadmapContent.length });
|
|
522
|
+
}
|
|
523
|
+
for (const section of sections) {
|
|
524
|
+
const content = roadmapContent.slice(section.start, section.end);
|
|
525
|
+
const phaseRx = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
|
|
526
|
+
let pm;
|
|
527
|
+
while ((pm = phaseRx.exec(content)) !== null) {
|
|
528
|
+
const phaseId = pm[1];
|
|
529
|
+
const expectedMilestone = getMilestoneFromPhaseId(phaseId);
|
|
530
|
+
if (expectedMilestone !== null && expectedMilestone !== section.version) {
|
|
531
|
+
mismatches.push({ phaseId, foundInMilestone: section.version, expectedMilestone });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return mismatches;
|
|
536
|
+
}
|
|
537
|
+
|
|
510
538
|
function cmdValidateConsistency(cwd, raw) {
|
|
511
539
|
const planBase = planningDir(cwd);
|
|
512
540
|
const roadmapPath = path.join(planBase, 'ROADMAP.md');
|
|
@@ -527,7 +555,9 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
527
555
|
// stripped). Used for the "in ROADMAP but not on disk" check — we only require
|
|
528
556
|
// disk dirs for the active milestone's phases.
|
|
529
557
|
const roadmapPhases = new Set();
|
|
530
|
-
|
|
558
|
+
// Matches both legacy numeric (Phase 1:), decimal (Phase 2.1:), and
|
|
559
|
+
// milestone-prefixed (Phase 2-01:) headings, including bracket-prefixed form.
|
|
560
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
|
|
531
561
|
let m;
|
|
532
562
|
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
|
533
563
|
roadmapPhases.add(m[1]);
|
|
@@ -539,7 +569,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
539
569
|
// though it is absent from the active-milestone scope. Without this, narrowing
|
|
540
570
|
// the scope (#501) would flag every shipped phase dir as a spurious orphan.
|
|
541
571
|
const fullRoadmapPhases = new Set();
|
|
542
|
-
const fullPhasePattern = /#{2,4}\s*Phase\s+(\
|
|
572
|
+
const fullPhasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
|
|
543
573
|
let fm;
|
|
544
574
|
while ((fm = fullPhasePattern.exec(roadmapContentRaw)) !== null) {
|
|
545
575
|
fullRoadmapPhases.add(fm[1]);
|
|
@@ -558,8 +588,12 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
558
588
|
// Check: phases on disk but not in ROADMAP (compared against the FULL roadmap
|
|
559
589
|
// so shipped-milestone phase dirs are not flagged as orphans — #501)
|
|
560
590
|
for (const p of diskPhases) {
|
|
591
|
+
// For plain numeric IDs, also try the unpadded form (e.g. "02" → "2").
|
|
592
|
+
// For milestone-prefixed IDs (e.g. "02-01"), use normalizePhaseName to
|
|
593
|
+
// canonicalize padding before comparing with the ROADMAP entries.
|
|
594
|
+
const normalized = normalizePhaseName(p);
|
|
561
595
|
const unpadded = String(parseInt(p, 10));
|
|
562
|
-
if (!fullRoadmapPhases.has(p) && !fullRoadmapPhases.has(unpadded)) {
|
|
596
|
+
if (!fullRoadmapPhases.has(p) && !fullRoadmapPhases.has(normalized) && !fullRoadmapPhases.has(unpadded)) {
|
|
563
597
|
warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
|
|
564
598
|
}
|
|
565
599
|
}
|
|
@@ -1059,6 +1093,31 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1059
1093
|
}
|
|
1060
1094
|
} catch { /* git worktree not available or not a git repo — skip silently */ }
|
|
1061
1095
|
|
|
1096
|
+
// ─── Check 11b: Phase ID / milestone-section mismatch (W021) ─────────────
|
|
1097
|
+
// Only active when phase_id_convention === 'milestone-prefixed' in config.json.
|
|
1098
|
+
try {
|
|
1099
|
+
const phaseConvention = (() => {
|
|
1100
|
+
if (!fs.existsSync(configPath)) return null;
|
|
1101
|
+
try {
|
|
1102
|
+
const configRaw = fs.readFileSync(configPath, 'utf-8');
|
|
1103
|
+
const configParsed = JSON.parse(configRaw);
|
|
1104
|
+
return configParsed.phase_id_convention || null;
|
|
1105
|
+
} catch { return null; }
|
|
1106
|
+
})();
|
|
1107
|
+
if (phaseConvention === 'milestone-prefixed') {
|
|
1108
|
+
if (fs.existsSync(roadmapPath)) {
|
|
1109
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1110
|
+
const { getMilestoneFromPhaseId } = require('./core.cjs');
|
|
1111
|
+
const mismatches = checkMilestonePrefixMismatches(roadmapContent, { getMilestoneFromPhaseId });
|
|
1112
|
+
for (const m of mismatches) {
|
|
1113
|
+
addIssue('warning', 'W021',
|
|
1114
|
+
`Phase ${m.phaseId}: integer prefix implies ${m.expectedMilestone} but listed under ${m.foundInMilestone}`,
|
|
1115
|
+
'Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default)');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
} catch { /* W021 check is advisory — skip on error */ }
|
|
1120
|
+
|
|
1062
1121
|
// ─── Check 12: MILESTONES.md / archive snapshot drift (#2446) ─────────────
|
|
1063
1122
|
const milestonesPath = path.join(planBase, 'MILESTONES.md');
|
|
1064
1123
|
const milestonesArchiveDir = path.join(planBase, 'milestones');
|
|
@@ -1107,6 +1166,50 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1107
1166
|
}
|
|
1108
1167
|
} catch { /* artifact check is advisory — skip on error */ }
|
|
1109
1168
|
|
|
1169
|
+
// ─── Check 14: milestone-status vs. roadmap-progress incoherence (W021) ───
|
|
1170
|
+
try {
|
|
1171
|
+
if (fs.existsSync(statePath) && fs.existsSync(roadmapPath)) {
|
|
1172
|
+
const stateRaw = fs.readFileSync(statePath, 'utf-8');
|
|
1173
|
+
const statusMatch = stateRaw.match(/^status:\s*(.+)/im);
|
|
1174
|
+
const stateStatus = statusMatch ? statusMatch[1].trim().toLowerCase() : '';
|
|
1175
|
+
const isMarkedComplete = /milestone complete|archived/.test(stateStatus);
|
|
1176
|
+
if (isMarkedComplete) {
|
|
1177
|
+
// Inline phase scan: read roadmap, extract current milestone section,
|
|
1178
|
+
// then check for phases with no directory on disk (unstarted).
|
|
1179
|
+
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1180
|
+
const scopedContent = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1181
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
1182
|
+
const unstarted = [];
|
|
1183
|
+
let pm;
|
|
1184
|
+
const planningWorkspace = require('./planning-workspace.cjs');
|
|
1185
|
+
const phasesDir2 = planningWorkspace.planningPaths(cwd).phases;
|
|
1186
|
+
const phaseDirNames2 = (() => {
|
|
1187
|
+
try {
|
|
1188
|
+
return fs.readdirSync(phasesDir2, { withFileTypes: true })
|
|
1189
|
+
.filter(e => e.isDirectory()).map(e => e.name);
|
|
1190
|
+
} catch { return []; }
|
|
1191
|
+
})();
|
|
1192
|
+
while ((pm = phasePattern.exec(scopedContent)) !== null) {
|
|
1193
|
+
const phaseNum = pm[1];
|
|
1194
|
+
const normalized = normalizePhaseName(phaseNum);
|
|
1195
|
+
// Phase is unstarted (disk_status: no_directory) if no directory
|
|
1196
|
+
// with a matching token exists on disk. Use phaseTokenMatches (same
|
|
1197
|
+
// helper as roadmap.analyze) to correctly handle decimal (2.1) and
|
|
1198
|
+
// letter-suffix (12A) phase IDs without false positives.
|
|
1199
|
+
const hasDirectory = phaseDirNames2.some(d => phaseTokenMatches(d, normalized));
|
|
1200
|
+
if (!hasDirectory) {
|
|
1201
|
+
unstarted.push(phaseNum);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (unstarted.length > 0) {
|
|
1205
|
+
addIssue('warning', 'W021',
|
|
1206
|
+
`STATE says milestone complete but ROADMAP lists ${unstarted.length} unstarted phase(s) (e.g. Phase ${unstarted[0]})`,
|
|
1207
|
+
'Run validate consistency or re-run complete-milestone after verifying all phases are done');
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
} catch { /* W021 check is advisory — skip on error */ }
|
|
1212
|
+
|
|
1110
1213
|
// ─── Perform repairs if requested ─────────────────────────────────────────
|
|
1111
1214
|
const repairActions = [];
|
|
1112
1215
|
if (options.repair && repairs.length > 0) {
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"features.global_learnings",
|
|
84
84
|
"learnings.max_inject",
|
|
85
85
|
"project_code",
|
|
86
|
+
"phase_id_convention",
|
|
86
87
|
"phase_naming",
|
|
87
88
|
"manager.flags.discuss",
|
|
88
89
|
"manager.flags.plan",
|
|
@@ -100,7 +101,12 @@
|
|
|
100
101
|
"effort.default",
|
|
101
102
|
"fast_mode.enabled",
|
|
102
103
|
"plan_review.source_grounding",
|
|
103
|
-
"plan_review.source_grounding_authority"
|
|
104
|
+
"plan_review.source_grounding_authority",
|
|
105
|
+
"model_policy.provider",
|
|
106
|
+
"model_policy.budget",
|
|
107
|
+
"model_policy.high",
|
|
108
|
+
"model_policy.medium",
|
|
109
|
+
"model_policy.low"
|
|
104
110
|
],
|
|
105
111
|
"runtimeStateKeys": [
|
|
106
112
|
"workflow._auto_chain_active"
|
|
@@ -170,6 +176,11 @@
|
|
|
170
176
|
"topLevel": "review",
|
|
171
177
|
"source": "^review\\.max_prompt_tokens_per_reviewer\\.[a-zA-Z0-9_-]+$",
|
|
172
178
|
"description": "review.max_prompt_tokens_per_reviewer.<reviewer-slug>"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"topLevel": "model_policy",
|
|
182
|
+
"source": "^model_policy\\.runtime_tiers\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$",
|
|
183
|
+
"description": "model_policy.runtime_tiers.<runtime>.<opus|sonnet|haiku>"
|
|
173
184
|
}
|
|
174
185
|
]
|
|
175
186
|
}
|
|
@@ -83,6 +83,33 @@
|
|
|
83
83
|
"haiku": null
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
+
"providerPresets": {
|
|
87
|
+
"anthropic": {
|
|
88
|
+
"opus": { "low": { "model": "claude-opus-4-5" }, "medium": { "model": "claude-opus-4-8" }, "high": { "model": "claude-opus-4-8" } },
|
|
89
|
+
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-4-6" }, "high": { "model": "claude-opus-4-8" } },
|
|
90
|
+
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-4-6" } }
|
|
91
|
+
},
|
|
92
|
+
"openai": {
|
|
93
|
+
"opus": { "low": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },
|
|
94
|
+
"sonnet": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "low" }, "medium": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.5", "reasoning_effort": "medium" } },
|
|
95
|
+
"haiku": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "minimal" }, "medium": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" } }
|
|
96
|
+
},
|
|
97
|
+
"google": {
|
|
98
|
+
"opus": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-pro" } },
|
|
99
|
+
"sonnet": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-flash" } },
|
|
100
|
+
"haiku": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-2.5-flash-lite" }, "high": { "model": "gemini-3-flash" } }
|
|
101
|
+
},
|
|
102
|
+
"qwen": {
|
|
103
|
+
"opus": { "low": { "model": "qwen3-coder-plus" }, "medium": { "model": "qwen3-max-2026-01-23" }, "high": { "model": "qwen3-max-2026-01-23" } },
|
|
104
|
+
"sonnet": { "low": { "model": "qwen3-coder-next" }, "medium": { "model": "qwen3-coder-plus" }, "high": { "model": "qwen3-max-2026-01-23" } },
|
|
105
|
+
"haiku": { "low": { "model": "qwen3-coder-next" }, "medium": { "model": "qwen3-coder-next" }, "high": { "model": "qwen3-coder-plus" } }
|
|
106
|
+
},
|
|
107
|
+
"generic": {
|
|
108
|
+
"opus": { "low": null, "medium": null, "high": null },
|
|
109
|
+
"sonnet": { "low": null, "medium": null, "high": null },
|
|
110
|
+
"haiku": { "low": null, "medium": null, "high": null }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
86
113
|
"agents": {
|
|
87
114
|
"gsd-planner": { "golden": "opus", "balanced": "opus", "budget": "sonnet", "phaseType": "planning", "routingTier": "heavy" },
|
|
88
115
|
"gsd-roadmapper": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "planning", "routingTier": "heavy" },
|
|
@@ -18,7 +18,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
|
|
|
18
18
|
|
|
19
19
|
**When:** Claude completed automated work, human confirms it works correctly.
|
|
20
20
|
|
|
21
|
-
> **Default mode (#3309): `workflow.human_verify_mode = end-of-phase`.** New projects do NOT halt mid-flight at `checkpoint:human-verify`. The planner suppresses those task emissions and embeds the verification details into the relevant `auto` task's `<verify><human-check>` block; the verifier harvests every `<verify><human-check>` at end-of-phase (Step 8) and consolidates them into the existing `human_needed` →
|
|
21
|
+
> **Default mode (#3309): `workflow.human_verify_mode = end-of-phase`.** New projects do NOT halt mid-flight at `checkpoint:human-verify`. The planner suppresses those task emissions and embeds the verification details into the relevant `auto` task's `<verify><human-check>` block; the verifier harvests every `<verify><human-check>` at end-of-phase (Step 8) and consolidates them into the existing `human_needed` → `{phase_num}-UAT.md` flow in `workflows/execute-phase.md`. The user reviews everything in one batch.
|
|
22
22
|
>
|
|
23
23
|
> **Why this is the default:** every mid-flight halt costs a full executor cold-start (CLAUDE.md, MEMORY.md, STATE.md, plan re-read on respawn) because subagent context is discarded across the pause. A plan with N human-verify checkpoints pays the cold-start cost N+1 times — measured at "tens of thousands of tokens" per round-trip on real projects.
|
|
24
24
|
>
|
|
@@ -27,7 +27,7 @@ Instead, fold each would-be verification step into the relevant `auto` task usin
|
|
|
27
27
|
</task>
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The verifier (Step 8) harvests every `<verify><human-check>` block at end-of-phase and consolidates them into the existing `human_needed` →
|
|
30
|
+
The verifier (Step 8) harvests every `<verify><human-check>` block at end-of-phase and consolidates them into the existing `human_needed` → `{phase_num}-UAT.md` path in `workflows/execute-phase.md`. The user reviews everything in one batch instead of paying a cold-start cost per item.
|
|
31
31
|
|
|
32
32
|
### `mid-flight` (opt-back-in — pre-#3309 behavior)
|
|
33
33
|
|
|
@@ -83,10 +83,12 @@ Plans: 3/5 complete
|
|
|
83
83
|
|
|
84
84
|
## Spawning Indicators
|
|
85
85
|
|
|
86
|
+
**Liveness convention:** Every spawn announcement must carry the canonical phrase `runs in a subagent` inline so users know that silence during a subagent run is expected. Without this, a healthy 1–5 minute agent looks identical to a frozen session. Single spawns use the singular form; parallel spawns use the plural form.
|
|
87
|
+
|
|
86
88
|
```
|
|
87
|
-
◆ Spawning researcher...
|
|
89
|
+
◆ Spawning researcher... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
|
|
88
90
|
|
|
89
|
-
◆ Spawning 4 researchers in parallel...
|
|
91
|
+
◆ Spawning 4 researchers in parallel... (each runs in a subagent — no output until they return, ~1–5 min; expected, not a freeze)
|
|
90
92
|
→ Stack research
|
|
91
93
|
→ Features research
|
|
92
94
|
→ Architecture research
|
|
@@ -94,7 +94,7 @@ final output — do not proceed to fixing.
|
|
|
94
94
|
<step name="fix-loop">
|
|
95
95
|
For each **auto-fixable** finding (up to `--max`, ordered by severity desc):
|
|
96
96
|
|
|
97
|
-
**a. Spawn executor agent
|
|
97
|
+
**a. Spawn executor agent** (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)**:**
|
|
98
98
|
```
|
|
99
99
|
Agent(
|
|
100
100
|
prompt="Fix finding {ID}: {description}. Files: {file_refs}. Make the minimal change to resolve this specific finding. Do not refactor surrounding code.",
|
|
@@ -67,6 +67,8 @@ With phase context collected:
|
|
|
67
67
|
|
|
68
68
|
Extract `MILESTONE_REQ_IDS` from REQUIREMENTS.md traceability table — all REQ-IDs assigned to phases in this milestone.
|
|
69
69
|
|
|
70
|
+
Print: "Spawning integration checker (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)"
|
|
71
|
+
|
|
70
72
|
```
|
|
71
73
|
Agent(
|
|
72
74
|
prompt="Check cross-phase integration and E2E flows.
|