gsdd-cli 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/agents/approach-explorer.md +9 -0
- package/agents/executor.md +118 -39
- package/agents/planner.md +32 -3
- package/bin/adapters/claude.mjs +9 -7
- package/bin/adapters/opencode.mjs +9 -7
- package/bin/gsdd.mjs +4 -4
- package/bin/lib/evidence-contract.mjs +216 -3
- package/bin/lib/health-truth.mjs +25 -10
- package/bin/lib/health.mjs +1 -1
- package/bin/lib/init-runtime.mjs +2 -0
- package/bin/lib/lifecycle-preflight.mjs +418 -6
- package/bin/lib/lifecycle-state.mjs +9 -4
- package/bin/lib/phase.mjs +7 -2
- package/bin/lib/plan-constants.mjs +5 -0
- package/bin/lib/rendering.mjs +4 -0
- package/bin/lib/session-fingerprint.mjs +91 -14
- package/distilled/DESIGN.md +142 -14
- package/distilled/EVIDENCE-INDEX.md +29 -5
- package/distilled/templates/approach.md +40 -0
- package/distilled/templates/delegates/approach-explorer.md +3 -0
- package/distilled/templates/delegates/plan-checker.md +14 -4
- package/distilled/workflows/audit-milestone.md +29 -1
- package/distilled/workflows/complete-milestone.md +36 -23
- package/distilled/workflows/execute.md +35 -33
- package/distilled/workflows/plan.md +18 -16
- package/distilled/workflows/verify.md +8 -8
- package/package.json +1 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { output } from './cli-utils.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DELIVERY_POSTURES,
|
|
6
|
+
EVIDENCE_KINDS,
|
|
7
|
+
RELEASE_CLAIM_POSTURES,
|
|
8
|
+
describeEvidenceSurface,
|
|
9
|
+
evaluateReleaseClaimCloseoutContract,
|
|
10
|
+
getEvidenceContract,
|
|
11
|
+
} from './evidence-contract.mjs';
|
|
5
12
|
import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
|
|
6
13
|
import { checkDrift } from './session-fingerprint.mjs';
|
|
7
14
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
@@ -12,6 +19,12 @@ const SURFACE_POLICIES = {
|
|
|
12
19
|
ownedWrites: [],
|
|
13
20
|
explicitLifecycleMutation: 'none',
|
|
14
21
|
},
|
|
22
|
+
plan: {
|
|
23
|
+
classification: 'owned_write',
|
|
24
|
+
ownedWrites: ['research', 'plan'],
|
|
25
|
+
explicitLifecycleMutation: 'none',
|
|
26
|
+
phaseRequired: true,
|
|
27
|
+
},
|
|
15
28
|
execute: {
|
|
16
29
|
classification: 'owned_write',
|
|
17
30
|
ownedWrites: ['summary'],
|
|
@@ -46,6 +59,17 @@ const SURFACE_POLICIES = {
|
|
|
46
59
|
},
|
|
47
60
|
};
|
|
48
61
|
|
|
62
|
+
const RELEASE_CONTRADICTION_CHECKS = Object.freeze([
|
|
63
|
+
'evidence',
|
|
64
|
+
'public_surface',
|
|
65
|
+
'runtime',
|
|
66
|
+
'delivery',
|
|
67
|
+
'planning_drift',
|
|
68
|
+
'generated_surface',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const RELEASE_CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
|
|
72
|
+
|
|
49
73
|
export function evaluateLifecyclePreflight({
|
|
50
74
|
planningDir,
|
|
51
75
|
surface,
|
|
@@ -125,15 +149,33 @@ export function evaluateLifecyclePreflight({
|
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
const warnings = [];
|
|
152
|
+
let planningState = null;
|
|
128
153
|
|
|
129
154
|
if (existsSync(planningDir)) {
|
|
130
155
|
const drift = checkDrift(planningDir);
|
|
156
|
+
planningState = {
|
|
157
|
+
classification: drift.classification,
|
|
158
|
+
drifted: drift.drifted,
|
|
159
|
+
noBaseline: drift.noBaseline,
|
|
160
|
+
details: drift.details,
|
|
161
|
+
files: drift.files,
|
|
162
|
+
};
|
|
131
163
|
if (drift.drifted) {
|
|
132
|
-
|
|
164
|
+
const driftNotice = {
|
|
133
165
|
code: 'planning_state_drift',
|
|
134
|
-
message:
|
|
166
|
+
message: `${surface} cannot proceed because planning state drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
135
167
|
artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
|
|
136
|
-
|
|
168
|
+
details: drift.details,
|
|
169
|
+
files: drift.files,
|
|
170
|
+
};
|
|
171
|
+
if (policy.classification === 'owned_write') {
|
|
172
|
+
blockers.push(driftNotice);
|
|
173
|
+
} else {
|
|
174
|
+
warnings.push({
|
|
175
|
+
...driftNotice,
|
|
176
|
+
message: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
137
179
|
}
|
|
138
180
|
}
|
|
139
181
|
|
|
@@ -158,6 +200,7 @@ export function evaluateLifecyclePreflight({
|
|
|
158
200
|
reason: blockers[0]?.code ?? null,
|
|
159
201
|
blockers,
|
|
160
202
|
warnings,
|
|
203
|
+
planningState,
|
|
161
204
|
lifecycle: {
|
|
162
205
|
currentMilestone: lifecycle.currentMilestone,
|
|
163
206
|
currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
|
|
@@ -207,6 +250,16 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
207
250
|
}
|
|
208
251
|
}
|
|
209
252
|
|
|
253
|
+
if (surface === 'plan' && phaseEntry.status === 'done') {
|
|
254
|
+
blockers.push(
|
|
255
|
+
blocker(
|
|
256
|
+
'phase_already_complete',
|
|
257
|
+
`Phase ${phaseToken} is already complete and should not be planned again.`,
|
|
258
|
+
['.planning/ROADMAP.md']
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
210
263
|
if (surface === 'verify') {
|
|
211
264
|
if (planArtifacts.length === 0) {
|
|
212
265
|
blockers.push(
|
|
@@ -302,8 +355,9 @@ function buildCompletionBlockers(planningDir, lifecycle) {
|
|
|
302
355
|
}
|
|
303
356
|
|
|
304
357
|
const auditContent = readFileSync(auditPath, 'utf-8');
|
|
305
|
-
const
|
|
306
|
-
|
|
358
|
+
const auditFrontmatter = extractFrontmatter(auditContent);
|
|
359
|
+
const auditStatus = readTopLevelScalar(auditFrontmatter || auditContent, 'status');
|
|
360
|
+
if (auditStatus !== 'passed') {
|
|
307
361
|
return [
|
|
308
362
|
blocker(
|
|
309
363
|
'audit_not_passed',
|
|
@@ -313,9 +367,367 @@ function buildCompletionBlockers(planningDir, lifecycle) {
|
|
|
313
367
|
];
|
|
314
368
|
}
|
|
315
369
|
|
|
370
|
+
const releaseContractBlockers = buildReleaseClaimCompletionBlockers(auditContent, auditPath);
|
|
371
|
+
if (releaseContractBlockers.length > 0) return releaseContractBlockers;
|
|
372
|
+
|
|
316
373
|
return [];
|
|
317
374
|
}
|
|
318
375
|
|
|
376
|
+
function buildReleaseClaimCompletionBlockers(auditContent, auditPath) {
|
|
377
|
+
const frontmatter = extractFrontmatter(auditContent);
|
|
378
|
+
const deliveryPosture = readTopLevelScalar(frontmatter, 'delivery_posture');
|
|
379
|
+
const releaseClaimPosture = readTopLevelScalar(frontmatter, 'release_claim_posture');
|
|
380
|
+
const evidenceBlock = extractYamlBlock(frontmatter, 'evidence_contract');
|
|
381
|
+
const releaseBlock = extractYamlBlock(frontmatter, 'release_claim_contract');
|
|
382
|
+
const missing = [];
|
|
383
|
+
|
|
384
|
+
if (!deliveryPosture) missing.push('delivery_posture');
|
|
385
|
+
if (!releaseClaimPosture) missing.push('release_claim_posture');
|
|
386
|
+
if (!evidenceBlock) missing.push('evidence_contract');
|
|
387
|
+
if (!releaseBlock) missing.push('release_claim_contract');
|
|
388
|
+
|
|
389
|
+
if (missing.length > 0) {
|
|
390
|
+
return [blocker(
|
|
391
|
+
'missing_release_claim_contract',
|
|
392
|
+
`Milestone audit is missing release closeout metadata (${missing.join(', ')}). Re-run audit before completion.`,
|
|
393
|
+
[auditPath]
|
|
394
|
+
)];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const requiredKinds = readBlockList(evidenceBlock, 'required_kinds');
|
|
398
|
+
const observedKinds = readBlockList(evidenceBlock, 'observed_kinds');
|
|
399
|
+
const missingKinds = readBlockList(evidenceBlock, 'missing_kinds');
|
|
400
|
+
const unsupportedClaims = readBlockList(releaseBlock, 'unsupported_claims');
|
|
401
|
+
const waivedKinds = readBlockList(releaseBlock, 'waivers');
|
|
402
|
+
const deferrals = readBlockList(releaseBlock, 'deferrals');
|
|
403
|
+
const contradictionChecks = readNestedStatusBlock(releaseBlock, 'contradiction_checks');
|
|
404
|
+
const blockers = [];
|
|
405
|
+
const invalidEvidenceKinds = [
|
|
406
|
+
...findInvalidEvidenceKinds('required_kinds', requiredKinds),
|
|
407
|
+
...findInvalidEvidenceKinds('observed_kinds', observedKinds),
|
|
408
|
+
...findInvalidEvidenceKinds('missing_kinds', missingKinds),
|
|
409
|
+
...findInvalidEvidenceKinds('waivers', waivedKinds),
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
if (requiredKinds.length === 0 && observedKinds.length === 0) {
|
|
413
|
+
blockers.push(blocker(
|
|
414
|
+
'missing_release_evidence_contract',
|
|
415
|
+
'Milestone audit evidence_contract must include required_kinds and observed_kinds before completion.',
|
|
416
|
+
[auditPath]
|
|
417
|
+
));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
421
|
+
blockers.push(blocker(
|
|
422
|
+
'invalid_delivery_posture',
|
|
423
|
+
`Milestone audit has invalid delivery_posture (${deliveryPosture}). Re-run audit before completion.`,
|
|
424
|
+
[auditPath]
|
|
425
|
+
));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
429
|
+
blockers.push(blocker(
|
|
430
|
+
'invalid_release_claim_posture',
|
|
431
|
+
`Milestone audit has invalid release_claim_posture (${releaseClaimPosture}). Re-run audit before completion.`,
|
|
432
|
+
[auditPath]
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (invalidEvidenceKinds.length > 0) {
|
|
437
|
+
blockers.push(blocker(
|
|
438
|
+
'invalid_release_evidence_kinds',
|
|
439
|
+
`Milestone audit has invalid release evidence kind values (${invalidEvidenceKinds.join(', ')}). Supported values are ${EVIDENCE_KINDS.join(', ')}.`,
|
|
440
|
+
[auditPath]
|
|
441
|
+
));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const missingContradictionChecks = RELEASE_CONTRADICTION_CHECKS.filter((name) => !(name in contradictionChecks));
|
|
445
|
+
const unknownContradictionChecks = Object.keys(contradictionChecks)
|
|
446
|
+
.filter((name) => !RELEASE_CONTRADICTION_CHECKS.includes(name));
|
|
447
|
+
const invalidContradictionChecks = Object.entries(contradictionChecks)
|
|
448
|
+
.filter(([, status]) => !RELEASE_CONTRADICTION_STATUSES.includes(status))
|
|
449
|
+
.map(([name]) => name);
|
|
450
|
+
|
|
451
|
+
if (missingContradictionChecks.length > 0) {
|
|
452
|
+
blockers.push(blocker(
|
|
453
|
+
'missing_release_contradiction_checks',
|
|
454
|
+
`Milestone audit release_claim_contract.contradiction_checks is missing required checks (${missingContradictionChecks.join(', ')}).`,
|
|
455
|
+
[auditPath]
|
|
456
|
+
));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (invalidContradictionChecks.length > 0) {
|
|
460
|
+
blockers.push(blocker(
|
|
461
|
+
'invalid_release_contradiction_checks',
|
|
462
|
+
`Milestone audit release_claim_contract.contradiction_checks has invalid statuses (${invalidContradictionChecks.join(', ')}).`,
|
|
463
|
+
[auditPath]
|
|
464
|
+
));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (unknownContradictionChecks.length > 0) {
|
|
468
|
+
blockers.push(blocker(
|
|
469
|
+
'unknown_release_contradiction_checks',
|
|
470
|
+
`Milestone audit release_claim_contract.contradiction_checks has unknown checks (${unknownContradictionChecks.join(', ')}). Supported checks are ${RELEASE_CONTRADICTION_CHECKS.join(', ')}.`,
|
|
471
|
+
[auditPath]
|
|
472
|
+
));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!DELIVERY_POSTURES.includes(deliveryPosture) || !RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
476
|
+
return blockers;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const releaseEvaluation = evaluateReleaseClaimCloseoutContract({
|
|
480
|
+
surface: 'complete-milestone',
|
|
481
|
+
deliveryPosture,
|
|
482
|
+
releaseClaimPosture,
|
|
483
|
+
observedKinds,
|
|
484
|
+
waivedKinds,
|
|
485
|
+
unsupportedClaims,
|
|
486
|
+
deferrals,
|
|
487
|
+
contradictionChecks,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
491
|
+
const evidenceContract = getEvidenceContract('complete-milestone', deliveryPosture);
|
|
492
|
+
const enforcedRequiredKinds = [...new Set([...evidenceContract.requiredKinds, ...releaseEvaluation.requiredKinds])];
|
|
493
|
+
const undeclaredRequiredKinds = enforcedRequiredKinds.filter((kind) => !requiredKinds.includes(kind));
|
|
494
|
+
const recomputedMissingKinds = enforcedRequiredKinds.filter((kind) => !observedKinds.includes(kind));
|
|
495
|
+
|
|
496
|
+
if (undeclaredRequiredKinds.length > 0) {
|
|
497
|
+
blockers.push(blocker(
|
|
498
|
+
'invalid_release_evidence_contract',
|
|
499
|
+
`Milestone audit evidence_contract.required_kinds omits required closeout evidence (${undeclaredRequiredKinds.join(', ')}).`,
|
|
500
|
+
[auditPath]
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (recomputedMissingKinds.length > 0) {
|
|
505
|
+
blockers.push(blocker(
|
|
506
|
+
'missing_required_release_evidence',
|
|
507
|
+
`Milestone audit observed evidence is missing required closeout kinds (${recomputedMissingKinds.join(', ')}).`,
|
|
508
|
+
[auditPath]
|
|
509
|
+
));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (missingKinds.length > 0) {
|
|
514
|
+
blockers.push(blocker(
|
|
515
|
+
'missing_required_release_evidence',
|
|
516
|
+
`Milestone audit is missing required evidence kinds for closeout (${missingKinds.join(', ')}).`,
|
|
517
|
+
[auditPath]
|
|
518
|
+
));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (releaseEvaluation.invalidWaivers.length > 0) {
|
|
522
|
+
blockers.push(blocker(
|
|
523
|
+
'invalid_release_waivers',
|
|
524
|
+
`Milestone audit has invalid waivers for missing required evidence (${releaseEvaluation.invalidWaivers.join(', ')}).`,
|
|
525
|
+
[auditPath]
|
|
526
|
+
));
|
|
527
|
+
}
|
|
528
|
+
if (releaseEvaluation.blockers.some((releaseBlocker) => releaseBlocker.code === 'incompatible_release_claim_posture')) {
|
|
529
|
+
blockers.push(blocker(
|
|
530
|
+
'incompatible_release_claim_posture',
|
|
531
|
+
`Milestone audit release_claim_posture (${releaseClaimPosture}) is incompatible with delivery_posture (${deliveryPosture}).`,
|
|
532
|
+
[auditPath]
|
|
533
|
+
));
|
|
534
|
+
}
|
|
535
|
+
if (releaseEvaluation.unresolvedUnsupportedClaims.length > 0) {
|
|
536
|
+
blockers.push(blocker(
|
|
537
|
+
'unsupported_release_claims',
|
|
538
|
+
`Milestone audit has unsupported release claims without downgrade or deferral (${releaseEvaluation.unresolvedUnsupportedClaims.join(', ')}).`,
|
|
539
|
+
[auditPath]
|
|
540
|
+
));
|
|
541
|
+
}
|
|
542
|
+
if (releaseEvaluation.failedContradictionChecks.length > 0) {
|
|
543
|
+
blockers.push(blocker(
|
|
544
|
+
'failed_release_contradiction_checks',
|
|
545
|
+
`Milestone audit has failed release contradiction checks (${releaseEvaluation.failedContradictionChecks.join(', ')}).`,
|
|
546
|
+
[auditPath]
|
|
547
|
+
));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return blockers;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function extractFrontmatter(content) {
|
|
554
|
+
const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
|
|
555
|
+
return match ? match[1] : '';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function readTopLevelScalar(frontmatter, key) {
|
|
559
|
+
const match = String(frontmatter || '').match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
560
|
+
return match ? cleanYamlValue(match[1]) : null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function extractYamlBlock(frontmatter, key) {
|
|
564
|
+
const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n');
|
|
565
|
+
const startIndex = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line.trim()));
|
|
566
|
+
if (startIndex === -1) return '';
|
|
567
|
+
|
|
568
|
+
const collected = [];
|
|
569
|
+
for (const line of lines.slice(startIndex + 1)) {
|
|
570
|
+
if (/^[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
571
|
+
collected.push(line);
|
|
572
|
+
}
|
|
573
|
+
return collected.join('\n');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function readBlockList(block, key) {
|
|
577
|
+
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
578
|
+
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
579
|
+
if (startIndex === -1) return [];
|
|
580
|
+
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
581
|
+
|
|
582
|
+
const inline = lines[startIndex].match(/^\s+[^:]+:\s*\[([^\]]*)\]/);
|
|
583
|
+
if (inline) return splitInlineList(inline[1]);
|
|
584
|
+
|
|
585
|
+
const collected = [];
|
|
586
|
+
for (const line of lines.slice(startIndex + 1)) {
|
|
587
|
+
const indent = line.match(/^\s*/)[0].length;
|
|
588
|
+
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
589
|
+
collected.push(line);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return parseYamlListItems(collected, baseIndent)
|
|
593
|
+
.map((item) => item.join(' '))
|
|
594
|
+
.map(cleanYamlValue);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function parseYamlListItems(lines, baseIndent) {
|
|
598
|
+
const items = [];
|
|
599
|
+
let current = null;
|
|
600
|
+
let itemIndent = null;
|
|
601
|
+
|
|
602
|
+
for (const line of lines) {
|
|
603
|
+
const match = line.match(/^(\s*)-\s*(.+?)\s*$/);
|
|
604
|
+
const indent = line.match(/^\s*/)[0].length;
|
|
605
|
+
|
|
606
|
+
if (match && indent > baseIndent && (itemIndent === null || indent === itemIndent)) {
|
|
607
|
+
if (current) items.push(current);
|
|
608
|
+
current = [match[2]];
|
|
609
|
+
itemIndent = indent;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (current && line.trim()) {
|
|
614
|
+
current.push(line.trim());
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (current) items.push(current);
|
|
619
|
+
return items;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function findInvalidEvidenceKinds(field, kinds) {
|
|
623
|
+
return kinds
|
|
624
|
+
.filter((kind) => !EVIDENCE_KINDS.includes(kind))
|
|
625
|
+
.map((kind) => `${field}: ${kind}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function readNestedStatusBlock(block, key) {
|
|
629
|
+
const nested = extractIndentedBlock(block, key);
|
|
630
|
+
const statuses = {};
|
|
631
|
+
for (const line of nested.split('\n')) {
|
|
632
|
+
const match = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
633
|
+
if (match) statuses[match[1]] = cleanYamlValue(match[2]);
|
|
634
|
+
}
|
|
635
|
+
return statuses;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function extractIndentedBlock(block, key) {
|
|
639
|
+
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
640
|
+
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
641
|
+
if (startIndex === -1) return '';
|
|
642
|
+
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
643
|
+
|
|
644
|
+
const collected = [];
|
|
645
|
+
for (const line of lines.slice(startIndex + 1)) {
|
|
646
|
+
const indent = line.match(/^\s*/)[0].length;
|
|
647
|
+
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
648
|
+
collected.push(line);
|
|
649
|
+
}
|
|
650
|
+
return collected.join('\n');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function splitInlineList(value) {
|
|
654
|
+
return splitCommaAware(value)
|
|
655
|
+
.map(cleanYamlValue)
|
|
656
|
+
.filter(Boolean);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function splitCommaAware(value) {
|
|
660
|
+
const items = [];
|
|
661
|
+
let current = '';
|
|
662
|
+
let quote = null;
|
|
663
|
+
let escaped = false;
|
|
664
|
+
|
|
665
|
+
for (const char of String(value || '')) {
|
|
666
|
+
if (escaped) {
|
|
667
|
+
current += char;
|
|
668
|
+
escaped = false;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (char === '\\' && quote) {
|
|
672
|
+
current += char;
|
|
673
|
+
escaped = true;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
677
|
+
quote = quote ? null : char;
|
|
678
|
+
current += char;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (char === ',' && !quote) {
|
|
682
|
+
items.push(current);
|
|
683
|
+
current = '';
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
current += char;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
items.push(current);
|
|
690
|
+
return items;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function cleanYamlValue(value) {
|
|
694
|
+
return stripInlineYamlComment(String(value || ''))
|
|
695
|
+
.trim()
|
|
696
|
+
.replace(/^['"]|['"]$/g, '')
|
|
697
|
+
.trim();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function stripInlineYamlComment(value) {
|
|
701
|
+
let current = '';
|
|
702
|
+
let quote = null;
|
|
703
|
+
let escaped = false;
|
|
704
|
+
|
|
705
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
706
|
+
const char = value[index];
|
|
707
|
+
if (escaped) {
|
|
708
|
+
current += char;
|
|
709
|
+
escaped = false;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (char === '\\' && quote) {
|
|
713
|
+
current += char;
|
|
714
|
+
escaped = true;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
718
|
+
quote = quote ? null : char;
|
|
719
|
+
current += char;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (char === '#' && !quote && (index === 0 || /\s/.test(value[index - 1]))) {
|
|
723
|
+
return current.trimEnd();
|
|
724
|
+
}
|
|
725
|
+
current += char;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return current;
|
|
729
|
+
}
|
|
730
|
+
|
|
319
731
|
function blocker(code, message, artifacts) {
|
|
320
732
|
return { code, message, artifacts };
|
|
321
733
|
}
|
|
@@ -42,6 +42,7 @@ export function evaluateLifecycleState({ planningDir, provenance = null } = {})
|
|
|
42
42
|
return {
|
|
43
43
|
...phase,
|
|
44
44
|
hasArtifacts: matchingArtifacts.length > 0,
|
|
45
|
+
hasLifecycleArtifacts: hasPlan || hasSummary,
|
|
45
46
|
hasPlan,
|
|
46
47
|
hasSummary,
|
|
47
48
|
artifacts: matchingArtifacts,
|
|
@@ -339,10 +340,10 @@ function classifyPhaseArtifact(dir, name) {
|
|
|
339
340
|
if (!baseIdMatch || !phaseTokenMatch) return null;
|
|
340
341
|
|
|
341
342
|
let kind = 'other';
|
|
342
|
-
if (name
|
|
343
|
-
else if (name
|
|
344
|
-
else if (name
|
|
345
|
-
else if (name
|
|
343
|
+
if (isNamedPhaseArtifact(name, baseIdMatch[1], 'PLAN')) kind = 'plan';
|
|
344
|
+
else if (isNamedPhaseArtifact(name, baseIdMatch[1], 'SUMMARY')) kind = 'summary';
|
|
345
|
+
else if (isNamedPhaseArtifact(name, baseIdMatch[1], 'VERIFICATION')) kind = 'verification';
|
|
346
|
+
else if (isNamedPhaseArtifact(name, baseIdMatch[1], 'APPROACH')) kind = 'approach';
|
|
346
347
|
|
|
347
348
|
return {
|
|
348
349
|
dir,
|
|
@@ -354,6 +355,10 @@ function classifyPhaseArtifact(dir, name) {
|
|
|
354
355
|
};
|
|
355
356
|
}
|
|
356
357
|
|
|
358
|
+
function isNamedPhaseArtifact(name, baseId, kind) {
|
|
359
|
+
return name.toLowerCase() === `${baseId.toLowerCase()}-${kind.toLowerCase()}.md`;
|
|
360
|
+
}
|
|
361
|
+
|
|
357
362
|
function parseMilestoneLedger(content) {
|
|
358
363
|
if (!content) return [];
|
|
359
364
|
|
package/bin/lib/phase.mjs
CHANGED
|
@@ -72,13 +72,14 @@ function listPhaseArtifacts(dir) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function classifyPhaseArtifact(dir, name) {
|
|
75
|
+
const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
|
|
75
76
|
const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
|
|
76
77
|
const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
|
|
77
78
|
const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
|
|
78
79
|
|
|
79
80
|
let kind = 'OTHER';
|
|
80
|
-
if (name
|
|
81
|
-
else if (name
|
|
81
|
+
if (baseIdMatch && isNamedPhaseArtifact(name, baseIdMatch[1], 'PLAN')) kind = 'PLAN';
|
|
82
|
+
else if (baseIdMatch && isNamedPhaseArtifact(name, baseIdMatch[1], 'SUMMARY')) kind = 'SUMMARY';
|
|
82
83
|
|
|
83
84
|
return {
|
|
84
85
|
dir,
|
|
@@ -89,6 +90,10 @@ function classifyPhaseArtifact(dir, name) {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
function isNamedPhaseArtifact(name, baseId, kind) {
|
|
94
|
+
return name.toLowerCase() === `${baseId.toLowerCase()}-${kind.toLowerCase()}.md`;
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
function padPhase(n) {
|
|
93
98
|
return String(n).padStart(2, '0');
|
|
94
99
|
}
|
|
@@ -7,6 +7,11 @@ export const PLAN_CHECK_DIMENSIONS = [
|
|
|
7
7
|
'must_have_quality',
|
|
8
8
|
'context_compliance',
|
|
9
9
|
'goal_achievement',
|
|
10
|
+
'scope_boundaries',
|
|
11
|
+
'anti_regression_capture',
|
|
12
|
+
'escalation_integrity',
|
|
13
|
+
'closure_honesty',
|
|
14
|
+
'high_leverage_review',
|
|
10
15
|
'approach_alignment',
|
|
11
16
|
];
|
|
12
17
|
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -46,12 +46,14 @@ function renderPlanningCliLauncher() {
|
|
|
46
46
|
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
47
47
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
48
48
|
import { cmdPhaseStatus } from './lib/phase.mjs';
|
|
49
|
+
import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
|
|
49
50
|
import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
50
51
|
|
|
51
52
|
const COMMANDS = {
|
|
52
53
|
'file-op': cmdFileOp,
|
|
53
54
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
54
55
|
'phase-status': cmdPhaseStatus,
|
|
56
|
+
'session-fingerprint': cmdSessionFingerprint,
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function printHelp() {
|
|
@@ -67,6 +69,8 @@ function printHelp() {
|
|
|
67
69
|
' lifecycle-preflight <surface> [phase]',
|
|
68
70
|
' Inspect lifecycle gate results for a workflow surface',
|
|
69
71
|
' Example: node .planning/bin/gsdd.mjs lifecycle-preflight verify 1 --expects-mutation phase-status',
|
|
72
|
+
' session-fingerprint write',
|
|
73
|
+
' Rebaseline planning-state drift after reviewing changed planning files',
|
|
70
74
|
'',
|
|
71
75
|
'Advanced option:',
|
|
72
76
|
' --workspace-root <path> Override workspace root discovery before or after the subcommand',
|