gsdd-cli 0.25.0 → 0.26.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/README.md +97 -548
- package/bin/adapters/claude.mjs +18 -8
- package/bin/adapters/opencode.mjs +3 -3
- package/bin/gsdd.mjs +13 -30
- package/bin/lib/closeout-report.mjs +32 -6
- package/bin/lib/control-map.mjs +94 -19
- package/bin/lib/global-install.mjs +616 -0
- package/bin/lib/global-manifest.mjs +122 -0
- package/bin/lib/health.mjs +2 -1
- package/bin/lib/init-flow.mjs +3 -0
- package/bin/lib/init-prompts.mjs +6 -4
- package/bin/lib/init-runtime.mjs +25 -2
- package/bin/lib/models.mjs +136 -5
- package/bin/lib/next.mjs +834 -0
- package/bin/lib/work-context.mjs +760 -0
- package/bin/lib/workflows.mjs +27 -0
- package/distilled/README.md +30 -3
- package/distilled/templates/agents.block.md +1 -1
- package/distilled/templates/agents.md +0 -1
- package/distilled/templates/codebase/architecture.md +0 -1
- package/distilled/templates/codebase/concerns.md +0 -1
- package/distilled/templates/codebase/conventions.md +0 -1
- package/distilled/templates/codebase/stack.md +0 -1
- package/distilled/templates/roadmap.md +0 -1
- package/distilled/templates/spec.md +0 -1
- package/distilled/workflows/new-project.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +23 -3
- package/docs/USER-GUIDE.md +40 -4
- package/docs/VERIFICATION-DISCIPLINE.md +3 -1
- package/package.json +2 -2
package/bin/adapters/claude.mjs
CHANGED
|
@@ -36,7 +36,20 @@ ${delegateContent.trim()}
|
|
|
36
36
|
`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function renderClaudePlanSkill() {
|
|
39
|
+
function renderClaudePlanSkill({ portableContractPath = '.agents/skills/gsdd-plan/SKILL.md' } = {}) {
|
|
40
|
+
const contractSection = portableContractPath
|
|
41
|
+
? `Portable contract:
|
|
42
|
+
- Read \`${portableContractPath}\` first. That file remains the canonical vendor-agnostic plan contract.
|
|
43
|
+
- Keep the portable contract honest: it defines the workflow, but it does not by itself prove fresh-context checker orchestration across runtimes.
|
|
44
|
+
- If the portable skill says plan is still a stub, treat that as a portability-status warning for the generic surface, not as a stop signal for this Claude-native adapter path.`
|
|
45
|
+
: `Workflow contract:
|
|
46
|
+
- This globally installed skill is the canonical Claude-native \`gsdd-plan\` workflow contract.
|
|
47
|
+
- Keep the workflow portable: use repo-local \`.planning/\` artifacts for project state and do not require repo-local \`.agents/skills/\` files to exist.
|
|
48
|
+
- Do not claim that other runtimes have the same behavior unless their own adapters explicitly implement and prove it.`;
|
|
49
|
+
const planningContract = portableContractPath
|
|
50
|
+
? `\`${portableContractPath}\``
|
|
51
|
+
: 'this skill';
|
|
52
|
+
|
|
40
53
|
return `---
|
|
41
54
|
name: gsdd-plan
|
|
42
55
|
description: Claude-native Phase planning with fresh-context plan checking for GSDD
|
|
@@ -45,10 +58,7 @@ argument-hint: [phase-number]
|
|
|
45
58
|
|
|
46
59
|
You are the Claude-native \`/gsdd-plan\` skill for GSDD phase planning.
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
- Read \`.agents/skills/gsdd-plan/SKILL.md\` first. That file remains the canonical vendor-agnostic plan contract.
|
|
50
|
-
- Keep the portable contract honest: it defines the workflow, but it does not by itself prove fresh-context checker orchestration across runtimes.
|
|
51
|
-
- If the portable skill says plan is still a stub, treat that as a portability-status warning for the generic surface, not as a stop signal for this Claude-native adapter path.
|
|
61
|
+
${contractSection}
|
|
52
62
|
|
|
53
63
|
Native Claude adapter rule:
|
|
54
64
|
- This skill is the canonical Claude-native entry surface for \`/gsdd-plan\`.
|
|
@@ -66,7 +76,7 @@ Execution flow:
|
|
|
66
76
|
d. The explorer runs a GSD-style interactive conversation with the user (gray areas, research, deep-dive questions, assumptions) and writes APPROACH.md.
|
|
67
77
|
e. Before planning, confirm APPROACH.md records all canonical proof fields: \`alignment_status\`, \`alignment_method\`, \`user_confirmed_at\`, \`explicit_skip_approved\`, \`skip_scope\`, \`skip_rationale\`, and \`confirmed_decisions\`. For \`alignment_status: user_confirmed\`, \`confirmed_decisions\` must name the locked decisions and skip fields may be \`false\`/\`N/A\`; for \`alignment_status: approved_skip\`, \`explicit_skip_approved: true\`, \`skip_scope\`, and \`skip_rationale\` must be substantive. Agent-only "No questions needed" is not valid proof under \`workflow.discuss: true\`.
|
|
68
78
|
f. Load APPROACH.md decisions as locked constraints alongside SPEC.md decisions.
|
|
69
|
-
4. Produce the initial phase plan according to
|
|
79
|
+
4. Produce the initial phase plan according to ${planningContract}. Pass APPROACH.md decisions (if any) as locked constraints to the planner.
|
|
70
80
|
5. If \`.planning/config.json\` has \`workflow.planCheck: false\`, stop after planner self-check and explicitly report reduced assurance. This only skips the independent checker; it does not skip the step 3 alignment-proof gate when \`workflow.discuss: true\`.
|
|
71
81
|
6. If \`workflow.planCheck: true\`, invoke the native \`gsdd-plan-checker\` subagent with fresh context.
|
|
72
82
|
7. Pass only explicit inputs to the checker:
|
|
@@ -107,14 +117,14 @@ Never return raw checker JSON without summarizing it.
|
|
|
107
117
|
`;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
function renderClaudePlanCommand() {
|
|
120
|
+
function renderClaudePlanCommand({ skillPath = '.claude/skills/gsdd-plan/SKILL.md' } = {}) {
|
|
111
121
|
return `---
|
|
112
122
|
description: Compatibility alias for the Claude-native \`/gsdd-plan\` skill
|
|
113
123
|
argument-hint: [phase-number]
|
|
114
124
|
allowed-tools: Read
|
|
115
125
|
---
|
|
116
126
|
|
|
117
|
-
Read
|
|
127
|
+
Read \`${skillPath}\` and execute that skill as the canonical Claude-native \`/gsdd-plan\` entry.
|
|
118
128
|
|
|
119
129
|
Rules:
|
|
120
130
|
- Do NOT duplicate orchestration logic here.
|
|
@@ -145,7 +145,7 @@ ${delegateContent.trim()}
|
|
|
145
145
|
`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function renderOpenCodePlanCommand() {
|
|
148
|
+
function renderOpenCodePlanCommand({ skillPath = '.agents/skills/gsdd-plan/SKILL.md' } = {}) {
|
|
149
149
|
return `---
|
|
150
150
|
description: OpenCode-native phase planning with fresh-context plan checking for GSDD
|
|
151
151
|
subtask: false
|
|
@@ -154,7 +154,7 @@ subtask: false
|
|
|
154
154
|
You are the OpenCode-native \`/gsdd-plan\` command for GSDD phase planning.
|
|
155
155
|
|
|
156
156
|
Portable contract:
|
|
157
|
-
- Read
|
|
157
|
+
- Read \`${skillPath}\` first. That file remains the canonical vendor-agnostic plan contract.
|
|
158
158
|
- Keep the portable contract honest: it defines the workflow, but it does not by itself prove fresh-context checker orchestration across runtimes.
|
|
159
159
|
- If the portable skill says plan is still a stub, treat that as a portability-status warning for the generic surface, not as a stop signal for this OpenCode-native adapter path.
|
|
160
160
|
|
|
@@ -174,7 +174,7 @@ Execution flow:
|
|
|
174
174
|
d. The explorer runs a GSD-style interactive conversation with the user (gray areas, research, deep-dive questions, assumptions) and writes APPROACH.md.
|
|
175
175
|
e. Before planning, confirm APPROACH.md records all canonical proof fields: \`alignment_status\`, \`alignment_method\`, \`user_confirmed_at\`, \`explicit_skip_approved\`, \`skip_scope\`, \`skip_rationale\`, and \`confirmed_decisions\`. For \`alignment_status: user_confirmed\`, \`confirmed_decisions\` must name the locked decisions and skip fields may be \`false\`/\`N/A\`; for \`alignment_status: approved_skip\`, \`explicit_skip_approved: true\`, \`skip_scope\`, and \`skip_rationale\` must be substantive. Agent-only "No questions needed" is not valid proof under \`workflow.discuss: true\`.
|
|
176
176
|
f. Load APPROACH.md decisions as locked constraints alongside SPEC.md decisions.
|
|
177
|
-
4. Produce the initial phase plan according to \`.
|
|
177
|
+
4. Produce the initial phase plan according to \`${skillPath}\`. Pass APPROACH.md decisions (if any) as locked constraints to the planner.
|
|
178
178
|
5. If \`.planning/config.json\` has \`workflow.planCheck: false\`, stop after planner self-check and explicitly report reduced assurance. This only skips the independent checker; it does not skip the step 3 alignment-proof gate when \`workflow.discuss: true\`.
|
|
179
179
|
6. If \`workflow.planCheck: true\`, invoke the hidden \`gsdd-plan-checker\` subagent with fresh context.
|
|
180
180
|
7. Pass only explicit inputs to the checker:
|
package/bin/gsdd.mjs
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
upsertBoundedBlock,
|
|
12
12
|
getDelegateContent,
|
|
13
13
|
} from './lib/rendering.mjs';
|
|
14
|
-
import { loadProjectModelConfig, getRuntimeModelOverride, resolveRuntimeAgentModel, cmdModels } from './lib/models.mjs';
|
|
14
|
+
import { loadProjectModelConfig, getRuntimeModelOverride, resolveRuntimeAgentModel, cmdModels, cmdRigor } from './lib/models.mjs';
|
|
15
15
|
import { createCmdInit, createCmdUpdate, cmdHelp } from './lib/init.mjs';
|
|
16
|
+
import { createCmdInstall } from './lib/global-install.mjs';
|
|
16
17
|
import { cmdFindPhase, cmdVerify, cmdScaffold, cmdPhaseStatus } from './lib/phase.mjs';
|
|
17
18
|
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
18
19
|
import { createCmdHealth } from './lib/health.mjs';
|
|
@@ -21,7 +22,9 @@ import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
|
21
22
|
import { cmdUiProof } from './lib/ui-proof.mjs';
|
|
22
23
|
import { cmdControlMap } from './lib/control-map.mjs';
|
|
23
24
|
import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
|
|
25
|
+
import { createCmdNext } from './lib/next.mjs';
|
|
24
26
|
import { resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
27
|
+
import { FRAMEWORK_VERSION, WORKFLOWS } from './lib/workflows.mjs';
|
|
25
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
29
|
const __dirname = dirname(__filename);
|
|
27
30
|
const DISTILLED_DIR = join(__dirname, '..', 'distilled');
|
|
@@ -31,33 +34,6 @@ const IS_MAIN = process.argv[1] ? realpathSync(process.argv[1]) === realpathSync
|
|
|
31
34
|
|
|
32
35
|
const [,, command, ...args] = process.argv;
|
|
33
36
|
|
|
34
|
-
function defineWorkflow({ mutatesArtifacts = true, ...workflow }) {
|
|
35
|
-
return {
|
|
36
|
-
...workflow,
|
|
37
|
-
mutatesArtifacts,
|
|
38
|
-
agent: mutatesArtifacts ? 'Code' : 'Plan',
|
|
39
|
-
opencodeType: mutatesArtifacts ? 'edit' : 'plan',
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const WORKFLOWS = [
|
|
44
|
-
defineWorkflow({ name: 'gsdd-new-project', workflow: 'new-project.md', description: 'New project - questioning, codebase audit, research, spec, roadmap' }),
|
|
45
|
-
defineWorkflow({ name: 'gsdd-map-codebase', workflow: 'map-codebase.md', description: 'Map or refresh codebase - 4 parallel mappers, staleness check, secrets scan' }),
|
|
46
|
-
defineWorkflow({ name: 'gsdd-plan', workflow: 'plan.md', description: 'Plan a phase - research check, backward planning, task creation' }),
|
|
47
|
-
defineWorkflow({ name: 'gsdd-execute', workflow: 'execute.md', description: 'Execute a phase plan - implement tasks, verify changes, follow repo git conventions' }),
|
|
48
|
-
defineWorkflow({ name: 'gsdd-verify', workflow: 'verify.md', description: 'Verify a completed phase - 3-level checks, anti-pattern scan' }),
|
|
49
|
-
defineWorkflow({ name: 'gsdd-verify-work', workflow: 'verify-work.md', description: 'Conversational UAT testing - validate user-facing behavior with structured gap tracking' }),
|
|
50
|
-
defineWorkflow({ name: 'gsdd-audit-milestone', workflow: 'audit-milestone.md', description: 'Audit a completed milestone - cross-phase integration, requirements coverage, E2E flows' }),
|
|
51
|
-
defineWorkflow({ name: 'gsdd-complete-milestone', workflow: 'complete-milestone.md', description: 'Complete milestone - archive, evolve spec, collapse roadmap' }),
|
|
52
|
-
defineWorkflow({ name: 'gsdd-new-milestone', workflow: 'new-milestone.md', description: 'New milestone - gather goals, define requirements, create roadmap phases' }),
|
|
53
|
-
defineWorkflow({ name: 'gsdd-plan-milestone-gaps', workflow: 'plan-milestone-gaps.md', description: 'Plan gap closure phases from audit results' }),
|
|
54
|
-
defineWorkflow({ name: 'gsdd-quick', workflow: 'quick.md', description: 'Quick task - plan and execute a sub-hour task outside the phase cycle' }),
|
|
55
|
-
defineWorkflow({ name: 'gsdd-pause', workflow: 'pause.md', description: 'Pause work - save session context for seamless resumption' }),
|
|
56
|
-
defineWorkflow({ name: 'gsdd-resume', workflow: 'resume.md', description: 'Resume work - restore context and route to next action' }),
|
|
57
|
-
defineWorkflow({ name: 'gsdd-progress', workflow: 'progress.md', description: 'Check progress - show project status and route to next action', mutatesArtifacts: false }),
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const FRAMEWORK_VERSION = 'v1.4';
|
|
61
37
|
function createCliContext(cwd = process.cwd()) {
|
|
62
38
|
return {
|
|
63
39
|
cwd,
|
|
@@ -68,6 +44,9 @@ function createCliContext(cwd = process.cwd()) {
|
|
|
68
44
|
packageVersion: PACKAGE_JSON.version,
|
|
69
45
|
workflows: WORKFLOWS,
|
|
70
46
|
frameworkVersion: FRAMEWORK_VERSION,
|
|
47
|
+
loadProjectModelConfig,
|
|
48
|
+
getRuntimeModelOverride,
|
|
49
|
+
resolveRuntimeAgentModel,
|
|
71
50
|
adapters: createAdapterRegistry({
|
|
72
51
|
cwd,
|
|
73
52
|
workflows: WORKFLOWS,
|
|
@@ -86,8 +65,10 @@ function createCliContext(cwd = process.cwd()) {
|
|
|
86
65
|
|
|
87
66
|
const INIT_CONTEXT = createCliContext(process.cwd());
|
|
88
67
|
const cmdInit = createCmdInit(INIT_CONTEXT);
|
|
68
|
+
const cmdInstall = createCmdInstall(INIT_CONTEXT);
|
|
89
69
|
const cmdHealth = createCmdHealth(INIT_CONTEXT);
|
|
90
70
|
const cmdCloseoutReport = createCmdCloseoutReport(INIT_CONTEXT);
|
|
71
|
+
const cmdNext = createCmdNext(INIT_CONTEXT);
|
|
91
72
|
|
|
92
73
|
const cmdUpdate = (...updateArgs) => {
|
|
93
74
|
const { args: normalizedArgs, workspaceRoot, invalid, error } = resolveWorkspaceContext(updateArgs, { cwd: INIT_CONTEXT.cwd });
|
|
@@ -98,12 +79,14 @@ const cmdUpdate = (...updateArgs) => {
|
|
|
98
79
|
}
|
|
99
80
|
return createCmdUpdate(createCliContext(workspaceRoot))(...normalizedArgs);
|
|
100
81
|
};
|
|
101
|
-
|
|
102
82
|
const COMMANDS = {
|
|
103
83
|
init: cmdInit,
|
|
84
|
+
install: cmdInstall,
|
|
104
85
|
update: cmdUpdate,
|
|
105
86
|
models: cmdModels,
|
|
87
|
+
rigor: cmdRigor,
|
|
106
88
|
health: cmdHealth,
|
|
89
|
+
next: cmdNext,
|
|
107
90
|
'file-op': cmdFileOp,
|
|
108
91
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
109
92
|
'session-fingerprint': cmdSessionFingerprint,
|
|
@@ -136,4 +119,4 @@ async function runCli(cliCommand = command, ...cliArgs) {
|
|
|
136
119
|
}
|
|
137
120
|
|
|
138
121
|
if (IS_MAIN) await runCli();
|
|
139
|
-
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFileOp, cmdLifecyclePreflight, cmdSessionFingerprint, cmdUiProof, cmdControlMap, cmdCloseoutReport, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
|
122
|
+
export { cmdHelp, cmdInit, cmdInstall, cmdUpdate, cmdModels, cmdRigor, cmdHealth, cmdNext, cmdFileOp, cmdLifecyclePreflight, cmdSessionFingerprint, cmdUiProof, cmdControlMap, cmdCloseoutReport, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
|
@@ -21,7 +21,7 @@ function notice(source, severity, entry) {
|
|
|
21
21
|
severity,
|
|
22
22
|
code: entry.code || entry.id || 'unknown',
|
|
23
23
|
message: entry.message,
|
|
24
|
-
fix: entry.
|
|
24
|
+
fix: entry.fix_hint || entry.fix || null,
|
|
25
25
|
path: entry.path || null,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
@@ -160,18 +160,38 @@ function nextSafeAction({ blockers, warnings, phaseNumber }) {
|
|
|
160
160
|
if (blockers.length > 0) {
|
|
161
161
|
return {
|
|
162
162
|
command: `gsdd verify ${phaseNumber}`,
|
|
163
|
-
reason: '
|
|
163
|
+
reason: 'Fix blockers first, then re-run closeout replay.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const hasWarn = warnings.some((entry) => entry.severity === 'warn');
|
|
167
|
+
if (warnings.length > 0 && hasWarn) {
|
|
168
|
+
const sources = new Set(warnings.filter((entry) => entry.severity === 'warn').map((entry) => entry.source));
|
|
169
|
+
if (sources.has('health')) {
|
|
170
|
+
return {
|
|
171
|
+
command: 'gsdd health --json',
|
|
172
|
+
reason: 'Resolve workspace health warnings before claiming the environment is clean.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (sources.has('ui_proof') || sources.has('phase_verification')) {
|
|
176
|
+
return {
|
|
177
|
+
command: `gsdd verify ${phaseNumber}`,
|
|
178
|
+
reason: 'Resolve phase verification warnings before claiming closeout is replay-clean.',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
command: 'gsdd control-map --json',
|
|
183
|
+
reason: 'Resolve local state warnings before claiming the environment is clean.',
|
|
164
184
|
};
|
|
165
185
|
}
|
|
166
186
|
if (warnings.length > 0) {
|
|
167
187
|
return {
|
|
168
188
|
command: 'gsdd control-map --json',
|
|
169
|
-
reason: 'Review
|
|
189
|
+
reason: 'Review the informational notices before claiming the local environment is clean.',
|
|
170
190
|
};
|
|
171
191
|
}
|
|
172
192
|
return {
|
|
173
193
|
command: `gsdd verify ${phaseNumber}`,
|
|
174
|
-
reason: '
|
|
194
|
+
reason: 'Closeout replay is clean; run formal verification for closure if it has not already been recorded.',
|
|
175
195
|
};
|
|
176
196
|
}
|
|
177
197
|
|
|
@@ -266,11 +286,17 @@ function printHuman(report) {
|
|
|
266
286
|
console.log(`Status: ${report.status}`);
|
|
267
287
|
if (report.blockers.length > 0) {
|
|
268
288
|
console.log('\nBlockers:');
|
|
269
|
-
for (const blocker of report.blockers)
|
|
289
|
+
for (const blocker of report.blockers) {
|
|
290
|
+
console.log(` - [${blocker.source}] ${blocker.code}: ${blocker.message}`);
|
|
291
|
+
if (blocker.fix) console.log(` Fix: ${blocker.fix}`);
|
|
292
|
+
}
|
|
270
293
|
}
|
|
271
294
|
if (report.warnings.length > 0) {
|
|
272
295
|
console.log('\nWarnings:');
|
|
273
|
-
for (const warning of report.warnings)
|
|
296
|
+
for (const warning of report.warnings) {
|
|
297
|
+
console.log(` - [${warning.source}] ${warning.code}: ${warning.message}`);
|
|
298
|
+
if (warning.fix) console.log(` Fix: ${warning.fix}`);
|
|
299
|
+
}
|
|
274
300
|
}
|
|
275
301
|
console.log(`\nNext safe action: ${report.next_safe_action.command}`);
|
|
276
302
|
console.log(`Reason: ${report.next_safe_action.reason}`);
|
package/bin/lib/control-map.mjs
CHANGED
|
@@ -982,29 +982,73 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
982
982
|
const writeSetOverlaps = findWriteSetOverlaps(writeEntries);
|
|
983
983
|
const dirtyWriteSetOverlaps = findDirtyWriteSetOverlaps(writeEntries, dirtyEntries);
|
|
984
984
|
|
|
985
|
+
function fixHintForRisk(risk) {
|
|
986
|
+
const code = risk.code;
|
|
987
|
+
switch (code) {
|
|
988
|
+
case 'canonical_git_invalid':
|
|
989
|
+
case 'worktree_git_invalid': {
|
|
990
|
+
const targetPath = risk.worktree_id || canonical.path;
|
|
991
|
+
return `Run \`git config --global --add safe.directory ${targetPath}\`, then re-run \`gsdd control-map --json\`.`;
|
|
992
|
+
}
|
|
993
|
+
case 'canonical_dirty':
|
|
994
|
+
return 'Commit, stash, or checkpoint the canonical changes before planning, cleanup, merge, or broad execution.';
|
|
995
|
+
case 'canonical_dirty_behind_upstream':
|
|
996
|
+
return 'Commit/stash the canonical changes or sync the branch; do not mutate a dirty checkout that is behind upstream.';
|
|
997
|
+
case 'canonical_branch_behind_upstream':
|
|
998
|
+
case 'canonical_branch_diverged_upstream':
|
|
999
|
+
case 'worktree_branch_behind_upstream':
|
|
1000
|
+
case 'worktree_branch_diverged_upstream':
|
|
1001
|
+
return 'Review upstream divergence (fetch/merge/rebase) before treating this branch state as an execution surface.';
|
|
1002
|
+
case 'detached_candidate_worktree':
|
|
1003
|
+
return 'Classify the detached worktree intent (active vs abandoned) before using it for execution or cleanup decisions.';
|
|
1004
|
+
case 'sibling_worktree_dirty':
|
|
1005
|
+
case 'unannotated_candidate_worktree':
|
|
1006
|
+
return 'Review sibling worktree ownership and write set before starting overlapping implementation.';
|
|
1007
|
+
case 'write_set_overlap':
|
|
1008
|
+
return 'Resolve overlapping local annotation write sets before starting another owned-write workflow.';
|
|
1009
|
+
case 'dirty_path_write_set_overlap':
|
|
1010
|
+
return 'Checkpoint or classify dirty paths that overlap annotated write sets before owned-write transitions.';
|
|
1011
|
+
case 'planning_state_drift':
|
|
1012
|
+
return 'Review drift and rebaseline with session-fingerprint only after confirming the planning changes are intentional.';
|
|
1013
|
+
default:
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
985
1018
|
for (const error of gitErrors) {
|
|
986
1019
|
risks.push({ code: error.code, severity: 'warn', message: error.message });
|
|
987
1020
|
}
|
|
988
1021
|
if (!canonical.git_valid) {
|
|
989
|
-
|
|
1022
|
+
const risk = {
|
|
1023
|
+
code: 'canonical_git_invalid',
|
|
1024
|
+
severity: 'warn',
|
|
1025
|
+
path: normalizeSlashes(canonical.path),
|
|
1026
|
+
message: `Canonical worktree git status failed: ${canonical.status_error || 'unknown error'}`,
|
|
1027
|
+
};
|
|
1028
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1029
|
+
risks.push(risk);
|
|
990
1030
|
}
|
|
991
1031
|
addBranchStateRisks(risks, canonical, { canonical: true });
|
|
992
1032
|
if (canonical.dirty.counts.tracked > 0 || canonical.dirty.counts.untracked > 0) {
|
|
993
|
-
|
|
1033
|
+
const risk = {
|
|
994
1034
|
code: 'canonical_dirty',
|
|
995
1035
|
severity: 'warn',
|
|
996
1036
|
message: `Canonical worktree has tracked/untracked changes (${canonical.dirty.counts.tracked} tracked, ${canonical.dirty.counts.untracked} untracked).`,
|
|
997
|
-
}
|
|
1037
|
+
};
|
|
1038
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1039
|
+
risks.push(risk);
|
|
998
1040
|
}
|
|
999
1041
|
if (canonical.dirty.counts.tracked > 0 && (canonical.ahead_behind?.behind || 0) > 0) {
|
|
1000
|
-
|
|
1042
|
+
const risk = {
|
|
1001
1043
|
code: 'canonical_dirty_behind_upstream',
|
|
1002
1044
|
severity: 'block',
|
|
1003
1045
|
branch: canonical.branch,
|
|
1004
1046
|
ahead: canonical.ahead_behind?.ahead,
|
|
1005
1047
|
behind: canonical.ahead_behind?.behind,
|
|
1006
1048
|
message: `Canonical worktree has tracked changes while behind upstream by ${canonical.ahead_behind.behind} commit(s).`,
|
|
1007
|
-
}
|
|
1049
|
+
};
|
|
1050
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1051
|
+
risks.push(risk);
|
|
1008
1052
|
}
|
|
1009
1053
|
if (canonical.dirty.counts.ignored > 0) {
|
|
1010
1054
|
risks.push({
|
|
@@ -1015,51 +1059,68 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1015
1059
|
}
|
|
1016
1060
|
for (const worktree of worktrees.filter((entry) => entry.path !== canonical.path)) {
|
|
1017
1061
|
if (!worktree.git_valid) {
|
|
1018
|
-
|
|
1062
|
+
const risk = {
|
|
1063
|
+
code: 'worktree_git_invalid',
|
|
1064
|
+
severity: 'warn',
|
|
1065
|
+
worktree_id: worktree.id,
|
|
1066
|
+
message: `Worktree ${worktree.id} could not be inspected by git.`,
|
|
1067
|
+
};
|
|
1068
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1069
|
+
risks.push(risk);
|
|
1019
1070
|
}
|
|
1020
1071
|
addBranchStateRisks(risks, worktree);
|
|
1021
1072
|
if (worktree.detached) {
|
|
1022
|
-
|
|
1073
|
+
const risk = {
|
|
1023
1074
|
code: 'detached_candidate_worktree',
|
|
1024
1075
|
severity: 'warn',
|
|
1025
1076
|
worktree_id: worktree.id,
|
|
1026
1077
|
message: `Worktree ${worktree.id} is detached; classify its intent before treating it as an execution surface.`,
|
|
1027
|
-
}
|
|
1078
|
+
};
|
|
1079
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1080
|
+
risks.push(risk);
|
|
1028
1081
|
}
|
|
1029
1082
|
if (worktree.dirty.counts.tracked > 0 || worktree.dirty.counts.untracked > 0) {
|
|
1030
|
-
|
|
1083
|
+
const risk = {
|
|
1031
1084
|
code: 'sibling_worktree_dirty',
|
|
1032
1085
|
severity: 'warn',
|
|
1033
1086
|
worktree_id: worktree.id,
|
|
1034
1087
|
message: `Sibling worktree ${worktree.id} has tracked/untracked changes.`,
|
|
1035
|
-
}
|
|
1088
|
+
};
|
|
1089
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1090
|
+
risks.push(risk);
|
|
1036
1091
|
}
|
|
1037
1092
|
if (!worktree.annotation && (worktree.dirty.counts.tracked > 0 || worktree.dirty.counts.untracked > 0 || worktree.detached)) {
|
|
1038
|
-
|
|
1093
|
+
const risk = {
|
|
1039
1094
|
code: 'unannotated_candidate_worktree',
|
|
1040
1095
|
severity: 'info',
|
|
1041
1096
|
worktree_id: worktree.id,
|
|
1042
1097
|
message: `Worktree ${worktree.id} has candidate-work signals but no local control-map annotation.`,
|
|
1043
|
-
}
|
|
1098
|
+
};
|
|
1099
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1100
|
+
risks.push(risk);
|
|
1044
1101
|
}
|
|
1045
1102
|
}
|
|
1046
1103
|
if (writeSetOverlaps.length > 0) {
|
|
1047
|
-
|
|
1104
|
+
const risk = {
|
|
1048
1105
|
code: 'write_set_overlap',
|
|
1049
1106
|
severity: 'block',
|
|
1050
1107
|
message: `Active control-map annotations have ${writeSetOverlaps.length} concrete write-set overlap(s).`,
|
|
1051
1108
|
overlaps: writeSetOverlaps.slice(0, MAX_DIRTY_BUCKET_ENTRIES),
|
|
1052
1109
|
omitted_count: Math.max(0, writeSetOverlaps.length - MAX_DIRTY_BUCKET_ENTRIES),
|
|
1053
|
-
}
|
|
1110
|
+
};
|
|
1111
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1112
|
+
risks.push(risk);
|
|
1054
1113
|
}
|
|
1055
1114
|
if (dirtyWriteSetOverlaps.length > 0) {
|
|
1056
|
-
|
|
1115
|
+
const risk = {
|
|
1057
1116
|
code: 'dirty_path_write_set_overlap',
|
|
1058
1117
|
severity: 'block',
|
|
1059
1118
|
message: `Live dirty paths overlap annotated write sets (${dirtyWriteSetOverlaps.length} overlap(s)).`,
|
|
1060
1119
|
overlaps: dirtyWriteSetOverlaps.slice(0, MAX_DIRTY_BUCKET_ENTRIES),
|
|
1061
1120
|
omitted_count: Math.max(0, dirtyWriteSetOverlaps.length - MAX_DIRTY_BUCKET_ENTRIES),
|
|
1062
|
-
}
|
|
1121
|
+
};
|
|
1122
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1123
|
+
risks.push(risk);
|
|
1063
1124
|
}
|
|
1064
1125
|
for (const warning of annotations.warnings || []) risks.push(warning);
|
|
1065
1126
|
for (const error of annotations.errors || []) {
|
|
@@ -1074,11 +1135,22 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1074
1135
|
});
|
|
1075
1136
|
}
|
|
1076
1137
|
if (workflowState.planning_drift.drifted) {
|
|
1077
|
-
|
|
1138
|
+
const risk = {
|
|
1078
1139
|
code: 'planning_state_drift',
|
|
1079
1140
|
severity: 'warn',
|
|
1080
1141
|
message: `Planning state drifted since the last fingerprint: ${workflowState.planning_drift.details.join('; ')}`,
|
|
1081
|
-
}
|
|
1142
|
+
};
|
|
1143
|
+
risk.fix_hint = fixHintForRisk(risk);
|
|
1144
|
+
risks.push(risk);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Ensure the common closure risks expose actionable fix guidance even when
|
|
1148
|
+
// the originating helper (for example branch-state risks) didn't attach it.
|
|
1149
|
+
for (const risk of risks) {
|
|
1150
|
+
if (!risk.fix_hint) {
|
|
1151
|
+
const hint = fixHintForRisk(risk);
|
|
1152
|
+
if (hint) risk.fix_hint = hint;
|
|
1153
|
+
}
|
|
1082
1154
|
}
|
|
1083
1155
|
return risks;
|
|
1084
1156
|
}
|
|
@@ -1175,7 +1247,10 @@ function printHuman(map) {
|
|
|
1175
1247
|
}
|
|
1176
1248
|
if (map.risks.length > 0) {
|
|
1177
1249
|
console.log('\nRisks:');
|
|
1178
|
-
for (const risk of map.risks)
|
|
1250
|
+
for (const risk of map.risks) {
|
|
1251
|
+
console.log(` - [${risk.severity || 'info'}] ${risk.code}: ${risk.message}`);
|
|
1252
|
+
if (risk.fix_hint) console.log(` Fix: ${risk.fix_hint}`);
|
|
1253
|
+
}
|
|
1179
1254
|
}
|
|
1180
1255
|
console.log('\nInterventions:');
|
|
1181
1256
|
for (const intervention of map.interventions) console.log(` - ${intervention}`);
|