gsdd-cli 0.23.0 → 0.25.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 +3 -1
- package/bin/gsdd.mjs +4 -4
- package/bin/lib/closeout-report.mjs +292 -0
- package/bin/lib/control-map.mjs +612 -15
- package/bin/lib/health.mjs +46 -29
- package/bin/lib/init-runtime.mjs +8 -1
- package/bin/lib/lifecycle-preflight.mjs +58 -1
- package/bin/lib/phase.mjs +386 -11
- package/bin/lib/rendering.mjs +15 -1
- package/bin/lib/ui-proof.mjs +39 -4
- package/distilled/DESIGN.md +17 -9
- package/distilled/README.md +3 -1
- package/distilled/workflows/verify.md +3 -2
- package/docs/USER-GUIDE.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -347,6 +347,8 @@ Workflows are agent skills or commands, not plain shell utilities. How you invok
|
|
|
347
347
|
| `npx -y gsdd-cli update [--tools <platform>] [--templates]` | Regenerate skills/adapters and refresh the repo-local helper runtime; `--templates` refreshes `.planning/templates/` and role contracts |
|
|
348
348
|
| `npx -y gsdd-cli health [--json]` | Check workspace integrity and generated-surface freshness (healthy/degraded/broken) |
|
|
349
349
|
| `npx -y gsdd-cli control-map [--json] [--with-ignored]` | Report computed repo/worktree/planning state, dirty buckets, optional ignored-path scan, local annotations, and safe next interventions |
|
|
350
|
+
| `npx -y gsdd-cli control-map annotate <set\|clear>` | Maintain optional local intent annotations; stale updates fail closed unless explicitly refreshed |
|
|
351
|
+
| `npx -y gsdd-cli closeout-report [--json] [--phase <N>]` | Replay read-only closure status from control-map, health/preflight, verify, and UI-proof signals |
|
|
350
352
|
| `npx -y gsdd-cli ui-proof validate <path> [--claim <public\|publication\|tracked\|delivery\|release>]` | Validate UI proof bundle metadata without requiring browser tooling; use `--claim` only when validating that stronger proof use |
|
|
351
353
|
| `npx -y gsdd-cli file-op <copy\|delete\|regex-sub>` | Run deterministic workspace-confined file copy, delete, and regex substitution |
|
|
352
354
|
| `npx -y gsdd-cli find-phase [N]` | Show phase info as JSON (for agent consumption) |
|
|
@@ -416,7 +418,7 @@ Model IDs pass through a two-layer injection guard: a regex whitelist (`/^[a-zA-
|
|
|
416
418
|
| `.planning/research/` | Research outputs |
|
|
417
419
|
| `.planning/codebase/` | Codebase maps (4 files) |
|
|
418
420
|
| `.planning/quick/` | Quick task tracking |
|
|
419
|
-
| `.planning/.local/` | Local-only operational annotations such as control-map intent; never product truth |
|
|
421
|
+
| `.planning/.local/` | Local-only operational annotations such as control-map intent; `control-map annotate` can maintain them, but they are never product truth |
|
|
420
422
|
| `.planning/.continue-here.md` | Session checkpoint (created by pause, consumed by resume) |
|
|
421
423
|
|
|
422
424
|
### Advisory Git Protocol
|
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';
|
|
@@ -21,8 +20,8 @@ import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
|
21
20
|
import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
22
21
|
import { cmdUiProof } from './lib/ui-proof.mjs';
|
|
23
22
|
import { cmdControlMap } from './lib/control-map.mjs';
|
|
23
|
+
import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
|
|
24
24
|
import { resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
25
|
-
|
|
26
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
26
|
const __dirname = dirname(__filename);
|
|
28
27
|
const DISTILLED_DIR = join(__dirname, '..', 'distilled');
|
|
@@ -59,7 +58,6 @@ const WORKFLOWS = [
|
|
|
59
58
|
];
|
|
60
59
|
|
|
61
60
|
const FRAMEWORK_VERSION = 'v1.4';
|
|
62
|
-
|
|
63
61
|
function createCliContext(cwd = process.cwd()) {
|
|
64
62
|
return {
|
|
65
63
|
cwd,
|
|
@@ -89,6 +87,7 @@ function createCliContext(cwd = process.cwd()) {
|
|
|
89
87
|
const INIT_CONTEXT = createCliContext(process.cwd());
|
|
90
88
|
const cmdInit = createCmdInit(INIT_CONTEXT);
|
|
91
89
|
const cmdHealth = createCmdHealth(INIT_CONTEXT);
|
|
90
|
+
const cmdCloseoutReport = createCmdCloseoutReport(INIT_CONTEXT);
|
|
92
91
|
|
|
93
92
|
const cmdUpdate = (...updateArgs) => {
|
|
94
93
|
const { args: normalizedArgs, workspaceRoot, invalid, error } = resolveWorkspaceContext(updateArgs, { cwd: INIT_CONTEXT.cwd });
|
|
@@ -110,6 +109,7 @@ const COMMANDS = {
|
|
|
110
109
|
'session-fingerprint': cmdSessionFingerprint,
|
|
111
110
|
'ui-proof': cmdUiProof,
|
|
112
111
|
'control-map': cmdControlMap,
|
|
112
|
+
'closeout-report': cmdCloseoutReport,
|
|
113
113
|
'find-phase': cmdFindPhase,
|
|
114
114
|
'phase-status': cmdPhaseStatus,
|
|
115
115
|
verify: cmdVerify,
|
|
@@ -136,4 +136,4 @@ async function runCli(cliCommand = command, ...cliArgs) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
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 };
|
|
139
|
+
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFileOp, cmdLifecyclePreflight, cmdSessionFingerprint, cmdUiProof, cmdControlMap, cmdCloseoutReport, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
|
@@ -0,0 +1,292 @@
|
|
|
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 || entry.fix_hint || 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: 'Repair blockers before treating closeout as replayed.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (warnings.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
command: 'gsdd control-map --json',
|
|
169
|
+
reason: 'Review warnings before claiming the local environment is clean.',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
command: `gsdd verify ${phaseNumber}`,
|
|
174
|
+
reason: 'Phase implementation is replay-clean; run formal verification for closure if it has not already been recorded.',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function buildCloseoutReport(ctx = {}, args = []) {
|
|
179
|
+
const context = resolveWorkspaceContext(args, { cwd: ctx.cwd || process.cwd() });
|
|
180
|
+
if (context.invalid) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: context.error,
|
|
184
|
+
exitCode: 1,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const phaseArg = parseFlagValue(context.args, '--phase');
|
|
189
|
+
if (phaseArg.invalid) {
|
|
190
|
+
return { ok: false, error: USAGE, exitCode: 1 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const filteredArgs = context.args.filter((arg, index) => {
|
|
194
|
+
if (arg === '--json') return false;
|
|
195
|
+
if (arg === '--phase') return false;
|
|
196
|
+
if (index > 0 && context.args[index - 1] === '--phase') return false;
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
if (filteredArgs.length > 0) {
|
|
200
|
+
return { ok: false, error: USAGE, exitCode: 1 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lifecycle = evaluateLifecycleState({ planningDir: context.planningDir });
|
|
204
|
+
const selectedPhase = phaseArg.present
|
|
205
|
+
? normalizePhaseToken(phaseArg.value)
|
|
206
|
+
: latestCompletedPhase(lifecycle)?.number || null;
|
|
207
|
+
if (!selectedPhase) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: 'No completed phase found. Pass --phase <N> to replay a specific phase.',
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const controlMap = buildControlMap({
|
|
216
|
+
workspaceRoot: context.workspaceRoot,
|
|
217
|
+
planningDir: context.planningDir,
|
|
218
|
+
});
|
|
219
|
+
const health = await buildHealthReportSafe(ctx, ['--workspace-root', context.workspaceRoot]);
|
|
220
|
+
const preflight = evaluateLifecyclePreflight({
|
|
221
|
+
planningDir: context.planningDir,
|
|
222
|
+
surface: 'verify',
|
|
223
|
+
phaseNumber: selectedPhase,
|
|
224
|
+
expectsMutation: 'phase-status',
|
|
225
|
+
controlMapReport: controlMap,
|
|
226
|
+
});
|
|
227
|
+
const phaseReport = buildPhaseVerificationReport('--workspace-root', context.workspaceRoot, selectedPhase);
|
|
228
|
+
const blockers = collectBlockers({ health, preflight, phaseReport });
|
|
229
|
+
const warnings = collectWarnings({ controlMap, health, preflight, phaseReport });
|
|
230
|
+
const status = blockers.length > 0 ? 'blocked' : warnings.length > 0 ? 'warnings' : 'clear';
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
exitCode: blockers.length > 0 ? 1 : 0,
|
|
235
|
+
result: {
|
|
236
|
+
schema_version: 1,
|
|
237
|
+
operation: 'closeout-report',
|
|
238
|
+
generated_at: new Date().toISOString(),
|
|
239
|
+
workspace_root: context.workspaceRoot.replace(/\\/g, '/'),
|
|
240
|
+
phase: selectedPhase,
|
|
241
|
+
scope: {
|
|
242
|
+
defaulted_to_latest_completed: !phaseArg.present,
|
|
243
|
+
latest_completed_phase: latestCompletedPhase(lifecycle)?.number || null,
|
|
244
|
+
},
|
|
245
|
+
status,
|
|
246
|
+
blockers,
|
|
247
|
+
warnings,
|
|
248
|
+
next_safe_action: nextSafeAction({ blockers, warnings, phaseNumber: selectedPhase }),
|
|
249
|
+
control_map: summarizeControlMap(controlMap),
|
|
250
|
+
health: {
|
|
251
|
+
status: health.status,
|
|
252
|
+
errors: health.errors,
|
|
253
|
+
warnings: health.warnings,
|
|
254
|
+
info: health.info,
|
|
255
|
+
},
|
|
256
|
+
preflight: summarizePreflight(preflight),
|
|
257
|
+
phase_verification: summarizePhaseVerification(phaseReport),
|
|
258
|
+
ui_proof: phaseReport.ok ? phaseReport.result.ui_proof : null,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function printHuman(report) {
|
|
264
|
+
console.log('gsdd closeout-report - read-only closure replay\n');
|
|
265
|
+
console.log(`Phase: ${report.phase}`);
|
|
266
|
+
console.log(`Status: ${report.status}`);
|
|
267
|
+
if (report.blockers.length > 0) {
|
|
268
|
+
console.log('\nBlockers:');
|
|
269
|
+
for (const blocker of report.blockers) console.log(` - [${blocker.source}] ${blocker.code}: ${blocker.message}`);
|
|
270
|
+
}
|
|
271
|
+
if (report.warnings.length > 0) {
|
|
272
|
+
console.log('\nWarnings:');
|
|
273
|
+
for (const warning of report.warnings) console.log(` - [${warning.source}] ${warning.code}: ${warning.message}`);
|
|
274
|
+
}
|
|
275
|
+
console.log(`\nNext safe action: ${report.next_safe_action.command}`);
|
|
276
|
+
console.log(`Reason: ${report.next_safe_action.reason}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function createCmdCloseoutReport(ctx = {}) {
|
|
280
|
+
return async function cmdCloseoutReport(...args) {
|
|
281
|
+
const jsonMode = args.includes('--json');
|
|
282
|
+
const report = await buildCloseoutReport(ctx, args);
|
|
283
|
+
if (!report.ok) {
|
|
284
|
+
console.error(report.error);
|
|
285
|
+
process.exitCode = report.exitCode;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (jsonMode) output(report.result);
|
|
289
|
+
else printHuman(report.result);
|
|
290
|
+
if (report.exitCode !== 0) process.exitCode = report.exitCode;
|
|
291
|
+
};
|
|
292
|
+
}
|