session-orchestrator 3.20.0 → 3.22.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/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -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 +7 -5
- 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/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- 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 +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- 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 +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- 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/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- 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/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -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-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- 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-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -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 +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- 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/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- 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 +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- 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 +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -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
|
|
|
@@ -249,12 +259,23 @@ if (runCheck('check-owner-leakage.mjs') !== 0) checkFailed = 1;
|
|
|
249
259
|
process.stdout.write('\n');
|
|
250
260
|
if (runCheck('check-rules.mjs') !== 0) checkFailed = 1;
|
|
251
261
|
|
|
262
|
+
process.stdout.write('\n');
|
|
263
|
+
if (runCheck('check-auq-clarity.mjs') !== 0) checkFailed = 1;
|
|
264
|
+
|
|
252
265
|
process.stdout.write('\n');
|
|
253
266
|
if (runCheck('check-unicode-safety.mjs') !== 0) checkFailed = 1;
|
|
254
267
|
|
|
255
268
|
process.stdout.write('\n');
|
|
256
269
|
if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
|
|
257
270
|
|
|
271
|
+
// FAIL-capable, unlike the WARN-only censuses below: R2+R4 were measured at
|
|
272
|
+
// 1 hit / 1 TP / 0 FP each against the full test corpus, so this check cannot be
|
|
273
|
+
// red on arrival for anything but a real instance of the class. The alternatives
|
|
274
|
+
// (R1 at 67 FP, R3 at 47 FP) are exactly the shape that gets demoted to WARN and
|
|
275
|
+
// then ignored — see the rule table in the check's header.
|
|
276
|
+
process.stdout.write('\n');
|
|
277
|
+
if (runCheck('check-untracked-test-deps.mjs') !== 0) checkFailed = 1;
|
|
278
|
+
|
|
258
279
|
// WARN-only (v1): the unwired-config-key census reports but never fails the
|
|
259
280
|
// build — see the rationale in the check's header (a blocking gate on today's
|
|
260
281
|
// inventory would be red from day one and get disabled). Exit code is
|
|
@@ -270,6 +291,49 @@ runCheck('check-unwired-features.mjs');
|
|
|
270
291
|
process.stdout.write('\n');
|
|
271
292
|
runCheck('check-learning-provenance.mjs');
|
|
272
293
|
|
|
294
|
+
// WARN-only (#971), same rationale as the two censuses above: a `gh`/`glab` call
|
|
295
|
+
// without `--repo`/`-R` resolves its target project from the ambient cwd remote —
|
|
296
|
+
// silently the wrong project in a sibling worktree, an /autopilot child, or a repo
|
|
297
|
+
// whose origin is a fork — and a gate that blocks on a backlog no single pass can
|
|
298
|
+
// drain gets switched off rather than obeyed. What WARN buys is that each newly
|
|
299
|
+
// added bare call site shows up in every validator run while the sweep drains the
|
|
300
|
+
// rest. No headcount is quoted here on purpose: the live number moves with every
|
|
301
|
+
// commit, and only the check's own `--json` summary can state it as of a SHA. The
|
|
302
|
+
// exit code is deliberately ignored; a tool error still surfaces because that path
|
|
303
|
+
// prints FAIL: lines, which runCheck tallies into totalFail.
|
|
304
|
+
process.stdout.write('\n');
|
|
305
|
+
runCheck('check-vcs-repo-flag.mjs');
|
|
306
|
+
|
|
307
|
+
// WARN-only (#1023): a `gh`/`glab` command cited in docs that no released CLI
|
|
308
|
+
// ever had — `glab repo edit --visibility`, `glab group list` — costs an
|
|
309
|
+
// operator a failed command and a re-derivation. The oracle is the CLI's own
|
|
310
|
+
// `--help` COMMANDS section, never an exit code (`glab repo <anything> --help`
|
|
311
|
+
// exits 0). WARN rather than FAIL because that oracle is the LOCALLY installed
|
|
312
|
+
// binary: a version skew must not red an unrelated commit, and a missing binary
|
|
313
|
+
// SKIPs. The exit code is deliberately ignored; a tool error still surfaces
|
|
314
|
+
// because that path prints FAIL: lines, which runCheck tallies into totalFail.
|
|
315
|
+
process.stdout.write('\n');
|
|
316
|
+
runCheck('check-doc-cli-commands.mjs');
|
|
317
|
+
|
|
318
|
+
// WARN-only: a state-mutating `git` call in tests/ that names no target resolves
|
|
319
|
+
// its destination from the ambient cwd (or an inherited GIT_DIR) — the 2026-08-19
|
|
320
|
+
// incident, where fixture commits, a fixture remote and a fixture identity landed
|
|
321
|
+
// in the REAL .git and two commits reached both remotes with the wrong author.
|
|
322
|
+
// WARN rather than FAIL is a MEASURED posture, not caution: the v1 rule reported
|
|
323
|
+
// 11 findings against tests/ and all 11 were false positives (`git init <dir>`,
|
|
324
|
+
// whose positional IS the target). After that refinement the live corpus reports
|
|
325
|
+
// 0 findings — i.e. zero measured true positives to weigh against a textual
|
|
326
|
+
// detector's residual false-positive surface (the variable argv arrays it cannot
|
|
327
|
+
// judge; the live count is in its own PASS line, never restated here).
|
|
328
|
+
// check-untracked-test-deps.mjs above earned FAIL-capability on a
|
|
329
|
+
// measured 1 TP / 0 FP; this check has not, and a single FAIL: line here reds the
|
|
330
|
+
// whole validator. Ratchet to FAIL once a live true positive has been observed
|
|
331
|
+
// and the GIT_DIR-inheritance gap (reported as `gitDirInheritable`) is closed
|
|
332
|
+
// centrally. The exit code is deliberately ignored; a tool error still surfaces
|
|
333
|
+
// because that path prints FAIL: lines, which runCheck tallies into totalFail.
|
|
334
|
+
process.stdout.write('\n');
|
|
335
|
+
runCheck('check-test-git-config-target.mjs');
|
|
336
|
+
|
|
273
337
|
// ---------------------------------------------------------------------------
|
|
274
338
|
// Summary
|
|
275
339
|
// ---------------------------------------------------------------------------
|
|
@@ -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,58 @@ 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
|
+
* An EMPTY value is the same failure class and is refused for the same reason.
|
|
119
|
+
* `--assert-disjoint ""` is what a failed `$(...)` capture of the materializer's
|
|
120
|
+
* stdout produces; since the mode is gated on a truthy path, the empty string
|
|
121
|
+
* silently skipped the collision check and still exited 0 (#1083).
|
|
122
|
+
*
|
|
123
|
+
* @param {string[]} argv
|
|
124
|
+
* @param {number} i - index of the FLAG token
|
|
125
|
+
* @param {string} flag - the flag name, for the error message
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function flagValue(argv, i, flag) {
|
|
129
|
+
const value = argv[i + 1];
|
|
130
|
+
if (value === undefined || value === '' || value.startsWith('--')) {
|
|
131
|
+
die(`${flag} requires a file-path argument`, 1);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
57
136
|
/**
|
|
58
137
|
* Parse CLI flags out of argv, leaving positional args behind.
|
|
59
138
|
*
|
|
60
|
-
* Recognised: `--assert-subset <path>` (#796)
|
|
61
|
-
* (#
|
|
62
|
-
* wave-scope.json file path), preserving
|
|
63
|
-
* input file.
|
|
139
|
+
* Recognised: `--assert-subset <path>` (#796), `--expand-test-siblings` (#970),
|
|
140
|
+
* `--assert-disjoint <path>` and `--union <path>` (#1020). Everything else is
|
|
141
|
+
* treated as a positional argument (the wave-scope.json file path), preserving
|
|
142
|
+
* legacy behaviour where argv[2] is the input file.
|
|
143
|
+
*
|
|
144
|
+
* The #1020 branches sit BEFORE the positional fallback, as their own `else if`
|
|
145
|
+
* arms: routed through the fallback instead, `--assert-disjoint` would be read
|
|
146
|
+
* as a wave-scope.json path and the mode would never run.
|
|
64
147
|
*
|
|
65
148
|
* @param {string[]} argv - full process.argv
|
|
66
|
-
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
149
|
+
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
150
|
+
* assertDisjoint: string|null, union: string|null, positionals: string[] }}
|
|
67
151
|
*/
|
|
68
152
|
function parseArgs(argv) {
|
|
69
153
|
const positionals = [];
|
|
70
154
|
let assertSubset = null;
|
|
71
155
|
let expandTestSiblings = false;
|
|
156
|
+
let assertDisjoint = null;
|
|
157
|
+
let union = null;
|
|
72
158
|
for (let i = 2; i < argv.length; i++) {
|
|
73
159
|
const a = argv[i];
|
|
74
160
|
if (a === '--expand-test-siblings') {
|
|
@@ -79,11 +165,17 @@ function parseArgs(argv) {
|
|
|
79
165
|
die('--assert-subset requires a file-path argument', 1);
|
|
80
166
|
}
|
|
81
167
|
i++; // consume the value
|
|
168
|
+
} else if (a === '--assert-disjoint') {
|
|
169
|
+
assertDisjoint = flagValue(argv, i, '--assert-disjoint');
|
|
170
|
+
i++; // consume the value
|
|
171
|
+
} else if (a === '--union') {
|
|
172
|
+
union = flagValue(argv, i, '--union');
|
|
173
|
+
i++; // consume the value
|
|
82
174
|
} else {
|
|
83
175
|
positionals.push(a);
|
|
84
176
|
}
|
|
85
177
|
}
|
|
86
|
-
return { assertSubset, expandTestSiblings, positionals };
|
|
178
|
+
return { assertSubset, expandTestSiblings, assertDisjoint, union, positionals };
|
|
87
179
|
}
|
|
88
180
|
|
|
89
181
|
/**
|
|
@@ -259,6 +351,27 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
259
351
|
errors.push(`allowedPaths must be an array, got type: ${ap === null ? 'null' : typeof ap}`);
|
|
260
352
|
return;
|
|
261
353
|
}
|
|
354
|
+
// #1057 — an empty union under a WRITABLE role. WARN, never error, and the
|
|
355
|
+
// distinction is measured rather than stylistic: `skills/wave-executor/wave-loop.md`
|
|
356
|
+
// § Scope Manifest deliberately feeds a skeleton with `"allowedPaths": []`
|
|
357
|
+
// through THIS validator in `--assert-disjoint` and `--union` mode, BEFORE the
|
|
358
|
+
// union exists to be written. An error would break the documented procedure
|
|
359
|
+
// that produces the very field it complains about.
|
|
360
|
+
//
|
|
361
|
+
// Named ceiling (BV-004): the warning therefore also fires on that legitimate
|
|
362
|
+
// skeleton run — one stderr line on a happy path, accepted because the
|
|
363
|
+
// alternative is a mode-conditional warning, i.e. a second place that has to
|
|
364
|
+
// enumerate the modes correctly. Revisit if a third empty-skeleton mode lands.
|
|
365
|
+
if (ap.length === 0 && typeof obj.role === 'string' && obj.role.trim().length > 0
|
|
366
|
+
&& !isReadOnlyWaveRole(obj.role)) {
|
|
367
|
+
warnings.push(
|
|
368
|
+
`allowedPaths is empty for role "${obj.role}" — every write in this wave will be DENIED by ` +
|
|
369
|
+
`hooks/enforce-scope.mjs. Empty is intentional only for a read-only role ` +
|
|
370
|
+
`(${READ_ONLY_WAVE_ROLES.join(', ')}); for a writable role it usually means the coordinator's ` +
|
|
371
|
+
`--union step did not complete. Expected while validating the pre-union skeleton; otherwise ` +
|
|
372
|
+
`re-run --union and rewrite the manifest.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
262
375
|
for (const entry of ap) {
|
|
263
376
|
if (typeof entry !== 'string' || entry.length === 0) {
|
|
264
377
|
errors.push('allowedPaths contains empty string');
|
|
@@ -414,13 +527,157 @@ function assertSubsetOrDie(obj, fileScopePath, expandTestSiblings = false) {
|
|
|
414
527
|
}
|
|
415
528
|
}
|
|
416
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Read + shape-check the per-agent scope SIDECAR shared by `--assert-disjoint`
|
|
532
|
+
* and `--union` (#1020). Exits on any defect; returns the records on success.
|
|
533
|
+
*
|
|
534
|
+
* Exit codes mirror {@link assertSubsetOrDie} exactly: 2 for I/O (missing, not a
|
|
535
|
+
* regular file, unreadable), 1 for every content defect.
|
|
536
|
+
*
|
|
537
|
+
* ## Why the shape check is STRICTER than the library's tolerance
|
|
538
|
+
* `findScopeCollisions` / `unionFileScopes` are fail-closed and never throw:
|
|
539
|
+
* they SKIP a member that is not an object, and treat a missing `files` as `[]`.
|
|
540
|
+
* That is right for a hook-hot-path primitive and wrong for a CLI. A sidecar
|
|
541
|
+
* that spells the key `file:` instead of `files:` would then contribute nothing
|
|
542
|
+
* and both modes would report success on a scope that silently vanished — a
|
|
543
|
+
* path the operator NAMED and the tool did not honour. The absent-input guard
|
|
544
|
+
* belongs in the CLI layer (recorded learning, conf 0.80: a tolerant reader
|
|
545
|
+
* cannot carry a CLI's absent-input guard), so `files` is REQUIRED here.
|
|
546
|
+
*
|
|
547
|
+
* `id` is deliberately NOT required: scope-gate's `normalizeAgentScopes` runs a
|
|
548
|
+
* record with no usable id as `<unnamed#i>` rather than dropping it, precisely
|
|
549
|
+
* because an unreviewed scope is the one that collides. Requiring it here would
|
|
550
|
+
* reject exactly the input that contract was written to keep.
|
|
551
|
+
*
|
|
552
|
+
* @param {string} sidecarPath
|
|
553
|
+
* @param {string} flag - the flag name, for error messages
|
|
554
|
+
* @returns {Array<{id?: string, files: string[]}>}
|
|
555
|
+
*/
|
|
556
|
+
function readAgentScopesOrDie(sidecarPath, flag) {
|
|
557
|
+
if (!existsSync(sidecarPath) || !statSync(sidecarPath).isFile()) {
|
|
558
|
+
die(`Cannot read ${flag} file: ${sidecarPath}`, 2);
|
|
559
|
+
}
|
|
560
|
+
let raw;
|
|
561
|
+
try {
|
|
562
|
+
raw = readFileSync(sidecarPath, 'utf8');
|
|
563
|
+
} catch (err) {
|
|
564
|
+
die(`Cannot read ${flag} file ${sidecarPath}: ${err.message}`, 2);
|
|
565
|
+
}
|
|
566
|
+
let parsed;
|
|
567
|
+
try {
|
|
568
|
+
parsed = JSON.parse(raw);
|
|
569
|
+
} catch {
|
|
570
|
+
die(`${flag} file is not valid JSON: ${sidecarPath}`, 1);
|
|
571
|
+
}
|
|
572
|
+
if (!Array.isArray(parsed)) {
|
|
573
|
+
const t = parsed === null ? 'null' : typeof parsed;
|
|
574
|
+
die(
|
|
575
|
+
`${flag} file must be a JSON array of {id, files} records, got type: ${t} — an object map would silently swallow a duplicate agent id`,
|
|
576
|
+
1,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
580
|
+
const rec = parsed[i];
|
|
581
|
+
if (rec === null || typeof rec !== 'object' || Array.isArray(rec)) {
|
|
582
|
+
die(`${flag} file entry #${i} must be an object with a "files" array`, 1);
|
|
583
|
+
}
|
|
584
|
+
if (!Array.isArray(rec.files) || !rec.files.every((f) => typeof f === 'string')) {
|
|
585
|
+
die(`${flag} file entry #${i} ("${rec.id ?? '<unnamed>'}") must have a "files" string array`, 1);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return parsed;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The repo's tracked files, for {@link findScopeCollisions}' glob∩glob witness
|
|
593
|
+
* stage. Spawned HERE and injected as a parameter because
|
|
594
|
+
* `scripts/lib/scope-gate.mjs` is hook-safe (pure, sync, no I/O, no spawn) and
|
|
595
|
+
* `hooks/enforce-scope.mjs` reaches it on a hot path.
|
|
596
|
+
*
|
|
597
|
+
* An unavailable git (not a repo, git missing, huge output) is NOT an error:
|
|
598
|
+
* the library documents `knownFiles` as optional — stage 3a simply has fewer
|
|
599
|
+
* witnesses and the prefix fallback of stage 3b carries the load. Silent by
|
|
600
|
+
* design: a WARN here would print on the success path of a mode whose contract
|
|
601
|
+
* is "quiet when clean".
|
|
602
|
+
*
|
|
603
|
+
* @returns {string[]}
|
|
604
|
+
*/
|
|
605
|
+
function knownRepoFiles() {
|
|
606
|
+
const opts = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 };
|
|
607
|
+
try {
|
|
608
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], opts).trim();
|
|
609
|
+
if (!root) return [];
|
|
610
|
+
// -z: NUL-separated. Without it git QUOTES paths containing non-ASCII or
|
|
611
|
+
// special characters, and a quoted path would never match a scope entry.
|
|
612
|
+
return execFileSync('git', ['ls-files', '-z'], { ...opts, cwd: root })
|
|
613
|
+
.split('\0')
|
|
614
|
+
.filter((f) => f.length > 0);
|
|
615
|
+
} catch {
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Assert that no file is claimed by two agents of the SAME wave (#1020).
|
|
622
|
+
* Exits 1 with one message per collision (plus one per duplicate agent id);
|
|
623
|
+
* returns silently when the wave is clean.
|
|
624
|
+
*
|
|
625
|
+
* @param {string} sidecarPath
|
|
626
|
+
*/
|
|
627
|
+
function assertDisjointOrDie(sidecarPath) {
|
|
628
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--assert-disjoint');
|
|
629
|
+
const { ok, collisions, duplicateIds } = findScopeCollisions(agentScopes, {
|
|
630
|
+
knownFiles: knownRepoFiles(),
|
|
631
|
+
});
|
|
632
|
+
if (ok) return;
|
|
633
|
+
|
|
634
|
+
// Duplicate ids FIRST: they are a malformed plan, and a reader who fixes them
|
|
635
|
+
// may well change which collisions remain.
|
|
636
|
+
for (const id of duplicateIds) {
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
`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`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
for (const c of collisions) {
|
|
642
|
+
process.stderr.write(
|
|
643
|
+
`ERROR: wave scope collision (${c.kind}): agents "${c.a}" and "${c.b}" both claim [${c.evidence.join(', ')}]\n`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
process.stderr.write(
|
|
647
|
+
`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`,
|
|
648
|
+
);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* QUERY MODE (#1020): print `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
654
|
+
* as JSON on stdout, using the MANIFEST'S own role. Replaces the manifest echo —
|
|
655
|
+
* see the STDOUT CONTRACT note in the file header.
|
|
656
|
+
*
|
|
657
|
+
* @param {Record<string, unknown>} obj - the already schema-validated wave-scope object
|
|
658
|
+
* @param {string} sidecarPath
|
|
659
|
+
*/
|
|
660
|
+
function emitUnion(obj, sidecarPath) {
|
|
661
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--union');
|
|
662
|
+
const allowedPaths = expandScopeTestSiblings(unionFileScopes(agentScopes), { role: obj.role });
|
|
663
|
+
process.stdout.write(`${JSON.stringify(allowedPaths, null, 2)}\n`);
|
|
664
|
+
}
|
|
665
|
+
|
|
417
666
|
/**
|
|
418
667
|
* Main validation entry point. Reads input, validates, exits with appropriate code.
|
|
419
668
|
* @param {string} input - raw JSON string
|
|
420
669
|
* @param {string|null} [assertSubsetPath] - optional agent fileScope file for the #796 subset assertion
|
|
421
670
|
* @param {boolean} [expandTestSiblings] - opt-in #970 test-sibling coverage assertion
|
|
671
|
+
* @param {string|null} [assertDisjointPath] - optional per-agent scope sidecar for the #1020 collision check
|
|
672
|
+
* @param {string|null} [unionPath] - optional per-agent scope sidecar for the #1020 union query mode
|
|
422
673
|
*/
|
|
423
|
-
function validate(
|
|
674
|
+
function validate(
|
|
675
|
+
input,
|
|
676
|
+
assertSubsetPath = null,
|
|
677
|
+
expandTestSiblings = false,
|
|
678
|
+
assertDisjointPath = null,
|
|
679
|
+
unionPath = null,
|
|
680
|
+
) {
|
|
424
681
|
const obj = parseJson(input);
|
|
425
682
|
const errors = [];
|
|
426
683
|
const warnings = [];
|
|
@@ -446,9 +703,26 @@ function validate(input, assertSubsetPath = null, expandTestSiblings = false) {
|
|
|
446
703
|
assertSubsetOrDie(obj, assertSubsetPath, expandTestSiblings);
|
|
447
704
|
}
|
|
448
705
|
|
|
706
|
+
// #1020 — collision check runs AFTER the #796/#970 assertions, for the same
|
|
707
|
+
// reason #970 runs after #796 (see assertSubsetOrDie): a manifest that
|
|
708
|
+
// violates BOTH the subset relation and disjointness must keep the older,
|
|
709
|
+
// byte-pinned subset message. Only ever ADDS a failure mode.
|
|
710
|
+
if (assertDisjointPath) {
|
|
711
|
+
assertDisjointOrDie(assertDisjointPath);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// #1020 QUERY MODE — replaces the echo below; see the STDOUT CONTRACT note in
|
|
715
|
+
// the file header. Last, so every assertion above still gates it.
|
|
716
|
+
if (unionPath) {
|
|
717
|
+
emitUnion(obj, unionPath);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
449
721
|
// Echo validated JSON to stdout (trailing newline normalised)
|
|
450
722
|
process.stdout.write(input.endsWith('\n') ? input : input + '\n');
|
|
451
723
|
}
|
|
452
724
|
|
|
453
|
-
const { assertSubset, expandTestSiblings, positionals } = parseArgs(
|
|
454
|
-
|
|
725
|
+
const { assertSubset, expandTestSiblings, assertDisjoint, union, positionals } = parseArgs(
|
|
726
|
+
process.argv,
|
|
727
|
+
);
|
|
728
|
+
validate(readInput(positionals[0]), assertSubset, expandTestSiblings, assertDisjoint, union);
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
28
|
-
import { join, resolve, dirname } from 'node:path';
|
|
28
|
+
import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
|
|
29
29
|
import { createInterface } from 'node:readline';
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
32
|
assertGlabExists, setVerbose as setGlabVerbose,
|
|
33
|
-
listGroupRepos, checkVaultYaml,
|
|
33
|
+
listGroupRepos, checkVaultYaml,
|
|
34
34
|
} from './lib/vault-backfill/glab.mjs';
|
|
35
35
|
|
|
36
36
|
import {
|
|
@@ -155,11 +155,25 @@ function createVaultFolderStub(vaultDir, slug) {
|
|
|
155
155
|
|
|
156
156
|
// ── Process one repo ──────────────────────────────────────────────────────────
|
|
157
157
|
|
|
158
|
+
/** Derive the complete parent namespace from a canonical GitLab project path. */
|
|
159
|
+
function parentNamespace(repoPath) {
|
|
160
|
+
if (typeof repoPath !== 'string') return 'unknown';
|
|
161
|
+
|
|
162
|
+
const segments = repoPath.split('/');
|
|
163
|
+
if (
|
|
164
|
+
segments.length < 2 ||
|
|
165
|
+
segments.some((segment) => !segment || segment === '.' || segment === '..')
|
|
166
|
+
) {
|
|
167
|
+
return 'unknown';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return segments.slice(0, -1).join('/');
|
|
171
|
+
}
|
|
172
|
+
|
|
158
173
|
function processRepo(entry, vaultDir, templateContent) {
|
|
159
174
|
const { path: repoPath, slug, tier, visibility, group, id } = entry;
|
|
160
175
|
const humanName = slugToHumanName(slug);
|
|
161
|
-
|
|
162
|
-
const owner = applyWrites ? fetchRepoOwner(id) : 'unknown';
|
|
176
|
+
const owner = parentNamespace(repoPath);
|
|
163
177
|
|
|
164
178
|
let renderedContent;
|
|
165
179
|
try {
|
|
@@ -184,7 +198,20 @@ function processRepo(entry, vaultDir, templateContent) {
|
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
// Apply: write to staging directory at <out-dir>/<group>/<repo>/.vault.yaml
|
|
187
|
-
const writePath =
|
|
201
|
+
const writePath = resolve(outDir, repoPath, '.vault.yaml');
|
|
202
|
+
const writePathRelativeToOutDir = relative(outDir, writePath);
|
|
203
|
+
if (
|
|
204
|
+
writePathRelativeToOutDir === '..' ||
|
|
205
|
+
writePathRelativeToOutDir.startsWith(`..${sep}`) ||
|
|
206
|
+
isAbsolute(writePathRelativeToOutDir)
|
|
207
|
+
) {
|
|
208
|
+
const error = 'refusing to write outside staging directory';
|
|
209
|
+
emitAction('write-failed', '', { error });
|
|
210
|
+
process.stderr.write(`[vault-backfill] ERROR: ${error}\n`);
|
|
211
|
+
hadWriteError = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
try {
|
|
189
216
|
mkdirSync(dirname(writePath), { recursive: true });
|
|
190
217
|
writeFileSync(writePath, renderedContent, 'utf8');
|
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
|
|