@opengsd/gsd-core 1.5.0-rc.3 → 1.5.0-rc.4
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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +36 -57
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +167 -3
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-state.cjs +97 -3
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/config.cjs +80 -24
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +28 -6
- package/gsd-core/bin/lib/install-profiles.cjs +55 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/phase.cjs +28 -12
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +246 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -2
- package/gsd-core/bin/lib/state.cjs +240 -59
- package/gsd-core/bin/lib/verify.cjs +73 -4
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/execute-phase.md +4 -3
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +4 -0
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +2 -0
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/quick.md +3 -2
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +1 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +14 -1
- package/scripts/lint-allow-test-rule-refs.allowlist.json +2 -1
- package/scripts/lint-test-file-count.allowlist.json +6 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +107 -6
|
@@ -1327,6 +1327,19 @@ function acquireStateLock(statePath, clock) {
|
|
|
1327
1327
|
const staleThresholdMs = 10000;
|
|
1328
1328
|
const maxWaitMs = 30000;
|
|
1329
1329
|
const startedAt = clock.now();
|
|
1330
|
+
// Shared helper: check the time budget then back off with jitter before the
|
|
1331
|
+
// next retry. Both the EEXIST contention path and the recoverable-errno path
|
|
1332
|
+
// must go through this so neither can busy-spin (#1217).
|
|
1333
|
+
const checkBudgetAndSleep = (context) => {
|
|
1334
|
+
if (clock.now() - startedAt >= maxWaitMs) {
|
|
1335
|
+
const e = new Error('acquireStateLock: ' + lockPath + ' ' + context + ' for ' +
|
|
1336
|
+
(clock.now() - startedAt) + 'ms (exceeded ' + maxWaitMs + 'ms budget)');
|
|
1337
|
+
e.lockBudgetExceeded = true;
|
|
1338
|
+
throw e;
|
|
1339
|
+
}
|
|
1340
|
+
const jitter = Math.floor(Math.random() * 50);
|
|
1341
|
+
clock.sleep(retryDelay + jitter);
|
|
1342
|
+
};
|
|
1330
1343
|
while (true) {
|
|
1331
1344
|
try {
|
|
1332
1345
|
const fd = node_fs_1.default.openSync(lockPath, node_fs_1.default.constants.O_CREAT | node_fs_1.default.constants.O_EXCL | node_fs_1.default.constants.O_WRONLY);
|
|
@@ -1338,9 +1351,11 @@ function acquireStateLock(statePath, clock) {
|
|
|
1338
1351
|
}
|
|
1339
1352
|
catch (err) {
|
|
1340
1353
|
// Transient filesystem errors (Docker overlay-fs, NFS, OS signals, AV scanners)
|
|
1341
|
-
// are recoverable — retry the
|
|
1354
|
+
// are recoverable — retry with the same budget + backoff as the EEXIST path so
|
|
1355
|
+
// a permanently-failing errno cannot busy-spin at 100% CPU (#1217).
|
|
1342
1356
|
// See ACQUIRE_LOCK_RETRY_ERRNOS for the full list and rationale.
|
|
1343
1357
|
if (ACQUIRE_LOCK_RETRY_ERRNOS.has(err.code)) {
|
|
1358
|
+
checkBudgetAndSleep(err.code + ' persisted');
|
|
1344
1359
|
continue;
|
|
1345
1360
|
}
|
|
1346
1361
|
if (err.code !== 'EEXIST')
|
|
@@ -1351,22 +1366,39 @@ function acquireStateLock(statePath, clock) {
|
|
|
1351
1366
|
try {
|
|
1352
1367
|
const stat = node_fs_1.default.statSync(lockPath);
|
|
1353
1368
|
if ((clock).now() - stat.mtimeMs > staleThresholdMs) {
|
|
1369
|
+
let removed = false;
|
|
1354
1370
|
try {
|
|
1355
1371
|
node_fs_1.default.unlinkSync(lockPath);
|
|
1372
|
+
removed = true;
|
|
1373
|
+
}
|
|
1374
|
+
catch { /* swallow: bounded below */ }
|
|
1375
|
+
if (removed) {
|
|
1376
|
+
// Successful steal — retry immediately to grab the just-freed lock.
|
|
1377
|
+
// Must NOT call checkBudgetAndSleep here: a throw-after-delete would
|
|
1378
|
+
// corrupt the filesystem state, and the budget is already bounded on
|
|
1379
|
+
// the next iteration's EEXIST or open attempt (#1217 regression fix).
|
|
1380
|
+
continue;
|
|
1356
1381
|
}
|
|
1357
|
-
|
|
1382
|
+
// Persistent unlinkSync failure — apply budget + backoff so it cannot
|
|
1383
|
+
// busy-spin (#1217).
|
|
1384
|
+
checkBudgetAndSleep('stale lock removal failed');
|
|
1358
1385
|
continue;
|
|
1359
1386
|
}
|
|
1360
1387
|
}
|
|
1361
|
-
catch {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1388
|
+
catch (err) {
|
|
1389
|
+
// Re-throw a budget-exceeded error from the unlinkSync failure path above
|
|
1390
|
+
// unchanged — its message already names the real cause ("stale lock removal
|
|
1391
|
+
// failed") and double-wrapping it would replace that with the misleading
|
|
1392
|
+
// "statSync failed after EEXIST" context string (#1217 diagnostic fix).
|
|
1393
|
+
if (err?.lockBudgetExceeded)
|
|
1394
|
+
throw err;
|
|
1395
|
+
// statSync failed — lock was likely released between our EEXIST and this
|
|
1396
|
+
// stat call. Apply budget + backoff so a persistent statSync failure
|
|
1397
|
+
// cannot busy-spin (#1217).
|
|
1398
|
+
checkBudgetAndSleep('statSync failed after EEXIST');
|
|
1399
|
+
continue;
|
|
1367
1400
|
}
|
|
1368
|
-
|
|
1369
|
-
(clock).sleep(retryDelay + jitter);
|
|
1401
|
+
checkBudgetAndSleep('held by live process');
|
|
1370
1402
|
}
|
|
1371
1403
|
}
|
|
1372
1404
|
}
|
|
@@ -1441,6 +1473,26 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1441
1473
|
// Snapshot the existing progress block BEFORE the transform so we can
|
|
1442
1474
|
// restore it when resync is false.
|
|
1443
1475
|
const preFm = resync ? null : extractFrontmatter(content);
|
|
1476
|
+
// Bug #1230: delta heuristic — snapshot pre-transform body source fields so
|
|
1477
|
+
// we can detect whether THIS write changed them. syncStateFrontmatter
|
|
1478
|
+
// re-derives frontmatter status/stopped_at from the body on every write;
|
|
1479
|
+
// when the body's source field was NOT changed by the transform, the
|
|
1480
|
+
// existing frontmatter value (e.g. a hand-set 'completed') must win over
|
|
1481
|
+
// the body-derived value (e.g. 'verifying' from a stale "Status: Verifying
|
|
1482
|
+
// Phase 3" line that an earlier tool wrote). We do NOT disturb `preFm`
|
|
1483
|
+
// above (null when resync:true) — these are independent snapshots.
|
|
1484
|
+
// Strip frontmatter before calling stateExtractField so the YAML `status:`
|
|
1485
|
+
// key in the frontmatter block cannot shadow the body field we are tracking.
|
|
1486
|
+
const preBody = stripFrontmatter(content);
|
|
1487
|
+
const preFmSnapshot = extractFrontmatter(content);
|
|
1488
|
+
const preBodyStatus = (0, state_document_cjs_1.stateExtractField)(preBody, 'Status');
|
|
1489
|
+
// Bug #1230 / Change B: scope stopped_at delta to the ## Session section,
|
|
1490
|
+
// mirroring buildStateFrontmatter's sessionBodyScope logic (line ~1172).
|
|
1491
|
+
// A stale "Stopped at:" in a non-Session section (e.g. Session Continuity
|
|
1492
|
+
// Archive prose) must not interfere with the delta comparison.
|
|
1493
|
+
const preSessionMatch = matchSessionSection(preBody);
|
|
1494
|
+
const preSessionScope = preSessionMatch ? preSessionMatch[1] : preBody;
|
|
1495
|
+
const preBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped at');
|
|
1444
1496
|
const modified = transformFn(content);
|
|
1445
1497
|
// Bug #948: no-op guard — if the transform produced no change, do NOT write
|
|
1446
1498
|
// the file. An unconditional write would bump `last_updated`, reset
|
|
@@ -1453,14 +1505,53 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1453
1505
|
return;
|
|
1454
1506
|
}
|
|
1455
1507
|
let synced = syncStateFrontmatter(modified, cwd);
|
|
1456
|
-
|
|
1508
|
+
// Compute postFm once and apply BOTH the progress-restore (when !resync)
|
|
1509
|
+
// AND the status/stopped_at preservation (#1230) before reconstructing.
|
|
1510
|
+
// This avoids double-wrapping the frontmatter block.
|
|
1511
|
+
const needsProgressRestore = !resync && preFm && preFm['progress'];
|
|
1512
|
+
// Post-transform body source fields used for the delta comparison (#1230).
|
|
1513
|
+
// Use `modified` (not `synced`): syncStateFrontmatter only rewrites the frontmatter block, so the body is identical in both — and we need the body the transform produced.
|
|
1514
|
+
// Strip frontmatter so the YAML status key cannot shadow the body field.
|
|
1515
|
+
const postBody = stripFrontmatter(modified);
|
|
1516
|
+
const postBodyStatus = (0, state_document_cjs_1.stateExtractField)(postBody, 'Status');
|
|
1517
|
+
// Bug #1230 / Change B: scope stopped_at delta to the ## Session section,
|
|
1518
|
+
// consistent with the pre-transform snapshot above and buildStateFrontmatter.
|
|
1519
|
+
const postSessionMatch = matchSessionSection(postBody);
|
|
1520
|
+
const postSessionScope = postSessionMatch ? postSessionMatch[1] : postBody;
|
|
1521
|
+
const postBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped at');
|
|
1522
|
+
let mutated = false;
|
|
1523
|
+
const postFm = extractFrontmatter(synced);
|
|
1524
|
+
if (needsProgressRestore) {
|
|
1457
1525
|
// Re-apply the curated progress block that syncStateFrontmatter just
|
|
1458
1526
|
// overwrote with disk-derived values. Only restore keys that were present
|
|
1459
1527
|
// in the snapshot — this preserves any new non-progress frontmatter fields
|
|
1460
1528
|
// (e.g., status, current_phase) that syncStateFrontmatter legitimately
|
|
1461
1529
|
// derived from the updated body.
|
|
1462
|
-
const postFm = extractFrontmatter(synced);
|
|
1463
1530
|
postFm['progress'] = preFm['progress'];
|
|
1531
|
+
mutated = true;
|
|
1532
|
+
}
|
|
1533
|
+
// Bug #1230: preserve existing frontmatter status when this write did NOT
|
|
1534
|
+
// change the body's Status field. A write that doesn't touch Status must
|
|
1535
|
+
// not silently revert a hand-set frontmatter status (e.g. 'completed') to
|
|
1536
|
+
// whatever the stale body Status happens to derive (e.g. 'verifying').
|
|
1537
|
+
// Only apply when the existing frontmatter held a real, non-unknown status.
|
|
1538
|
+
if (postBodyStatus === preBodyStatus &&
|
|
1539
|
+
typeof preFmSnapshot['status'] === 'string' &&
|
|
1540
|
+
preFmSnapshot['status'].length > 0 &&
|
|
1541
|
+
preFmSnapshot['status'] !== 'unknown' &&
|
|
1542
|
+
postFm['status'] !== preFmSnapshot['status']) {
|
|
1543
|
+
postFm['status'] = preFmSnapshot['status'];
|
|
1544
|
+
mutated = true;
|
|
1545
|
+
}
|
|
1546
|
+
// Bug #1230: same delta heuristic for stopped_at.
|
|
1547
|
+
if (postBodyStoppedAt === preBodyStoppedAt &&
|
|
1548
|
+
typeof preFmSnapshot['stopped_at'] === 'string' &&
|
|
1549
|
+
preFmSnapshot['stopped_at'].length > 0 &&
|
|
1550
|
+
postFm['stopped_at'] !== preFmSnapshot['stopped_at']) {
|
|
1551
|
+
postFm['stopped_at'] = preFmSnapshot['stopped_at'];
|
|
1552
|
+
mutated = true;
|
|
1553
|
+
}
|
|
1554
|
+
if (mutated) {
|
|
1464
1555
|
const yamlStr = reconstructFrontmatter(postFm);
|
|
1465
1556
|
const body = stripFrontmatter(synced);
|
|
1466
1557
|
synced = `---\n${yamlStr}\n---\n\n${body}`;
|
|
@@ -1529,74 +1620,89 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1529
1620
|
const today = clock_cjs_1.realClock.today();
|
|
1530
1621
|
const updated = [];
|
|
1531
1622
|
readModifyWriteStateMd(statePath, (content) => {
|
|
1623
|
+
// Bug #1255: all body-field replacements must operate on the body only
|
|
1624
|
+
// (frontmatter stripped), not on the full content. When the full content is
|
|
1625
|
+
// passed to stateReplaceField the YAML `status: planning` key matches the
|
|
1626
|
+
// plain-text pattern (`^Status:\s*`) before the body pipe-table row, so the
|
|
1627
|
+
// pipe-table `| Status | Planning |` is never updated and syncStateFrontmatter
|
|
1628
|
+
// re-derives 'planning' from the unchanged body — the status never advances.
|
|
1629
|
+
const existingFm = extractFrontmatter(content);
|
|
1630
|
+
const hasFrontmatter = Object.keys(existingFm).length > 0;
|
|
1631
|
+
let body = stripFrontmatter(content);
|
|
1632
|
+
// Helper to reassemble content for field-replacement checks; callers that
|
|
1633
|
+
// only need to test/replace body fields use `body` directly, and the final
|
|
1634
|
+
// return reassembles the frontmatter block with the updated body.
|
|
1635
|
+
const reassemble = (b) => hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}` : b;
|
|
1532
1636
|
// Idempotency guard (#3127): if the phase is already mid-flight, do NOT
|
|
1533
1637
|
// overwrite execution-progress fields (Current Plan, plan body line,
|
|
1534
1638
|
// Last Activity Description). Only update fields that are safe to
|
|
1535
1639
|
// refresh on resume (Last Activity date, Status if inconsistent).
|
|
1536
1640
|
// A phase is considered mid-flight when Status contains 'Executing Phase N'
|
|
1537
1641
|
// for the current phase number.
|
|
1538
|
-
|
|
1642
|
+
// #1255: extract from body (not full content) so the YAML `status:` key
|
|
1643
|
+
// cannot shadow the body Status field.
|
|
1644
|
+
const currentStatus = (0, state_document_cjs_1.stateExtractField)(body, 'Status') || '';
|
|
1539
1645
|
const isAlreadyExecuting = new RegExp(`Executing Phase\\s+${escapeRegex(String(phaseNumber))}\\b`, 'i').test(currentStatus);
|
|
1540
|
-
// Update Status field
|
|
1646
|
+
// Update Status field (body only — #1255)
|
|
1541
1647
|
const statusValue = `Executing Phase ${phaseNumber}`;
|
|
1542
|
-
let result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1648
|
+
let result = (0, state_document_cjs_1.stateReplaceField)(body, 'Status', statusValue);
|
|
1543
1649
|
if (result) {
|
|
1544
|
-
|
|
1650
|
+
body = result;
|
|
1545
1651
|
updated.push('Status');
|
|
1546
1652
|
}
|
|
1547
1653
|
// Update Last Activity (safe to update on resume — tracks when execute-phase ran)
|
|
1548
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1654
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Last Activity', today);
|
|
1549
1655
|
if (result) {
|
|
1550
|
-
|
|
1656
|
+
body = result;
|
|
1551
1657
|
updated.push('Last Activity');
|
|
1552
1658
|
}
|
|
1553
1659
|
if (!isAlreadyExecuting) {
|
|
1554
1660
|
// First-time execution: set all progress fields
|
|
1555
1661
|
// Update Last Activity Description
|
|
1556
1662
|
const activityDesc = `Phase ${phaseNumber} execution started`;
|
|
1557
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1663
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Last Activity Description', activityDesc);
|
|
1558
1664
|
if (result) {
|
|
1559
|
-
|
|
1665
|
+
body = result;
|
|
1560
1666
|
updated.push('Last Activity Description');
|
|
1561
1667
|
}
|
|
1562
1668
|
// Update Current Phase
|
|
1563
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1669
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Phase', String(phaseNumber));
|
|
1564
1670
|
if (result) {
|
|
1565
|
-
|
|
1671
|
+
body = result;
|
|
1566
1672
|
updated.push('Current Phase');
|
|
1567
1673
|
}
|
|
1568
1674
|
// Update Current Phase Name
|
|
1569
1675
|
if (phaseName) {
|
|
1570
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1676
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Phase Name', phaseName);
|
|
1571
1677
|
if (result) {
|
|
1572
|
-
|
|
1678
|
+
body = result;
|
|
1573
1679
|
updated.push('Current Phase Name');
|
|
1574
1680
|
}
|
|
1575
1681
|
}
|
|
1576
1682
|
// Update Current Plan to 1 (starting from the first plan)
|
|
1577
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1683
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', '1');
|
|
1578
1684
|
if (result) {
|
|
1579
|
-
|
|
1685
|
+
body = result;
|
|
1580
1686
|
updated.push('Current Plan');
|
|
1581
1687
|
}
|
|
1582
1688
|
// Update Total Plans in Phase
|
|
1583
1689
|
if (planCount) {
|
|
1584
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1690
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Total Plans in Phase', String(planCount));
|
|
1585
1691
|
if (result) {
|
|
1586
|
-
|
|
1692
|
+
body = result;
|
|
1587
1693
|
updated.push('Total Plans in Phase');
|
|
1588
1694
|
}
|
|
1589
1695
|
}
|
|
1590
1696
|
// Update **Current focus:** body text line (#1104)
|
|
1591
1697
|
const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
|
|
1592
1698
|
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
|
|
1593
|
-
if (focusPattern.test(
|
|
1594
|
-
|
|
1699
|
+
if (focusPattern.test(body)) {
|
|
1700
|
+
body = body.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
|
|
1595
1701
|
updated.push('Current focus');
|
|
1596
1702
|
}
|
|
1597
1703
|
// Update ## Current Position section (#1104, #1365)
|
|
1598
1704
|
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
1599
|
-
const positionMatch =
|
|
1705
|
+
const positionMatch = body.match(positionPattern);
|
|
1600
1706
|
if (positionMatch) {
|
|
1601
1707
|
const header = positionMatch[1];
|
|
1602
1708
|
let posBody = positionMatch[2];
|
|
@@ -1606,7 +1712,13 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1606
1712
|
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
1607
1713
|
}
|
|
1608
1714
|
else {
|
|
1609
|
-
|
|
1715
|
+
// Pipe-table format in Current Position (#1257): update the | Phase | … |
|
|
1716
|
+
// cell rather than prepending a spurious inline `Phase:` line (which left
|
|
1717
|
+
// the table cell stale). Mirrors the Status/Last-activity table branches.
|
|
1718
|
+
const phaseValue = `${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
1719
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Phase', phaseValue);
|
|
1720
|
+
if (replaced !== null)
|
|
1721
|
+
posBody = replaced;
|
|
1610
1722
|
}
|
|
1611
1723
|
// Update or insert Plan line
|
|
1612
1724
|
const newPlan = `Plan: 1 of ${planCount || '?'}`;
|
|
@@ -1614,19 +1726,39 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1614
1726
|
posBody = posBody.replace(/^Plan:.*$/m, newPlan);
|
|
1615
1727
|
}
|
|
1616
1728
|
else {
|
|
1617
|
-
|
|
1729
|
+
// Pipe-table format in Current Position (#1257): update the | Plan | … |
|
|
1730
|
+
// cell rather than appending after a prepended inline line.
|
|
1731
|
+
const planValue = `1 of ${planCount || '?'}`;
|
|
1732
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Plan', planValue);
|
|
1733
|
+
if (replaced !== null)
|
|
1734
|
+
posBody = replaced;
|
|
1618
1735
|
}
|
|
1619
1736
|
// Update Status line if present
|
|
1620
1737
|
const newStatus = `Status: Executing Phase ${phaseNumber}`;
|
|
1621
1738
|
if (/^Status:/m.test(posBody)) {
|
|
1622
1739
|
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
1623
1740
|
}
|
|
1741
|
+
else {
|
|
1742
|
+
// Pipe-table format in Current Position (#1255)
|
|
1743
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Status', `Executing Phase ${phaseNumber}`);
|
|
1744
|
+
if (replaced !== null)
|
|
1745
|
+
posBody = replaced;
|
|
1746
|
+
}
|
|
1624
1747
|
// Update Last activity line if present
|
|
1625
1748
|
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
|
|
1626
1749
|
if (/^Last activity:/im.test(posBody)) {
|
|
1627
1750
|
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
1628
1751
|
}
|
|
1629
|
-
|
|
1752
|
+
else {
|
|
1753
|
+
// Pipe-table format in Current Position (#1255)
|
|
1754
|
+
// Value must match the inline branch (date + narrative), not bare date.
|
|
1755
|
+
const activityValue = `${today} -- Phase ${phaseNumber} execution started`;
|
|
1756
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', activityValue)
|
|
1757
|
+
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', activityValue);
|
|
1758
|
+
if (replaced !== null)
|
|
1759
|
+
posBody = replaced;
|
|
1760
|
+
}
|
|
1761
|
+
body = body.replace(positionPattern, () => `${header}${posBody}`);
|
|
1630
1762
|
updated.push('Current Position');
|
|
1631
1763
|
}
|
|
1632
1764
|
}
|
|
@@ -1634,19 +1766,29 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1634
1766
|
// Resume path: only update Last activity timestamp in Current Position
|
|
1635
1767
|
// (do not touch Plan:, stopped_at, progress.percent, or plan counter)
|
|
1636
1768
|
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
1637
|
-
const positionMatch =
|
|
1769
|
+
const positionMatch = body.match(positionPattern);
|
|
1638
1770
|
if (positionMatch) {
|
|
1639
1771
|
const header = positionMatch[1];
|
|
1640
1772
|
let posBody = positionMatch[2];
|
|
1641
1773
|
const resumeActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution resumed (wave continue)`;
|
|
1642
1774
|
if (/^Last activity:/im.test(posBody)) {
|
|
1643
1775
|
posBody = posBody.replace(/^Last activity:.*$/im, resumeActivity);
|
|
1644
|
-
|
|
1776
|
+
body = body.replace(positionPattern, () => `${header}${posBody}`);
|
|
1645
1777
|
updated.push('Last activity (resume)');
|
|
1646
1778
|
}
|
|
1779
|
+
else {
|
|
1780
|
+
// Pipe-table format in Current Position (#1255)
|
|
1781
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', resumeActivity)
|
|
1782
|
+
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', resumeActivity);
|
|
1783
|
+
if (replaced !== null) {
|
|
1784
|
+
posBody = replaced;
|
|
1785
|
+
body = body.replace(positionPattern, () => `${header}${posBody}`);
|
|
1786
|
+
updated.push('Last activity (resume)');
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1647
1789
|
}
|
|
1648
1790
|
}
|
|
1649
|
-
return
|
|
1791
|
+
return reassemble(body);
|
|
1650
1792
|
}, cwd);
|
|
1651
1793
|
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1652
1794
|
}
|
|
@@ -1746,43 +1888,54 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
1746
1888
|
// doing so tramples curated/known-good counters. Route through the body-only
|
|
1747
1889
|
// write contract (resync:false), the same guard state.update uses. (#500 RC1)
|
|
1748
1890
|
readModifyWriteStateMd(statePath, (content) => {
|
|
1891
|
+
// Bug #1257: all body-field replacements must operate on the body only
|
|
1892
|
+
// (frontmatter stripped), not on the full content. When the full content is
|
|
1893
|
+
// passed to stateReplaceFieldIfTemplate the YAML `status: planning` key matches
|
|
1894
|
+
// the plain-text pattern (`^Status:\s*`) before the body pipe-table row, so the
|
|
1895
|
+
// pipe-table `| Status | Planning |` cell is never updated and syncStateFrontmatter
|
|
1896
|
+
// re-derives 'planning' from the unchanged body — the status never advances.
|
|
1897
|
+
// (Mirrors the begin/complete-phase fix from #1255/#1256.)
|
|
1898
|
+
const existingFm = extractFrontmatter(content);
|
|
1899
|
+
const hasFrontmatter = Object.keys(existingFm).length > 0;
|
|
1900
|
+
let body = stripFrontmatter(content);
|
|
1901
|
+
const reassemble = (b) => hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}` : b;
|
|
1749
1902
|
// Update Status — only when the existing value is a known template default
|
|
1750
1903
|
// (Knuth invariant: preserve executor-authored values).
|
|
1751
|
-
const
|
|
1752
|
-
if (
|
|
1753
|
-
|
|
1904
|
+
const newBody = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Status', statusDefaults, 'Ready to execute');
|
|
1905
|
+
if (newBody !== body) {
|
|
1906
|
+
body = newBody;
|
|
1754
1907
|
updated.push('Status');
|
|
1755
1908
|
}
|
|
1756
1909
|
// Update Total Plans in Phase
|
|
1757
1910
|
if (planCount !== null && planCount !== undefined) {
|
|
1758
|
-
const result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1911
|
+
const result = (0, state_document_cjs_1.stateReplaceField)(body, 'Total Plans in Phase', String(planCount));
|
|
1759
1912
|
if (result) {
|
|
1760
|
-
|
|
1913
|
+
body = result;
|
|
1761
1914
|
updated.push('Total Plans in Phase');
|
|
1762
1915
|
}
|
|
1763
1916
|
}
|
|
1764
1917
|
// Update Last Activity — only when the existing value is a known template default
|
|
1765
1918
|
{
|
|
1766
|
-
const after = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(
|
|
1767
|
-
if (after !==
|
|
1768
|
-
|
|
1919
|
+
const after = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last Activity', lastActivityDefaults, today);
|
|
1920
|
+
if (after !== body) {
|
|
1921
|
+
body = after;
|
|
1769
1922
|
updated.push('Last Activity');
|
|
1770
1923
|
}
|
|
1771
1924
|
}
|
|
1772
1925
|
// Update Last Activity Description
|
|
1773
1926
|
{
|
|
1774
|
-
const result = (0, state_document_cjs_1.stateReplaceField)(
|
|
1927
|
+
const result = (0, state_document_cjs_1.stateReplaceField)(body, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
|
1775
1928
|
if (result) {
|
|
1776
|
-
|
|
1929
|
+
body = result;
|
|
1777
1930
|
updated.push('Last Activity Description');
|
|
1778
1931
|
}
|
|
1779
1932
|
}
|
|
1780
1933
|
// Update Current Position section
|
|
1781
|
-
|
|
1934
|
+
body = updateCurrentPositionFields(body, {
|
|
1782
1935
|
status: 'Ready to execute',
|
|
1783
1936
|
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
|
|
1784
1937
|
});
|
|
1785
|
-
return
|
|
1938
|
+
return reassemble(body);
|
|
1786
1939
|
}, cwd, { resync: false });
|
|
1787
1940
|
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1788
1941
|
}
|
|
@@ -2263,29 +2416,35 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2263
2416
|
const updated = [];
|
|
2264
2417
|
readModifyWriteStateMd(statePath, (content) => {
|
|
2265
2418
|
const currentPhase = resolvedPhase;
|
|
2266
|
-
//
|
|
2419
|
+
// Bug #1255: operate on body only so the YAML frontmatter `status:` key
|
|
2420
|
+
// cannot shadow the body Status field (pipe-table or inline).
|
|
2421
|
+
const existingFm = extractFrontmatter(content);
|
|
2422
|
+
const hasFrontmatter = Object.keys(existingFm).length > 0;
|
|
2423
|
+
let body = stripFrontmatter(content);
|
|
2424
|
+
const reassemble = (b) => hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}` : b;
|
|
2425
|
+
// Update Status field (body only — #1255)
|
|
2267
2426
|
const statusValue = `Phase ${currentPhase} complete`;
|
|
2268
|
-
let result = (0, state_document_cjs_1.stateReplaceField)(
|
|
2427
|
+
let result = (0, state_document_cjs_1.stateReplaceField)(body, 'Status', statusValue);
|
|
2269
2428
|
if (result) {
|
|
2270
|
-
|
|
2429
|
+
body = result;
|
|
2271
2430
|
updated.push('Status');
|
|
2272
2431
|
}
|
|
2273
2432
|
// Update Last Activity date
|
|
2274
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
2433
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Last Activity', today);
|
|
2275
2434
|
if (result) {
|
|
2276
|
-
|
|
2435
|
+
body = result;
|
|
2277
2436
|
updated.push('Last Activity');
|
|
2278
2437
|
}
|
|
2279
2438
|
// Update Last Activity Description
|
|
2280
2439
|
const activityDesc = `Phase ${currentPhase} marked complete`;
|
|
2281
|
-
result = (0, state_document_cjs_1.stateReplaceField)(
|
|
2440
|
+
result = (0, state_document_cjs_1.stateReplaceField)(body, 'Last Activity Description', activityDesc);
|
|
2282
2441
|
if (result) {
|
|
2283
|
-
|
|
2442
|
+
body = result;
|
|
2284
2443
|
updated.push('Last Activity Description');
|
|
2285
2444
|
}
|
|
2286
2445
|
// Update ## Current Position section
|
|
2287
2446
|
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
2288
|
-
const positionMatch =
|
|
2447
|
+
const positionMatch = body.match(positionPattern);
|
|
2289
2448
|
if (positionMatch) {
|
|
2290
2449
|
const header = positionMatch[1];
|
|
2291
2450
|
let posBody = positionMatch[2];
|
|
@@ -2294,20 +2453,42 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2294
2453
|
if (/^Phase:/m.test(posBody)) {
|
|
2295
2454
|
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
2296
2455
|
}
|
|
2456
|
+
else {
|
|
2457
|
+
// Pipe-table format in Current Position (#1255)
|
|
2458
|
+
// Value cell must be bare (no "Phase:" label prefix) — the column header already provides the label.
|
|
2459
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Phase', `${currentPhase} — COMPLETE`);
|
|
2460
|
+
if (replaced !== null)
|
|
2461
|
+
posBody = replaced;
|
|
2462
|
+
}
|
|
2297
2463
|
// Update Status line if present
|
|
2298
2464
|
const newStatus = `Status: Phase ${currentPhase} complete`;
|
|
2299
2465
|
if (/^Status:/m.test(posBody)) {
|
|
2300
2466
|
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
2301
2467
|
}
|
|
2468
|
+
else {
|
|
2469
|
+
// Pipe-table format in Current Position (#1255)
|
|
2470
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Status', `Phase ${currentPhase} complete`);
|
|
2471
|
+
if (replaced !== null)
|
|
2472
|
+
posBody = replaced;
|
|
2473
|
+
}
|
|
2302
2474
|
// Update Last activity line if present
|
|
2303
2475
|
const newActivity = `Last activity: ${today} -- Phase ${currentPhase} marked complete`;
|
|
2304
2476
|
if (/^Last activity:/im.test(posBody)) {
|
|
2305
2477
|
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
2306
2478
|
}
|
|
2307
|
-
|
|
2479
|
+
else {
|
|
2480
|
+
// Pipe-table format in Current Position (#1255)
|
|
2481
|
+
// Value must match the inline branch (date + narrative), not bare date.
|
|
2482
|
+
const activityValue = `${today} -- Phase ${currentPhase} marked complete`;
|
|
2483
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', activityValue)
|
|
2484
|
+
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', activityValue);
|
|
2485
|
+
if (replaced !== null)
|
|
2486
|
+
posBody = replaced;
|
|
2487
|
+
}
|
|
2488
|
+
body = body.replace(positionPattern, () => `${header}${posBody}`);
|
|
2308
2489
|
updated.push('Current Position');
|
|
2309
2490
|
}
|
|
2310
|
-
return
|
|
2491
|
+
return reassemble(body);
|
|
2311
2492
|
}, cwd);
|
|
2312
2493
|
output({ updated, phase: resolvedPhase }, raw, updated.length > 0 ? 'true' : 'false');
|
|
2313
2494
|
}
|
|
@@ -24,6 +24,8 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
24
24
|
const stateMod = require("./state.cjs");
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-profiles.cjs is an export= CommonJS module
|
|
26
26
|
const modelProfilesMod = require("./model-profiles.cjs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
28
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
27
29
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
28
30
|
const package_identity_cjs_1 = require("./package-identity.cjs");
|
|
29
31
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
@@ -450,6 +452,39 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
450
452
|
artifacts: results,
|
|
451
453
|
}, raw, passed === results.length ? 'valid' : 'invalid');
|
|
452
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Returns a Set of file paths (relative to cwd) that are promised by plans in
|
|
457
|
+
* the same phase directory at a wave number >= minWave.
|
|
458
|
+
*
|
|
459
|
+
* Used by cmdVerifyKeyLinks to avoid hard-failing a missing `from:` file that
|
|
460
|
+
* is a planned future artifact (fix #1202).
|
|
461
|
+
*/
|
|
462
|
+
function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
|
|
463
|
+
const promised = new Set();
|
|
464
|
+
const { planFiles } = planScanMod.scanPhasePlans(phaseDir);
|
|
465
|
+
for (const planFile of planFiles) {
|
|
466
|
+
const planFullPath = node_path_1.default.join(phaseDir, planFile);
|
|
467
|
+
const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
|
|
468
|
+
if (!planContent)
|
|
469
|
+
continue;
|
|
470
|
+
const fm = extractFrontmatter(planContent);
|
|
471
|
+
const waveRaw = fm['wave'];
|
|
472
|
+
const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
|
|
473
|
+
if (isNaN(wave) || wave < minWave)
|
|
474
|
+
continue;
|
|
475
|
+
const filesModified = fm['files_modified'];
|
|
476
|
+
if (!filesModified)
|
|
477
|
+
continue;
|
|
478
|
+
const files = Array.isArray(filesModified)
|
|
479
|
+
? filesModified
|
|
480
|
+
: (typeof filesModified === 'string' ? [filesModified] : []);
|
|
481
|
+
for (const f of files) {
|
|
482
|
+
if (typeof f === 'string' && f.trim())
|
|
483
|
+
promised.add(f.trim());
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return promised;
|
|
487
|
+
}
|
|
453
488
|
function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
454
489
|
if (!planFilePath) {
|
|
455
490
|
error('plan file path required');
|
|
@@ -465,7 +500,25 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
465
500
|
output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
|
|
466
501
|
return;
|
|
467
502
|
}
|
|
503
|
+
// Derive the current plan's wave number and phase directory for wave-aware
|
|
504
|
+
// missing-file handling (fix #1202).
|
|
505
|
+
const currentFm = extractFrontmatter(content);
|
|
506
|
+
const currentWaveRaw = currentFm['wave'];
|
|
507
|
+
const currentWave = typeof currentWaveRaw === 'string'
|
|
508
|
+
? parseInt(currentWaveRaw, 10)
|
|
509
|
+
: (typeof currentWaveRaw === 'number' ? currentWaveRaw : 1);
|
|
510
|
+
const phaseDir = node_path_1.default.dirname(fullPath);
|
|
511
|
+
// Collect files promised by plans at wave >= currentWave (lazy: computed once
|
|
512
|
+
// the first time a missing source is encountered).
|
|
513
|
+
let promisedFiles = null;
|
|
514
|
+
function getPromisedFiles() {
|
|
515
|
+
if (promisedFiles === null) {
|
|
516
|
+
promisedFiles = collectPromisedFilesAtOrAfterWave(phaseDir, isNaN(currentWave) ? 1 : currentWave);
|
|
517
|
+
}
|
|
518
|
+
return promisedFiles;
|
|
519
|
+
}
|
|
468
520
|
const results = [];
|
|
521
|
+
let pendingCount = 0;
|
|
469
522
|
for (const link of keyLinks) {
|
|
470
523
|
if (typeof link === 'string')
|
|
471
524
|
continue;
|
|
@@ -476,9 +529,20 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
476
529
|
verified: false,
|
|
477
530
|
detail: '',
|
|
478
531
|
};
|
|
479
|
-
const
|
|
532
|
+
const fromPath = link['from'] || '';
|
|
533
|
+
const sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, fromPath));
|
|
480
534
|
if (!sourceContent) {
|
|
481
|
-
|
|
535
|
+
// Check if the missing file is promised by a plan at the same or later wave.
|
|
536
|
+
const promised = getPromisedFiles();
|
|
537
|
+
const isPromised = fromPath.trim() !== '' && promised.has(fromPath.trim());
|
|
538
|
+
if (isPromised) {
|
|
539
|
+
check['pending'] = true;
|
|
540
|
+
check['detail'] = 'Source file not yet created — declared in files_modified of a same-or-later-wave plan';
|
|
541
|
+
pendingCount++;
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
|
|
545
|
+
}
|
|
482
546
|
}
|
|
483
547
|
else if (link['pattern']) {
|
|
484
548
|
try {
|
|
@@ -514,12 +578,17 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
514
578
|
results.push(check);
|
|
515
579
|
}
|
|
516
580
|
const verified = results.filter((r) => r['verified']).length;
|
|
581
|
+
// A pending link (from: file promised by a same-or-later-wave plan) is not a
|
|
582
|
+
// hard failure — it should not count against the all_verified gate (#1202).
|
|
583
|
+
const hardFailed = results.filter((r) => !r['verified'] && !r['pending']).length;
|
|
584
|
+
const allVerified = hardFailed === 0;
|
|
517
585
|
output({
|
|
518
|
-
all_verified:
|
|
586
|
+
all_verified: allVerified,
|
|
519
587
|
verified,
|
|
588
|
+
pending: pendingCount,
|
|
520
589
|
total: results.length,
|
|
521
590
|
links: results,
|
|
522
|
-
}, raw,
|
|
591
|
+
}, raw, allVerified ? 'valid' : 'invalid');
|
|
523
592
|
}
|
|
524
593
|
function listMilestoneArchiveDirs(planBase) {
|
|
525
594
|
const milestonesDir = node_path_1.default.join(planBase, 'milestones');
|