gsdd-cli 0.24.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 -546
- package/bin/adapters/claude.mjs +18 -8
- package/bin/adapters/opencode.mjs +3 -3
- package/bin/gsdd.mjs +16 -33
- package/bin/lib/closeout-report.mjs +318 -0
- package/bin/lib/control-map.mjs +698 -26
- package/bin/lib/global-install.mjs +616 -0
- package/bin/lib/global-manifest.mjs +122 -0
- package/bin/lib/health.mjs +48 -30
- package/bin/lib/init-flow.mjs +3 -0
- package/bin/lib/init-prompts.mjs +6 -4
- package/bin/lib/init-runtime.mjs +33 -3
- package/bin/lib/lifecycle-preflight.mjs +58 -1
- package/bin/lib/models.mjs +136 -5
- package/bin/lib/next.mjs +834 -0
- package/bin/lib/phase.mjs +188 -41
- package/bin/lib/rendering.mjs +15 -1
- package/bin/lib/work-context.mjs +760 -0
- package/bin/lib/workflows.mjs +27 -0
- package/distilled/DESIGN.md +17 -9
- package/distilled/README.md +32 -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/distilled/workflows/verify.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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
import { realpathSync, readFileSync } from 'fs';
|
|
4
3
|
import { join, dirname } from 'path';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
@@ -12,8 +11,9 @@ import {
|
|
|
12
11
|
upsertBoundedBlock,
|
|
13
12
|
getDelegateContent,
|
|
14
13
|
} from './lib/rendering.mjs';
|
|
15
|
-
import { loadProjectModelConfig, getRuntimeModelOverride, resolveRuntimeAgentModel, cmdModels } from './lib/models.mjs';
|
|
14
|
+
import { loadProjectModelConfig, getRuntimeModelOverride, resolveRuntimeAgentModel, cmdModels, cmdRigor } from './lib/models.mjs';
|
|
16
15
|
import { createCmdInit, createCmdUpdate, cmdHelp } from './lib/init.mjs';
|
|
16
|
+
import { createCmdInstall } from './lib/global-install.mjs';
|
|
17
17
|
import { cmdFindPhase, cmdVerify, cmdScaffold, cmdPhaseStatus } from './lib/phase.mjs';
|
|
18
18
|
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
19
19
|
import { createCmdHealth } from './lib/health.mjs';
|
|
@@ -21,8 +21,10 @@ import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
|
21
21
|
import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
22
22
|
import { cmdUiProof } from './lib/ui-proof.mjs';
|
|
23
23
|
import { cmdControlMap } from './lib/control-map.mjs';
|
|
24
|
+
import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
|
|
25
|
+
import { createCmdNext } from './lib/next.mjs';
|
|
24
26
|
import { resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
25
|
-
|
|
27
|
+
import { FRAMEWORK_VERSION, WORKFLOWS } from './lib/workflows.mjs';
|
|
26
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
29
|
const __dirname = dirname(__filename);
|
|
28
30
|
const DISTILLED_DIR = join(__dirname, '..', 'distilled');
|
|
@@ -32,34 +34,6 @@ const IS_MAIN = process.argv[1] ? realpathSync(process.argv[1]) === realpathSync
|
|
|
32
34
|
|
|
33
35
|
const [,, command, ...args] = process.argv;
|
|
34
36
|
|
|
35
|
-
function defineWorkflow({ mutatesArtifacts = true, ...workflow }) {
|
|
36
|
-
return {
|
|
37
|
-
...workflow,
|
|
38
|
-
mutatesArtifacts,
|
|
39
|
-
agent: mutatesArtifacts ? 'Code' : 'Plan',
|
|
40
|
-
opencodeType: mutatesArtifacts ? 'edit' : 'plan',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const WORKFLOWS = [
|
|
45
|
-
defineWorkflow({ name: 'gsdd-new-project', workflow: 'new-project.md', description: 'New project - questioning, codebase audit, research, spec, roadmap' }),
|
|
46
|
-
defineWorkflow({ name: 'gsdd-map-codebase', workflow: 'map-codebase.md', description: 'Map or refresh codebase - 4 parallel mappers, staleness check, secrets scan' }),
|
|
47
|
-
defineWorkflow({ name: 'gsdd-plan', workflow: 'plan.md', description: 'Plan a phase - research check, backward planning, task creation' }),
|
|
48
|
-
defineWorkflow({ name: 'gsdd-execute', workflow: 'execute.md', description: 'Execute a phase plan - implement tasks, verify changes, follow repo git conventions' }),
|
|
49
|
-
defineWorkflow({ name: 'gsdd-verify', workflow: 'verify.md', description: 'Verify a completed phase - 3-level checks, anti-pattern scan' }),
|
|
50
|
-
defineWorkflow({ name: 'gsdd-verify-work', workflow: 'verify-work.md', description: 'Conversational UAT testing - validate user-facing behavior with structured gap tracking' }),
|
|
51
|
-
defineWorkflow({ name: 'gsdd-audit-milestone', workflow: 'audit-milestone.md', description: 'Audit a completed milestone - cross-phase integration, requirements coverage, E2E flows' }),
|
|
52
|
-
defineWorkflow({ name: 'gsdd-complete-milestone', workflow: 'complete-milestone.md', description: 'Complete milestone - archive, evolve spec, collapse roadmap' }),
|
|
53
|
-
defineWorkflow({ name: 'gsdd-new-milestone', workflow: 'new-milestone.md', description: 'New milestone - gather goals, define requirements, create roadmap phases' }),
|
|
54
|
-
defineWorkflow({ name: 'gsdd-plan-milestone-gaps', workflow: 'plan-milestone-gaps.md', description: 'Plan gap closure phases from audit results' }),
|
|
55
|
-
defineWorkflow({ name: 'gsdd-quick', workflow: 'quick.md', description: 'Quick task - plan and execute a sub-hour task outside the phase cycle' }),
|
|
56
|
-
defineWorkflow({ name: 'gsdd-pause', workflow: 'pause.md', description: 'Pause work - save session context for seamless resumption' }),
|
|
57
|
-
defineWorkflow({ name: 'gsdd-resume', workflow: 'resume.md', description: 'Resume work - restore context and route to next action' }),
|
|
58
|
-
defineWorkflow({ name: 'gsdd-progress', workflow: 'progress.md', description: 'Check progress - show project status and route to next action', mutatesArtifacts: false }),
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const FRAMEWORK_VERSION = 'v1.4';
|
|
62
|
-
|
|
63
37
|
function createCliContext(cwd = process.cwd()) {
|
|
64
38
|
return {
|
|
65
39
|
cwd,
|
|
@@ -70,6 +44,9 @@ function createCliContext(cwd = process.cwd()) {
|
|
|
70
44
|
packageVersion: PACKAGE_JSON.version,
|
|
71
45
|
workflows: WORKFLOWS,
|
|
72
46
|
frameworkVersion: FRAMEWORK_VERSION,
|
|
47
|
+
loadProjectModelConfig,
|
|
48
|
+
getRuntimeModelOverride,
|
|
49
|
+
resolveRuntimeAgentModel,
|
|
73
50
|
adapters: createAdapterRegistry({
|
|
74
51
|
cwd,
|
|
75
52
|
workflows: WORKFLOWS,
|
|
@@ -88,7 +65,10 @@ function createCliContext(cwd = process.cwd()) {
|
|
|
88
65
|
|
|
89
66
|
const INIT_CONTEXT = createCliContext(process.cwd());
|
|
90
67
|
const cmdInit = createCmdInit(INIT_CONTEXT);
|
|
68
|
+
const cmdInstall = createCmdInstall(INIT_CONTEXT);
|
|
91
69
|
const cmdHealth = createCmdHealth(INIT_CONTEXT);
|
|
70
|
+
const cmdCloseoutReport = createCmdCloseoutReport(INIT_CONTEXT);
|
|
71
|
+
const cmdNext = createCmdNext(INIT_CONTEXT);
|
|
92
72
|
|
|
93
73
|
const cmdUpdate = (...updateArgs) => {
|
|
94
74
|
const { args: normalizedArgs, workspaceRoot, invalid, error } = resolveWorkspaceContext(updateArgs, { cwd: INIT_CONTEXT.cwd });
|
|
@@ -99,17 +79,20 @@ const cmdUpdate = (...updateArgs) => {
|
|
|
99
79
|
}
|
|
100
80
|
return createCmdUpdate(createCliContext(workspaceRoot))(...normalizedArgs);
|
|
101
81
|
};
|
|
102
|
-
|
|
103
82
|
const COMMANDS = {
|
|
104
83
|
init: cmdInit,
|
|
84
|
+
install: cmdInstall,
|
|
105
85
|
update: cmdUpdate,
|
|
106
86
|
models: cmdModels,
|
|
87
|
+
rigor: cmdRigor,
|
|
107
88
|
health: cmdHealth,
|
|
89
|
+
next: cmdNext,
|
|
108
90
|
'file-op': cmdFileOp,
|
|
109
91
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
110
92
|
'session-fingerprint': cmdSessionFingerprint,
|
|
111
93
|
'ui-proof': cmdUiProof,
|
|
112
94
|
'control-map': cmdControlMap,
|
|
95
|
+
'closeout-report': cmdCloseoutReport,
|
|
113
96
|
'find-phase': cmdFindPhase,
|
|
114
97
|
'phase-status': cmdPhaseStatus,
|
|
115
98
|
verify: cmdVerify,
|
|
@@ -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, 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 };
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// closeout-report.mjs - read-only post-merge closure replay report
|
|
2
|
+
|
|
3
|
+
import { output, parseFlagValue } from './cli-utils.mjs';
|
|
4
|
+
import { buildControlMap } from './control-map.mjs';
|
|
5
|
+
import { evaluateLifecyclePreflight } from './lifecycle-preflight.mjs';
|
|
6
|
+
import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
|
|
7
|
+
import { buildPhaseVerificationReport } from './phase.mjs';
|
|
8
|
+
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
9
|
+
|
|
10
|
+
const USAGE = 'Usage: gsdd closeout-report [--json] [--phase <N>]';
|
|
11
|
+
|
|
12
|
+
function severityFromRisk(risk) {
|
|
13
|
+
if (risk.severity === 'block') return 'blocker';
|
|
14
|
+
if (risk.severity === 'warn') return 'warn';
|
|
15
|
+
return risk.severity || 'info';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function notice(source, severity, entry) {
|
|
19
|
+
return {
|
|
20
|
+
source,
|
|
21
|
+
severity,
|
|
22
|
+
code: entry.code || entry.id || 'unknown',
|
|
23
|
+
message: entry.message,
|
|
24
|
+
fix: entry.fix_hint || entry.fix || null,
|
|
25
|
+
path: entry.path || null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function latestCompletedPhase(lifecycle) {
|
|
30
|
+
const completed = lifecycle.phases.filter((phase) => phase.status === 'done');
|
|
31
|
+
return completed.length > 0 ? completed[completed.length - 1] : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function buildHealthReportSafe(ctx, args) {
|
|
35
|
+
try {
|
|
36
|
+
const health = await import('./health.mjs');
|
|
37
|
+
return health.buildHealthReport(ctx, args);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
status: 'degraded',
|
|
41
|
+
errors: [],
|
|
42
|
+
warnings: [{
|
|
43
|
+
id: 'W_CLOSEOUT_HEALTH_UNAVAILABLE',
|
|
44
|
+
severity: 'WARN',
|
|
45
|
+
message: `Health report builder unavailable in this helper runtime: ${error.message}`,
|
|
46
|
+
fix: 'Run the source CLI `gsdd health --json` for full health diagnostics, or refresh generated helper support.',
|
|
47
|
+
}],
|
|
48
|
+
info: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function summarizeControlMap(map) {
|
|
54
|
+
return {
|
|
55
|
+
status: map.risks.some((risk) => risk.severity === 'block')
|
|
56
|
+
? 'blocked'
|
|
57
|
+
: map.risks.some((risk) => risk.severity === 'warn')
|
|
58
|
+
? 'warnings'
|
|
59
|
+
: 'clear',
|
|
60
|
+
authority: map.authority,
|
|
61
|
+
canonical_worktree: {
|
|
62
|
+
branch: map.canonical_worktree.branch,
|
|
63
|
+
head: map.canonical_worktree.head,
|
|
64
|
+
dirty: map.canonical_worktree.dirty,
|
|
65
|
+
ahead_behind: map.canonical_worktree.ahead_behind,
|
|
66
|
+
},
|
|
67
|
+
worktree_count: map.worktrees.length,
|
|
68
|
+
risks: map.risks,
|
|
69
|
+
interventions: map.interventions,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function summarizePreflight(preflight) {
|
|
74
|
+
return {
|
|
75
|
+
status: preflight.status,
|
|
76
|
+
allowed: preflight.allowed,
|
|
77
|
+
reason: preflight.reason,
|
|
78
|
+
blockers: preflight.blockers,
|
|
79
|
+
warnings: preflight.warnings,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function summarizePhaseVerification(report) {
|
|
84
|
+
if (!report.ok) {
|
|
85
|
+
return {
|
|
86
|
+
status: 'blocked',
|
|
87
|
+
verified: false,
|
|
88
|
+
error: report.error,
|
|
89
|
+
blocked_on: ['verification'],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
status: report.result.verified ? 'passed' : 'blocked',
|
|
94
|
+
verified: report.result.verified,
|
|
95
|
+
phase_artifacts_present: report.result.phase_artifacts_present,
|
|
96
|
+
prerequisite_status: report.result.prerequisite_status,
|
|
97
|
+
artifact_status: report.result.artifact_status,
|
|
98
|
+
blocked_on: report.result.blocked_on,
|
|
99
|
+
blocks_verification: report.result.blocks_verification,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function collectBlockers({ health, preflight, phaseReport }) {
|
|
104
|
+
const blockers = [];
|
|
105
|
+
const uiProofErrors = phaseReport.result?.uiProof?.errors || [];
|
|
106
|
+
const uiProofGate = phaseReport.result?.ui_proof || {};
|
|
107
|
+
|
|
108
|
+
blockers.push(...health.errors.map((entry) => notice('health', 'blocker', entry)));
|
|
109
|
+
blockers.push(...preflight.blockers.map((entry) => notice('preflight', 'blocker', entry)));
|
|
110
|
+
|
|
111
|
+
if (!phaseReport.ok) {
|
|
112
|
+
blockers.push(notice('phase_verification', 'blocker', {
|
|
113
|
+
code: 'phase_verification_error',
|
|
114
|
+
message: phaseReport.error,
|
|
115
|
+
fix_hint: 'Run the direct verify command for the selected phase and repair the reported prerequisite.',
|
|
116
|
+
}));
|
|
117
|
+
} else {
|
|
118
|
+
for (const blockerEntry of phaseReport.result.prerequisite_status.blockers || []) {
|
|
119
|
+
blockers.push(notice('phase_verification', 'blocker', blockerEntry));
|
|
120
|
+
}
|
|
121
|
+
for (const artifact of phaseReport.result.artifact_status.unsatisfied || []) {
|
|
122
|
+
blockers.push(notice('phase_verification', 'blocker', {
|
|
123
|
+
code: 'unsatisfied_plan_artifact',
|
|
124
|
+
message: `${artifact.file} is not ${artifact.expected}.`,
|
|
125
|
+
fix_hint: artifact.fix_hint,
|
|
126
|
+
path: artifact.file,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
for (const entry of uiProofErrors) {
|
|
130
|
+
blockers.push(notice('ui_proof', entry.severity === 'warn' ? 'warn' : 'blocker', entry));
|
|
131
|
+
}
|
|
132
|
+
if (uiProofErrors.length === 0 && uiProofGate.blocks_verification) {
|
|
133
|
+
blockers.push(notice('ui_proof', 'blocker', {
|
|
134
|
+
code: uiProofGate.required_block || 'ui_proof_verification_failed',
|
|
135
|
+
message: `UI proof verification is required for phase ${phaseReport.result.phase} but reported status ${uiProofGate.status}.`,
|
|
136
|
+
fix_hint: 'Run the phase-specific UI proof checks and supply a passing observed proof bundle for each declared slot.',
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return blockers.filter((entry) => entry.severity === 'blocker');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectWarnings({ controlMap, health, preflight, phaseReport }) {
|
|
145
|
+
const warnings = [];
|
|
146
|
+
warnings.push(...controlMap.risks
|
|
147
|
+
.filter((risk) => risk.severity !== 'block')
|
|
148
|
+
.map((risk) => notice('control_map', severityFromRisk(risk), risk)));
|
|
149
|
+
warnings.push(...health.warnings.map((entry) => notice('health', 'warn', entry)));
|
|
150
|
+
warnings.push(...preflight.warnings
|
|
151
|
+
.filter((entry) => entry.source !== 'control-map')
|
|
152
|
+
.map((entry) => notice('preflight', entry.severity === 'info' ? 'info' : 'warn', entry)));
|
|
153
|
+
if (phaseReport.ok) {
|
|
154
|
+
warnings.push(...(phaseReport.result.uiProof?.warnings || []).map((entry) => notice('ui_proof', 'warn', entry)));
|
|
155
|
+
}
|
|
156
|
+
return warnings;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function nextSafeAction({ blockers, warnings, phaseNumber }) {
|
|
160
|
+
if (blockers.length > 0) {
|
|
161
|
+
return {
|
|
162
|
+
command: `gsdd verify ${phaseNumber}`,
|
|
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.',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (warnings.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
command: 'gsdd control-map --json',
|
|
189
|
+
reason: 'Review the informational notices before claiming the local environment is clean.',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
command: `gsdd verify ${phaseNumber}`,
|
|
194
|
+
reason: 'Closeout replay is clean; run formal verification for closure if it has not already been recorded.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function buildCloseoutReport(ctx = {}, args = []) {
|
|
199
|
+
const context = resolveWorkspaceContext(args, { cwd: ctx.cwd || process.cwd() });
|
|
200
|
+
if (context.invalid) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: context.error,
|
|
204
|
+
exitCode: 1,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const phaseArg = parseFlagValue(context.args, '--phase');
|
|
209
|
+
if (phaseArg.invalid) {
|
|
210
|
+
return { ok: false, error: USAGE, exitCode: 1 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const filteredArgs = context.args.filter((arg, index) => {
|
|
214
|
+
if (arg === '--json') return false;
|
|
215
|
+
if (arg === '--phase') return false;
|
|
216
|
+
if (index > 0 && context.args[index - 1] === '--phase') return false;
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
if (filteredArgs.length > 0) {
|
|
220
|
+
return { ok: false, error: USAGE, exitCode: 1 };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lifecycle = evaluateLifecycleState({ planningDir: context.planningDir });
|
|
224
|
+
const selectedPhase = phaseArg.present
|
|
225
|
+
? normalizePhaseToken(phaseArg.value)
|
|
226
|
+
: latestCompletedPhase(lifecycle)?.number || null;
|
|
227
|
+
if (!selectedPhase) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
error: 'No completed phase found. Pass --phase <N> to replay a specific phase.',
|
|
231
|
+
exitCode: 1,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const controlMap = buildControlMap({
|
|
236
|
+
workspaceRoot: context.workspaceRoot,
|
|
237
|
+
planningDir: context.planningDir,
|
|
238
|
+
});
|
|
239
|
+
const health = await buildHealthReportSafe(ctx, ['--workspace-root', context.workspaceRoot]);
|
|
240
|
+
const preflight = evaluateLifecyclePreflight({
|
|
241
|
+
planningDir: context.planningDir,
|
|
242
|
+
surface: 'verify',
|
|
243
|
+
phaseNumber: selectedPhase,
|
|
244
|
+
expectsMutation: 'phase-status',
|
|
245
|
+
controlMapReport: controlMap,
|
|
246
|
+
});
|
|
247
|
+
const phaseReport = buildPhaseVerificationReport('--workspace-root', context.workspaceRoot, selectedPhase);
|
|
248
|
+
const blockers = collectBlockers({ health, preflight, phaseReport });
|
|
249
|
+
const warnings = collectWarnings({ controlMap, health, preflight, phaseReport });
|
|
250
|
+
const status = blockers.length > 0 ? 'blocked' : warnings.length > 0 ? 'warnings' : 'clear';
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
exitCode: blockers.length > 0 ? 1 : 0,
|
|
255
|
+
result: {
|
|
256
|
+
schema_version: 1,
|
|
257
|
+
operation: 'closeout-report',
|
|
258
|
+
generated_at: new Date().toISOString(),
|
|
259
|
+
workspace_root: context.workspaceRoot.replace(/\\/g, '/'),
|
|
260
|
+
phase: selectedPhase,
|
|
261
|
+
scope: {
|
|
262
|
+
defaulted_to_latest_completed: !phaseArg.present,
|
|
263
|
+
latest_completed_phase: latestCompletedPhase(lifecycle)?.number || null,
|
|
264
|
+
},
|
|
265
|
+
status,
|
|
266
|
+
blockers,
|
|
267
|
+
warnings,
|
|
268
|
+
next_safe_action: nextSafeAction({ blockers, warnings, phaseNumber: selectedPhase }),
|
|
269
|
+
control_map: summarizeControlMap(controlMap),
|
|
270
|
+
health: {
|
|
271
|
+
status: health.status,
|
|
272
|
+
errors: health.errors,
|
|
273
|
+
warnings: health.warnings,
|
|
274
|
+
info: health.info,
|
|
275
|
+
},
|
|
276
|
+
preflight: summarizePreflight(preflight),
|
|
277
|
+
phase_verification: summarizePhaseVerification(phaseReport),
|
|
278
|
+
ui_proof: phaseReport.ok ? phaseReport.result.ui_proof : null,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function printHuman(report) {
|
|
284
|
+
console.log('gsdd closeout-report - read-only closure replay\n');
|
|
285
|
+
console.log(`Phase: ${report.phase}`);
|
|
286
|
+
console.log(`Status: ${report.status}`);
|
|
287
|
+
if (report.blockers.length > 0) {
|
|
288
|
+
console.log('\nBlockers:');
|
|
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
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (report.warnings.length > 0) {
|
|
295
|
+
console.log('\nWarnings:');
|
|
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
|
+
}
|
|
300
|
+
}
|
|
301
|
+
console.log(`\nNext safe action: ${report.next_safe_action.command}`);
|
|
302
|
+
console.log(`Reason: ${report.next_safe_action.reason}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createCmdCloseoutReport(ctx = {}) {
|
|
306
|
+
return async function cmdCloseoutReport(...args) {
|
|
307
|
+
const jsonMode = args.includes('--json');
|
|
308
|
+
const report = await buildCloseoutReport(ctx, args);
|
|
309
|
+
if (!report.ok) {
|
|
310
|
+
console.error(report.error);
|
|
311
|
+
process.exitCode = report.exitCode;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (jsonMode) output(report.result);
|
|
315
|
+
else printHuman(report.result);
|
|
316
|
+
if (report.exitCode !== 0) process.exitCode = report.exitCode;
|
|
317
|
+
};
|
|
318
|
+
}
|