session-orchestrator 4.2.0 → 5.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.
Files changed (129) hide show
  1. package/.agents/skills/session-start/SKILL.md +1 -1
  2. package/.agents/skills/ux-grill/SKILL.md +22 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +3 -2
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  7. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  8. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  9. package/.cursor/commands/ux-grill.md +14 -0
  10. package/.cursor/skills/session-start/SKILL.md +1 -1
  11. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  12. package/.cursor-plugin/plugin.json +1 -1
  13. package/AGENTS.md +2 -1
  14. package/CHANGELOG.md +94 -1
  15. package/README.md +98 -86
  16. package/agents/dialectic-deriver.md +11 -0
  17. package/commands/ux-grill.md +51 -0
  18. package/docs/USER-GUIDE.md +2 -2
  19. package/docs/codex-setup.md +8 -0
  20. package/docs/components.md +7 -7
  21. package/docs/events-schema.md +9 -5
  22. package/docs/github-mirror-protection.md +61 -20
  23. package/docs/migration-v5.md +62 -0
  24. package/docs/scope-collision-guard.md +14 -0
  25. package/hooks/_lib/hook-import-set.json +44 -2
  26. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  27. package/hooks/_lib/vcs-create-matcher.mjs +190 -3
  28. package/hooks/enforce-scope.mjs +201 -0
  29. package/hooks/hooks-codex.json +1 -1
  30. package/hooks/hooks-cursor.json +5 -0
  31. package/hooks/hooks.json +7 -2
  32. package/hooks/on-session-start.mjs +171 -49
  33. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  34. package/hooks/pre-auq-clarity.mjs +70 -18
  35. package/hooks/pre-bash-issue-budget.mjs +51 -4
  36. package/package.json +2 -1
  37. package/pi/prompts/ux-grill.md +12 -0
  38. package/scripts/ci/assert-vitest-green.mjs +4 -2
  39. package/scripts/dialectic-deriver.mjs +32 -8
  40. package/scripts/emit-session.mjs +72 -1
  41. package/scripts/lib/agent-status.mjs +441 -9
  42. package/scripts/lib/auq/schema.mjs +10 -3
  43. package/scripts/lib/ci-status-banner.mjs +29 -6
  44. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  45. package/scripts/lib/config.mjs +12 -1
  46. package/scripts/lib/eval/engine.mjs +7 -1
  47. package/scripts/lib/file-lock.mjs +114 -13
  48. package/scripts/lib/git-porcelain.mjs +113 -0
  49. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  50. package/scripts/lib/io.mjs +29 -4
  51. package/scripts/lib/issue-budget.mjs +336 -6
  52. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  53. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  54. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  55. package/scripts/lib/maintenance-due-banner.mjs +11 -1
  56. package/scripts/lib/peer-cards/merger.mjs +143 -0
  57. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  58. package/scripts/lib/project-hygiene.mjs +81 -30
  59. package/scripts/lib/quality-gate.mjs +14 -65
  60. package/scripts/lib/reconcile/engine.mjs +19 -1
  61. package/scripts/lib/reconcile/writer.mjs +278 -11
  62. package/scripts/lib/scope-echo.mjs +346 -0
  63. package/scripts/lib/session-lock.mjs +62 -2
  64. package/scripts/lib/session-record-repair.mjs +91 -0
  65. package/scripts/lib/session-schema/filters.mjs +26 -1
  66. package/scripts/lib/session-start-probes.mjs +419 -53
  67. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  68. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  69. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  70. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  71. package/scripts/lib/ux-grill/compare.mjs +285 -0
  72. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  73. package/scripts/lib/ux-grill/measures.mjs +431 -0
  74. package/scripts/lib/ux-grill/paths.mjs +224 -0
  75. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  76. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  77. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  78. package/scripts/lib/ux-grill/schema.mjs +321 -0
  79. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  80. package/scripts/lib/validate/check-unwired-features.mjs +48 -20
  81. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  82. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  83. package/scripts/mcp-server.sh +16 -1
  84. package/scripts/release.mjs +7 -2
  85. package/skills/bootstrap/SKILL.md +12 -209
  86. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  87. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  88. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  89. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  90. package/skills/claude-md-drift-check/SKILL.md +9 -2
  91. package/skills/claude-md-drift-check/checker.mjs +213 -21
  92. package/skills/discovery/SKILL.md +6 -173
  93. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  94. package/skills/discovery/probes-docs.md +8 -4
  95. package/skills/discovery/probes-supply-chain.md +4 -2
  96. package/skills/discovery/probes-ui.md +7 -3
  97. package/skills/discovery/probes-vault.md +12 -4
  98. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  99. package/skills/discovery/references/discovery-triage-state.md +54 -0
  100. package/skills/eval/rubric-v1.md +13 -0
  101. package/skills/evolve/SKILL.md +2 -458
  102. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  103. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  104. package/skills/plan/mode-retro.md +4 -3
  105. package/skills/reconcile/SKILL.md +10 -0
  106. package/skills/session-end/drift-operations.md +20 -5
  107. package/skills/session-end/metrics-collection.md +1 -0
  108. package/skills/session-end/phase-3-6-tail.md +4 -2
  109. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  110. package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
  111. package/skills/session-end/session-metrics-write.md +2 -0
  112. package/skills/session-plan/SKILL.md +2 -144
  113. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  114. package/skills/session-start/SKILL.md +24 -6
  115. package/skills/session-start/references/operations-contract.md +114 -0
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
  117. package/skills/session-start/soul.md +2 -2
  118. package/skills/test-runner/SKILL.md +1 -1
  119. package/skills/tmux-layout/SKILL.md +3 -1
  120. package/skills/ux-grill/SKILL.md +211 -0
  121. package/skills/ux-grill/rubric-v2.md +201 -0
  122. package/skills/ux-grill/soul.md +76 -0
  123. package/skills/wave-executor/SKILL.md +3 -128
  124. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  125. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  126. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  127. package/skills/wave-executor/references/wave-loop-review.md +18 -5
  128. package/templates/_shared/ux-manifest.template.md +149 -0
  129. package/templates/_shared/journey-manifest.md +0 -114
@@ -629,28 +629,42 @@ function importClosure(repoRoot, entry, readSource) {
629
629
  */
630
630
  function checkIgnoreBatch(repoRoot, specs, out) {
631
631
  if (specs.length === 0) return;
632
- const ci = spawnSync('git', ['check-ignore', '--stdin'], {
633
- cwd: repoRoot, input: specs.join('\n'), encoding: 'utf8',
634
- maxBuffer: 64 * 1024 * 1024,
635
- });
636
- // 0 = at least one ignored (listed on stdout); 1 = none ignored; else fatal.
637
- if (ci.status === 0) {
638
- for (const line of (ci.stdout || '').split('\n')) {
639
- const p = line.trim();
640
- if (p) out.add(p);
632
+ let pending = specs;
633
+ for (;;) {
634
+ const ci = spawnSync('git', ['check-ignore', '--stdin'], {
635
+ cwd: repoRoot, input: pending.join('\n'), encoding: 'utf8',
636
+ maxBuffer: 64 * 1024 * 1024,
637
+ });
638
+ // 0 = at least one ignored (listed on stdout); 1 = none ignored; else fatal.
639
+ if (ci.status === 0) {
640
+ for (const line of (ci.stdout || '').split('\n')) {
641
+ const p = line.trim();
642
+ if (p) out.add(p);
643
+ }
644
+ return;
641
645
  }
646
+ if (ci.status === 1) return;
647
+ // Git identifies this refused candidate precisely. Retrying only the rest
648
+ // preserves the old dropped-candidate semantics without bisecting healthy
649
+ // paths (78 subprocesses for five symlink descendants in the tracked tree).
650
+ const rejected = /^fatal: pathspec '([^'\n]+)' is beyond a symbolic link\r?\n?$/.exec(ci.stderr || '')?.[1];
651
+ if (rejected && pending.includes(rejected)) {
652
+ process.stderr.write(` WARN: git check-ignore rejected "${rejected}" — candidate dropped (${(ci.stderr || '').trim()})\n`);
653
+ pending = pending.filter((spec) => spec !== rejected);
654
+ if (pending.length === 0) return;
655
+ continue;
656
+ }
657
+ if (pending.length === 1) {
658
+ process.stderr.write(
659
+ ` WARN: git check-ignore rejected "${pending[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
660
+ );
661
+ return;
662
+ }
663
+ const mid = pending.length >> 1;
664
+ checkIgnoreBatch(repoRoot, pending.slice(0, mid), out);
665
+ checkIgnoreBatch(repoRoot, pending.slice(mid), out);
642
666
  return;
643
667
  }
644
- if (ci.status === 1) return;
645
- if (specs.length === 1) {
646
- process.stderr.write(
647
- ` WARN: git check-ignore rejected "${specs[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
648
- );
649
- return;
650
- }
651
- const mid = specs.length >> 1;
652
- checkIgnoreBatch(repoRoot, specs.slice(0, mid), out);
653
- checkIgnoreBatch(repoRoot, specs.slice(mid), out);
654
668
  }
655
669
 
656
670
  /**
@@ -795,9 +795,18 @@ function mentionedModuleTokens(lines) {
795
795
  * switched off. Revisit if a real module-resolver (import-specifier resolution
796
796
  * relative to the importing file) becomes cheap, or if a collided basename is
797
797
  * ever confirmed to mask a true positive. The `coordinator-invoked-module`
798
- * DOWNGRADE is exempt: there a colliding basename must be named with its
799
- * `dirname/base` suffix, because that match moves a module OUT of the
800
- * reportable class and would otherwise hide a true unreachable sibling.
798
+ * DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter: in both
799
+ * a colliding basename must be named with its `dirname/base` suffix, because
800
+ * those matches move a module OUT of the reportable class and would otherwise
801
+ * hide a true unreachable sibling. Measured cost of leaving the root filter
802
+ * on bare basenames: `locks/index.mjs` and `worktree/index.mjs` were both
803
+ * suppressed by a third unreachable module that merely mentioned bare
804
+ * `index.mjs`, and only resurfaced when that module was deleted for an
805
+ * unrelated reason. Remaining ceiling: two ambiguous roots whose mentioning
806
+ * module ALSO carries the qualified form (e.g. an unreachable module that
807
+ * literally writes `locks/index.mjs`) are still suppressed — correct when it
808
+ * is a real reference, a mask when it is prose. Revisit if a qualified
809
+ * mention is ever confirmed to hide a root.
801
810
  * - **Reachable ≠ executed.** A module imported by a hook that never takes that
802
811
  * branch reads as wired here. Proving execution needs coverage data, not a graph.
803
812
  * - **Reachable from SOME entrypoint is not reachable from the PROMISED one.**
@@ -840,6 +849,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
840
849
  // module an operator flagged. Measured 2026-08-28 on the first S4
841
850
  // allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
842
851
  mentions: relative === SELF_REL ? new Set() : mentionedModuleTokens(lines),
852
+ // Raw text, kept for the QUALIFIED (`dirname/base`) re-check in the
853
+ // root filter below: `mentionedModuleTokens` strips the directory, so
854
+ // a colliding basename can only be disambiguated against the body.
855
+ rawBody: relative === SELF_REL ? '' : body,
843
856
  };
844
857
  });
845
858
 
@@ -877,10 +890,38 @@ export function collectUnreachableLibraryModules(pluginRoot) {
877
890
  (module) => !reachable.has(module.relative) && !module.entrypoint && module.exports.length > 0,
878
891
  );
879
892
  const unreachableSet = new Set(unreachable.map((module) => module.relative));
880
- const roots = unreachable.filter(
881
- (module) =>
882
- !unreachable.some((other) => other.relative !== module.relative && other.mentions.has(module.base)),
883
- );
893
+ // Basename census, shared by the root filter below and the downgrade half
894
+ // further down. A bare basename is only a valid module reference when it is
895
+ // UNIQUE in the corpus: `writer.mjs` names both `peer-cards/writer.mjs` and
896
+ // `reconcile/writer.mjs` (measured 2026-09-07), so a doc naming ONE of them
897
+ // would otherwise downgrade BOTH out of the reportable class — a true
898
+ // unreachable silently moved into the advisory half. For a colliding basename
899
+ // the reference must therefore carry at least the `dirname/base` suffix
900
+ // (`reconcile/writer.mjs`); unique basenames keep the cheaper bare match.
901
+ // Direction matters in both consumers: this can only ever ADD findings back to
902
+ // the reportable class, never remove one.
903
+ /** @type {Map<string, number>} */
904
+ const basenameCount = new Map();
905
+ for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
906
+
907
+ const roots = unreachable.filter((module) => {
908
+ // A bare-basename mention only suppresses when the basename is UNIQUE
909
+ // (see the census above). When it collides, the mentioning module must name
910
+ // the `dirname/base` form in its body — otherwise ONE unreachable module
911
+ // mentioning bare `index.mjs` masks EVERY differently-pathed `index.mjs`
912
+ // root at once (#1293: `locks/index.mjs` + `worktree/index.mjs` were masked
913
+ // by a third unreachable module until that module was deleted for an
914
+ // unrelated reason).
915
+ const ambiguous = (basenameCount.get(module.base) ?? 0) > 1;
916
+ const qualified = module.relative.split(path.sep).slice(-2).join('/');
917
+ const qualifiedRe = ambiguous ? tokenMatcher(qualified) : null;
918
+ return !unreachable.some(
919
+ (other) =>
920
+ other.relative !== module.relative &&
921
+ other.mentions.has(module.base) &&
922
+ (qualifiedRe === null || qualifiedRe.test(other.rawBody)),
923
+ );
924
+ });
884
925
 
885
926
  // Category split (see § Category split in the doc block above): an INSTRUCTION
886
927
  // document that names both the module AND one of its exported symbols is an
@@ -894,19 +935,6 @@ export function collectUnreachableLibraryModules(pluginRoot) {
894
935
  .sort()
895
936
  .map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
896
937
 
897
- // Basename census for the downgrade half. A bare basename is only a valid
898
- // module reference when it is UNIQUE in the corpus: `writer.mjs` names both
899
- // `peer-cards/writer.mjs` and `reconcile/writer.mjs` (measured 2026-09-07),
900
- // so a doc naming ONE of them would otherwise downgrade BOTH out of the
901
- // reportable class — a true unreachable silently moved into the advisory
902
- // half. For a colliding basename the doc must therefore carry at least the
903
- // `dirname/base` suffix (`reconcile/writer.mjs`); unique basenames keep the
904
- // cheaper bare match. Direction matters: this can only ever ADD findings back
905
- // to the reportable class, never remove one.
906
- /** @type {Map<string, number>} */
907
- const basenameCount = new Map();
908
- for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
909
-
910
938
  let coordinatorInvoked = 0;
911
939
  const findings = roots.map((module) => {
912
940
  // Docs write POSIX separators regardless of host; `path.relative` does not.
@@ -60,6 +60,15 @@ const DEFAULT_POLL_MS = 50;
60
60
  * REVISIT when a board sweep is measured above 30 s (half the TTL — the point
61
61
  * at which a slow host crosses it), or when an `onLockOutcome` carrying
62
62
  * `staleOverride` is observed in the events ledger on a host that had no crash.
63
+ *
64
+ * What expiry does NOT do: protect the section it bounds — past 60 s the next
65
+ * writer takes over while this one may still run (see file-lock.mjs § Lease
66
+ * semantics). What IS guaranteed since #1285: the late writer's release can no
67
+ * longer delete the successor's lock, because release runs under the same
68
+ * `.acquire` guard as takeover. A `not-owner` release here therefore means THIS
69
+ * writer's lease expired mid-section — the lost-update case above, never a
70
+ * benign miss. Since #1336 that release result reaches callers through
71
+ * `onReleaseOutcome` (see withBoardLock).
63
72
  */
64
73
  const DEFAULT_STALE_MS = 60_000;
65
74
 
@@ -105,6 +114,11 @@ export function boardLockPathFor(vaultDir) {
105
114
  * is present only when this acquire OVERRODE an aged lock, and carries
106
115
  * `file-lock.mjs`'s own reason token — the observable behind the
107
116
  * DEFAULT_STALE_MS revisit trigger.
117
+ * @param {(release: { ok: boolean, reason?: string }) => void} [opts.onReleaseOutcome]
118
+ * — diagnostic sink for the release result, called at most once, AFTER `fn`
119
+ * (only on the locked path). `{ ok: false, reason: 'not-owner' }` means this
120
+ * writer's lease expired mid-section; `'busy'` means the release gave up on
121
+ * the `.acquire` guard and left the lock in place.
108
122
  * @param {(lockPath: string, fn: Function, opts: object) => Promise<object>} [opts.lockImpl]
109
123
  * — test seam; defaults to {@link withFileLock}. Must honour the same
110
124
  * `{ ok: true, value } | { ok: false, reason }` contract.
@@ -123,6 +137,7 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
123
137
  staleMs = DEFAULT_STALE_MS,
124
138
  holder: holderOpt,
125
139
  onLockOutcome,
140
+ onReleaseOutcome,
126
141
  lockImpl = withFileLock,
127
142
  warn = (msg) => process.stderr.write(msg),
128
143
  } = opts;
@@ -167,6 +182,9 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
167
182
  indent: 2,
168
183
  tmpPrefix: '.board.lock',
169
184
  warn: warnAndWatch,
185
+ // Separate sink, not a second onLockOutcome call — that one stays
186
+ // "exactly once, before fn".
187
+ ...(typeof onReleaseOutcome === 'function' ? { onRelease: onReleaseOutcome } : {}),
170
188
  },
171
189
  );
172
190
 
@@ -1095,6 +1095,14 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
1095
1095
  waited_ms: Date.now() - acquireStartedAt,
1096
1096
  };
1097
1097
  },
1098
+ // #1336: the release runs in withFileLock's finally, i.e. before
1099
+ // withBoardLock returns — so this lands on the SAME board_written event,
1100
+ // which mirrorBoard emits only after this function returns.
1101
+ onReleaseOutcome: (r) => {
1102
+ if (lockOutcome && r && r.ok === false && typeof r.reason === 'string') {
1103
+ lockOutcome.release = r.reason;
1104
+ }
1105
+ },
1098
1106
  });
1099
1107
 
1100
1108
  return lockOutcome === undefined ? inner : { ...inner, lock: lockOutcome };
@@ -194,8 +194,20 @@ tool_session_metrics() {
194
194
  local entries
195
195
  entries=$(jq -R -c 'fromjson? | select(.status != "abandoned")' "$metrics_file" 2>/dev/null | tail -n 5) || true
196
196
 
197
+ # Report the stubs the filter above dropped (#1296) — counted over the WHOLE
198
+ # file, not the tail, with the same torn-line tolerance (`fromjson?`, plus
199
+ # `.status?` so a non-object line cannot abort the count either). Computed
200
+ # BEFORE the empty check: a stubs-only ledger is not an empty file.
201
+ local stub_count
202
+ stub_count=$(jq -R -n '[inputs | fromjson? | select(.status? == "abandoned")] | length' "$metrics_file" 2>/dev/null) || stub_count=0
203
+ [[ -z "$stub_count" ]] && stub_count=0
204
+
197
205
  if [[ -z "$entries" ]]; then
198
- respond "$id" "$(text_content "No metrics found (file is empty)")"
206
+ if [[ "$stub_count" -gt 0 ]]; then
207
+ respond "$id" "$(text_content "No real sessions (abandoned stubs excluded: ${stub_count})")"
208
+ else
209
+ respond "$id" "$(text_content "No metrics found (file is empty)")"
210
+ fi
199
211
  return
200
212
  fi
201
213
 
@@ -229,6 +241,9 @@ tool_session_metrics() {
229
241
  ' 2>/dev/null) || token_summary=""
230
242
 
231
243
  local output
244
+ entries="${entries}
245
+
246
+ abandoned stubs excluded: ${stub_count}"
232
247
  if [[ -n "$token_summary" ]]; then
233
248
  output="${entries}
234
249
 
@@ -378,8 +378,13 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
378
378
  // (same 2026-09-07 sweep): site/llms-full.txt ("The v4.0.0 release REMOVES public surfaces" — its
379
379
  // version SURFACE is checked separately by the SURFACES row, so the sweep on it is redundant),
380
380
  // skills/architecture/references/domain-model.md ("Merged here in v4.0.0"), skills/autopilot/SKILL.md
381
- // ("4.0.0 — see docs/migration-v4.md") and templates/_shared/journey-manifest.md ("Retired … in 4.0.0").
382
- export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|templates\/_shared\/journey-manifest\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|commands\/release\.md)/;
381
+ // ("4.0.0 — see docs/migration-v4.md"). A fifth, templates/_shared/journey-manifest.md ("Retired …
382
+ // in 4.0.0"), was DELETED on 2026-09-12 (replaced by ux-manifest.template.md, Epic #1322) and its
383
+ // allowlist entry went with it — re-add only if a file of that name returns.
384
+ // September 10 campaign snapshot: inputs and receipt reproduce the dated 4.3-planned
385
+ // render made while 4.2.0 was current. Exact files only; neighboring marketing
386
+ // remains checked. Do not bump receipt props without a newly reviewed render.
387
+ export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|commands\/release\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
383
388
 
384
389
  /** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
385
390
  export function checkLeakage(lines) {
@@ -180,136 +180,21 @@ Entered when `$ARGUMENTS` contains `--upgrade <tier>`. No scaffolding questions
180
180
 
181
181
  ## Retroactive Flow (`--retroactive`)
182
182
 
183
- Entered when `$ARGUMENTS` contains `--retroactive`. Writes the lock file and, per #182, optionally patches missing mandatory Session Config fields with defaults.
183
+ Adopts an existing repo that already has `CLAUDE.md`/`AGENTS.md` + `## Session Config` but no `bootstrap.lock` infers tier from file inventory and patches missing mandatory Session Config fields with defaults.
184
184
 
185
- **Purpose:** Adopt an existing repo that already has `CLAUDE.md` + `## Session Config` but was bootstrapped manually (no `bootstrap.lock`). Writes the lock so the gate passes on all future invocations, and ensures the Session Config block satisfies the validated schema defined in `scripts/lib/config-schema.mjs`.
185
+ See [references/bootstrap-retroactive-flow.md](references/bootstrap-retroactive-flow.md).
186
186
 
187
- **Steps:**
188
-
189
- 1. **Verify preconditions.** Confirm `CLAUDE.md` (or `AGENTS.md`) exists and contains `## Session Config`. If not, abort: `Error: CLAUDE.md with Session Config required for retroactive bootstrap.`
190
-
191
- 2. **Check lock not already present.** If `.orchestrator/bootstrap.lock` already exists and has valid `version` + `tier` fields, report: `bootstrap.lock already present (tier: <tier>). Nothing to do.` and exit 0 (idempotent).
192
-
193
- 3. **Infer tier from file inventory.** Examine the repo root:
194
-
195
- | Condition (evaluated in order) | Inferred Tier |
196
- |---|---|
197
- | CI file present (`.gitlab-ci.yml` OR `.github/workflows/`) AND `CHANGELOG.md` present | `deep` |
198
- | Package manifest present (`package.json` OR `pyproject.toml`) | `standard` |
199
- | Neither of the above | `fast` |
200
-
201
- Store as `INFERRED_TIER`.
202
-
203
- 4. **Infer archetype.** Run Phase 0.5's read-only source detection. For a private
204
- contract, use its detected `selected.id`; retain `null` with an explicit
205
- `insufficient-evidence` report if no markers match. An invalid configured
206
- contract aborts. Do not scaffold or apply rules in this retroactive flow.
207
- For the public path, use best-effort detection from existing files:
208
- - `pyproject.toml` present → `python-uv`
209
- - `package.json` with `next` in dependencies → `nextjs-minimal`
210
- - `package.json` without `next` → `node-minimal`
211
- - No manifest → `null`
212
-
213
- Store as `INFERRED_ARCHETYPE`.
214
-
215
- 5. **Write bootstrap.lock.** Create `.orchestrator/` if needed, then write:
216
- ```yaml
217
- # .orchestrator/bootstrap.lock
218
- version: 1
219
- tier: <INFERRED_TIER>
220
- archetype: <INFERRED_ARCHETYPE or null>
221
- timestamp: <current ISO 8601 UTC>
222
- source: retroactive
223
- plugin-version: <current plugin version from $PLUGIN_ROOT/package.json>
224
- ```
225
-
226
- 6. **Patch Session Config (#182).** Run the validator against the current `## Session Config` block; append any missing mandatory fields with defaults. The 7 mandatory fields (per `scripts/lib/config-schema.mjs`) are: `test-command`, `typecheck-command`, `lint-command`, `agents-per-wave`, `waves`, `persistence`, `enforcement`.
227
-
228
- ```bash
229
- CONFIG_OUT="$(node "$PLUGIN_ROOT/scripts/parse-config.mjs" 2>&1 >/dev/null)"
230
- # parse-config.mjs emits validation warnings to stderr when enforcement=warn.
231
- # Grep for 'must be' lines (issued by validate-config.mjs) to detect missing fields.
232
- MISSING_FIELDS="$(echo "$CONFIG_OUT" | grep -oE '(test-command|typecheck-command|lint-command|agents-per-wave|waves|persistence|enforcement)' | sort -u || true)"
233
- if [[ -n "$MISSING_FIELDS" ]]; then
234
- # Detect package manager to pick sensible defaults for commands.
235
- PM_DEFAULTS="$(node --input-type=module -e "
236
- import {detectPackageManager, defaultQualityGateCommands} from '$PLUGIN_ROOT/scripts/lib/package-manager.mjs';
237
- const pm = detectPackageManager(process.cwd());
238
- const cmds = defaultQualityGateCommands(pm);
239
- console.log('test-command: ' + cmds.test.command);
240
- console.log('typecheck-command: ' + cmds.typecheck.command);
241
- console.log('lint-command: ' + cmds.lint.command);
242
- " 2>/dev/null)"
243
-
244
- CONFIG_FILE="CLAUDE.md"
245
- [[ -f "AGENTS.md" ]] && CONFIG_FILE="AGENTS.md"
246
-
247
- # Append each missing field under the ## Session Config block.
248
- for field in $MISSING_FIELDS; do
249
- case "$field" in
250
- test-command|typecheck-command|lint-command)
251
- default_line="$(echo "$PM_DEFAULTS" | grep "^$field:")" ;;
252
- agents-per-wave) default_line="agents-per-wave: 6" ;;
253
- waves) default_line="waves: 5" ;;
254
- persistence) default_line="persistence: true" ;;
255
- enforcement) default_line="enforcement: warn" ;;
256
- esac
257
- # Insert after `## Session Config` line if not already present.
258
- grep -q "^$field:" "$CONFIG_FILE" \
259
- || awk -v insert="$default_line" '/^## Session Config/ && !done { print; print ""; print insert; done=1; next } { print }' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" \
260
- && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
261
- done
262
- echo "Patched $CONFIG_FILE with defaults for: $MISSING_FIELDS"
263
- fi
264
- ```
265
-
266
- This patch is best-effort: existing fields are never overwritten. If no fields are missing, this step is a no-op.
267
-
268
- 7. **Commit.** Stage the lock file (and the patched config file, if it changed) and commit:
269
- ```bash
270
- mkdir -p .orchestrator
271
- git add .orchestrator/bootstrap.lock
272
- # Also stage CLAUDE.md/AGENTS.md if step 6 patched it.
273
- git diff --name-only --cached CLAUDE.md AGENTS.md 2>/dev/null | head -1 >/dev/null || {
274
- [[ -f CLAUDE.md ]] && git diff --quiet CLAUDE.md || git add CLAUDE.md
275
- [[ -f AGENTS.md ]] && git diff --quiet AGENTS.md || git add AGENTS.md
276
- }
277
- git commit -m "chore: bootstrap lock (retroactive)"
278
- ```
279
-
280
- 8. **Report.** Print: `Retroactive bootstrap complete. Lock written (tier: <INFERRED_TIER>, source: retroactive).` Include a second line `Patched Session Config: <fields>` when step 6 applied any patches, otherwise `No config changes.`.
187
+ **Read WHEN:** `$ARGUMENTS` contains `--retroactive`.
281
188
 
282
189
  ---
283
190
 
284
191
  ## Refresh-Lock Flow (`--refresh-lock`)
285
192
 
286
- Entered when `$ARGUMENTS` contains `--refresh-lock`. No scaffolding questions are asked, and unlike the Retroactive Flow above this is NOT a no-op once the lock already has valid `version`/`tier` fields: refreshing is the load-bearing action.
287
-
288
- **Purpose (#57):** Acknowledge the current plugin version and reset the freshness clock on an existing, already-valid `bootstrap.lock` without disturbing its original bootstrap provenance. This closes the gap left by the Retroactive Flow: once a lock already has `version` + `tier`, re-running `/bootstrap --retroactive` reports "bootstrap.lock already present ... Nothing to do." and changes nothing — exactly the no-op the bootstrap-lock-freshness probe (#186/#290) was recommending as its remediation. `--refresh-lock` is the actual remediation for a present-but-stale or version-drifted lock.
193
+ Acknowledges the current plugin version and resets the freshness clock on an already-valid `bootstrap.lock` (`refreshed-at` + `refreshed-plugin-version`) without disturbing its original bootstrap provenance or re-running scaffolding.
289
194
 
290
- **Steps:**
291
-
292
- 1. **Precondition check.** Read `.orchestrator/bootstrap.lock`. If missing, or present but missing a non-empty `version` or `tier` field, abort with: `Error: No valid bootstrap.lock found. Run /bootstrap or /bootstrap --retroactive first.` Do not fabricate a lock — this flow only refreshes an existing one.
293
-
294
- 2. **Resolve the current plugin version.** Read `plugin-version` from `$PLUGIN_ROOT/package.json` (same source Phase 4 uses).
295
-
296
- 3. **Call the refresh writer.**
297
-
298
- ```js
299
- import { refreshBootstrapLock } from '$PLUGIN_ROOT/scripts/lib/bootstrap-lock-refresh.mjs';
300
- const result = refreshBootstrapLock({
301
- repoRoot: REPO_ROOT,
302
- currentPluginVersion: PLUGIN_VERSION,
303
- });
304
- ```
305
-
306
- `refreshBootstrapLock` writes (or replaces, if already present) exactly two lines — `refreshed-at: <ISO 8601 UTC>` and `refreshed-plugin-version: <current plugin version>` — via the same atomic tmp-file + rename pattern used by the Retroactive Flow's lock write: write to a sibling tmp file, then rename over the target so the lock is never observed half-written. **Every other line of the lock — `bootstrapped-at`, `timestamp`, `plugin-version`, `tier`, `archetype`, `source`, … — is left byte-identical.** This is the provenance-honesty guarantee: a refresh is an acknowledgement, not a re-bootstrap. On failure (`result.ok === false`), surface `result.message` and stop — do not retry with a fabricated lock.
195
+ See [references/bootstrap-refresh-lock-flow.md](references/bootstrap-refresh-lock-flow.md).
307
196
 
308
- 4. **No auto-commit.** Unlike the Retroactive Flow, `--refresh-lock` does not stage or commit. The refreshed lock is a small, reviewable diff (two changed/added lines); the user commits it alongside their own work at their own cadence.
309
-
310
- 5. **Report.** Print: `Lock refreshed (refreshed-at: <now>, plugin-version: <current>). Original bootstrap provenance unchanged.`
311
-
312
- **Idempotency.** Running `/bootstrap --refresh-lock` twice in a row replaces the same two lines in place — it never duplicates them.
197
+ **Read WHEN:** `$ARGUMENTS` contains `--refresh-lock`.
313
198
 
314
199
  ---
315
200
 
@@ -477,103 +362,21 @@ if (!isDispatcherAutonomyBlockPresent(content)) {
477
362
 
478
363
  ## Phase 3.6: (Optional) Rules-Fetch Bridge
479
364
 
480
- > Closes session-orchestrator issue #110.
481
-
482
- After scaffolding, the Standard and Deep templates execute S99. On the private
483
- path, it applies the selected contract's local rule union, rechecks conditional
484
- dependencies and preserves existing files. It excludes every plugin-owned
485
- basename and aborts on an invalid configured contract; see `private-contract.md`.
486
-
487
- On the public path, S99 retains the optional remote rules-fetch step. It pulls
488
- canonical `.claude/rules/*.md` directly from the configured baseline GitLab
489
- project, excluding all plugin-owned basenames. The remote step only fires when:
490
-
491
- - `baseline-ref` is present in Session Config
492
- - `GITLAB_TOKEN` env var is set
493
- - `scripts/lib/fetch-baseline.mjs` is present in the plugin
494
- - A GitLab host is resolvable from the `gitlab-host` Session Config key (or the `GITLAB_HOST` env var) — never a hardcoded default
495
-
496
- When triggered, the step:
365
+ Pulls canonical `.claude/rules/*.md` from the configured baseline GitLab project on the public path (or applies the private contract's local rule union), writes `.claude/.baseline-fetch.lock`, and falls back to the legacy Clank sync flow on any fetch failure.
497
366
 
498
- 1. Loops over a default rule manifest, invoking `node scripts/lib/fetch-baseline.mjs <project_id> <file_path> <baseline-ref>` once per rule. The CLI prints one file body to stdout (exit 0 success; 1 auth, 2 not-found, 3 network) — bootstrap redirects stdout to the target path and skips failures so a single 404 cannot abort the batch.
499
- 2. Fetches each rule listed in the default manifest from the configured `baseline-project-id` (default `52`) at the configured `baseline-ref`
500
- 3. Writes `.claude/.baseline-fetch.lock` (via an inline `node --input-type=module -e`) recording what was fetched
501
- 4. Populates `.claude/.baseline-cache/` for offline fallback on subsequent invocations
367
+ See [references/bootstrap-rules-fetch-bridge.md](references/bootstrap-rules-fetch-bridge.md).
502
368
 
503
- When the fetch fails (network error, auth, missing file), bootstrap **does not abort**. Rules will arrive in the repo via Clank's weekly baseline sync MRs (the legacy path). A warning is printed.
504
-
505
- **Why opt-in:** Repos without `baseline-ref` continue to receive rules via the existing Clank sync flow. The fetch bridge is a faster on-demand alternative for newly-bootstrapped repos that want current rules immediately.
506
-
507
- **Local edits:** Re-running bootstrap with `baseline-ref` set will overwrite `.claude/rules/*.md` (rules are canonical). Repo-specific extensions belong in `.claude/rules/local/*.md` (not fetched, not overwritten).
508
-
509
- See `standard-template.md` (Step S99) and `deep-template.md` (Step D99) for the implementation, and `docs/session-config-reference.md` for the `baseline-ref` and `baseline-project-id` field definitions.
510
-
511
- ### `.claude/.baseline-fetch.lock` Schema
512
-
513
- The lock file is committed to git and records what was fetched.
514
-
515
- ```yaml
516
- # .claude/.baseline-fetch.lock
517
- version: 1
518
- project_id: 52
519
- baseline_ref: main
520
- fetched_at: 2026-04-17T13:42:00Z # ISO 8601 UTC
521
- files:
522
- - .claude/rules/development.md
523
- - .claude/rules/security.md
524
- - .claude/rules/...
525
- ```
526
-
527
- | Field | Description |
528
- |---|---|
529
- | `version` | Lock file schema version. Currently `1`. |
530
- | `project_id` | GitLab project ID the files were fetched from. |
531
- | `baseline_ref` | The git ref (branch/tag/SHA) at fetch time. |
532
- | `fetched_at` | ISO 8601 UTC timestamp. |
533
- | `files` | List of fetched file paths (relative to repo root). |
369
+ **Read WHEN:** Phase 3 (Dispatch to Template) reaches step S99/D99, or when investigating `.claude/.baseline-fetch.lock` contents.
534
370
 
535
371
  ---
536
372
 
537
373
  ## Ecosystem-Health Flow (`--ecosystem-health`)
538
374
 
539
- Entered when `$ARGUMENTS` contains `--ecosystem-health`. This is a **standalone flow** — it does not scaffold repo structure and does not write `bootstrap.lock`. Dispatch immediately; do not proceed to Phase 1.
540
-
541
- **Purpose:** Populate the `health-endpoints`, `pipelines`, and `criticalIssueLabels` configuration consumed by `skills/ecosystem-health/SKILL.md`. Runs the interactive wizard in `scripts/lib/ecosystem-wizard.mjs`, which detects CI provider + package manager automatically and prompts the user for the remaining values.
542
-
543
- **Steps:**
544
-
545
- 1. **Run the wizard.**
546
-
547
- ```bash
548
- node "$PLUGIN_ROOT/scripts/lib/ecosystem-wizard.mjs" --repo-root "$(pwd)"
549
- ```
550
-
551
- The wizard will:
552
- - Detect CI provider (`.gitlab-ci.yml` → `gitlab`; `.github/workflows/` → `github`; else `none`)
553
- - Detect package manager from lockfile
554
- - Prompt for health endpoints (format: `Name|URL`, comma-separated)
555
- - Prompt for CI pipeline identifiers (format: `id` or `id:label`, comma-separated)
556
- - Prompt for critical issue labels (comma-separated strings)
557
-
558
- 2. **Wizard writes two files** (or skips each if already present):
559
- - `CLAUDE.md` (or `AGENTS.md`) — appends `ecosystem-health:` block inside `## Session Config`
560
- - `.orchestrator/policy/ecosystem.json` — full policy file (schema: `.orchestrator/policy/ecosystem.schema.json`)
561
-
562
- 3. **No auto-commit.** The wizard prints what it wrote. The user reviews with `git status && git diff` and commits manually.
563
-
564
- **Report:** The wizard prints a one-line summary per file:
565
-
566
- ```
567
- Ecosystem-Health Wizard complete.
568
- Written: .orchestrator/policy/ecosystem.json, CLAUDE.md
569
- Skipped (already present): (none)
570
-
571
- Review changes with: git status && git diff
572
- ```
375
+ A **standalone flow** — does not scaffold repo structure or write `bootstrap.lock`. Walks the ecosystem-health wizard and writes `.orchestrator/policy/ecosystem.json`.
573
376
 
574
- **Idempotency:** Safe to re-run. If both output files are already present with matching content, the wizard exits 0 with "Nothing to do." To update, remove the existing `ecosystem-health:` key from Session Config and delete `.orchestrator/policy/ecosystem.json`, then re-run.
377
+ See [references/bootstrap-ecosystem-health-flow.md](references/bootstrap-ecosystem-health-flow.md).
575
378
 
576
- See `skills/ecosystem-health/wizard.md` for the full prompt spec and schema details.
379
+ **Read WHEN:** `$ARGUMENTS` contains `--ecosystem-health`.
577
380
 
578
381
  ---
579
382
 
@@ -0,0 +1,48 @@
1
+ # Bootstrap — Ecosystem-Health Flow (`--ecosystem-health`)
2
+
3
+ > Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
5
+
6
+ ## Ecosystem-Health Flow (`--ecosystem-health`)
7
+
8
+ Entered when `$ARGUMENTS` contains `--ecosystem-health`. This is a **standalone flow** — it does not scaffold repo structure and does not write `bootstrap.lock`. Dispatch immediately; do not proceed to Phase 1.
9
+
10
+ **Purpose:** Populate the `health-endpoints`, `pipelines`, and `criticalIssueLabels` configuration consumed by `skills/ecosystem-health/SKILL.md`. Runs the interactive wizard in `scripts/lib/ecosystem-wizard.mjs`, which detects CI provider + package manager automatically and prompts the user for the remaining values.
11
+
12
+ **Steps:**
13
+
14
+ 1. **Run the wizard.**
15
+
16
+ ```bash
17
+ node "$PLUGIN_ROOT/scripts/lib/ecosystem-wizard.mjs" --repo-root "$(pwd)"
18
+ ```
19
+
20
+ The wizard will:
21
+ - Detect CI provider (`.gitlab-ci.yml` → `gitlab`; `.github/workflows/` → `github`; else `none`)
22
+ - Detect package manager from lockfile
23
+ - Prompt for health endpoints (format: `Name|URL`, comma-separated)
24
+ - Prompt for CI pipeline identifiers (format: `id` or `id:label`, comma-separated)
25
+ - Prompt for critical issue labels (comma-separated strings)
26
+
27
+ 2. **Wizard writes two files** (or skips each if already present):
28
+ - `CLAUDE.md` (or `AGENTS.md`) — appends `ecosystem-health:` block inside `## Session Config`
29
+ - `.orchestrator/policy/ecosystem.json` — full policy file (schema: `.orchestrator/policy/ecosystem.schema.json`)
30
+
31
+ 3. **No auto-commit.** The wizard prints what it wrote. The user reviews with `git status && git diff` and commits manually.
32
+
33
+ **Report:** The wizard prints a one-line summary per file:
34
+
35
+ ```
36
+ Ecosystem-Health Wizard complete.
37
+ Written: .orchestrator/policy/ecosystem.json, CLAUDE.md
38
+ Skipped (already present): (none)
39
+
40
+ Review changes with: git status && git diff
41
+ ```
42
+
43
+ **Idempotency:** Safe to re-run. If both output files are already present with matching content, the wizard exits 0 with "Nothing to do." To update, remove the existing `ecosystem-health:` key from Session Config and delete `.orchestrator/policy/ecosystem.json`, then re-run.
44
+
45
+ See `skills/ecosystem-health/wizard.md` for the full prompt spec and schema details.
46
+
47
+ ---
48
+
@@ -0,0 +1,37 @@
1
+ # Bootstrap — Refresh-Lock Flow (`--refresh-lock`)
2
+
3
+ > Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
5
+
6
+ ## Refresh-Lock Flow (`--refresh-lock`)
7
+
8
+ Entered when `$ARGUMENTS` contains `--refresh-lock`. No scaffolding questions are asked, and — unlike the Retroactive Flow above — this is NOT a no-op once the lock already has valid `version`/`tier` fields: refreshing is the load-bearing action.
9
+
10
+ **Purpose (#57):** Acknowledge the current plugin version and reset the freshness clock on an existing, already-valid `bootstrap.lock` without disturbing its original bootstrap provenance. This closes the gap left by the Retroactive Flow: once a lock already has `version` + `tier`, re-running `/bootstrap --retroactive` reports "bootstrap.lock already present ... Nothing to do." and changes nothing — exactly the no-op the bootstrap-lock-freshness probe (#186/#290) was recommending as its remediation. `--refresh-lock` is the actual remediation for a present-but-stale or version-drifted lock.
11
+
12
+ **Steps:**
13
+
14
+ 1. **Precondition check.** Read `.orchestrator/bootstrap.lock`. If missing, or present but missing a non-empty `version` or `tier` field, abort with: `Error: No valid bootstrap.lock found. Run /bootstrap or /bootstrap --retroactive first.` Do not fabricate a lock — this flow only refreshes an existing one.
15
+
16
+ 2. **Resolve the current plugin version.** Read `plugin-version` from `$PLUGIN_ROOT/package.json` (same source Phase 4 uses).
17
+
18
+ 3. **Call the refresh writer.**
19
+
20
+ ```js
21
+ import { refreshBootstrapLock } from '$PLUGIN_ROOT/scripts/lib/bootstrap-lock-refresh.mjs';
22
+ const result = refreshBootstrapLock({
23
+ repoRoot: REPO_ROOT,
24
+ currentPluginVersion: PLUGIN_VERSION,
25
+ });
26
+ ```
27
+
28
+ `refreshBootstrapLock` writes (or replaces, if already present) exactly two lines — `refreshed-at: <ISO 8601 UTC>` and `refreshed-plugin-version: <current plugin version>` — via the same atomic tmp-file + rename pattern used by the Retroactive Flow's lock write: write to a sibling tmp file, then rename over the target so the lock is never observed half-written. **Every other line of the lock — `bootstrapped-at`, `timestamp`, `plugin-version`, `tier`, `archetype`, `source`, … — is left byte-identical.** This is the provenance-honesty guarantee: a refresh is an acknowledgement, not a re-bootstrap. On failure (`result.ok === false`), surface `result.message` and stop — do not retry with a fabricated lock.
29
+
30
+ 4. **No auto-commit.** Unlike the Retroactive Flow, `--refresh-lock` does not stage or commit. The refreshed lock is a small, reviewable diff (two changed/added lines); the user commits it alongside their own work at their own cadence.
31
+
32
+ 5. **Report.** Print: `Lock refreshed (refreshed-at: <now>, plugin-version: <current>). Original bootstrap provenance unchanged.`
33
+
34
+ **Idempotency.** Running `/bootstrap --refresh-lock` twice in a row replaces the same two lines in place — it never duplicates them.
35
+
36
+ ---
37
+