@sabaiway/agent-workflow-kit 1.34.0 → 1.36.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -0
  3. package/SKILL.md +23 -13
  4. package/bin/install.mjs +15 -0
  5. package/bridges/antigravity-cli-bridge/SKILL.md +13 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +96 -1
  7. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +204 -1
  8. package/bridges/antigravity-cli-bridge/bin/agy.sh +94 -0
  9. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +245 -1
  10. package/bridges/antigravity-cli-bridge/capability.json +17 -1
  11. package/bridges/codex-cli-bridge/SKILL.md +15 -1
  12. package/bridges/codex-cli-bridge/bin/codex-exec.sh +113 -0
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +288 -0
  14. package/bridges/codex-cli-bridge/bin/codex-review.sh +114 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +229 -1
  16. package/bridges/codex-cli-bridge/capability.json +29 -1
  17. package/capability.json +1 -1
  18. package/package.json +1 -1
  19. package/references/modes/bridge-settings.md +29 -0
  20. package/references/modes/review-ledger.md +28 -0
  21. package/references/modes/set-autonomy.md +29 -0
  22. package/references/modes/upgrade.md +8 -2
  23. package/references/modes/velocity.md +13 -0
  24. package/tools/atomic-write.mjs +59 -21
  25. package/tools/autonomy-config.mjs +306 -0
  26. package/tools/autonomy-write.mjs +27 -0
  27. package/tools/bridge-settings-read.mjs +221 -0
  28. package/tools/bridge-settings.mjs +288 -0
  29. package/tools/commands.mjs +21 -0
  30. package/tools/family-registry.mjs +12 -1
  31. package/tools/manifest/schema.md +31 -0
  32. package/tools/manifest/validate.mjs +85 -0
  33. package/tools/procedures.mjs +26 -4
  34. package/tools/recipes.mjs +16 -6
  35. package/tools/renderers.mjs +7 -0
  36. package/tools/review-ledger-write.mjs +257 -0
  37. package/tools/review-ledger.mjs +508 -0
  38. package/tools/seed-gates.mjs +45 -4
  39. package/tools/set-autonomy.mjs +195 -0
  40. package/tools/setup-backends.mjs +107 -9
  41. package/tools/velocity-profile.mjs +468 -5
  42. package/tools/view-model.mjs +7 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ // set-autonomy.mjs — the WRITER for docs/ai/autonomy.json (the per-project autonomy policy). Mirrors
3
+ // set-recipe.mjs (AD-044): the AGENT turns plain language into explicit `--set <section>.<key>=<value>`
4
+ // / `--unset <section>.<key>` ops; the KIT does the deterministic validate → merge → preview → write.
5
+ // No NL parser (stays dependency-free + deterministic), no `all`-magic — the agent expands plain
6
+ // language into explicit per-key ops (asking if scope is unclear). Ops route through the SAME
7
+ // accept/reject grammar as validateAutonomy (autonomy-config.mjs), so the writer and the config
8
+ // validator can never disagree.
9
+ //
10
+ // Posture: PREVIEW BY DEFAULT (dry-run); `--write` applies. It NEVER commits, NEVER runs a backend, and
11
+ // NEVER renders enforcement — it writes ONLY docs/ai/autonomy.json (the render into .claude/settings.json
12
+ // is the separate velocity autonomy mode). It writes only via the hardened writeAutonomy (deployment
13
+ // gate; exclusive-create tmp + rename; symlink/TOCTOU-safe; last-writer-wins). A no-op set never writes
14
+ // and never spuriously seeds the _README. `--unset` returns a key to its computed default, so reverting
15
+ // needs no hand-edit either. Hand-edit stays first-class — this is an OFFERED convenience, never a lock.
16
+ //
17
+ // Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
18
+ // Exit codes: 0 success; 2 usage (bad/duplicate op, --write with zero ops); 1 config error
19
+ // (malformed/unreadable policy) or a write STOP (no deployment / symlinked leaf). main(argv, ctx) →
20
+ // { code, stdout, stderr }; cwd / fs are injectable for host-independent tests.
21
+ //
22
+ // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
23
+
24
+ import { readFileSync, lstatSync } from 'node:fs';
25
+ import { pathToFileURL } from 'node:url';
26
+ import {
27
+ AUTONOMY_REL,
28
+ fail,
29
+ loadAutonomy,
30
+ validateAutonomy,
31
+ parseAutonomyOp,
32
+ applyAutonomyOps,
33
+ serializeAutonomy,
34
+ resolveAutonomy,
35
+ AUTONOMY_README,
36
+ } from './autonomy-config.mjs';
37
+ import { writeAutonomy as writeAutonomyFs } from './autonomy-write.mjs';
38
+
39
+ // ── argument parsing (usage errors → exit 2) ────────────────────────────────────────
40
+
41
+ // Parse argv → { ops, write, json }. `--set`/`--unset` take a fully-qualified token (parseAutonomyOp
42
+ // validates it). A duplicate op for the same section.key, a `--write` with zero ops, an unknown flag, or
43
+ // a bad token → exit 2. `--set=<tok>` / `--unset=<tok>` inline forms are accepted too.
44
+ const parseArgs = (argv) => {
45
+ const ops = [];
46
+ const seen = new Set();
47
+ let write = false;
48
+ let json = false;
49
+ const takeOp = (kind, tok) => {
50
+ if (tok === undefined || tok.startsWith('--')) throw fail(2, `--${kind} requires <section>.<key>${kind === 'set' ? '=<value>' : ''}`);
51
+ const op = parseAutonomyOp(kind, tok);
52
+ const key = `${op.section}.${op.key}`;
53
+ if (seen.has(key)) throw fail(2, `duplicate op for "${key}" — name each section.key at most once`);
54
+ seen.add(key);
55
+ ops.push(op);
56
+ };
57
+ for (let i = 0; i < argv.length; i += 1) {
58
+ const a = argv[i];
59
+ if (a === '--json') json = true;
60
+ else if (a === '--write') write = true;
61
+ else if (a === '--set') { takeOp('set', argv[i + 1]); i += 1; }
62
+ else if (a === '--unset') { takeOp('unset', argv[i + 1]); i += 1; }
63
+ else if (a.startsWith('--set=')) takeOp('set', a.slice('--set='.length));
64
+ else if (a.startsWith('--unset=')) takeOp('unset', a.slice('--unset='.length));
65
+ else if (a.startsWith('-')) throw fail(2, `unknown flag: ${a}`);
66
+ else throw fail(2, `unexpected argument: ${a}`);
67
+ }
68
+ if (write && ops.length === 0) throw fail(2, 'nothing to write — pass at least one --set/--unset (a bare --write is a no-op)');
69
+ return { ops, write, json };
70
+ };
71
+
72
+ // ── per-op before/after + resolved effective value ──────────────────────────────────
73
+
74
+ // The resolved effective value for a section.key against a resolved policy (computed defaults filled).
75
+ const resolvedValueFor = (resolved, section, key) =>
76
+ section === 'redlines' ? resolved.redlines[key] : resolved.activities[section]?.[key];
77
+
78
+ // A single op's before/after value + the effective value it resolves to (the computed default shows for
79
+ // an unset). `to` is null for an unset (falls to the computed default).
80
+ const resolveOp = (op, current, after) => {
81
+ const from = current?.[op.section]?.[op.key] ?? null;
82
+ const to = after?.[op.section]?.[op.key] ?? null;
83
+ const effective = resolvedValueFor(resolveAutonomy(after), op.section, op.key);
84
+ return { section: op.section, key: op.key, from, to, effective };
85
+ };
86
+
87
+ // ── rendering (ENGLISH; the agent localizes) ────────────────────────────────────────
88
+
89
+ const valueLabel = (v) => (v == null ? '(computed default)' : v);
90
+
91
+ const APPLY_HINT =
92
+ 'Next: render this policy into .claude/settings.json with the velocity autonomy mode (previews first) — the writer changes only the policy file, never the settings.';
93
+
94
+ const formatHuman = ({ changed, unchanged, wrote, fileBody }) => {
95
+ const lines = [];
96
+ if (wrote) lines.push(`wrote ${AUTONOMY_REL}`);
97
+ else if (changed.length) lines.push('set-autonomy — preview (nothing written; re-run with --write to apply)');
98
+ for (const e of changed) {
99
+ lines.push(` ${e.section}.${e.key}: ${valueLabel(e.from)} → ${valueLabel(e.to)}`);
100
+ lines.push(` ↳ effective: ${e.effective}`);
101
+ }
102
+ for (const e of unchanged) lines.push(` ${e.section}.${e.key}: already ${valueLabel(e.from)} (no change)`);
103
+ if (wrote && fileBody) lines.push('', `${AUTONOMY_REL} now reads:`, fileBody.replace(/\n$/, ''), '', APPLY_HINT);
104
+ if (!wrote) {
105
+ if (!changed.length) lines.push(' no changes — nothing to write.');
106
+ else lines.push('', `would write ${AUTONOMY_REL} — re-run with --write to apply.`);
107
+ }
108
+ return lines.join('\n');
109
+ };
110
+
111
+ const buildJson = ({ changed, unchanged, writtenPath, noop }) => ({
112
+ changed: changed.map((e) => ({ section: e.section, key: e.key, from: e.from, to: e.to, effective: e.effective })),
113
+ unchanged: unchanged.map((e) => ({ section: e.section, key: e.key, value: e.from })),
114
+ writtenPath: writtenPath ?? null,
115
+ noop,
116
+ });
117
+
118
+ const HELP = `set-autonomy — write the per-project autonomy policy (docs/ai/autonomy.json).
119
+
120
+ Usage:
121
+ node set-autonomy.mjs [--set <section>.<key>=<value>]... [--unset <section>.<key>]... [--write] [--json]
122
+
123
+ --set <section>.<key>=<value> pin a policy value (fully-qualified; e.g. plan-execution.autonomy=sandbox)
124
+ --unset <section>.<key> return a key to its computed default
125
+ --write apply the change (default: preview only — writes nothing)
126
+ --json machine-readable output
127
+ --help, -h this help
128
+
129
+ Sections/keys: redlines → commit|push|publish|network|credentials|fs_outside_repo (each ask|deny);
130
+ plan-authoring.autonomy, plan-execution.autonomy (each sandbox|prompt)
131
+
132
+ Previews by default; --write applies via an atomic, symlink/TOCTOU-safe write behind a deployment gate.
133
+ Policy writer only: it NEVER renders enforcement (that is the velocity autonomy mode), NEVER runs a
134
+ backend, and NEVER commits. Hand-editing the file stays fully supported.
135
+
136
+ Exit codes: 0 success; 2 usage (bad/duplicate op, or --write with no ops);
137
+ 1 config error (malformed/unreadable policy) or a write STOP (no deployment / symlinked leaf).`;
138
+
139
+ // ── main ────────────────────────────────────────────────────────────────────────────
140
+
141
+ export const main = (argv, ctx = {}) => {
142
+ const cwd = ctx.cwd ?? process.cwd();
143
+ const readFile = ctx.readFileSync ?? readFileSync;
144
+ const lstat = ctx.lstatSync ?? lstatSync;
145
+ const writeAutonomy = ctx.writeAutonomy ?? writeAutonomyFs;
146
+ try {
147
+ if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
148
+ const { ops, write, json } = parseArgs(argv);
149
+
150
+ // Load the current policy first (loadAutonomy throws fail(1) loud on malformed/unreadable — a write
151
+ // never clobbers an unparseable file; the message points the agent at the parse error to help fix it).
152
+ const { config: current, source } = loadAutonomy(cwd, readFile, lstat);
153
+
154
+ // No ops + no --write → show the current policy + a hint (read-only; nothing changes).
155
+ if (ops.length === 0) {
156
+ if (json) {
157
+ return { code: 0, stdout: JSON.stringify(buildJson({ changed: [], unchanged: [], writtenPath: null, noop: true }), null, 2), stderr: '' };
158
+ }
159
+ const shown = current == null ? `(no ${AUTONOMY_REL} yet — computed defaults apply)` : serializeAutonomy(current).replace(/\n$/, '');
160
+ const hint = `\nPass --set <section>.<key>=<value> (preview) then --write to apply. Sections: redlines, plan-authoring.autonomy, plan-execution.autonomy.`;
161
+ return { code: 0, stdout: `${source === 'none' ? '' : `${AUTONOMY_REL}:\n`}${shown}${hint}`, stderr: '' };
162
+ }
163
+
164
+ const after = applyAutonomyOps(current, ops, { seedReadme: AUTONOMY_README });
165
+ const resolved = ops.map((op) => resolveOp(op, current, after));
166
+ const changed = resolved.filter((e) => e.from !== e.to);
167
+ const unchanged = resolved.filter((e) => e.from === e.to);
168
+ const noop = changed.length === 0;
169
+
170
+ if (!write || noop) {
171
+ const stdout = json
172
+ ? JSON.stringify(buildJson({ changed, unchanged, writtenPath: null, noop }), null, 2)
173
+ : formatHuman({ changed, unchanged, wrote: false });
174
+ return { code: 0, stdout, stderr: '' };
175
+ }
176
+
177
+ validateAutonomy(after); // defensive re-validate immediately before the write
178
+ const { writtenPath } = writeAutonomy(cwd, after, ctx);
179
+ const fileBody = serializeAutonomy(after);
180
+ const stdout = json
181
+ ? JSON.stringify(buildJson({ changed, unchanged, writtenPath, noop: false }), null, 2)
182
+ : formatHuman({ changed, unchanged, wrote: true, fileBody });
183
+ return { code: 0, stdout, stderr: '' };
184
+ } catch (err) {
185
+ return { code: err.exitCode ?? 1, stdout: '', stderr: `set-autonomy: ${err.message}` };
186
+ }
187
+ };
188
+
189
+ const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
190
+ if (isDirectRun) {
191
+ const r = main(process.argv.slice(2));
192
+ if (r.stdout) console.log(r.stdout);
193
+ if (r.stderr) console.error(r.stderr);
194
+ process.exit(r.code);
195
+ }
@@ -99,6 +99,78 @@ const probeMarker = (file, fs) => {
99
99
  }
100
100
  };
101
101
 
102
+ // ── overwrite honesty (D5) — state what a refresh replaced, never a silent wipe ────────────────────
103
+
104
+ // The host-level settings surface a refresh NEVER touches — the one place a bridge tweak survives a
105
+ // kit upgrade (bridge CODE stays kit-owned; only these knobs persist). Named wherever a refresh
106
+ // reports overwriting a local edit, so the recovery is always a copy-paste away.
107
+ const SETTINGS_FILE_HINT = '${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf';
108
+ const SETTINGS_CMD_HINT = '/agent-workflow-kit bridge-settings';
109
+
110
+ // Bundle-owned regular files whose PLACED copy differs from the bundle (a local edit an equal-version
111
+ // refresh would overwrite) or cannot be read (indeterminate — an honest degrade). Mirrors
112
+ // copyTreeRefresh's src dispatch so it flags EXACTLY the files the overwrite touches: a symlink src is
113
+ // additive (copyTreeRefresh skips an existing dest — never overwritten) and a dir recurses; a
114
+ // bundled-only file (no placed copy) is a pure add, no loss. The BUNDLE read is our own shipped
115
+ // artifact — a failure there is a loud corrupt-kit error upstream (never swallowed here); only the
116
+ // PLACED read is caught (EACCES/EIO → 'unreadable', and the refresh still proceeds). Sorted output is
117
+ // deterministic for the stated line. Exported for a direct unit test (the full driver cannot observe a
118
+ // symlinked placed file — copyTreeRefresh refuses to overwrite one, so the refresh fails before any line).
119
+ export const scanBundleOwnedDrift = (bundleDir, skillDir, fs) => {
120
+ const drifted = [];
121
+ const unreadable = [];
122
+ const walk = (rel) => {
123
+ const src = join(bundleDir, rel);
124
+ const dest = join(skillDir, rel);
125
+ const st = fs.lstat(src);
126
+ if (st.isSymbolicLink()) return;
127
+ if (st.isDirectory()) {
128
+ for (const entry of fs.readdir(src)) walk(rel ? join(rel, entry) : entry);
129
+ return;
130
+ }
131
+ // lstat the PLACED path NO-FOLLOW first — never read THROUGH a symlink (copyTreeRefresh's
132
+ // assertContainedRealPath would refuse to overwrite a symlinked dest, so reading its target here
133
+ // would be both unsafe and moot). Absent → a bundled-only addition (no local loss); a symlink /
134
+ // non-regular / unreadable placed node → "could not compare" without a read-through.
135
+ const destStat = (() => {
136
+ try {
137
+ return fs.lstat(dest);
138
+ } catch (err) {
139
+ return err && err.code === 'ENOENT' ? 'absent' : 'error';
140
+ }
141
+ })();
142
+ if (destStat === 'absent') return;
143
+ if (destStat === 'error' || destStat.isSymbolicLink() || !destStat.isFile()) {
144
+ unreadable.push(rel);
145
+ return;
146
+ }
147
+ const srcBytes = fs.readFile(src);
148
+ const destBytes = (() => {
149
+ try {
150
+ return fs.readFile(dest);
151
+ } catch {
152
+ unreadable.push(rel);
153
+ return null;
154
+ }
155
+ })();
156
+ if (destBytes === null) return;
157
+ if (!Buffer.from(srcBytes).equals(Buffer.from(destBytes))) drifted.push(rel);
158
+ };
159
+ walk('');
160
+ return { drifted: drifted.sort(), unreadable: unreadable.sort() };
161
+ };
162
+
163
+ // One user-facing sentence naming what an equal-version re-sync overwrote — or null when nothing local
164
+ // was lost. Callers apply their own indent; the pointer names the settings file that survives a refresh.
165
+ const driftSummary = (drift) => {
166
+ if (!drift) return null;
167
+ const parts = [];
168
+ if (drift.drifted.length) parts.push(`overwrote ${drift.drifted.length} locally-changed file(s): ${drift.drifted.join(', ')}`);
169
+ if (drift.unreadable.length) parts.push(`could not compare ${drift.unreadable.length} file(s) (kept the bundled copy): ${drift.unreadable.join(', ')}`);
170
+ if (!parts.length) return null;
171
+ return `${parts.join('; ')} — bridge code is kit-owned and always refreshed; persist host tweaks in ${SETTINGS_FILE_HINT} (survives every refresh): ${SETTINGS_CMD_HINT}`;
172
+ };
173
+
102
174
  // ── path resolution ──────────────────────────────────────────────────────────
103
175
 
104
176
  const skillDirOf = (entry, deps) =>
@@ -253,14 +325,26 @@ const assertNoDowngrade = (plan, deps = {}) => {
253
325
  if (compareSemver(placed, bundled) === 1) throw stop(downgradeReason(placed, bundled), { skillDir: plan.skillDir, wouldDowngrade: true });
254
326
  };
255
327
 
328
+ // Copy the bundle over the placed skill dir (the refresh overwrite), FIRST scanning bundle-owned files
329
+ // for local drift so the caller can STATE what the overwrite replaced (D5 — overwrite honesty). Scan
330
+ // only on a refresh: a `place` writes into an absent/empty dir, so there is nothing local to lose. The
331
+ // copy proceeds either way — bridge code is kit-owned. Returns the drift report (null for a place).
332
+ const copyBridgeWithHonesty = (action, bundleDir, skillDir, deps) => {
333
+ const fs = fsDeps(deps);
334
+ const drift = action === 'refresh' ? scanBundleOwnedDrift(bundleDir, skillDir, fs) : null;
335
+ copyTreeRefresh(bundleDir, skillDir, skillDir, fs);
336
+ return drift;
337
+ };
338
+
256
339
  // Place/refresh the bundled bridge skill. Re-inspects before writing (never trusts a stale plan).
340
+ // Returns the plan plus `drift`: on a refresh, the bundle-owned files whose local edits it overwrote.
257
341
  export const placeSkill = (name, deps = {}) => {
258
342
  const entry = registryEntry(name);
259
343
  if (!entry) throw stop(`unknown backend: ${name}`);
260
344
  const plan = inspectSkillDir(entry, deps);
261
345
  assertNoDowngrade(plan, deps);
262
- copyTreeRefresh(plan.bundleDir, plan.skillDir, plan.skillDir, fsDeps(deps));
263
- return plan;
346
+ const drift = copyBridgeWithHonesty(plan.action, plan.bundleDir, plan.skillDir, deps);
347
+ return { ...plan, drift };
264
348
  };
265
349
 
266
350
  // Link the wrappers onto `bindir`. PREFLIGHT all (source is a regular non-symlink file inside the
@@ -413,9 +497,12 @@ export const planFor = (backend, deps = {}) => {
413
497
  // in the plan across the read→write gap). Throws on a mid-flight conflict / fs error (honest partial
414
498
  // failure — placeSkill/chmod/symlink all converge on a re-run, so there is nothing to roll back).
415
499
  const applyBackend = (plan, deps) => {
416
- if (plan.place.action === 'place' || plan.place.action === 'refresh') placeSkill(plan.name, deps);
500
+ const placed = (plan.place.action === 'place' || plan.place.action === 'refresh')
501
+ ? placeSkill(plan.name, deps)
502
+ : null;
417
503
  const manifest = readBundledManifest(plan.place.bundleDir, deps);
418
504
  linkWrappers(plan.skillDir, manifest, { ...deps, bindir: plan.bindir, platform: plan.platform });
505
+ return { drift: placed?.drift ?? null };
419
506
  };
420
507
 
421
508
  // ── formatting ─────────────────────────────────────────────────────────────────
@@ -442,6 +529,11 @@ const formatBackend = (plan, applied) => {
442
529
  }
443
530
  const placeVerb = applied ? { place: 'placed', refresh: 'refreshed' } : { place: 'will place', refresh: 'will refresh' };
444
531
  lines.push(` • skill: ${placeVerb[plan.place.action]}${versionLabel(plan)} → ${plan.skillDir}`);
532
+ // Overwrite honesty (D5): on an equal-version re-sync that clobbered a local edit, say so (a version
533
+ // upgrade's diffs are the version delta — versionLabel's arrow already signals that change).
534
+ const equalVersionRefresh = plan.place.action === 'refresh' && (!plan.priorVersion || plan.priorVersion === plan.version);
535
+ const driftLine = equalVersionRefresh ? driftSummary(plan.drift) : null;
536
+ if (driftLine) lines.push(` ↳ ${driftLine}`);
445
537
  for (const l of plan.links) {
446
538
  const verb = l.dstState === 'ours' ? 'already linked' : applied ? 'linked' : 'will link';
447
539
  lines.push(` • wrapper ${l.cmd}: ${verb} → ${l.dst}`);
@@ -463,10 +555,10 @@ const formatBackend = (plan, applied) => {
463
555
  // a per-bridge lock file would be new machinery for a user-driven, self-healing race.
464
556
  const refreshSkillOnly = (entry, deps = {}) => {
465
557
  const fresh = inspectSkillDir(entry, deps);
466
- if (fresh.action !== 'refresh') return false;
558
+ if (fresh.action !== 'refresh') return { refreshed: false, drift: null };
467
559
  assertNoDowngrade(fresh, deps);
468
- copyTreeRefresh(fresh.bundleDir, fresh.skillDir, fresh.skillDir, fsDeps(deps));
469
- return true;
560
+ const drift = copyBridgeWithHonesty('refresh', fresh.bundleDir, fresh.skillDir, deps);
561
+ return { refreshed: true, drift };
470
562
  };
471
563
 
472
564
  const NOT_PLACED_LINE = 'skipped — not placed (placement is opt-in: /agent-workflow-kit setup)';
@@ -499,7 +591,8 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
499
591
  if (plan.outcome === 'stop' || plan.outcome === 'error') {
500
592
  return { name: plan.name, outcome: 'failed', line: ` ${plan.name}: could not refresh — ${stripPrefix(plan.reason)}; recover with /agent-workflow-kit setup` };
501
593
  }
502
- if (!refreshSkillOnly(registryEntry(plan.name), deps)) {
594
+ const refresh = refreshSkillOnly(registryEntry(plan.name), deps);
595
+ if (!refresh.refreshed) {
503
596
  return { name: plan.name, outcome: 'not-placed', line: ` ${plan.name}: ${NOT_PLACED_LINE}` };
504
597
  }
505
598
  const manifest = readBundledManifest(plan.place.bundleDir, deps);
@@ -507,10 +600,14 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
507
600
  const current = plan.version !== null && plan.priorVersion === plan.version;
508
601
  // The equal-version line still states the copy that ran (repair-on-rerun) — the tool never
509
602
  // reports a mutation-free "already current" while it re-synced files underneath.
603
+ const base = ` ${plan.name}: ${current ? `already current${versionLabel(plan)} — files re-synced from the bundled copy` : `refreshed${versionLabel(plan)}`}`;
604
+ // Overwrite honesty (D5): only an EQUAL-version re-sync can prove a byte diff is a LOCAL edit —
605
+ // a version upgrade's diffs are the version delta, which the (vOld → vNew) arrow already states.
606
+ const summary = current ? driftSummary(refresh.drift) : null;
510
607
  return {
511
608
  name: plan.name,
512
609
  outcome: current ? 'already-current' : 'refreshed',
513
- line: ` ${plan.name}: ${current ? `already current${versionLabel(plan)} — files re-synced from the bundled copy` : `refreshed${versionLabel(plan)}`}`,
610
+ line: summary ? `${base}\n ↳ ${summary}` : base,
514
611
  };
515
612
  } catch (err) {
516
613
  // A downgrade STOP raised at the write boundary (a newer bridge landed between plan and apply)
@@ -647,7 +744,8 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
647
744
  let plan = planFor(name, runDeps);
648
745
  if (!args.dryRun && plan.outcome === 'ok') {
649
746
  try {
650
- applyBackend(plan, runDeps);
747
+ const { drift } = applyBackend(plan, runDeps);
748
+ plan = { ...plan, drift };
651
749
  appliedOk = true;
652
750
  } catch (err) {
653
751
  plan = { ...plan, outcome: err.code === SETUP_STOP ? 'stop' : 'error', reason: err.message };