session-orchestrator 3.20.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 +414 -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/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +1 -1
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/emit-session.mjs +42 -0
- 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/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/select.mjs +157 -3
- 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/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- 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-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- 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 +219 -11
- 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-learnings-index.mjs +30 -3
- package/scripts/release.mjs +616 -61
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +61 -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/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/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +49 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -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/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +44 -2
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +13 -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/wave-loop.md +42 -12
- package/scripts/lib/mission-status-schema.mjs +0 -114
|
@@ -204,6 +204,16 @@ 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
|
|
|
@@ -255,6 +265,14 @@ if (runCheck('check-unicode-safety.mjs') !== 0) checkFailed = 1;
|
|
|
255
265
|
process.stdout.write('\n');
|
|
256
266
|
if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
|
|
257
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
|
+
|
|
258
276
|
// WARN-only (v1): the unwired-config-key census reports but never fails the
|
|
259
277
|
// build — see the rationale in the check's header (a blocking gate on today's
|
|
260
278
|
// inventory would be red from day one and get disabled). Exit code is
|
|
@@ -270,6 +288,49 @@ runCheck('check-unwired-features.mjs');
|
|
|
270
288
|
process.stdout.write('\n');
|
|
271
289
|
runCheck('check-learning-provenance.mjs');
|
|
272
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
|
+
|
|
273
334
|
// ---------------------------------------------------------------------------
|
|
274
335
|
// Summary
|
|
275
336
|
// ---------------------------------------------------------------------------
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* node scripts/validate-wave-scope.mjs <path-to-wave-scope.json>
|
|
10
10
|
* cat wave-scope.json | node scripts/validate-wave-scope.mjs
|
|
11
11
|
* node scripts/validate-wave-scope.mjs --assert-subset <agent-filescope.json> < wave-scope.json
|
|
12
|
+
* node scripts/validate-wave-scope.mjs --assert-disjoint <agent-scopes.json> < wave-scope.json
|
|
13
|
+
* node scripts/validate-wave-scope.mjs --union <agent-scopes.json> < wave-scope.json
|
|
12
14
|
*
|
|
13
15
|
* Flags:
|
|
14
16
|
* --assert-subset <path> After schema validation passes, read the agent
|
|
@@ -26,21 +28,68 @@
|
|
|
26
28
|
* the caller may pass the flag unconditionally on
|
|
27
29
|
* every pre-dispatch check. A skip is announced on
|
|
28
30
|
* stderr as a WARN.
|
|
31
|
+
* --assert-disjoint <p> #1020. Read the wave's per-agent scope SIDECAR and
|
|
32
|
+
* assert no file is claimed by two agents of the same
|
|
33
|
+
* wave. Fails (exit 1) with one message per collision.
|
|
34
|
+
* `knownFiles` for the glob∩glob stage comes from
|
|
35
|
+
* `git ls-files` — spawned HERE, in the CLI layer,
|
|
36
|
+
* because scripts/lib/scope-gate.mjs is hook-safe and
|
|
37
|
+
* must not spawn a process (see its module header).
|
|
38
|
+
* --union <path> #1020. QUERY MODE. Read the same sidecar, compute
|
|
39
|
+
* `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
40
|
+
* using the MANIFEST'S OWN `role`, and print the
|
|
41
|
+
* resulting allowedPaths array as JSON on stdout.
|
|
42
|
+
* Mechanical replacement for the "Collect all file
|
|
43
|
+
* paths … Deduplicate entries" prose in
|
|
44
|
+
* skills/wave-executor/wave-loop.md § Scope Manifest #3.
|
|
45
|
+
*
|
|
46
|
+
* ## SIDECAR FORMAT (both #1020 flags) — an ARRAY, never an object map
|
|
47
|
+
* [{ "id": "W2-C1", "files": ["scripts/a.mjs"] }, { "id": "W2-C4", "files": [...] }]
|
|
48
|
+
* An object keyed by agent id would swallow a DUPLICATE agent id silently, and a
|
|
49
|
+
* duplicated id is a real copy-paste failure mode (it hides one agent's scope
|
|
50
|
+
* from every per-agent check). The array form keeps both records, and
|
|
51
|
+
* `findScopeCollisions` reports the duplicate as its own finding.
|
|
52
|
+
*
|
|
53
|
+
* ## STDOUT CONTRACT (why --union suppresses the manifest echo)
|
|
54
|
+
* Without `--union` this script writes EXACTLY ONE thing to stdout: the input
|
|
55
|
+
* manifest, echoed back verbatim. Callers rely on that — `JSON.parse(stdout)`.
|
|
56
|
+
* `--union` is the first mode that has something else to say, so it is a pure
|
|
57
|
+
* QUERY MODE: it REPLACES the echo rather than adding to it, and stdout carries
|
|
58
|
+
* only the computed allowedPaths array. Mixing both on stdout would break every
|
|
59
|
+
* `JSON.parse(stdout)` caller; writing the union to a second sink would need a
|
|
60
|
+
* file argument the caller must then read back. One JSON document per run, and
|
|
61
|
+
* the flag decides which one.
|
|
29
62
|
*
|
|
30
63
|
* Exit codes:
|
|
31
|
-
* 0 — valid (validated JSON echoed to stdout)
|
|
32
|
-
* 1 — invalid input / validation failure (error messages written to stderr)
|
|
33
|
-
*
|
|
64
|
+
* 0 — valid (validated JSON echoed to stdout; with --union: the union array)
|
|
65
|
+
* 1 — invalid input / validation failure (error messages written to stderr).
|
|
66
|
+
* A scope COLLISION is a validation finding, exactly like the #796 subset
|
|
67
|
+
* and #970 test-sibling violations — the collision-vs-subset distinction
|
|
68
|
+
* lives in the MESSAGE, not in a new exit code.
|
|
69
|
+
* 2 — I/O error (file not found, unreadable stdin, unreadable sidecar file)
|
|
34
70
|
*/
|
|
35
71
|
|
|
36
72
|
import path from 'node:path';
|
|
37
73
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
74
|
+
import { execFileSync } from 'node:child_process';
|
|
38
75
|
import { warn } from './lib/common.mjs';
|
|
39
76
|
import {
|
|
40
77
|
assertFileScopeSubset,
|
|
41
78
|
assertTestSiblingCoverage,
|
|
42
79
|
testSiblingExpansionApplies,
|
|
43
80
|
TEST_SIBLING_EXPANSION_ROLES,
|
|
81
|
+
findScopeCollisions,
|
|
82
|
+
unionFileScopes,
|
|
83
|
+
// #1057 — the read-only-role predicate + THE list. Imported rather than
|
|
84
|
+
// re-listed so the validator and hooks/enforce-scope.mjs cannot disagree about
|
|
85
|
+
// which roles are allowed to grant zero paths.
|
|
86
|
+
isReadOnlyWaveRole,
|
|
87
|
+
READ_ONLY_WAVE_ROLES,
|
|
88
|
+
// Aliased: `expandTestSiblings` is ALSO the name of the pre-existing
|
|
89
|
+
// boolean parameter threaded through validate()/assertSubsetOrDie for the
|
|
90
|
+
// #970 flag. Aliasing the import avoids shadowing that parameter rather than
|
|
91
|
+
// renaming it — the #970 call path stays byte-identical.
|
|
92
|
+
expandTestSiblings as expandScopeTestSiblings,
|
|
44
93
|
} from './lib/scope-gate.mjs';
|
|
45
94
|
|
|
46
95
|
/**
|
|
@@ -54,21 +103,53 @@ function die(msg, code = 1) {
|
|
|
54
103
|
process.exit(code);
|
|
55
104
|
}
|
|
56
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Read the value operand of a value-taking flag, REFUSING one that is itself a
|
|
108
|
+
* flag. Used by the #1020 flags only.
|
|
109
|
+
*
|
|
110
|
+
* `--assert-subset` (#796) consumes `argv[i + 1]` BLIND, so
|
|
111
|
+
* `--assert-subset --assert-disjoint x.json` reads `--assert-disjoint` as its
|
|
112
|
+
* path value. That behaviour is deliberately left untouched — its no-value
|
|
113
|
+
* message is pinned byte-for-byte by
|
|
114
|
+
* tests/scripts/validate-wave-scope.test.mjs — but the flags added here do not
|
|
115
|
+
* inherit it: a swallowed flag is silent (the mode never runs, and the caller
|
|
116
|
+
* believes it did), whereas this refusal is loud and one line long.
|
|
117
|
+
*
|
|
118
|
+
* @param {string[]} argv
|
|
119
|
+
* @param {number} i - index of the FLAG token
|
|
120
|
+
* @param {string} flag - the flag name, for the error message
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
function flagValue(argv, i, flag) {
|
|
124
|
+
const value = argv[i + 1];
|
|
125
|
+
if (value === undefined || value.startsWith('--')) {
|
|
126
|
+
die(`${flag} requires a file-path argument`, 1);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
57
131
|
/**
|
|
58
132
|
* Parse CLI flags out of argv, leaving positional args behind.
|
|
59
133
|
*
|
|
60
|
-
* Recognised: `--assert-subset <path>` (#796)
|
|
61
|
-
* (#
|
|
62
|
-
* wave-scope.json file path), preserving
|
|
63
|
-
* input file.
|
|
134
|
+
* Recognised: `--assert-subset <path>` (#796), `--expand-test-siblings` (#970),
|
|
135
|
+
* `--assert-disjoint <path>` and `--union <path>` (#1020). Everything else is
|
|
136
|
+
* treated as a positional argument (the wave-scope.json file path), preserving
|
|
137
|
+
* legacy behaviour where argv[2] is the input file.
|
|
138
|
+
*
|
|
139
|
+
* The #1020 branches sit BEFORE the positional fallback, as their own `else if`
|
|
140
|
+
* arms: routed through the fallback instead, `--assert-disjoint` would be read
|
|
141
|
+
* as a wave-scope.json path and the mode would never run.
|
|
64
142
|
*
|
|
65
143
|
* @param {string[]} argv - full process.argv
|
|
66
|
-
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
144
|
+
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
145
|
+
* assertDisjoint: string|null, union: string|null, positionals: string[] }}
|
|
67
146
|
*/
|
|
68
147
|
function parseArgs(argv) {
|
|
69
148
|
const positionals = [];
|
|
70
149
|
let assertSubset = null;
|
|
71
150
|
let expandTestSiblings = false;
|
|
151
|
+
let assertDisjoint = null;
|
|
152
|
+
let union = null;
|
|
72
153
|
for (let i = 2; i < argv.length; i++) {
|
|
73
154
|
const a = argv[i];
|
|
74
155
|
if (a === '--expand-test-siblings') {
|
|
@@ -79,11 +160,17 @@ function parseArgs(argv) {
|
|
|
79
160
|
die('--assert-subset requires a file-path argument', 1);
|
|
80
161
|
}
|
|
81
162
|
i++; // consume the value
|
|
163
|
+
} else if (a === '--assert-disjoint') {
|
|
164
|
+
assertDisjoint = flagValue(argv, i, '--assert-disjoint');
|
|
165
|
+
i++; // consume the value
|
|
166
|
+
} else if (a === '--union') {
|
|
167
|
+
union = flagValue(argv, i, '--union');
|
|
168
|
+
i++; // consume the value
|
|
82
169
|
} else {
|
|
83
170
|
positionals.push(a);
|
|
84
171
|
}
|
|
85
172
|
}
|
|
86
|
-
return { assertSubset, expandTestSiblings, positionals };
|
|
173
|
+
return { assertSubset, expandTestSiblings, assertDisjoint, union, positionals };
|
|
87
174
|
}
|
|
88
175
|
|
|
89
176
|
/**
|
|
@@ -259,6 +346,27 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
259
346
|
errors.push(`allowedPaths must be an array, got type: ${ap === null ? 'null' : typeof ap}`);
|
|
260
347
|
return;
|
|
261
348
|
}
|
|
349
|
+
// #1057 — an empty union under a WRITABLE role. WARN, never error, and the
|
|
350
|
+
// distinction is measured rather than stylistic: `skills/wave-executor/wave-loop.md`
|
|
351
|
+
// § Scope Manifest deliberately feeds a skeleton with `"allowedPaths": []`
|
|
352
|
+
// through THIS validator in `--assert-disjoint` and `--union` mode, BEFORE the
|
|
353
|
+
// union exists to be written. An error would break the documented procedure
|
|
354
|
+
// that produces the very field it complains about.
|
|
355
|
+
//
|
|
356
|
+
// Named ceiling (BV-004): the warning therefore also fires on that legitimate
|
|
357
|
+
// skeleton run — one stderr line on a happy path, accepted because the
|
|
358
|
+
// alternative is a mode-conditional warning, i.e. a second place that has to
|
|
359
|
+
// enumerate the modes correctly. Revisit if a third empty-skeleton mode lands.
|
|
360
|
+
if (ap.length === 0 && typeof obj.role === 'string' && obj.role.trim().length > 0
|
|
361
|
+
&& !isReadOnlyWaveRole(obj.role)) {
|
|
362
|
+
warnings.push(
|
|
363
|
+
`allowedPaths is empty for role "${obj.role}" — every write in this wave will be DENIED by ` +
|
|
364
|
+
`hooks/enforce-scope.mjs. Empty is intentional only for a read-only role ` +
|
|
365
|
+
`(${READ_ONLY_WAVE_ROLES.join(', ')}); for a writable role it usually means the coordinator's ` +
|
|
366
|
+
`--union step did not complete. Expected while validating the pre-union skeleton; otherwise ` +
|
|
367
|
+
`re-run --union and rewrite the manifest.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
262
370
|
for (const entry of ap) {
|
|
263
371
|
if (typeof entry !== 'string' || entry.length === 0) {
|
|
264
372
|
errors.push('allowedPaths contains empty string');
|
|
@@ -414,13 +522,157 @@ function assertSubsetOrDie(obj, fileScopePath, expandTestSiblings = false) {
|
|
|
414
522
|
}
|
|
415
523
|
}
|
|
416
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Read + shape-check the per-agent scope SIDECAR shared by `--assert-disjoint`
|
|
527
|
+
* and `--union` (#1020). Exits on any defect; returns the records on success.
|
|
528
|
+
*
|
|
529
|
+
* Exit codes mirror {@link assertSubsetOrDie} exactly: 2 for I/O (missing, not a
|
|
530
|
+
* regular file, unreadable), 1 for every content defect.
|
|
531
|
+
*
|
|
532
|
+
* ## Why the shape check is STRICTER than the library's tolerance
|
|
533
|
+
* `findScopeCollisions` / `unionFileScopes` are fail-closed and never throw:
|
|
534
|
+
* they SKIP a member that is not an object, and treat a missing `files` as `[]`.
|
|
535
|
+
* That is right for a hook-hot-path primitive and wrong for a CLI. A sidecar
|
|
536
|
+
* that spells the key `file:` instead of `files:` would then contribute nothing
|
|
537
|
+
* and both modes would report success on a scope that silently vanished — a
|
|
538
|
+
* path the operator NAMED and the tool did not honour. The absent-input guard
|
|
539
|
+
* belongs in the CLI layer (recorded learning, conf 0.80: a tolerant reader
|
|
540
|
+
* cannot carry a CLI's absent-input guard), so `files` is REQUIRED here.
|
|
541
|
+
*
|
|
542
|
+
* `id` is deliberately NOT required: scope-gate's `normalizeAgentScopes` runs a
|
|
543
|
+
* record with no usable id as `<unnamed#i>` rather than dropping it, precisely
|
|
544
|
+
* because an unreviewed scope is the one that collides. Requiring it here would
|
|
545
|
+
* reject exactly the input that contract was written to keep.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} sidecarPath
|
|
548
|
+
* @param {string} flag - the flag name, for error messages
|
|
549
|
+
* @returns {Array<{id?: string, files: string[]}>}
|
|
550
|
+
*/
|
|
551
|
+
function readAgentScopesOrDie(sidecarPath, flag) {
|
|
552
|
+
if (!existsSync(sidecarPath) || !statSync(sidecarPath).isFile()) {
|
|
553
|
+
die(`Cannot read ${flag} file: ${sidecarPath}`, 2);
|
|
554
|
+
}
|
|
555
|
+
let raw;
|
|
556
|
+
try {
|
|
557
|
+
raw = readFileSync(sidecarPath, 'utf8');
|
|
558
|
+
} catch (err) {
|
|
559
|
+
die(`Cannot read ${flag} file ${sidecarPath}: ${err.message}`, 2);
|
|
560
|
+
}
|
|
561
|
+
let parsed;
|
|
562
|
+
try {
|
|
563
|
+
parsed = JSON.parse(raw);
|
|
564
|
+
} catch {
|
|
565
|
+
die(`${flag} file is not valid JSON: ${sidecarPath}`, 1);
|
|
566
|
+
}
|
|
567
|
+
if (!Array.isArray(parsed)) {
|
|
568
|
+
const t = parsed === null ? 'null' : typeof parsed;
|
|
569
|
+
die(
|
|
570
|
+
`${flag} file must be a JSON array of {id, files} records, got type: ${t} — an object map would silently swallow a duplicate agent id`,
|
|
571
|
+
1,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
575
|
+
const rec = parsed[i];
|
|
576
|
+
if (rec === null || typeof rec !== 'object' || Array.isArray(rec)) {
|
|
577
|
+
die(`${flag} file entry #${i} must be an object with a "files" array`, 1);
|
|
578
|
+
}
|
|
579
|
+
if (!Array.isArray(rec.files) || !rec.files.every((f) => typeof f === 'string')) {
|
|
580
|
+
die(`${flag} file entry #${i} ("${rec.id ?? '<unnamed>'}") must have a "files" string array`, 1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return parsed;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The repo's tracked files, for {@link findScopeCollisions}' glob∩glob witness
|
|
588
|
+
* stage. Spawned HERE and injected as a parameter because
|
|
589
|
+
* `scripts/lib/scope-gate.mjs` is hook-safe (pure, sync, no I/O, no spawn) and
|
|
590
|
+
* `hooks/enforce-scope.mjs` reaches it on a hot path.
|
|
591
|
+
*
|
|
592
|
+
* An unavailable git (not a repo, git missing, huge output) is NOT an error:
|
|
593
|
+
* the library documents `knownFiles` as optional — stage 3a simply has fewer
|
|
594
|
+
* witnesses and the prefix fallback of stage 3b carries the load. Silent by
|
|
595
|
+
* design: a WARN here would print on the success path of a mode whose contract
|
|
596
|
+
* is "quiet when clean".
|
|
597
|
+
*
|
|
598
|
+
* @returns {string[]}
|
|
599
|
+
*/
|
|
600
|
+
function knownRepoFiles() {
|
|
601
|
+
const opts = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 };
|
|
602
|
+
try {
|
|
603
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], opts).trim();
|
|
604
|
+
if (!root) return [];
|
|
605
|
+
// -z: NUL-separated. Without it git QUOTES paths containing non-ASCII or
|
|
606
|
+
// special characters, and a quoted path would never match a scope entry.
|
|
607
|
+
return execFileSync('git', ['ls-files', '-z'], { ...opts, cwd: root })
|
|
608
|
+
.split('\0')
|
|
609
|
+
.filter((f) => f.length > 0);
|
|
610
|
+
} catch {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Assert that no file is claimed by two agents of the SAME wave (#1020).
|
|
617
|
+
* Exits 1 with one message per collision (plus one per duplicate agent id);
|
|
618
|
+
* returns silently when the wave is clean.
|
|
619
|
+
*
|
|
620
|
+
* @param {string} sidecarPath
|
|
621
|
+
*/
|
|
622
|
+
function assertDisjointOrDie(sidecarPath) {
|
|
623
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--assert-disjoint');
|
|
624
|
+
const { ok, collisions, duplicateIds } = findScopeCollisions(agentScopes, {
|
|
625
|
+
knownFiles: knownRepoFiles(),
|
|
626
|
+
});
|
|
627
|
+
if (ok) return;
|
|
628
|
+
|
|
629
|
+
// Duplicate ids FIRST: they are a malformed plan, and a reader who fixes them
|
|
630
|
+
// may well change which collisions remain.
|
|
631
|
+
for (const id of duplicateIds) {
|
|
632
|
+
process.stderr.write(
|
|
633
|
+
`ERROR: duplicate agent id in ${sidecarPath}: "${id}" — ids must be unique per wave; a copy-paste duplicate hides one agent's scope from every per-agent check\n`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
for (const c of collisions) {
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
`ERROR: wave scope collision (${c.kind}): agents "${c.a}" and "${c.b}" both claim [${c.evidence.join(', ')}]\n`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`ERROR: ${collisions.length} scope collision(s), ${duplicateIds.length} duplicate id(s) — every file must belong to exactly ONE agent per wave (#1020; .claude/rules/parallel-sessions.md § Decision Tree)\n`,
|
|
643
|
+
);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* QUERY MODE (#1020): print `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
649
|
+
* as JSON on stdout, using the MANIFEST'S own role. Replaces the manifest echo —
|
|
650
|
+
* see the STDOUT CONTRACT note in the file header.
|
|
651
|
+
*
|
|
652
|
+
* @param {Record<string, unknown>} obj - the already schema-validated wave-scope object
|
|
653
|
+
* @param {string} sidecarPath
|
|
654
|
+
*/
|
|
655
|
+
function emitUnion(obj, sidecarPath) {
|
|
656
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--union');
|
|
657
|
+
const allowedPaths = expandScopeTestSiblings(unionFileScopes(agentScopes), { role: obj.role });
|
|
658
|
+
process.stdout.write(`${JSON.stringify(allowedPaths, null, 2)}\n`);
|
|
659
|
+
}
|
|
660
|
+
|
|
417
661
|
/**
|
|
418
662
|
* Main validation entry point. Reads input, validates, exits with appropriate code.
|
|
419
663
|
* @param {string} input - raw JSON string
|
|
420
664
|
* @param {string|null} [assertSubsetPath] - optional agent fileScope file for the #796 subset assertion
|
|
421
665
|
* @param {boolean} [expandTestSiblings] - opt-in #970 test-sibling coverage assertion
|
|
666
|
+
* @param {string|null} [assertDisjointPath] - optional per-agent scope sidecar for the #1020 collision check
|
|
667
|
+
* @param {string|null} [unionPath] - optional per-agent scope sidecar for the #1020 union query mode
|
|
422
668
|
*/
|
|
423
|
-
function validate(
|
|
669
|
+
function validate(
|
|
670
|
+
input,
|
|
671
|
+
assertSubsetPath = null,
|
|
672
|
+
expandTestSiblings = false,
|
|
673
|
+
assertDisjointPath = null,
|
|
674
|
+
unionPath = null,
|
|
675
|
+
) {
|
|
424
676
|
const obj = parseJson(input);
|
|
425
677
|
const errors = [];
|
|
426
678
|
const warnings = [];
|
|
@@ -446,9 +698,26 @@ function validate(input, assertSubsetPath = null, expandTestSiblings = false) {
|
|
|
446
698
|
assertSubsetOrDie(obj, assertSubsetPath, expandTestSiblings);
|
|
447
699
|
}
|
|
448
700
|
|
|
701
|
+
// #1020 — collision check runs AFTER the #796/#970 assertions, for the same
|
|
702
|
+
// reason #970 runs after #796 (see assertSubsetOrDie): a manifest that
|
|
703
|
+
// violates BOTH the subset relation and disjointness must keep the older,
|
|
704
|
+
// byte-pinned subset message. Only ever ADDS a failure mode.
|
|
705
|
+
if (assertDisjointPath) {
|
|
706
|
+
assertDisjointOrDie(assertDisjointPath);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// #1020 QUERY MODE — replaces the echo below; see the STDOUT CONTRACT note in
|
|
710
|
+
// the file header. Last, so every assertion above still gates it.
|
|
711
|
+
if (unionPath) {
|
|
712
|
+
emitUnion(obj, unionPath);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
449
716
|
// Echo validated JSON to stdout (trailing newline normalised)
|
|
450
717
|
process.stdout.write(input.endsWith('\n') ? input : input + '\n');
|
|
451
718
|
}
|
|
452
719
|
|
|
453
|
-
const { assertSubset, expandTestSiblings, positionals } = parseArgs(
|
|
454
|
-
|
|
720
|
+
const { assertSubset, expandTestSiblings, assertDisjoint, union, positionals } = parseArgs(
|
|
721
|
+
process.argv,
|
|
722
|
+
);
|
|
723
|
+
validate(readInput(positionals[0]), assertSubset, expandTestSiblings, assertDisjoint, union);
|
package/scripts/vault-mirror.mjs
CHANGED
|
@@ -55,7 +55,8 @@ import { createInterface } from 'node:readline';
|
|
|
55
55
|
import { createReadStream } from 'node:fs';
|
|
56
56
|
import { pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
|
-
import { processLearning, processSession } from './lib/vault-mirror/process.mjs';
|
|
58
|
+
import { processLearning, processSession, getMaskerStats } from './lib/vault-mirror/process.mjs';
|
|
59
|
+
import { emitEvent } from './lib/events.mjs';
|
|
59
60
|
import { autoCommitVaultMirror } from './lib/vault-mirror/auto-commit.mjs';
|
|
60
61
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
61
62
|
import { resolveRepoNamespace } from './lib/vault-mirror/namespace.mjs';
|
|
@@ -403,6 +404,30 @@ async function main() {
|
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
406
|
|
|
407
|
+
// ── Masking telemetry (#1025) ───────────────────────────────────────────────
|
|
408
|
+
//
|
|
409
|
+
// Emitted UNCONDITIONALLY, exactly once per channel run, and HERE — at the end
|
|
410
|
+
// of the run rather than at the lazy build site inside process.mjs. The build
|
|
411
|
+
// site is only reached once a record is actually processed, so a run over an
|
|
412
|
+
// empty/fully-skipped source would emit nothing and "the masker never ran" would
|
|
413
|
+
// be indistinguishable from "this channel has no masker wired". Placed BEFORE
|
|
414
|
+
// the --strict-schema abort so a failing run still reports its masking posture.
|
|
415
|
+
//
|
|
416
|
+
// Counts only — never a needle, never a prefix of one, never masked text.
|
|
417
|
+
// Best-effort: a telemetry write must never be the reason a mirror run fails.
|
|
418
|
+
try {
|
|
419
|
+
const maskerStats = getMaskerStats();
|
|
420
|
+
await emitEvent('orchestrator.secret_masker.applied', {
|
|
421
|
+
channel: 'vault-mirror',
|
|
422
|
+
needle_count: maskerStats.needleCount,
|
|
423
|
+
records: maskerStats.records,
|
|
424
|
+
hits: maskerStats.hits,
|
|
425
|
+
dry_run: dryRun,
|
|
426
|
+
});
|
|
427
|
+
} catch {
|
|
428
|
+
// Silent no-op — see the note above.
|
|
429
|
+
}
|
|
430
|
+
|
|
406
431
|
// --strict-schema: abort with exit 1 when any entry was skipped-invalid.
|
|
407
432
|
// Useful in CI to catch producer-side schema drift early (issue #249).
|
|
408
433
|
if (strictSchema && skippedInvalidCount > 0) {
|
|
@@ -36,11 +36,11 @@ the GitHub mirror's main-branch SHA so the operator can confirm parity.
|
|
|
36
36
|
```bash
|
|
37
37
|
prev=""
|
|
38
38
|
while true; do
|
|
39
|
-
s=$(glab ci status
|
|
40
|
-
cur=$(jq -r '.[] | select(.status!="running" and .status!="pending") | "\(.name): \(.status)"' <<<"$s" 2>/dev/null | sort)
|
|
39
|
+
s=$(glab ci status -R <OWNER>/<REPO> --output json 2>/dev/null || echo '{"jobs":[]}')
|
|
40
|
+
cur=$(jq -r '.jobs[] | select(.status!="running" and .status!="pending") | "\(.name): \(.status)"' <<<"$s" 2>/dev/null | sort)
|
|
41
41
|
comm -13 <(echo "$prev") <(echo "$cur")
|
|
42
42
|
prev=$cur
|
|
43
|
-
jq -e 'all(.status=="success" or .status=="failed" or .status=="canceled" or .status=="skipped")' <<<"$s" >/dev/null 2>&1 && {
|
|
43
|
+
jq -e '(.jobs|length) > 0 and (.jobs | all(.status=="success" or .status=="failed" or .status=="canceled" or .status=="skipped"))' <<<"$s" >/dev/null 2>&1 && {
|
|
44
44
|
sha=$(gh api repos/Kanevry/session-orchestrator/commits/main --jq '.sha' 2>/dev/null || echo "(mirror unreachable)")
|
|
45
45
|
echo "GitHub mirror main: $sha"
|
|
46
46
|
break
|
|
@@ -55,8 +55,28 @@ done
|
|
|
55
55
|
SHA — silence at the end means glab JSON parsing failed (the `||` fallbacks
|
|
56
56
|
prevent the whole loop from dying).
|
|
57
57
|
|
|
58
|
+
**Probed 2026-08-14 (glab 1.91.0), three corrections — #1022.** The snippet
|
|
59
|
+
above was silence-is-not-success in its own right until that date, and each
|
|
60
|
+
half failed into the next one's fallback:
|
|
61
|
+
|
|
62
|
+
- `--pipeline-id LATEST` is not a glab flag on any `ci` subcommand
|
|
63
|
+
(`ERROR Unknown flag`, exit 1), so `$s` was always the `||` fallback. There is
|
|
64
|
+
no replacement — the argument-less form already selects the current branch's
|
|
65
|
+
pipeline. Avoid `--branch=<name>` here: it pins a snapshot taken when the
|
|
66
|
+
monitor was armed.
|
|
67
|
+
- The payload is an OBJECT (`{"jobs":[…],"pipeline":{…}}`), so `.[]` raised
|
|
68
|
+
`Cannot index array with string "status"` (jq exit 5) on every real response.
|
|
69
|
+
The accessor is `.jobs[]`.
|
|
70
|
+
- `all(…)` over the empty fallback returns **true**, so the loop broke and
|
|
71
|
+
printed the mirror SHA on its FIRST iteration whenever glab hiccuped — a
|
|
72
|
+
transient network error read as "pipeline finished". The `(.jobs|length) > 0`
|
|
73
|
+
guard is what makes the fallback non-terminal; verified by running the
|
|
74
|
+
terminal test against `{"jobs":[]}` (exit 1 = keep watching) versus the old
|
|
75
|
+
form against `[]` (exit 0 = break).
|
|
76
|
+
|
|
58
77
|
**GitHub-mirror equivalent.** When the pipeline is GitHub-Actions-native (PR
|
|
59
|
-
checks rather than a GitLab pipeline), use
|
|
78
|
+
checks rather than a GitLab pipeline), use
|
|
79
|
+
`gh pr checks -R <OWNER>/<REPO> <pr> --watch --fail-fast`
|
|
60
80
|
as the `command` source — it streams each check transition and exits non-zero on
|
|
61
81
|
the first failure, so the terminal state is never silent.
|
|
62
82
|
|
|
@@ -219,3 +219,20 @@ Net: `pid` (field notes above) stays forensic-only; `last_heartbeat` freshness i
|
|
|
219
219
|
### Schema v1 → v2 backward-compat
|
|
220
220
|
|
|
221
221
|
Readers (e.g., `readLock()` in `session-lock.mjs`, `discoverActiveSessions()`) MUST tolerate absent `last_heartbeat` and `semantic_session_id` fields (v1 locks written before Epic #583). When `last_heartbeat` is absent, fall back to TTL-based expiry from `started_at`. When `semantic_session_id` is absent, treat as unknown.
|
|
222
|
+
|
|
223
|
+
#### Schema v1 Sunset — evaluated 2026-08-15, tolerance RETAINED (#595)
|
|
224
|
+
|
|
225
|
+
The 90-day sunset window from Epic #583 (target 2026-08-25) came due and the removal was evaluated against the live fleet. **Verdict: keep the three reader tolerances; the blocker is not v1 data, it is a second production copy of the rule.**
|
|
226
|
+
|
|
227
|
+
**Precondition — zero v1 artefacts on disk (measured 2026-08-15, this host):**
|
|
228
|
+
|
|
229
|
+
- `find ~/Projects ~/.claude ~/.config /tmp/claude-501 -name 'session.lock' -not -path '*/node_modules/*'` → **12 files, 12/12 carry a non-empty `last_heartbeat`** (0 v1).
|
|
230
|
+
- `~/.config/session-orchestrator/sessions/active/*.json` → **3 entries, 3/3 carry the `mode` key** (0 v1).
|
|
231
|
+
- The only co-installed older plugin build (`~/.claude/plugins/cache/session-orchestrator/session-orchestrator/3.13.0`) already writes `last_heartbeat` (`session-lock.mjs:195`) and `mode` (`session-registry.mjs:209`) — **no v1 writer remains on this host.**
|
|
232
|
+
|
|
233
|
+
**Why the branches stay anyway:**
|
|
234
|
+
|
|
235
|
+
1. **`parseLock()` / `isLockLive()` — the rule is duplicated.** `scripts/lib/harness-audit/categories/category4.mjs` `lockIsLive()` inlines the same `last_heartbeat ?? started_at` fallback, and `tests/lib/lock-ttl-parity.test.mjs` asserts the mirror and the SSOT return identical verdicts *for a v1 lock*. Dropping it in `session-lock.mjs` alone breaks that parity by construction. A measured removal attempt turned **18 tests red across 4 files** (`session-discovery` 9, `session-discovery-fallback` 6, `lock-ttl-parity` 1, `on-session-start` 2) — all outside the lock/registry module pair, all seeding v1-shaped fixtures.
|
|
236
|
+
2. **`_validEntry()` optional `mode` — removal is a net safety LOSS.** Rejecting a mode-less registry entry drops a **live peer** from `readRegistry()`, making it invisible to the exclusivity matrix. An absent `mode` already degrades to the `parallel-ok` bucket, so strictening buys no detection and costs peer visibility — the wrong direction under `.claude/rules/development.md` § Guard & Threshold Design.
|
|
237
|
+
|
|
238
|
+
**What a real sunset needs (co-change set, one atomic MR):** `scripts/lib/session-lock.mjs` + `scripts/lib/harness-audit/categories/category4.mjs` (the mirror) + fixture updates in `tests/lib/session-discovery.test.mjs`, `tests/lib/session-discovery-fallback.test.mjs`, `tests/lib/lock-ttl-parity.test.mjs`, `tests/hooks/on-session-start.test.mjs`. Deleting the *mirror* in favour of importing the SSOT is the durable fix — the duplication, not the v1 data, is what keeps this class alive. The registry item should be closed as won't-do per point 2.
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-md-drift-check
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when detecting drift between CLAUDE.md (or AGENTS.md, the Codex CLI alias) / _meta narrative and
|
|
5
|
+
live repository state. Ten checks: absolute-path resolution, 01-projects/ count claims, issue-reference
|
|
6
|
+
freshness, session-file existence, command-count sync, session-config-parity (mandatory template keys =
|
|
7
|
+
error, opt-in gaps = warning), vault-dir-parity (CLAUDE.md vs AGENTS.md), generated-rule-staleness
|
|
8
|
+
(WARN-only), rule-scoping (paths:/globs: frontmatter defects, dangling rule citations, zero-match
|
|
9
|
+
globs), and docs-parity (docs/components.md count-claims vs on-disk counts, template-vs-reference
|
|
10
|
+
config-key parity, stale .claude/metrics/ paths). Full per-check spec in the body table. Invoked as an
|
|
11
|
+
opt-in session-end phase; mirrors vault-sync's lean JSON+exit-code contract.
|
|
4
12
|
model: haiku
|
|
5
13
|
---
|
|
6
14
|
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when encountering any bug, test failure, build break, or unexpected behavior — runs a 4-phase
|
|
5
|
+
systematic debugging process before proposing any fix. Iron Law: NO FIXES WITHOUT ROOT CAUSE
|
|
6
|
+
INVESTIGATION FIRST. Produces a `.orchestrator/debug/` artifact the fixer agent must reference.
|
|
4
7
|
model: inherit
|
|
5
8
|
color: red
|
|
6
9
|
tools: Read, Grep, Glob, Bash, Write
|
|
@@ -79,7 +79,7 @@ Used for a single finding from a single probe.
|
|
|
79
79
|
**CLI Example:**
|
|
80
80
|
```bash
|
|
81
81
|
# GitHub
|
|
82
|
-
gh issue create \
|
|
82
|
+
gh issue create -R <OWNER>/<REPO> \
|
|
83
83
|
--title "[Discovery] <finding_title>" \
|
|
84
84
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
85
85
|
--body "$(cat <<'EOF'
|
|
@@ -88,7 +88,7 @@ EOF
|
|
|
88
88
|
)"
|
|
89
89
|
|
|
90
90
|
# GitLab
|
|
91
|
-
glab issue create \
|
|
91
|
+
glab issue create -R <OWNER>/<REPO> \
|
|
92
92
|
--title "[Discovery] <finding_title>" \
|
|
93
93
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
94
94
|
--description "$(cat <<'EOF'
|
|
@@ -159,7 +159,7 @@ Used when multiple related findings from the same probe or category are grouped
|
|
|
159
159
|
**CLI Example:**
|
|
160
160
|
```bash
|
|
161
161
|
# GitHub
|
|
162
|
-
gh issue create \
|
|
162
|
+
gh issue create -R <OWNER>/<REPO> \
|
|
163
163
|
--title "[Discovery] <category> audit: <summary>" \
|
|
164
164
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
165
165
|
--body "$(cat <<'EOF'
|
|
@@ -168,7 +168,7 @@ EOF
|
|
|
168
168
|
)"
|
|
169
169
|
|
|
170
170
|
# GitLab
|
|
171
|
-
glab issue create \
|
|
171
|
+
glab issue create -R <OWNER>/<REPO> \
|
|
172
172
|
--title "[Discovery] <category> audit: <summary>" \
|
|
173
173
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
174
174
|
--description "$(cat <<'EOF'
|
|
@@ -58,9 +58,9 @@ Grep pattern: (TODO|FIXME|HACK|XXX|TEMP|WORKAROUND)[\s:()\-]
|
|
|
58
58
|
For each match, check whether a corresponding VCS issue exists:
|
|
59
59
|
```bash
|
|
60
60
|
# Search for issue referencing the annotation text
|
|
61
|
-
gh issue list --search "<annotation text>" --limit 5
|
|
61
|
+
gh issue list -R <OWNER>/<REPO> --search "<annotation text>" --limit 5
|
|
62
62
|
# or
|
|
63
|
-
glab issue list --search "<annotation text>" --per-page 5
|
|
63
|
+
glab issue list -R <OWNER>/<REPO> --search "<annotation text>" --per-page 5
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Flag annotations with no corresponding issue.
|