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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/CHANGELOG.md +494 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +1 -1
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/release.md +60 -0
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +1 -1
- package/docs/instruction-delivery.md +350 -0
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +1 -41
- package/docs/session-config-template.md +0 -23
- package/hooks/_lib/guard-source-loader.mjs +304 -91
- package/hooks/enforce-commands.mjs +216 -17
- package/hooks/enforce-scope.mjs +236 -12
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-session-start.mjs +7 -4
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-bash-destructive-guard.mjs +146 -59
- package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +2 -2
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/emit-session.mjs +45 -40
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +207 -23
- package/scripts/lib/command-blocker.mjs +322 -62
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/select.mjs +704 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/reconcile/emitter.mjs +107 -22
- package/scripts/lib/reconcile/engine.mjs +9 -15
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +134 -1
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +852 -72
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-close-backfill.mjs +2 -2
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
- package/scripts/lib/validate/check-rules.mjs +31 -5
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +757 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +500 -19
- package/scripts/print-applicable-rules.mjs +170 -7
- package/scripts/print-learnings-index.mjs +501 -0
- package/scripts/release.mjs +616 -61
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +82 -0
- package/scripts/validate-wave-scope.mjs +281 -12
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/state-ownership.md +17 -0
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/evolve/SKILL.md +116 -18
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +54 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/soul.md +44 -1
- package/skills/memory-cleanup/SKILL.md +18 -5
- package/skills/npm-publish/SKILL.md +22 -50
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/mode-new.md +23 -5
- package/skills/plan/soul.md +46 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +45 -26
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +30 -1
- package/skills/session-end/plan-verification.md +1 -5
- package/skills/session-end/session-metrics-write.md +6 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +15 -1
- package/skills/session-start/soul.md +41 -1
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/SKILL.md +1 -5
- package/skills/wave-executor/wave-loop.md +77 -82
- 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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
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`
|
|
13
|
-
* delegates the destructive store rewrite to
|
|
14
|
-
* `scripts/lib/learnings/io.mjs` — automatic
|
|
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
|
-
* --
|
|
23
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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 === '--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|