session-orchestrator 3.19.0 → 3.21.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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +10 -8
  5. package/CHANGELOG.md +494 -0
  6. package/README.md +16 -11
  7. package/agents/analyst.md +1 -1
  8. package/agents/architect-reviewer.md +1 -1
  9. package/agents/code-implementer.md +4 -2
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/dialectic-deriver.md +1 -1
  12. package/agents/docs-writer.md +1 -1
  13. package/agents/memory-proposal-collector.md +1 -1
  14. package/agents/qa-strategist.md +1 -1
  15. package/agents/security-reviewer.md +1 -1
  16. package/agents/session-reviewer.md +42 -1
  17. package/agents/skill-applied-judge.md +1 -1
  18. package/agents/test-writer.md +1 -1
  19. package/agents/ui-developer.md +1 -1
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/release.md +60 -0
  22. package/commands/session.md +6 -2
  23. package/docs/USER-GUIDE.md +1 -1
  24. package/docs/instruction-delivery.md +350 -0
  25. package/docs/migration-v3.md +9 -6
  26. package/docs/persona-panel.md +3 -1
  27. package/docs/scope-collision-guard.md +167 -0
  28. package/docs/session-config-reference.md +1 -41
  29. package/docs/session-config-template.md +0 -23
  30. package/hooks/_lib/guard-source-loader.mjs +304 -91
  31. package/hooks/enforce-commands.mjs +216 -17
  32. package/hooks/enforce-scope.mjs +236 -12
  33. package/hooks/hooks-codex.json +1 -1
  34. package/hooks/hooks.json +11 -1
  35. package/hooks/on-session-end.mjs +52 -5
  36. package/hooks/on-session-start.mjs +7 -4
  37. package/hooks/on-stop.mjs +127 -12
  38. package/hooks/post-bash-write-verify.mjs +8 -32
  39. package/hooks/pre-bash-destructive-guard.mjs +146 -59
  40. package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
  41. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  42. package/package.json +2 -2
  43. package/pi/prompts/release.md +12 -0
  44. package/scripts/autopilot.mjs +3 -1
  45. package/scripts/backfill-learnings-from-vault.mjs +967 -0
  46. package/scripts/emit-session.mjs +45 -40
  47. package/scripts/export-hw-learnings.mjs +61 -2
  48. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  49. package/scripts/lib/backlog-scan.mjs +106 -15
  50. package/scripts/lib/build-live-signals.mjs +7 -3
  51. package/scripts/lib/ci-status-banner.mjs +207 -23
  52. package/scripts/lib/command-blocker.mjs +322 -62
  53. package/scripts/lib/git-config-drift.mjs +471 -0
  54. package/scripts/lib/hardening.mjs +9 -9
  55. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  56. package/scripts/lib/io.mjs +193 -7
  57. package/scripts/lib/learnings/affinity.mjs +434 -0
  58. package/scripts/lib/learnings/candidates.mjs +736 -0
  59. package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
  60. package/scripts/lib/learnings/judgment.mjs +782 -0
  61. package/scripts/lib/learnings/kebab.mjs +128 -0
  62. package/scripts/lib/learnings/select.mjs +704 -0
  63. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  64. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  65. package/scripts/lib/named-vault-resolver.mjs +105 -16
  66. package/scripts/lib/peer-cards/schema.mjs +6 -2
  67. package/scripts/lib/reconcile/emitter.mjs +107 -22
  68. package/scripts/lib/reconcile/engine.mjs +9 -15
  69. package/scripts/lib/reconcile/renderer.mjs +141 -25
  70. package/scripts/lib/reconcile/sanitize.mjs +518 -0
  71. package/scripts/lib/reconcile/writer.mjs +134 -1
  72. package/scripts/lib/redact-spans.mjs +89 -0
  73. package/scripts/lib/scope-baseline.mjs +77 -17
  74. package/scripts/lib/scope-gate.mjs +852 -72
  75. package/scripts/lib/secret-masker.mjs +262 -0
  76. package/scripts/lib/session-close-backfill.mjs +2 -2
  77. package/scripts/lib/session-lock.mjs +34 -10
  78. package/scripts/lib/session-record-repair.mjs +551 -0
  79. package/scripts/lib/session-registry.mjs +9 -1
  80. package/scripts/lib/session-schema/serializer.mjs +54 -0
  81. package/scripts/lib/session-schema.mjs +1 -0
  82. package/scripts/lib/session-token-rollup.mjs +68 -6
  83. package/scripts/lib/soul-resolve.mjs +12 -0
  84. package/scripts/lib/state-md/mission-status.mjs +21 -12
  85. package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
  86. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  87. package/scripts/lib/validate/check-agents.mjs +77 -5
  88. package/scripts/lib/validate/check-banner-parity.mjs +376 -0
  89. package/scripts/lib/validate/check-commands.mjs +2 -20
  90. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  91. package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
  93. package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
  94. package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
  95. package/scripts/lib/validate/check-rules.mjs +31 -5
  96. package/scripts/lib/validate/check-skills.mjs +191 -0
  97. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  98. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  99. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  100. package/scripts/lib/validate/check-unwired-features.mjs +757 -0
  101. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  102. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  103. package/scripts/lib/validate/tier-inference.mjs +46 -8
  104. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  105. package/scripts/lib/vault-mirror/process.mjs +264 -31
  106. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  107. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  108. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  109. package/scripts/lib/vcs-repo-spec.mjs +500 -19
  110. package/scripts/print-applicable-rules.mjs +170 -7
  111. package/scripts/print-learnings-index.mjs +501 -0
  112. package/scripts/release.mjs +616 -61
  113. package/scripts/repair-invalid-sessions.mjs +209 -0
  114. package/scripts/site-numbers.mjs +1049 -0
  115. package/scripts/sweep-expired-learnings.mjs +192 -32
  116. package/scripts/validate-plugin.mjs +82 -0
  117. package/scripts/validate-wave-scope.mjs +281 -12
  118. package/scripts/vault-mirror.mjs +26 -1
  119. package/skills/_shared/monitor-patterns.md +24 -4
  120. package/skills/_shared/state-ownership.md +17 -0
  121. package/skills/brainstorm/soul.md +47 -1
  122. package/skills/claude-md-drift-check/SKILL.md +9 -1
  123. package/skills/debug/SKILL.md +4 -1
  124. package/skills/discovery/issue-templates.md +4 -4
  125. package/skills/discovery/probes-code.md +2 -2
  126. package/skills/discovery/probes-feature.md +6 -6
  127. package/skills/discovery/probes-infra.md +2 -2
  128. package/skills/discovery/probes-session.md +5 -5
  129. package/skills/dispatcher/SKILL.md +10 -1
  130. package/skills/evolve/SKILL.md +116 -18
  131. package/skills/frontmatter-guard/SKILL.md +9 -1
  132. package/skills/gitlab-ops/SKILL.md +54 -39
  133. package/skills/gitlab-portfolio/SKILL.md +10 -1
  134. package/skills/grill/soul.md +44 -1
  135. package/skills/memory-cleanup/SKILL.md +18 -5
  136. package/skills/npm-publish/SKILL.md +22 -50
  137. package/skills/persona-panel/SKILL.md +3 -1
  138. package/skills/plan/mode-new.md +23 -5
  139. package/skills/plan/soul.md +46 -3
  140. package/skills/repo-audit/SKILL.md +10 -1
  141. package/skills/session-end/SKILL.md +45 -26
  142. package/skills/session-end/metrics-collection.md +1 -1
  143. package/skills/session-end/phase-3-6-tail.md +30 -1
  144. package/skills/session-end/plan-verification.md +1 -5
  145. package/skills/session-end/session-metrics-write.md +6 -10
  146. package/skills/session-plan/SKILL.md +2 -2
  147. package/skills/session-plan/wave-template.md +1 -1
  148. package/skills/session-start/SKILL.md +15 -1
  149. package/skills/session-start/soul.md +41 -1
  150. package/skills/spinout/SKILL.md +5 -1
  151. package/skills/sunset-review/SKILL.md +11 -1
  152. package/skills/tmux-layout/SKILL.md +7 -2
  153. package/skills/vault-mirror/SKILL.md +10 -1
  154. package/skills/vault-sync/SKILL.md +10 -1
  155. package/skills/vault-sync/validator.mjs +55 -6
  156. package/skills/wave-executor/SKILL.md +1 -5
  157. package/skills/wave-executor/wave-loop.md +77 -82
  158. package/scripts/lib/mission-status-schema.mjs +0 -114
@@ -1,39 +1,56 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * sweep-expired-learnings.mjs — CLI for the mechanical expiry/archive sweep
4
- * (Epic #723 B4).
3
+ * sweep-expired-learnings.mjs — CLI for the two mechanical archive-safe writers
4
+ * over `learnings.jsonl`: the time-driven expiry sweep (Epic #723 B4, default)
5
+ * and the decision-driven prune (`--prune`, issue #1017).
5
6
  *
6
- * Moves `learnings.jsonl` entries that have been expired for longer than the
7
- * grace period into an append-only archive sidecar. NEVER deletes data
8
- * archived entries remain readable in the archive file, tagged with
9
- * `_archived_at` / `_archive_reason: "expired"`.
7
+ * Both move records OUT of the active store and INTO an append-only archive
8
+ * sidecar. NEVER deletes data archived entries remain readable in the archive
9
+ * file, tagged with `_archived_at` and an `_archive_reason` from the closed
10
+ * enum `expired | pruned | superseded | merged`.
10
11
  *
11
12
  * All read/partition/write logic lives in
12
- * `scripts/lib/learnings/expiry-sweep.mjs` (`sweepExpiredLearnings`), which
13
- * delegates the destructive store rewrite to `rewriteLearnings()` from
14
- * `scripts/lib/learnings/io.mjs` — automatic `.bak-<ISO>` backup + keep-3
15
- * rotation (#721) protects every `--apply` run.
13
+ * `scripts/lib/learnings/expiry-sweep.mjs` (`sweepExpiredLearnings` /
14
+ * `pruneLearnings`), which delegates the destructive store rewrite to
15
+ * `rewriteLearnings()` from `scripts/lib/learnings/io.mjs` — automatic
16
+ * `.bak-<ISO>` backup + keep-3 rotation (#721) protects every `--apply` run.
17
+ *
18
+ * Why `--prune` is a subcommand here and not an inline `node -e` block in
19
+ * `skills/evolve/SKILL.md`: a mechanism that lives inside prose has no test, no
20
+ * `--help`, and no exit-code contract. `/evolve` names this command; the
21
+ * mechanism stays in code.
16
22
  *
17
23
  * Usage:
18
- * node scripts/sweep-expired-learnings.mjs [--dry-run|--apply] [--json]
19
- * [--grace-days N] [--file PATH] [--archive PATH]
24
+ * node scripts/sweep-expired-learnings.mjs [--prune] [--dry-run|--apply] [--json]
25
+ * [--grace-days N] [--entries PATH] [--file PATH] [--archive PATH]
20
26
  *
21
27
  * Flags:
22
- * --dry-run Preview counts; write nothing (DEFAULT)
23
- * --apply Archive stale-expired entries + rewrite the store
28
+ * --prune Decision-driven prune+consolidate+rewrite instead of the
29
+ * time-driven expiry sweep (issue #1017)
30
+ * --dry-run Preview counts; write nothing (DEFAULT, both modes)
31
+ * --apply Perform the archive append + store rewrite
24
32
  * --json Emit a single machine-parseable JSON summary line
25
33
  * (default: human-readable one-liner)
26
- * --grace-days N Days past expiry before archiving (default: 14)
34
+ * --grace-days N Days past expiry before archiving (default: 14).
35
+ * SWEEP ONLY — `--prune` has no grace window by design.
36
+ * --entries PATH JSONL sidecar holding the caller's next store generation.
37
+ * PRUNE ONLY. Must exist, parse cleanly, and hold at least
38
+ * one record — absent/malformed/empty all exit 1 untouched.
39
+ * Omitted ⇒ a pure prune+consolidate pass over the on-disk
40
+ * store.
27
41
  * --file PATH Learnings store (default: .orchestrator/metrics/learnings.jsonl)
28
42
  * --archive PATH Archive sidecar (default: .orchestrator/metrics/learnings-archive.jsonl)
29
43
  *
30
44
  * Exit codes:
31
45
  * 0 Success (including no-op when nothing is archive-eligible)
32
- * 1 Usage error (bad flag/value)
33
- * 2 Sweep error (I/O or validation failure inside sweepExpiredLearnings)
46
+ * 1 Usage/input error (bad flag/value, flag used in the wrong mode, or an
47
+ * absent/malformed/empty `--entries` sidecar)
48
+ * 2 Sweep/prune error (I/O or validation failure inside the lib)
34
49
  */
35
50
 
36
- import { sweepExpiredLearnings } from './lib/learnings/expiry-sweep.mjs';
51
+ import { existsSync } from 'node:fs';
52
+ import { sweepExpiredLearnings, pruneLearnings } from './lib/learnings/expiry-sweep.mjs';
53
+ import { readLearnings } from './lib/learnings/io.mjs';
37
54
 
38
55
  const DEFAULT_FILE = '.orchestrator/metrics/learnings.jsonl';
39
56
  const DEFAULT_ARCHIVE = '.orchestrator/metrics/learnings-archive.jsonl';
@@ -41,32 +58,51 @@ const DEFAULT_GRACE_DAYS = 14;
41
58
 
42
59
  function printHelp() {
43
60
  process.stdout.write(
44
- `Usage: node scripts/sweep-expired-learnings.mjs [--dry-run|--apply] [--json] [--grace-days N] [--file PATH] [--archive PATH]
61
+ `Usage: node scripts/sweep-expired-learnings.mjs [--prune] [--dry-run|--apply] [--json] [--grace-days N] [--entries PATH] [--file PATH] [--archive PATH]
62
+
63
+ Modes:
64
+ (default) Expiry sweep — archive entries expired past the grace window
65
+ --prune Prune + consolidate + rewrite (issue #1017): archives
66
+ expired / zero-confidence / superseded / caller-dropped
67
+ records instead of deleting them
45
68
 
46
69
  Options:
47
70
  --dry-run Preview counts; write nothing (default)
48
- --apply Archive stale-expired entries + rewrite the store
71
+ --apply Perform the archive append + store rewrite
49
72
  --json Emit a single machine-parseable JSON summary line
50
- --grace-days N Days past expiry before archiving (default: ${DEFAULT_GRACE_DAYS})
73
+ --grace-days N Days past expiry before archiving (default: ${DEFAULT_GRACE_DAYS}); sweep only
74
+ --entries PATH JSONL sidecar with the next store generation; prune only.
75
+ Must exist, parse cleanly, and hold >= 1 record
51
76
  --file PATH Learnings store (default: ${DEFAULT_FILE})
52
77
  --archive PATH Archive sidecar (default: ${DEFAULT_ARCHIVE})
53
78
 
54
- Exit codes: 0 success 1 usage error 2 sweep error
79
+ Exit codes: 0 success 1 usage/input error 2 sweep/prune error
55
80
  `
56
81
  );
57
82
  }
58
83
 
84
+ /** Exit 1 with a diagnostic on stderr (usage/input errors). */
85
+ function usageError(message) {
86
+ process.stderr.write(`sweep-expired-learnings: ${message}\n`);
87
+ process.exit(1);
88
+ }
89
+
59
90
  function parseArgs(argv) {
60
91
  const args = {
92
+ prune: false,
61
93
  dryRun: true,
62
94
  json: false,
63
95
  graceDays: DEFAULT_GRACE_DAYS,
96
+ graceDaysExplicit: false,
97
+ entries: null,
64
98
  file: DEFAULT_FILE,
65
99
  archive: DEFAULT_ARCHIVE,
66
100
  };
67
101
  for (let i = 0; i < argv.length; i++) {
68
102
  const a = argv[i];
69
- if (a === '--apply') {
103
+ if (a === '--prune') {
104
+ args.prune = true;
105
+ } else if (a === '--apply') {
70
106
  args.dryRun = false;
71
107
  } else if (a === '--dry-run') {
72
108
  args.dryRun = true;
@@ -76,12 +112,12 @@ function parseArgs(argv) {
76
112
  const raw = argv[++i];
77
113
  const v = Number(raw);
78
114
  if (!Number.isFinite(v) || v < 0) {
79
- process.stderr.write(
80
- `sweep-expired-learnings: --grace-days requires a non-negative number, got: ${raw}\n`
81
- );
82
- process.exit(1);
115
+ usageError(`--grace-days requires a non-negative number, got: ${raw}`);
83
116
  }
84
117
  args.graceDays = v;
118
+ args.graceDaysExplicit = true;
119
+ } else if (a === '--entries') {
120
+ args.entries = argv[++i];
85
121
  } else if (a === '--file') {
86
122
  args.file = argv[++i];
87
123
  } else if (a === '--archive') {
@@ -90,16 +126,29 @@ function parseArgs(argv) {
90
126
  printHelp();
91
127
  process.exit(0);
92
128
  } else {
93
- process.stderr.write(`sweep-expired-learnings: unknown argument: ${a}\n`);
94
- process.exit(1);
129
+ usageError(`unknown argument: ${a}`);
95
130
  }
96
131
  }
132
+
133
+ // Mode/flag mismatches are usage errors, never silent no-ops: a `--grace-days`
134
+ // that the prune path ignores would read as "the grace window applied" in a
135
+ // transcript, and an `--entries` the sweep ignores would read as "my next
136
+ // generation was written".
137
+ if (args.prune && args.graceDaysExplicit) {
138
+ usageError('--grace-days is not valid with --prune (the prune path has no grace window)');
139
+ }
140
+ if (!args.prune && args.entries !== null) {
141
+ usageError('--entries is only valid with --prune');
142
+ }
97
143
  return args;
98
144
  }
99
145
 
100
- async function main() {
101
- const args = parseArgs(process.argv.slice(2));
102
-
146
+ /**
147
+ * Time-driven expiry sweep (Epic #723 B4) — the default mode.
148
+ *
149
+ * @param {ReturnType<typeof parseArgs>} args
150
+ */
151
+ async function runSweep(args) {
103
152
  let result;
104
153
  try {
105
154
  result = await sweepExpiredLearnings({
@@ -129,6 +178,117 @@ async function main() {
129
178
  }
130
179
  }
131
180
 
181
+ /**
182
+ * Resolve the `--entries` sidecar into the caller's next store generation.
183
+ *
184
+ * Fails closed on THREE input conditions, all of which yield the same lethal
185
+ * value — an empty next generation, which makes `pruneLearnings()` treat the
186
+ * ENTIRE store as caller-dropped:
187
+ *
188
+ * 1. **absent file** — `readLearnings()` returns `{entries: [], malformed: []}`
189
+ * for a missing path, so one mistyped path would archive every active
190
+ * learning. A path the operator named and the filesystem does not have is
191
+ * an input error, not an empty set.
192
+ * 2. **malformed line** — a half-written sidecar reads as a SHORTER next
193
+ * generation, pruning every record the truncated tail omitted.
194
+ * 3. **parses to zero records** — a 0-byte or blank-line-only file. Guard (1)
195
+ * closes ABSENCE, which is a different condition: an empty file EXISTS, so
196
+ * it sails past `existsSync` and parses to a legitimate-looking empty
197
+ * generation. Measured on a 3-record fixture before this guard: a 0-byte
198
+ * `--entries` archived all 3 and exited 0.
199
+ *
200
+ * Condition 3 is REJECTED rather than obeyed because at a file boundary an
201
+ * empty parse is indistinguishable from a truncated write, a failed producer,
202
+ * or a typo that landed on an unrelated empty file — and no caller expresses
203
+ * "archive the whole corpus" through this flag: `/evolve`'s next generation
204
+ * always carries the survivors. The cost of rejecting a genuinely-intended
205
+ * empty generation is one re-run; the cost of obeying a corrupt one is the
206
+ * active store. Note this guard is deliberately NOT in `pruneLearnings()`: an
207
+ * explicit `entries: []` written in CODE is a statement, and the lib keeps it
208
+ * expressible. Only the FILE is ambiguous, so only the file is guarded.
209
+ *
210
+ * @param {string} entriesPath
211
+ * @returns {Promise<object[]>} the validated, non-empty next generation
212
+ */
213
+ async function loadEntriesSidecar(entriesPath) {
214
+ if (!existsSync(entriesPath)) {
215
+ usageError(
216
+ `--entries sidecar not found: ${entriesPath} (refusing to prune — an absent ` +
217
+ `next generation would archive the whole store)`
218
+ );
219
+ }
220
+ let read;
221
+ try {
222
+ read = await readLearnings(entriesPath);
223
+ } catch (err) {
224
+ usageError(`--entries sidecar unreadable: ${entriesPath}: ${err.message}`);
225
+ }
226
+ if (read.malformed.length > 0) {
227
+ usageError(
228
+ `refusing to prune — ${read.malformed.length} malformed line(s) in ${entriesPath}`
229
+ );
230
+ }
231
+ if (read.entries.length === 0) {
232
+ usageError(
233
+ `--entries sidecar holds no records: ${entriesPath} (refusing to prune — an empty ` +
234
+ `next generation would archive every record in the store; omit --entries for a ` +
235
+ `pure prune+consolidate pass)`
236
+ );
237
+ }
238
+ return read.entries;
239
+ }
240
+
241
+ /**
242
+ * Decision-driven prune + consolidate + rewrite (issue #1017).
243
+ *
244
+ * @param {ReturnType<typeof parseArgs>} args
245
+ */
246
+ async function runPrune(args) {
247
+ const entries = args.entries === null ? undefined : await loadEntriesSidecar(args.entries);
248
+
249
+ let result;
250
+ try {
251
+ result = await pruneLearnings({
252
+ filePath: args.file,
253
+ archivePath: args.archive,
254
+ entries,
255
+ dryRun: args.dryRun,
256
+ });
257
+ } catch (err) {
258
+ process.stderr.write(`sweep-expired-learnings: prune failed: ${err.message}\n`);
259
+ process.exit(2);
260
+ }
261
+
262
+ const summary = {
263
+ file: args.file,
264
+ entries_from: args.entries,
265
+ ...result,
266
+ };
267
+
268
+ if (args.json) {
269
+ process.stdout.write(JSON.stringify(summary) + '\n');
270
+ } else {
271
+ const byReason =
272
+ Object.entries(summary.byReason)
273
+ .map(([reason, n]) => `${reason}:${n}`)
274
+ .join(',') || '-';
275
+ process.stdout.write(
276
+ `sweep-expired-learnings: prune scanned=${summary.scanned} kept=${summary.kept} ` +
277
+ `archived=${summary.archived} by_reason=${byReason} dry_run=${summary.dryRun} ` +
278
+ `archive=${summary.archivePath}\n`
279
+ );
280
+ }
281
+ }
282
+
283
+ async function main() {
284
+ const args = parseArgs(process.argv.slice(2));
285
+ if (args.prune) {
286
+ await runPrune(args);
287
+ return;
288
+ }
289
+ await runSweep(args);
290
+ }
291
+
132
292
  main().catch((err) => {
133
293
  process.stderr.write(`sweep-expired-learnings: unexpected error: ${err?.stack ?? err}\n`);
134
294
  process.exit(2);
@@ -204,12 +204,28 @@ if (runCheck('check-agents.mjs') !== 0) checkFailed = 1;
204
204
  process.stdout.write('\n');
205
205
  if (runCheck('check-commands.mjs') !== 0) checkFailed = 1;
206
206
 
207
+ // FAIL-capable from day one: every rule this check enforces (frontmatter present,
208
+ // parses as YAML, kebab-case name matching the directory, non-empty description)
209
+ // was measured at 0 violations across all 46 SKILL.md files at the commit that
210
+ // added it — so unlike the WARN-only censuses further down, it cannot be red on
211
+ // arrival. It sits next to check-commands.mjs because it shares that check's
212
+ // posture: a real js-yaml parse, not the line-regex approach of check-agents.mjs
213
+ // that let 12 unparseable SKILL.md frontmatter blocks go unnoticed.
214
+ process.stdout.write('\n');
215
+ if (runCheck('check-skills.mjs') !== 0) checkFailed = 1;
216
+
207
217
  process.stdout.write('\n');
208
218
  if (runDriftCheck() !== 0) checkFailed = 1;
209
219
 
210
220
  process.stdout.write('\n');
211
221
  if (runCheck('check-hooks-symmetry.mjs') !== 0) checkFailed = 1;
212
222
 
223
+ process.stdout.write('\n');
224
+ if (runCheck('check-guard-requires-parity.mjs') !== 0) checkFailed = 1;
225
+
226
+ process.stdout.write('\n');
227
+ if (runCheck('check-banner-parity.mjs') !== 0) checkFailed = 1;
228
+
213
229
  process.stdout.write('\n');
214
230
  if (runCheck('check-playwright-mcp-canary.mjs') !== 0) checkFailed = 1;
215
231
 
@@ -249,6 +265,72 @@ if (runCheck('check-unicode-safety.mjs') !== 0) checkFailed = 1;
249
265
  process.stdout.write('\n');
250
266
  if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
251
267
 
268
+ // FAIL-capable, unlike the WARN-only censuses below: R2+R4 were measured at
269
+ // 1 hit / 1 TP / 0 FP each against the full test corpus, so this check cannot be
270
+ // red on arrival for anything but a real instance of the class. The alternatives
271
+ // (R1 at 67 FP, R3 at 47 FP) are exactly the shape that gets demoted to WARN and
272
+ // then ignored — see the rule table in the check's header.
273
+ process.stdout.write('\n');
274
+ if (runCheck('check-untracked-test-deps.mjs') !== 0) checkFailed = 1;
275
+
276
+ // WARN-only (v1): the unwired-config-key census reports but never fails the
277
+ // build — see the rationale in the check's header (a blocking gate on today's
278
+ // inventory would be red from day one and get disabled). Exit code is
279
+ // deliberately ignored; only a tool error (2) would be worth escalating later.
280
+ process.stdout.write('\n');
281
+ runCheck('check-unwired-features.mjs');
282
+
283
+ // WARN-only (#1017), same rationale as the census above: 11 of 13 provenance
284
+ // pointers in .claude/rules/ dangle at the time this check landed, so a blocking
285
+ // gate would be red on arrival. The exit code is deliberately ignored; a tool
286
+ // error still surfaces because that path prints FAIL: lines, which runCheck
287
+ // tallies into totalFail.
288
+ process.stdout.write('\n');
289
+ runCheck('check-learning-provenance.mjs');
290
+
291
+ // WARN-only (#971), same rationale as the two censuses above: a `gh`/`glab` call
292
+ // without `--repo`/`-R` resolves its target project from the ambient cwd remote —
293
+ // silently the wrong project in a sibling worktree, an /autopilot child, or a repo
294
+ // whose origin is a fork — and a gate that blocks on a backlog no single pass can
295
+ // drain gets switched off rather than obeyed. What WARN buys is that each newly
296
+ // added bare call site shows up in every validator run while the sweep drains the
297
+ // rest. No headcount is quoted here on purpose: the live number moves with every
298
+ // commit, and only the check's own `--json` summary can state it as of a SHA. The
299
+ // exit code is deliberately ignored; a tool error still surfaces because that path
300
+ // prints FAIL: lines, which runCheck tallies into totalFail.
301
+ process.stdout.write('\n');
302
+ runCheck('check-vcs-repo-flag.mjs');
303
+
304
+ // WARN-only (#1023): a `gh`/`glab` command cited in docs that no released CLI
305
+ // ever had — `glab repo edit --visibility`, `glab group list` — costs an
306
+ // operator a failed command and a re-derivation. The oracle is the CLI's own
307
+ // `--help` COMMANDS section, never an exit code (`glab repo <anything> --help`
308
+ // exits 0). WARN rather than FAIL because that oracle is the LOCALLY installed
309
+ // binary: a version skew must not red an unrelated commit, and a missing binary
310
+ // SKIPs. The exit code is deliberately ignored; a tool error still surfaces
311
+ // because that path prints FAIL: lines, which runCheck tallies into totalFail.
312
+ process.stdout.write('\n');
313
+ runCheck('check-doc-cli-commands.mjs');
314
+
315
+ // WARN-only: a state-mutating `git` call in tests/ that names no target resolves
316
+ // its destination from the ambient cwd (or an inherited GIT_DIR) — the 2026-08-19
317
+ // incident, where fixture commits, a fixture remote and a fixture identity landed
318
+ // in the REAL .git and two commits reached both remotes with the wrong author.
319
+ // WARN rather than FAIL is a MEASURED posture, not caution: the v1 rule reported
320
+ // 11 findings against tests/ and all 11 were false positives (`git init <dir>`,
321
+ // whose positional IS the target). After that refinement the live corpus reports
322
+ // 0 findings — i.e. zero measured true positives to weigh against a textual
323
+ // detector's residual false-positive surface (the variable argv arrays it cannot
324
+ // judge; the live count is in its own PASS line, never restated here).
325
+ // check-untracked-test-deps.mjs above earned FAIL-capability on a
326
+ // measured 1 TP / 0 FP; this check has not, and a single FAIL: line here reds the
327
+ // whole validator. Ratchet to FAIL once a live true positive has been observed
328
+ // and the GIT_DIR-inheritance gap (reported as `gitDirInheritable`) is closed
329
+ // centrally. The exit code is deliberately ignored; a tool error still surfaces
330
+ // because that path prints FAIL: lines, which runCheck tallies into totalFail.
331
+ process.stdout.write('\n');
332
+ runCheck('check-test-git-config-target.mjs');
333
+
252
334
  // ---------------------------------------------------------------------------
253
335
  // Summary
254
336
  // ---------------------------------------------------------------------------