gsdd-cli 0.26.0 → 0.28.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 +55 -23
- package/agents/DISTILLATION.md +2 -2
- package/agents/README.md +4 -4
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +14 -14
- package/agents/integration-checker.md +2 -2
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +6 -6
- package/agents/synthesizer.md +18 -18
- package/agents/verifier.md +2 -2
- package/bin/adapters/agents.mjs +3 -3
- package/bin/adapters/claude.mjs +16 -14
- package/bin/adapters/opencode.mjs +15 -12
- package/bin/gsdd.mjs +16 -13
- package/bin/lib/{models.mjs → config.mjs} +23 -16
- package/bin/lib/control-map.mjs +17 -488
- package/bin/lib/global-install.mjs +16 -3
- package/bin/lib/health-truth.mjs +8 -13
- package/bin/lib/health.mjs +25 -39
- package/bin/lib/init-flow.mjs +44 -38
- package/bin/lib/init-prompts.mjs +3 -3
- package/bin/lib/init-runtime.mjs +17 -33
- package/bin/lib/lifecycle-preflight.mjs +297 -404
- package/bin/lib/lifecycle-state.mjs +2 -1
- package/bin/lib/next.mjs +243 -20
- package/bin/lib/phase.mjs +10 -315
- package/bin/lib/rendering.mjs +64 -44
- package/bin/lib/runtime-freshness.mjs +18 -15
- package/bin/lib/state-dir.mjs +45 -0
- package/bin/lib/templates.mjs +59 -22
- package/bin/lib/work-context.mjs +12 -1
- package/bin/lib/workflows.mjs +0 -1
- package/bin/lib/workspace-root.mjs +11 -6
- package/distilled/DESIGN.md +58 -2
- package/distilled/EVIDENCE-INDEX.md +15 -2
- package/distilled/README.md +23 -33
- package/distilled/SKILL.md +9 -10
- package/distilled/templates/agents.block.md +5 -5
- package/distilled/templates/approach.md +3 -3
- package/distilled/templates/auth-matrix.md +2 -2
- package/distilled/templates/brownfield-change/CHANGE.md +1 -1
- package/distilled/templates/delegates/approach-explorer.md +5 -5
- package/distilled/templates/delegates/mapper-arch.md +3 -3
- package/distilled/templates/delegates/mapper-concerns.md +4 -4
- package/distilled/templates/delegates/mapper-quality.md +3 -3
- package/distilled/templates/delegates/mapper-tech.md +3 -3
- package/distilled/templates/delegates/plan-checker.md +5 -5
- package/distilled/templates/delegates/researcher-architecture.md +3 -3
- package/distilled/templates/delegates/researcher-features.md +3 -3
- package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
- package/distilled/templates/delegates/researcher-stack.md +3 -3
- package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
- package/distilled/templates/research/architecture.md +1 -1
- package/distilled/templates/research/pitfalls.md +1 -1
- package/distilled/templates/research/stack.md +1 -1
- package/distilled/templates/roadmap.md +4 -4
- package/distilled/templates/spec.md +2 -2
- package/distilled/workflows/audit-milestone.md +22 -22
- package/distilled/workflows/complete-milestone.md +35 -35
- package/distilled/workflows/execute.md +29 -29
- package/distilled/workflows/map-codebase.md +30 -30
- package/distilled/workflows/new-milestone.md +18 -18
- package/distilled/workflows/new-project.md +45 -45
- package/distilled/workflows/pause.md +15 -15
- package/distilled/workflows/plan.md +63 -63
- package/distilled/workflows/progress.md +40 -39
- package/distilled/workflows/quick.md +43 -43
- package/distilled/workflows/resume.md +23 -22
- package/distilled/workflows/verify-work.md +7 -7
- package/distilled/workflows/verify.md +20 -20
- package/docs/BROWNFIELD-PROOF.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +16 -16
- package/docs/USER-GUIDE.md +21 -21
- package/docs/claude/context-monitor.md +1 -1
- package/docs/proof/consumer-node-cli/README.md +1 -1
- package/package.json +3 -3
- package/bin/lib/closeout-report.mjs +0 -318
- package/bin/lib/evidence-contract.mjs +0 -325
- package/bin/lib/provenance.mjs +0 -390
- package/bin/lib/session-fingerprint.mjs +0 -223
- package/bin/lib/ui-proof.mjs +0 -1007
- package/distilled/workflows/plan-milestone-gaps.md +0 -204
package/bin/lib/phase.mjs
CHANGED
|
@@ -4,16 +4,9 @@
|
|
|
4
4
|
// evaluate once, so CWD must be computed inside function bodies.
|
|
5
5
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
7
|
-
import { dirname, join, relative } from 'path';
|
|
7
|
+
import { basename, dirname, join, relative } from 'path';
|
|
8
8
|
import { output } from './cli-utils.mjs';
|
|
9
|
-
import { writeFingerprint } from './session-fingerprint.mjs';
|
|
10
9
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
11
|
-
import {
|
|
12
|
-
compareUiProofSlots,
|
|
13
|
-
findUiProofBundleFiles,
|
|
14
|
-
parseUiProofSlotsContent,
|
|
15
|
-
readUiProofBundleFile,
|
|
16
|
-
} from './ui-proof.mjs';
|
|
17
10
|
|
|
18
11
|
const PHASE_STATUS_MARKERS = {
|
|
19
12
|
not_started: '[ ]',
|
|
@@ -202,290 +195,6 @@ function evaluatePlanArtifacts(artifacts) {
|
|
|
202
195
|
};
|
|
203
196
|
}
|
|
204
197
|
|
|
205
|
-
function normalizeUiProofIssue(issue) {
|
|
206
|
-
return {
|
|
207
|
-
...issue,
|
|
208
|
-
severity: issue.severity || 'blocker',
|
|
209
|
-
fix_hint: issue.fix_hint || issue.fix || 'Fix the UI proof issue before claiming verification is complete.',
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function stripInlineComment(value) {
|
|
214
|
-
return String(value || '').replace(/\s+#.*$/, '').trim();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function stripOuterScalarQuotes(value) {
|
|
218
|
-
return String(value)
|
|
219
|
-
.trim()
|
|
220
|
-
.replace(/^(['"])([\s\S]*)\1$/g, '$2')
|
|
221
|
-
.trim();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function normalizeNullableFrontmatterValue(value) {
|
|
225
|
-
const stripped = stripOuterScalarQuotes(value);
|
|
226
|
-
if (!stripped) return '';
|
|
227
|
-
if (stripped === '~') return '';
|
|
228
|
-
if (/^null$/i.test(stripped)) return '';
|
|
229
|
-
return stripped;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function extractUiProofSlotIds(value) {
|
|
233
|
-
const ids = [];
|
|
234
|
-
const slotPattern = /['"]?slot_id['"]?\s*:\s*['"]?([^,'"\]\s}]+)['"]?/g;
|
|
235
|
-
for (const match of String(value || '').matchAll(slotPattern)) {
|
|
236
|
-
ids.push(match[1].replace(/^['"]|['"]$/g, ''));
|
|
237
|
-
}
|
|
238
|
-
return ids;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function readPlanFrontmatter(planContent) {
|
|
242
|
-
const content = String(planContent || '');
|
|
243
|
-
if (!content.startsWith('---')) return '';
|
|
244
|
-
const lines = content.split(/\r?\n/);
|
|
245
|
-
const frontmatter = [];
|
|
246
|
-
for (let index = 1; index < lines.length; index += 1) {
|
|
247
|
-
if (lines[index].trim() === '---') return frontmatter.join('\n');
|
|
248
|
-
frontmatter.push(lines[index]);
|
|
249
|
-
}
|
|
250
|
-
return '';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function frontmatterKeyBlock(frontmatter, key) {
|
|
254
|
-
const lines = String(frontmatter || '').split(/\r?\n/);
|
|
255
|
-
const keyPattern = new RegExp(`^${key}:[ \\t]*(.*)$`);
|
|
256
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
257
|
-
const match = lines[index].match(keyPattern);
|
|
258
|
-
if (!match) continue;
|
|
259
|
-
const block = [];
|
|
260
|
-
for (let next = index + 1; next < lines.length; next += 1) {
|
|
261
|
-
const line = lines[next];
|
|
262
|
-
if (/^\S[^:\n]*:\s*/.test(line)) break;
|
|
263
|
-
block.push(line);
|
|
264
|
-
}
|
|
265
|
-
return { inline: stripInlineComment(match[1]), block };
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function frontmatterScalar(frontmatter, key) {
|
|
271
|
-
const entry = frontmatterKeyBlock(frontmatter, key);
|
|
272
|
-
if (!entry) return null;
|
|
273
|
-
if (entry.inline && !['|', '>'].includes(entry.inline)) return entry.inline;
|
|
274
|
-
return entry.block
|
|
275
|
-
.map((line) => line.trim())
|
|
276
|
-
.filter(Boolean)
|
|
277
|
-
.join(' ')
|
|
278
|
-
.trim();
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function readPlanUiProofContract(planContent) {
|
|
282
|
-
const frontmatter = readPlanFrontmatter(planContent);
|
|
283
|
-
const slotsEntry = frontmatterKeyBlock(frontmatter, 'ui_proof_slots');
|
|
284
|
-
const rationale = normalizeNullableFrontmatterValue(frontmatterScalar(frontmatter, 'no_ui_proof_rationale') || '');
|
|
285
|
-
const result = {
|
|
286
|
-
hasUiProofKey: Boolean(slotsEntry),
|
|
287
|
-
declaresSlots: false,
|
|
288
|
-
explicitEmptySlots: false,
|
|
289
|
-
noUiProofRationale: rationale,
|
|
290
|
-
hasNoUiProofRationale: Boolean(rationale.trim()),
|
|
291
|
-
slotIds: [],
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
if (!slotsEntry) return result;
|
|
295
|
-
|
|
296
|
-
if (slotsEntry.inline) {
|
|
297
|
-
if (['[]', 'null', '~'].includes(slotsEntry.inline)) {
|
|
298
|
-
result.explicitEmptySlots = true;
|
|
299
|
-
return result;
|
|
300
|
-
}
|
|
301
|
-
result.declaresSlots = true;
|
|
302
|
-
result.slotIds.push(...extractUiProofSlotIds(slotsEntry.inline));
|
|
303
|
-
return result;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let sawMeaningfulLine = false;
|
|
307
|
-
for (const line of slotsEntry.block) {
|
|
308
|
-
const trimmed = line.trim();
|
|
309
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
310
|
-
sawMeaningfulLine = true;
|
|
311
|
-
if (/^\s+-\s+/.test(line)) result.declaresSlots = true;
|
|
312
|
-
result.slotIds.push(...extractUiProofSlotIds(trimmed));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
result.explicitEmptySlots = !result.declaresSlots && !sawMeaningfulLine;
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths) {
|
|
320
|
-
const candidates = new Set();
|
|
321
|
-
const declaredPlans = [];
|
|
322
|
-
const declaredSlotIds = [];
|
|
323
|
-
const noUiPlans = [];
|
|
324
|
-
const contractErrors = [];
|
|
325
|
-
const names = new Set([
|
|
326
|
-
'ui-proof-slots.json',
|
|
327
|
-
'ui-proof-slots.md',
|
|
328
|
-
'UI-PROOF-SLOTS.json',
|
|
329
|
-
'UI-PROOF-SLOTS.md',
|
|
330
|
-
'planned-ui-proof.json',
|
|
331
|
-
'planned-ui-proof.md',
|
|
332
|
-
]);
|
|
333
|
-
|
|
334
|
-
for (const planDisplayPath of planDisplayPaths) {
|
|
335
|
-
const fullPlanPath = join(planningDir, 'phases', planDisplayPath);
|
|
336
|
-
if (!existsSync(fullPlanPath)) continue;
|
|
337
|
-
const planContent = readFileSync(fullPlanPath, 'utf-8');
|
|
338
|
-
const relPlanPath = relative(planningDir, fullPlanPath).replace(/\\/g, '/');
|
|
339
|
-
const contract = readPlanUiProofContract(planContent);
|
|
340
|
-
const planDir = dirname(fullPlanPath);
|
|
341
|
-
const sidecars = [];
|
|
342
|
-
if (existsSync(planDir)) {
|
|
343
|
-
for (const entry of readdirSync(planDir, { withFileTypes: true })) {
|
|
344
|
-
if (entry.isFile() && names.has(entry.name)) {
|
|
345
|
-
sidecars.push(join(planDir, entry.name));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (contract.hasUiProofKey && !contract.declaresSlots && !contract.hasNoUiProofRationale) {
|
|
351
|
-
contractErrors.push(normalizeUiProofIssue({
|
|
352
|
-
code: 'missing_no_ui_proof_rationale',
|
|
353
|
-
path: `${relPlanPath}.no_ui_proof_rationale`,
|
|
354
|
-
message: 'Plan declares empty ui_proof_slots but does not provide a no_ui_proof_rationale.',
|
|
355
|
-
fix: 'Add a nonblank no_ui_proof_rationale for non-UI work, or declare required UI proof slots and add a planned slots artifact.',
|
|
356
|
-
}));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (contract.hasNoUiProofRationale && !contract.declaresSlots) {
|
|
360
|
-
noUiPlans.push({ plan: relPlanPath, sidecars });
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (!contract.declaresSlots) continue;
|
|
365
|
-
|
|
366
|
-
declaredPlans.push(relPlanPath);
|
|
367
|
-
for (const slotId of contract.slotIds) {
|
|
368
|
-
declaredSlotIds.push({ plan: relPlanPath, slot_id: slotId });
|
|
369
|
-
}
|
|
370
|
-
for (const entry of readdirSync(planDir, { withFileTypes: true })) {
|
|
371
|
-
if (entry.isFile() && names.has(entry.name)) {
|
|
372
|
-
candidates.add(join(planDir, entry.name));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return { declaredPlans, declaredSlotIds, noUiPlans, contractErrors, files: [...candidates].sort() };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function comparePhaseUiProof({ planningDir, workspaceRoot, planDisplayPaths }) {
|
|
380
|
-
const plannedDiscovery = findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths);
|
|
381
|
-
const plannedFiles = plannedDiscovery.files;
|
|
382
|
-
const phaseDirs = new Set(planDisplayPaths.map((planDisplayPath) => dirname(join(planningDir, 'phases', planDisplayPath))));
|
|
383
|
-
const observedFiles = findUiProofBundleFiles(planningDir)
|
|
384
|
-
.filter((filePath) => phaseDirs.has(dirname(filePath)));
|
|
385
|
-
|
|
386
|
-
const plannedSlots = [];
|
|
387
|
-
const errors = [];
|
|
388
|
-
const warnings = [];
|
|
389
|
-
const planned = [];
|
|
390
|
-
const observed = [];
|
|
391
|
-
|
|
392
|
-
errors.push(...plannedDiscovery.contractErrors);
|
|
393
|
-
for (const noUiPlan of plannedDiscovery.noUiPlans) {
|
|
394
|
-
for (const filePath of noUiPlan.sidecars) {
|
|
395
|
-
warnings.push({
|
|
396
|
-
code: 'stale_ui_proof_sidecar_ignored',
|
|
397
|
-
severity: 'warn',
|
|
398
|
-
path: relative(workspaceRoot, filePath).replace(/\\/g, '/'),
|
|
399
|
-
message: `Plan ${noUiPlan.plan} records no_ui_proof_rationale, so the UI proof sidecar is ignored for closure.`,
|
|
400
|
-
fix_hint: 'Remove or classify the stale sidecar if it no longer belongs to this non-UI phase.',
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
for (const filePath of plannedFiles) {
|
|
406
|
-
const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
|
|
407
|
-
const parsed = parseUiProofSlotsContent(readFileSync(filePath, 'utf-8'), rel);
|
|
408
|
-
planned.push(rel);
|
|
409
|
-
plannedSlots.push(...parsed.slots);
|
|
410
|
-
errors.push(...parsed.errors.map(normalizeUiProofIssue));
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (plannedSlots.length > 0 && plannedDiscovery.declaredSlotIds.length > 0) {
|
|
414
|
-
const plannedSlotIds = new Set(plannedSlots.map((slot) => String(slot?.slot_id || '')));
|
|
415
|
-
for (const declaredSlot of plannedDiscovery.declaredSlotIds) {
|
|
416
|
-
if (plannedSlotIds.has(String(declaredSlot.slot_id))) continue;
|
|
417
|
-
errors.push(normalizeUiProofIssue({
|
|
418
|
-
code: 'planned_ui_proof_slots_drift',
|
|
419
|
-
path: `${declaredSlot.plan}.ui_proof_slots`,
|
|
420
|
-
message: `Plan declares UI proof slot ${declaredSlot.slot_id}, but no matching slot exists in the planned UI proof artifact.`,
|
|
421
|
-
fix: 'Update ui-proof-slots.json or ui-proof-slots.md beside the plan so it matches the plan-declared slot IDs, or update the plan declaration.',
|
|
422
|
-
}));
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const observedBundles = [];
|
|
427
|
-
for (const filePath of observedFiles) {
|
|
428
|
-
const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
|
|
429
|
-
const parsed = readUiProofBundleFile(filePath);
|
|
430
|
-
observed.push(rel);
|
|
431
|
-
if (parsed.errors.length > 0) {
|
|
432
|
-
errors.push(...parsed.errors.map((error) => normalizeUiProofIssue({ ...error, path: error.path || rel })));
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
observedBundles.push({
|
|
436
|
-
source: rel,
|
|
437
|
-
bundle: parsed.bundle,
|
|
438
|
-
options: {
|
|
439
|
-
requireLocalArtifactExists: true,
|
|
440
|
-
workspaceRoot,
|
|
441
|
-
bundleDir: dirname(filePath),
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (plannedFiles.length === 0 && plannedDiscovery.declaredPlans.length > 0) {
|
|
447
|
-
const missingError = {
|
|
448
|
-
code: 'missing_planned_ui_proof_slots_file',
|
|
449
|
-
severity: 'blocker',
|
|
450
|
-
path: plannedDiscovery.declaredPlans[0],
|
|
451
|
-
message: 'Plan declares ui_proof_slots but no ui-proof-slots artifact was found beside the plan.',
|
|
452
|
-
fix_hint: 'Create ui-proof-slots.json or ui-proof-slots.md beside the plan, or set ui_proof_slots: [] with a no_ui_proof_rationale if the phase is not UI-sensitive.',
|
|
453
|
-
};
|
|
454
|
-
return {
|
|
455
|
-
planned,
|
|
456
|
-
observed,
|
|
457
|
-
status: 'missing',
|
|
458
|
-
comparison: { status: 'missing', slots: [], errors: [missingError] },
|
|
459
|
-
errors: [missingError],
|
|
460
|
-
warnings,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (plannedFiles.length === 0) {
|
|
465
|
-
return {
|
|
466
|
-
planned,
|
|
467
|
-
observed,
|
|
468
|
-
status: errors.length > 0 ? 'partial' : 'not_applicable',
|
|
469
|
-
comparison: null,
|
|
470
|
-
errors,
|
|
471
|
-
warnings,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const comparison = errors.length > 0
|
|
476
|
-
? { status: 'partial', slots: [], errors: errors.map(normalizeUiProofIssue) }
|
|
477
|
-
: compareUiProofSlots(plannedSlots, observedBundles);
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
planned,
|
|
481
|
-
observed,
|
|
482
|
-
status: comparison.status,
|
|
483
|
-
comparison,
|
|
484
|
-
errors: comparison.errors || errors,
|
|
485
|
-
warnings,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
198
|
export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
|
|
490
199
|
const marker = PHASE_STATUS_MARKERS[status];
|
|
491
200
|
if (!marker) {
|
|
@@ -568,6 +277,7 @@ export function cmdPhaseStatus(...args) {
|
|
|
568
277
|
return;
|
|
569
278
|
}
|
|
570
279
|
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
280
|
+
const stateName = basename(planningDir);
|
|
571
281
|
const [phaseNumber, status] = normalizedArgs;
|
|
572
282
|
|
|
573
283
|
if (!phaseNumber || !status) {
|
|
@@ -588,9 +298,8 @@ export function cmdPhaseStatus(...args) {
|
|
|
588
298
|
const changed = updated !== roadmap;
|
|
589
299
|
if (changed) {
|
|
590
300
|
writeFileSync(roadmapPath, updated);
|
|
591
|
-
try { writeFingerprint(planningDir); } catch { /* best-effort */ }
|
|
592
301
|
}
|
|
593
|
-
output({ phase: phaseNumber, status, roadmap:
|
|
302
|
+
output({ phase: phaseNumber, status, roadmap: `${stateName}/ROADMAP.md`, changed });
|
|
594
303
|
} catch (error) {
|
|
595
304
|
console.error(error.message);
|
|
596
305
|
process.exitCode = 1;
|
|
@@ -605,9 +314,10 @@ export function cmdFindPhase(...args) {
|
|
|
605
314
|
return;
|
|
606
315
|
}
|
|
607
316
|
const phaseNum = normalizedArgs[0];
|
|
317
|
+
const stateName = basename(planningDir);
|
|
608
318
|
|
|
609
319
|
if (!existsSync(planningDir)) {
|
|
610
|
-
output({ error:
|
|
320
|
+
output({ error: `No ${stateName}/ directory found. Run \`npx -y gsdd-cli init\` then the new-project workflow first.` });
|
|
611
321
|
return;
|
|
612
322
|
}
|
|
613
323
|
|
|
@@ -660,9 +370,10 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
660
370
|
if (!phaseNum) {
|
|
661
371
|
return { ok: false, error: 'Usage: gsdd verify <phase-number>', exitCode: 1 };
|
|
662
372
|
}
|
|
373
|
+
const stateName = basename(planningDir);
|
|
663
374
|
|
|
664
375
|
if (!existsSync(planningDir)) {
|
|
665
|
-
return { ok: false, error:
|
|
376
|
+
return { ok: false, error: `No ${stateName}/ directory found.`, exitCode: 1 };
|
|
666
377
|
}
|
|
667
378
|
const phasesDir = join(planningDir, 'phases');
|
|
668
379
|
const matchingPlans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
|
|
@@ -672,7 +383,7 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
672
383
|
prerequisiteBlockers.push({
|
|
673
384
|
code: 'missing_phase_plan',
|
|
674
385
|
severity: 'blocker',
|
|
675
|
-
path:
|
|
386
|
+
path: `${stateName}/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-PLAN.md`,
|
|
676
387
|
message: `No PLAN.md artifact was found for phase ${normalizePhaseToken(phaseNum)}.`,
|
|
677
388
|
fix_hint: `Run /gsdd-plan ${normalizePhaseToken(phaseNum)} before verifying this phase.`,
|
|
678
389
|
});
|
|
@@ -681,7 +392,7 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
681
392
|
prerequisiteBlockers.push({
|
|
682
393
|
code: 'missing_phase_summary',
|
|
683
394
|
severity: 'blocker',
|
|
684
|
-
path:
|
|
395
|
+
path: `${stateName}/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-SUMMARY.md`,
|
|
685
396
|
message: `No SUMMARY.md artifact was found for phase ${normalizePhaseToken(phaseNum)}.`,
|
|
686
397
|
fix_hint: `Run /gsdd-execute ${normalizePhaseToken(phaseNum)} before verifying this phase.`,
|
|
687
398
|
});
|
|
@@ -693,26 +404,12 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
693
404
|
: [];
|
|
694
405
|
});
|
|
695
406
|
const artifactStatus = evaluatePlanArtifacts(artifacts);
|
|
696
|
-
const uiProof = comparePhaseUiProof({
|
|
697
|
-
planningDir,
|
|
698
|
-
workspaceRoot,
|
|
699
|
-
planDisplayPaths: matchingPlans,
|
|
700
|
-
});
|
|
701
|
-
const uiProofSatisfied = ['satisfied', 'not_applicable'].includes(uiProof.status);
|
|
702
407
|
const legacyVerified = matchingPlans.length > 0 && matchingSummaries.length > 0;
|
|
703
|
-
const uiProofGate = {
|
|
704
|
-
status: uiProof.status,
|
|
705
|
-
required: uiProof.status !== 'not_applicable',
|
|
706
|
-
satisfied: uiProofSatisfied,
|
|
707
|
-
blocks_verification: uiProof.status !== 'not_applicable' && !uiProofSatisfied,
|
|
708
|
-
required_block: uiProof.status !== 'not_applicable' && !uiProofSatisfied ? 'ui-proof-failed' : null,
|
|
709
|
-
};
|
|
710
408
|
const blockedOn = [
|
|
711
409
|
...(prerequisiteBlockers.length > 0 ? ['prerequisites'] : []),
|
|
712
410
|
...(artifactStatus.satisfied ? [] : ['artifacts']),
|
|
713
|
-
...(uiProofGate.blocks_verification ? ['ui_proof'] : []),
|
|
714
411
|
];
|
|
715
|
-
const closureVerified = legacyVerified && prerequisiteBlockers.length === 0 && artifactStatus.satisfied
|
|
412
|
+
const closureVerified = legacyVerified && prerequisiteBlockers.length === 0 && artifactStatus.satisfied;
|
|
716
413
|
|
|
717
414
|
const result = {
|
|
718
415
|
phase: normalizePhaseToken(phaseNum),
|
|
@@ -722,7 +419,6 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
722
419
|
artifacts,
|
|
723
420
|
allExist: artifacts.every((artifact) => artifact.exists),
|
|
724
421
|
artifact_status: artifactStatus,
|
|
725
|
-
uiProof,
|
|
726
422
|
verified: closureVerified,
|
|
727
423
|
legacy_verified: legacyVerified,
|
|
728
424
|
phase_artifacts_present: legacyVerified,
|
|
@@ -730,7 +426,6 @@ export function buildPhaseVerificationReport(...args) {
|
|
|
730
426
|
satisfied: prerequisiteBlockers.length === 0,
|
|
731
427
|
blockers: prerequisiteBlockers,
|
|
732
428
|
},
|
|
733
|
-
ui_proof: uiProofGate,
|
|
734
429
|
blocked_on: blockedOn,
|
|
735
430
|
blocks_verification: blockedOn.length > 0,
|
|
736
431
|
};
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -7,17 +7,27 @@ const __dirname = dirname(__filename);
|
|
|
7
7
|
const DISTILLED_DIR = join(__dirname, '..', '..', 'distilled');
|
|
8
8
|
const HELPER_LIB_FILES = Object.freeze([
|
|
9
9
|
'cli-utils.mjs',
|
|
10
|
-
'closeout-report.mjs',
|
|
11
10
|
'control-map.mjs',
|
|
12
|
-
'evidence-contract.mjs',
|
|
13
11
|
'file-ops.mjs',
|
|
14
12
|
'lifecycle-preflight.mjs',
|
|
15
13
|
'lifecycle-state.mjs',
|
|
14
|
+
'next.mjs',
|
|
16
15
|
'phase.mjs',
|
|
17
|
-
'
|
|
18
|
-
'
|
|
16
|
+
'state-dir.mjs',
|
|
17
|
+
'work-context.mjs',
|
|
19
18
|
'workspace-root.mjs',
|
|
20
19
|
]);
|
|
20
|
+
const DEFAULT_STATE_DIR_NAME = '.work';
|
|
21
|
+
|
|
22
|
+
function normalizeStateDirName(stateDirName = DEFAULT_STATE_DIR_NAME) {
|
|
23
|
+
return stateDirName || DEFAULT_STATE_DIR_NAME;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function localizeStateDirReferences(content, { stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
|
|
27
|
+
const normalized = normalizeStateDirName(stateDirName);
|
|
28
|
+
if (normalized === DEFAULT_STATE_DIR_NAME) return content;
|
|
29
|
+
return String(content).replace(/\.work(?=\/|\\|`|'|"|\)|\]|\}|,|\.|:|;|\s|$)/g, normalized);
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
function getWorkflowContent(workflowFile) {
|
|
23
33
|
const filePath = join(DISTILLED_DIR, 'workflows', workflowFile);
|
|
@@ -31,8 +41,8 @@ function getDelegateContent(delegateFile) {
|
|
|
31
41
|
return `<!-- Delegate file not found: ${delegateFile} -->\n`;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
function renderSkillContent(workflow) {
|
|
35
|
-
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
44
|
+
function renderSkillContent(workflow, options = {}) {
|
|
45
|
+
const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
|
|
36
46
|
return `---
|
|
37
47
|
name: ${workflow.name}
|
|
38
48
|
description: ${workflow.description}
|
|
@@ -43,60 +53,63 @@ agent: ${workflow.agent}
|
|
|
43
53
|
${workflowContent}`;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
function renderPlanningCliLauncher() {
|
|
56
|
+
function renderPlanningCliLauncher({ stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
|
|
57
|
+
const helperPath = `${normalizeStateDirName(stateDirName)}/bin/gsdd.mjs`;
|
|
58
|
+
const checkpointBackupPath = `${normalizeStateDirName(stateDirName)}/.continue-here.bak`;
|
|
47
59
|
return `#!/usr/bin/env node
|
|
48
60
|
|
|
49
61
|
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
50
62
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
51
63
|
import { cmdPhaseStatus, cmdVerify } from './lib/phase.mjs';
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { cmdControlMap } from './lib/control-map.mjs';
|
|
55
|
-
import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
|
|
64
|
+
import { buildControlMap } from './lib/control-map.mjs';
|
|
65
|
+
import { createCmdNext } from './lib/next.mjs';
|
|
56
66
|
import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
57
67
|
|
|
58
68
|
const HELPER_CONTEXT = {
|
|
69
|
+
cwd: process.cwd(),
|
|
59
70
|
workflows: [],
|
|
60
71
|
frameworkVersion: 'generated-helper',
|
|
61
72
|
};
|
|
62
|
-
const
|
|
73
|
+
const cmdNext = createCmdNext(HELPER_CONTEXT);
|
|
74
|
+
|
|
75
|
+
function cmdControlMap(...controlArgs) {
|
|
76
|
+
const context = resolveWorkspaceContext([], { cwd: HELPER_CONTEXT.cwd });
|
|
77
|
+
const report = buildControlMap({
|
|
78
|
+
workspaceRoot: context.workspaceRoot,
|
|
79
|
+
planningDir: context.planningDir,
|
|
80
|
+
includeIgnoredPaths: controlArgs.includes('--with-ignored'),
|
|
81
|
+
});
|
|
82
|
+
console.log(JSON.stringify(report, null, 2));
|
|
83
|
+
}
|
|
63
84
|
|
|
64
85
|
const COMMANDS = {
|
|
86
|
+
'control-map': cmdControlMap,
|
|
65
87
|
'file-op': cmdFileOp,
|
|
66
88
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
67
89
|
'phase-status': cmdPhaseStatus,
|
|
68
90
|
verify: cmdVerify,
|
|
69
|
-
|
|
70
|
-
'ui-proof': cmdUiProof,
|
|
71
|
-
'control-map': cmdControlMap,
|
|
72
|
-
'closeout-report': cmdCloseoutReport,
|
|
91
|
+
next: cmdNext,
|
|
73
92
|
};
|
|
74
93
|
|
|
75
94
|
function printHelp() {
|
|
76
95
|
console.log([
|
|
77
|
-
'Usage: node
|
|
96
|
+
'Usage: node ${helperPath} [--workspace-root <path>] <command> [args]',
|
|
78
97
|
'',
|
|
79
98
|
'Local workflow helper commands:',
|
|
99
|
+
' control-map [--json] [--with-ignored]',
|
|
100
|
+
' Print computed repo/worktree/workflow state for workflow-internal checks',
|
|
80
101
|
' file-op <copy|delete|regex-sub>',
|
|
81
102
|
' Run deterministic workspace-confined file operations',
|
|
82
|
-
' Example: node
|
|
103
|
+
' Example: node ${helperPath} file-op delete ${checkpointBackupPath} --missing ok',
|
|
83
104
|
' phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])',
|
|
84
|
-
' Example: node
|
|
85
|
-
' verify <N> Run direct phase artifact
|
|
86
|
-
' Example: node
|
|
105
|
+
' Example: node ${helperPath} phase-status 1 done',
|
|
106
|
+
' verify <N> Run direct phase artifact checks',
|
|
107
|
+
' Example: node ${helperPath} verify 1',
|
|
87
108
|
' lifecycle-preflight <surface> [phase]',
|
|
88
109
|
' Inspect lifecycle gate results for a workflow surface',
|
|
89
|
-
' Example: node
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
' ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]',
|
|
93
|
-
' Validate UI proof metadata; use --claim for stronger proof uses',
|
|
94
|
-
' ui-proof compare <planned-slots-json> [observed-bundle-json ...]',
|
|
95
|
-
' Compare planned UI proof slots against observed bundles',
|
|
96
|
-
' control-map [--json] [--with-ignored] [--annotations <path>]',
|
|
97
|
-
' Report computed repo/worktree/planning state and local annotations',
|
|
98
|
-
' closeout-report [--json] [--phase <N>]',
|
|
99
|
-
' Replay read-only closeout status from control-map, health, preflight, verify, and UI-proof signals',
|
|
110
|
+
' Example: node ${helperPath} lifecycle-preflight verify 1 --expects-mutation phase-status',
|
|
111
|
+
' next [--json] [--init]',
|
|
112
|
+
' Route to the next safe Workspine action from ${normalizeStateDirName(stateDirName)}, brownfield, planning, and repo truth',
|
|
100
113
|
'',
|
|
101
114
|
'Advanced option:',
|
|
102
115
|
' --workspace-root <path> Override workspace root discovery before or after the subcommand',
|
|
@@ -106,6 +119,7 @@ function printHelp() {
|
|
|
106
119
|
function applyWorkspaceRootOverride(workspaceRootArg) {
|
|
107
120
|
if (!workspaceRootArg) {
|
|
108
121
|
bootstrapHelperWorkspace(import.meta.url);
|
|
122
|
+
HELPER_CONTEXT.cwd = process.cwd();
|
|
109
123
|
return true;
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -117,6 +131,7 @@ function applyWorkspaceRootOverride(workspaceRootArg) {
|
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
process.env.GSDD_WORKSPACE_ROOT = context.workspaceRoot;
|
|
134
|
+
HELPER_CONTEXT.cwd = context.workspaceRoot;
|
|
120
135
|
try {
|
|
121
136
|
process.chdir(context.workspaceRoot);
|
|
122
137
|
} catch {
|
|
@@ -183,11 +198,11 @@ function readHelperLibContent(fileName) {
|
|
|
183
198
|
return readFileSync(join(__dirname, fileName), 'utf-8');
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
function buildPlanningCliHelperEntries() {
|
|
201
|
+
function buildPlanningCliHelperEntries(options = {}) {
|
|
187
202
|
return [
|
|
188
203
|
{
|
|
189
204
|
relativePath: 'bin/gsdd.mjs',
|
|
190
|
-
content: renderPlanningCliLauncher(),
|
|
205
|
+
content: renderPlanningCliLauncher(options),
|
|
191
206
|
},
|
|
192
207
|
{
|
|
193
208
|
relativePath: 'bin/gsdd',
|
|
@@ -208,15 +223,15 @@ function buildPlanningCliHelperEntries() {
|
|
|
208
223
|
];
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
function buildPortableSkillEntries(workflows) {
|
|
226
|
+
function buildPortableSkillEntries(workflows, options = {}) {
|
|
212
227
|
return workflows.map((workflow) => ({
|
|
213
228
|
relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
|
|
214
|
-
content: renderSkillContent(workflow),
|
|
229
|
+
content: renderSkillContent(workflow, options),
|
|
215
230
|
}));
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
function renderOpenCodeCommandContent(workflow) {
|
|
219
|
-
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
233
|
+
function renderOpenCodeCommandContent(workflow, options = {}) {
|
|
234
|
+
const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
|
|
220
235
|
return `---
|
|
221
236
|
description: ${workflow.description}
|
|
222
237
|
---
|
|
@@ -224,19 +239,23 @@ description: ${workflow.description}
|
|
|
224
239
|
${workflowContent}`;
|
|
225
240
|
}
|
|
226
241
|
|
|
227
|
-
function renderAgentsBoundedBlock() {
|
|
242
|
+
function renderAgentsBoundedBlock(options = {}) {
|
|
228
243
|
const blockPath = join(DISTILLED_DIR, 'templates', 'agents.block.md');
|
|
229
|
-
if (existsSync(blockPath)) return readFileSync(blockPath, 'utf-8').trim();
|
|
230
|
-
|
|
244
|
+
if (existsSync(blockPath)) return localizeStateDirReferences(readFileSync(blockPath, 'utf-8'), options).trim();
|
|
245
|
+
const stateDirName = normalizeStateDirName(options.stateDirName);
|
|
246
|
+
const planningLine = stateDirName === DEFAULT_STATE_DIR_NAME
|
|
247
|
+
? 'Planning state: `.work/` (legacy `.planning/` workspaces are still read).'
|
|
248
|
+
: 'Planning state: `.planning/` (legacy workspace; new Workspine projects use `.work/`).';
|
|
249
|
+
return `## GSDD Governance (Generated)\n\n- Framework: GSDD\n- ${planningLine}\n- Workflows: .agents/skills/gsdd-*/SKILL.md`;
|
|
231
250
|
}
|
|
232
251
|
|
|
233
|
-
function renderAgentsFileContent() {
|
|
252
|
+
function renderAgentsFileContent(options = {}) {
|
|
234
253
|
const templatePath = join(DISTILLED_DIR, 'templates', 'agents.md');
|
|
235
254
|
if (existsSync(templatePath)) {
|
|
236
255
|
const template = readFileSync(templatePath, 'utf-8');
|
|
237
|
-
return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock()).trimEnd() + '\n';
|
|
256
|
+
return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock(options)).trimEnd() + '\n';
|
|
238
257
|
}
|
|
239
|
-
const block = renderAgentsBoundedBlock();
|
|
258
|
+
const block = renderAgentsBoundedBlock(options);
|
|
240
259
|
return `# AGENTS.md - GSDD Governance\n\n<!-- BEGIN GSDD -->\n${block}\n<!-- END GSDD -->\n`;
|
|
241
260
|
}
|
|
242
261
|
|
|
@@ -274,6 +293,7 @@ export {
|
|
|
274
293
|
buildPortableSkillEntries,
|
|
275
294
|
getDelegateContent,
|
|
276
295
|
getWorkflowContent,
|
|
296
|
+
localizeStateDirReferences,
|
|
277
297
|
renderAgentsBoundedBlock,
|
|
278
298
|
renderAgentsFileContent,
|
|
279
299
|
renderOpenCodeCommandContent,
|