@sabaiway/agent-workflow-kit 1.48.0 → 2.0.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/CHANGELOG.md +98 -0
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +42 -11
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +176 -4
- package/bridges/antigravity-cli-bridge/bin/agy.sh +20 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +79 -2
- package/bridges/antigravity-cli-bridge/capability.json +160 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +20 -2
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +136 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +42 -11
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +168 -4
- package/bridges/codex-cli-bridge/capability.json +118 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/review-ledger.md +1 -1
- package/references/modes/review-state.md +2 -2
- package/references/modes/setup.md +7 -2
- package/references/modes/upgrade.md +9 -9
- package/tools/detect-backends.mjs +2 -2
- package/tools/doc-parity.mjs +7 -0
- package/tools/fs-safe.mjs +28 -5
- package/tools/manifest/schema.md +62 -0
- package/tools/manifest/validate.mjs +327 -0
- package/tools/review-ledger-core.mjs +74 -0
- package/tools/review-ledger-write.mjs +19 -10
- package/tools/review-ledger.mjs +21 -12
- package/tools/review-state.mjs +89 -28
- package/tools/setup-backends.mjs +48 -3
package/tools/review-state.mjs
CHANGED
|
@@ -30,6 +30,18 @@
|
|
|
30
30
|
// Informational receipts NEVER satisfy (nor fail) the tree check: plan/diff-mode receipts
|
|
31
31
|
// (artifact ≠ "code") and continuations (fresh:false — agy --continue/--conversation cannot attest
|
|
32
32
|
// a folded tree; only a fresh grounded re-run mints a gate-satisfying receipt).
|
|
33
|
+
// PROBE receipts never satisfy either (BRIDGE-MODES-CATALOG, D3): a CODEX_PROBE=1 / AGY_PROBE=1
|
|
34
|
+
// review runs with the frontier-model/max-effort guard OFF, so the wrappers stamp `probe:true` and
|
|
35
|
+
// this checker drops those receipts — PER RECEIPT, so a normal receipt at the same fingerprint still
|
|
36
|
+
// satisfies, and a backend whose ONLY current receipts are probes fails with its own stated reason
|
|
37
|
+
// (never the stale one). Every receipt from a marker-aware wrapper SELF-DECLARES — `probe` is written
|
|
38
|
+
// on every successful review, true or false — so a NON-BOOLEAN or ABSENT marker is rejected
|
|
39
|
+
// fail-closed: silence is not a declaration, and the probe status of an unmarked receipt is
|
|
40
|
+
// untrustworthy whoever wrote it (the pre-D3 wrappers honoured the probe env vars while writing no
|
|
41
|
+
// marker; a hand-written line is no better evidence). Deliberately NOT keyed on wrapperVersion: the
|
|
42
|
+
// version bumps in a different release phase than the marker lands, so a version floor would reject
|
|
43
|
+
// the very receipts the current wrappers write. Accepted cost (maintainer, 2026-07-15): a pre-D3
|
|
44
|
+
// receipt stops satisfying — re-run the review with a marker-aware bridge.
|
|
33
45
|
//
|
|
34
46
|
// The fingerprint is the ONE canonical uncommitted-state identity — sha256 over: staged diff +
|
|
35
47
|
// unstaged diff + untracked-not-ignored file contents (binary untracked files, symlinks, and
|
|
@@ -61,7 +73,14 @@ import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mj
|
|
|
61
73
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
62
74
|
// The NEUTRAL ledger read-core (AD-050): review-state reads the review-ledger ONLY for the degraded
|
|
63
75
|
// exemption, through the neutral core — never review-ledger.mjs (which imports THIS module, the cycle).
|
|
64
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
resolveLedgerPath,
|
|
78
|
+
resolveBase,
|
|
79
|
+
readLedger,
|
|
80
|
+
filterSegmentRecords,
|
|
81
|
+
roundSequenceIntact,
|
|
82
|
+
summarizeReviewReceiptsForTree,
|
|
83
|
+
} from './review-ledger-core.mjs';
|
|
65
84
|
|
|
66
85
|
export const RECEIPTS_BASENAME = 'agent-workflow-review-receipts.jsonl';
|
|
67
86
|
export const PLANS_REL = 'docs/plans';
|
|
@@ -290,29 +309,35 @@ export const readReceipts = (path, readFile = readFileSync) => {
|
|
|
290
309
|
return { receipts, malformed };
|
|
291
310
|
};
|
|
292
311
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// ungrounded — current-fingerprint fresh receipts exist but every one carries grounded:false;
|
|
312
|
+
// Per-backend receipt status for the current fingerprint, over the ONE shared attesting-receipt
|
|
313
|
+
// predicate (review-ledger-core) that the round cross-check and the round writer read too — two
|
|
314
|
+
// gates disagreeing about what counts as an attestation is the class AD-050 closed:
|
|
315
|
+
// current — an ATTESTING receipt exists (fresh code, probe:false, grounded) — its verdict rides;
|
|
316
|
+
// ungrounded — real (probe:false) current receipts exist but every one carries grounded:false;
|
|
317
|
+
// probe — current receipts exist and EVERY one is a well-formed probe (D3);
|
|
318
|
+
// rejected — current receipts exist, none attests, and >=1 marker was malformed or absent;
|
|
301
319
|
// stale — this backend has receipts, none for the current fingerprint (edited after review);
|
|
302
|
-
// missing — no
|
|
320
|
+
// missing — no receipt from this backend at all.
|
|
321
|
+
// The counts state what was dropped, so a PASS over a partially-excluded set still says so
|
|
322
|
+
// (No-silent-failures) and the --json surface can show it.
|
|
303
323
|
export const backendReceiptStatus = (receipts, backend, fingerprint) => {
|
|
304
324
|
const own = receipts.filter((r) => r.backend === backend);
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
const summary = summarizeReviewReceiptsForTree(own, fingerprint);
|
|
326
|
+
const counts = {
|
|
327
|
+
probeExcluded: summary.probeExcluded,
|
|
328
|
+
markerRejected: summary.markerRejected,
|
|
329
|
+
unmarkedRejected: summary.unmarkedRejected,
|
|
330
|
+
};
|
|
331
|
+
if (summary.state === 'current') {
|
|
332
|
+
return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
310
333
|
}
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
return { state: 'ungrounded', verdict: latest.verdict ?? 'unknown', grounded: false, timestamp: latest.timestamp ?? null };
|
|
334
|
+
if (summary.state === 'ungrounded') {
|
|
335
|
+
return { state: 'ungrounded', verdict: summary.receipt.verdict ?? 'unknown', grounded: false, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
314
336
|
}
|
|
315
|
-
|
|
337
|
+
if (summary.state === 'probe' || summary.state === 'rejected') {
|
|
338
|
+
return { state: summary.state, verdict: null, grounded: null, timestamp: null, ...counts };
|
|
339
|
+
}
|
|
340
|
+
return { state: own.length > 0 ? 'stale' : 'missing', verdict: null, grounded: null, timestamp: null, ...counts };
|
|
316
341
|
};
|
|
317
342
|
|
|
318
343
|
// ── the degraded exemption (AD-050): read the review-ledger for a recorded current-tree degrade ────
|
|
@@ -411,6 +436,17 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
|
|
|
411
436
|
};
|
|
412
437
|
};
|
|
413
438
|
|
|
439
|
+
// Why a backend's current-tree receipts were all rejected — the two causes read differently and
|
|
440
|
+
// have different recoveries (fix the file vs refresh the bridge), so they are never collapsed.
|
|
441
|
+
const rejectionCause = (b) => {
|
|
442
|
+
const parts = [];
|
|
443
|
+
if (b.markerRejected > 0) parts.push(`${b.markerRejected} with a malformed probe marker`);
|
|
444
|
+
if (b.unmarkedRejected > 0) {
|
|
445
|
+
parts.push(`${b.unmarkedRejected} with no probe marker — silence is not a declaration, so the probe status is untrustworthy; re-run the review with a bridge that marks its runs`);
|
|
446
|
+
}
|
|
447
|
+
return parts.join(' + ');
|
|
448
|
+
};
|
|
449
|
+
|
|
414
450
|
// The normative --check decision (the header contract, in order). → { code, reason }.
|
|
415
451
|
export const decideCheck = (state) => {
|
|
416
452
|
// A DETECTOR FAILURE is unknown state, not "no reviewer ready" — the advisory tools warn and
|
|
@@ -449,23 +485,37 @@ export const decideCheck = (state) => {
|
|
|
449
485
|
// stays verdict-blind: the exemption proves the degrade was RECORDED, never that the tree converged.
|
|
450
486
|
const exempt = new Set(state.degradedExempt);
|
|
451
487
|
const failing = state.backends.filter((b) => b.state !== 'current' && !exempt.has(b.backend));
|
|
488
|
+
// A receipt dropped for an untrustworthy probe marker is never silently dropped either (D3): it
|
|
489
|
+
// cannot fail a tree that other receipts satisfy, but a report over a partially-rejected set says
|
|
490
|
+
// so. It counts only the exclusions NOT already named per backend — a FAILING `rejected` backend
|
|
491
|
+
// prints its own cause below, and repeating it in the summary is noise. Every other exclusion (a
|
|
492
|
+
// partial one on a satisfied backend, or one on a degraded-exempt backend) still needs a voice.
|
|
493
|
+
const namedPerBackend = new Set(failing.filter((b) => b.state === 'rejected').map((b) => b.backend));
|
|
494
|
+
const untrustedTotal = state.backends
|
|
495
|
+
.filter((b) => !namedPerBackend.has(b.backend))
|
|
496
|
+
.reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
|
|
497
|
+
const markerNote = untrustedTotal > 0
|
|
498
|
+
? ` — ${untrustedTotal} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${state.receiptsPath}`
|
|
499
|
+
: '';
|
|
452
500
|
if (failing.length === 0) {
|
|
453
501
|
if (exempt.size === 0) {
|
|
454
|
-
return { code: 0, reason: `every recipe-named backend has a fresh grounded receipt for the current tree (${state.requiredBackends.join(' + ')})${malformedNote}${ledgerNote}` };
|
|
502
|
+
return { code: 0, reason: `every recipe-named backend has a fresh grounded receipt for the current tree (${state.requiredBackends.join(' + ')})${markerNote}${malformedNote}${ledgerNote}` };
|
|
455
503
|
}
|
|
456
|
-
return { code: 0, reason: `every recipe-named backend reviewed the current tree (${state.requiredBackends.join(' + ')}) — degraded-exempt (recorded degraded for the current tree in the review ledger): ${[...exempt].join(', ')}${malformedNote}${ledgerNote}` };
|
|
504
|
+
return { code: 0, reason: `every recipe-named backend reviewed the current tree (${state.requiredBackends.join(' + ')}) — degraded-exempt (recorded degraded for the current tree in the review ledger): ${[...exempt].join(', ')}${markerNote}${malformedNote}${ledgerNote}` };
|
|
457
505
|
}
|
|
458
506
|
const parts = failing.map((b) => {
|
|
459
507
|
if (b.state === 'ungrounded') return `${b.backend}: only ungrounded receipts for the current tree — re-run grounded (--facts)`;
|
|
508
|
+
if (b.state === 'probe') return `${b.backend}: only probe receipts for the current tree (CODEX_PROBE=1 / AGY_PROBE=1 relaxes the quality guards) — a probe review never attests; re-run a real one`;
|
|
509
|
+
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
|
|
460
510
|
if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
|
|
461
511
|
return `${b.backend}: no receipt — run its review wrapper`;
|
|
462
512
|
});
|
|
463
|
-
return { code: 1, reason: `${parts.join('; ')}${malformedNote}${ledgerNote}` };
|
|
513
|
+
return { code: 1, reason: `${parts.join('; ')}${markerNote}${malformedNote}${ledgerNote}` };
|
|
464
514
|
};
|
|
465
515
|
|
|
466
516
|
// ── rendering ───────────────────────────────────────────────────────────────────────
|
|
467
517
|
|
|
468
|
-
const STATE_GLYPH = { current: '✓', ungrounded: '✗', stale: '✗', missing: '✗' };
|
|
518
|
+
const STATE_GLYPH = { current: '✓', ungrounded: '✗', probe: '✗', rejected: '✗', stale: '✗', missing: '✗' };
|
|
469
519
|
|
|
470
520
|
const formatHuman = (state, check) => {
|
|
471
521
|
const src = state.resolved.source === 'config' ? `from ${CONFIG_REL}` : 'computed default';
|
|
@@ -488,10 +538,17 @@ const formatHuman = (state, check) => {
|
|
|
488
538
|
? `current (verdict: ${b.verdict}, grounded, ${b.timestamp ?? '?'})`
|
|
489
539
|
: b.state === 'ungrounded'
|
|
490
540
|
? `ungrounded for the current tree (verdict: ${b.verdict}) — a grounded fresh run is required`
|
|
491
|
-
: b.state === '
|
|
492
|
-
? '
|
|
493
|
-
:
|
|
494
|
-
|
|
541
|
+
: b.state === 'probe'
|
|
542
|
+
? 'only probe receipts for the current tree (quality guards relaxed) — a probe review never attests'
|
|
543
|
+
: b.state === 'rejected'
|
|
544
|
+
? `current-tree receipts rejected — ${rejectionCause(b)} (fail-closed)`
|
|
545
|
+
: b.state === 'stale'
|
|
546
|
+
? 'stale — no receipt matches the current tree (edited after review)'
|
|
547
|
+
: 'missing — no receipt from this backend';
|
|
548
|
+
const excludedTag = b.probeExcluded || b.markerRejected || b.unmarkedRejected
|
|
549
|
+
? ` [excluded: ${b.probeExcluded} probe, ${b.markerRejected} malformed-marker, ${b.unmarkedRejected} unmarked]`
|
|
550
|
+
: '';
|
|
551
|
+
lines.push(` ${exempt.has(b.backend) ? '⊘' : STATE_GLYPH[b.state]} ${b.backend}: ${detail}${excludedTag}${exemptTag}`);
|
|
495
552
|
}
|
|
496
553
|
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
497
554
|
return lines.join('\n');
|
|
@@ -507,7 +564,11 @@ recomputes the canonical uncommitted-state fingerprint (staged + unstaged + untr
|
|
|
507
564
|
the review-payload domain), reads the receipt file the review wrappers append to
|
|
508
565
|
(<git dir>/${RECEIPTS_BASENAME}; AW_REVIEW_RECEIPTS overrides), and reports per-backend receipt
|
|
509
566
|
presence + verdict + grounding for the CURRENT tree. Plan/diff-mode receipts and continuations
|
|
510
|
-
(fresh:false) are informational-only — they never satisfy the tree check
|
|
567
|
+
(fresh:false) are informational-only — they never satisfy the tree check, and neither does a PROBE
|
|
568
|
+
receipt (probe:true — a CODEX_PROBE=1 / AGY_PROBE=1 run has the quality guards off). The probe
|
|
569
|
+
filter is per receipt, so a real receipt at the same fingerprint still satisfies. Every marker-aware
|
|
570
|
+
wrapper writes the probe field on EVERY review (true or false), so a malformed OR absent marker is
|
|
571
|
+
rejected fail-closed — silence is not a declaration.
|
|
511
572
|
|
|
512
573
|
--check exits 0/1 per the normative contract in the tool header: 0 for solo / no plan in flight /
|
|
513
574
|
a clean tree / not-a-git-tree / all recipe-named backends receipted-current-and-grounded OR
|
package/tools/setup-backends.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { join, resolve, relative, dirname, isAbsolute } from 'node:path';
|
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import { KNOWN_BACKENDS, detectBackend, detectBackends, resolveDir, guideFor, READY } from './detect-backends.mjs';
|
|
34
|
-
import { copyTreeRefresh, linkManaged } from './fs-safe.mjs';
|
|
34
|
+
import { copyTreeRefresh, linkManaged, isReadonlyWriteBoundary } from './fs-safe.mjs';
|
|
35
35
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
36
36
|
import { compareSemver } from './semver-lite.mjs';
|
|
37
37
|
|
|
@@ -54,6 +54,12 @@ export const SETUP_STOP = 'SETUP_STOP';
|
|
|
54
54
|
const stop = (message, fields = {}) =>
|
|
55
55
|
Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'SetupStop', code: SETUP_STOP, ...fields });
|
|
56
56
|
|
|
57
|
+
// A refresh-only outcome (REFRESH-EROFS-HONESTY / AD-056): an equal-version repair-on-rerun whose
|
|
58
|
+
// write hit a READ-ONLY skills dir. A STATED skip (exit 0), never a false "could not refresh" — the
|
|
59
|
+
// versions are already current and `setup` would hit the same read-only dir. Doc-parity binds this
|
|
60
|
+
// token into references/modes/setup.md + upgrade.md so the mode contracts track the constant.
|
|
61
|
+
export const SKIPPED_READONLY = 'skipped-readonly';
|
|
62
|
+
|
|
57
63
|
// ── injectable fs ──────────────────────────────────────────────────────────────
|
|
58
64
|
|
|
59
65
|
const fsDeps = (deps = {}) => ({
|
|
@@ -557,13 +563,42 @@ const refreshSkillOnly = (entry, deps = {}) => {
|
|
|
557
563
|
const fresh = inspectSkillDir(entry, deps);
|
|
558
564
|
if (fresh.action !== 'refresh') return { refreshed: false, drift: null };
|
|
559
565
|
assertNoDowngrade(fresh, deps);
|
|
560
|
-
|
|
561
|
-
|
|
566
|
+
// D1b: version equality for the read-only degrade is decided from the APPLY-TIME fresh inspection's
|
|
567
|
+
// fields (never the stale pre-apply plan) — both versions KNOWN and equal; two unknowns are not equal.
|
|
568
|
+
// assertNoDowngrade just read the bundled manifest, so this read cannot throw here; a versionless
|
|
569
|
+
// manifest yields null (never "equal" → no false skip).
|
|
570
|
+
const bundledManifest = readBundledManifest(fresh.bundleDir, deps);
|
|
571
|
+
const bundledVersion = typeof bundledManifest.version === 'string' ? bundledManifest.version : null;
|
|
572
|
+
const placedVersion = readPlacedVersionSafe(fresh.skillDir, deps);
|
|
573
|
+
const currentEqual = bundledVersion !== null && placedVersion === bundledVersion;
|
|
574
|
+
try {
|
|
575
|
+
const drift = copyBridgeWithHonesty('refresh', fresh.bundleDir, fresh.skillDir, deps);
|
|
576
|
+
return { refreshed: true, drift };
|
|
577
|
+
} catch (err) {
|
|
578
|
+
// D1/D1a: ONLY a read-only WRITE-boundary failure at an EQUAL version is a repair-on-rerun that
|
|
579
|
+
// cannot run — a STATED skip, never a false "could not refresh". Every other failure stays loud
|
|
580
|
+
// (a read-side / source-side / EIO failure, or a version-behind upgrade, re-throws here).
|
|
581
|
+
if (currentEqual && isReadonlyWriteBoundary(err)) return { refreshed: false, skippedReadonly: true, version: bundledVersion };
|
|
582
|
+
throw err;
|
|
583
|
+
}
|
|
562
584
|
};
|
|
563
585
|
|
|
564
586
|
const NOT_PLACED_LINE = 'skipped — not placed (placement is opt-in: /agent-workflow-kit setup)';
|
|
565
587
|
const stripPrefix = (message) => message.replace('[agent-workflow-kit] ', '');
|
|
566
588
|
|
|
589
|
+
// The read-only degrade wording (D1). The STATED-skip line: version current + the re-sync
|
|
590
|
+
// skipped/incomplete + the read-only cause + the residual — it never claims a re-sync RAN (that is
|
|
591
|
+
// already-current's line) nor file integrity (a partial copy may have happened before the throw). The
|
|
592
|
+
// FAILED line (a version-behind upgrade blocked by the same read-only dir) stays loud, but its
|
|
593
|
+
// recovery points at a writable rerun — the in-session `setup` would hit the same read-only dir.
|
|
594
|
+
const READONLY_RERUN_HINT = 're-run the refresh from a writable session (e.g. outside the read-only sandbox)';
|
|
595
|
+
const skippedReadonlyLine = (name, version) =>
|
|
596
|
+
` ${name}: already current${version ? ` (v${version})` : ''} — the re-sync was skipped/incomplete: ` +
|
|
597
|
+
`the skills directory is read-only this session (the tree may be PARTIALLY updated). Repair-on-rerun cannot ` +
|
|
598
|
+
`run here; any remaining drift persists until you ${READONLY_RERUN_HINT}.`;
|
|
599
|
+
const readonlyRefreshFailedLine = (name, message) =>
|
|
600
|
+
` ${name}: could not refresh — ${stripPrefix(message)}; the skills directory is read-only this session — ${READONLY_RERUN_HINT}`;
|
|
601
|
+
|
|
567
602
|
// Refresh every ALREADY-PLACED bridge from the kit's bundled copies and re-link its wrappers (a newer
|
|
568
603
|
// bridge can add one). One reported outcome per backend — never a crash; a per-backend STOP/error
|
|
569
604
|
// becomes a `failed` result. `line` is the tool-composed sentence callers print VERBATIM (the agent
|
|
@@ -592,6 +627,11 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
|
|
|
592
627
|
return { name: plan.name, outcome: 'failed', line: ` ${plan.name}: could not refresh — ${stripPrefix(plan.reason)}; recover with /agent-workflow-kit setup` };
|
|
593
628
|
}
|
|
594
629
|
const refresh = refreshSkillOnly(registryEntry(plan.name), deps);
|
|
630
|
+
// A read-only skills dir at an EQUAL version is a STATED skip (repair-on-rerun cannot run here) —
|
|
631
|
+
// never a false "could not refresh" and never a failure exit (D1/AD-056).
|
|
632
|
+
if (refresh.skippedReadonly) {
|
|
633
|
+
return { name: plan.name, outcome: SKIPPED_READONLY, line: skippedReadonlyLine(plan.name, refresh.version) };
|
|
634
|
+
}
|
|
595
635
|
if (!refresh.refreshed) {
|
|
596
636
|
return { name: plan.name, outcome: 'not-placed', line: ` ${plan.name}: ${NOT_PLACED_LINE}` };
|
|
597
637
|
}
|
|
@@ -615,6 +655,11 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
|
|
|
615
655
|
if (err.wouldDowngrade) {
|
|
616
656
|
return { name: canonical, outcome: 'kept-newer', line: ` ${canonical}: skipped — ${stripPrefix(err.message)}` };
|
|
617
657
|
}
|
|
658
|
+
// A read-only WRITE failure on a version-BEHIND (upgrade) refresh stays a loud failure — but the
|
|
659
|
+
// in-session "recover with setup" would hit the same read-only dir, so point at a writable rerun.
|
|
660
|
+
if (isReadonlyWriteBoundary(err)) {
|
|
661
|
+
return { name: canonical, outcome: 'failed', line: readonlyRefreshFailedLine(canonical, err.message) };
|
|
662
|
+
}
|
|
618
663
|
return { name: canonical, outcome: 'failed', line: ` ${canonical}: could not refresh — ${stripPrefix(err.message)}; recover with /agent-workflow-kit setup` };
|
|
619
664
|
}
|
|
620
665
|
});
|