session-orchestrator 3.21.0 → 3.23.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/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +11 -2
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +269 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/memory-proposal-collector.md +6 -4
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- 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 +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- 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/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +59 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +96 -48
- 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/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- 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
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -277,8 +277,22 @@ if (!existsSync(gatePath)) {
|
|
|
277
277
|
die(`Gate script not found: ${gatePath}`);
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
|
|
281
|
+
// invokes this script as `npm run --silent quality-gate` — which sets it to
|
|
282
|
+
// `silent`. That level then reached the gate's own children: `npm pack
|
|
283
|
+
// --dry-run` emitted ZERO `npm notice` lines instead of 818 (measured
|
|
284
|
+
// 2026-08-22), so the release leakage test saw an empty listing, and several
|
|
285
|
+
// validate-plugin/e2e tests that shell out to npm went red the same way. Every
|
|
286
|
+
// one of them passes under a bare `npm test` and fails only INSIDE the gate,
|
|
287
|
+
// which is the hardest shape to diagnose and cost an hour of chasing phantoms.
|
|
288
|
+
//
|
|
289
|
+
// Pinned rather than deleted: an explicit level makes the gate's children
|
|
290
|
+
// independent of how the gate itself was invoked. `--silent` still does its real
|
|
291
|
+
// job — keeping THIS process's stdout to the single JSON envelope — because the
|
|
292
|
+
// children's output is captured by `runCheck`, never streamed.
|
|
280
293
|
const env = {
|
|
281
294
|
...process.env,
|
|
295
|
+
npm_config_loglevel: 'notice',
|
|
282
296
|
TYPECHECK_CMD,
|
|
283
297
|
TEST_CMD,
|
|
284
298
|
LINT_CMD,
|
package/scripts/token-audit.sh
CHANGED
|
@@ -141,8 +141,15 @@ if [ -f "$CLAUDE_SETTINGS" ]; then
|
|
|
141
141
|
fi
|
|
142
142
|
fi
|
|
143
143
|
|
|
144
|
-
# Count enabled plugins
|
|
145
|
-
|
|
144
|
+
# Count enabled plugins.
|
|
145
|
+
# `|| true` rather than `|| echo "0"`: grep -c ALREADY prints 0 on no-match and
|
|
146
|
+
# still exits 1, so an echo fallback appends a second line — the capture becomes
|
|
147
|
+
# "0\n0", which every numeric test downstream rejects (bash-harness-pitfalls.md §1).
|
|
148
|
+
plugin_count=$(grep -c ': true' "$CLAUDE_SETTINGS" 2>/dev/null || true)
|
|
149
|
+
# NOTE: after the :-0 default an unreadable file prints the same as "zero
|
|
150
|
+
# plugins" — accepted, the only consumer is the informational echo below.
|
|
151
|
+
# The #1077 fix here is the removed double-"0" line, not a failure sentinel.
|
|
152
|
+
plugin_count=${plugin_count:-0}
|
|
146
153
|
echo " Enabled plugins: $plugin_count"
|
|
147
154
|
fi
|
|
148
155
|
echo ""
|
|
@@ -244,6 +244,9 @@ if (runCheck('check-pi-package.mjs') !== 0) checkFailed = 1;
|
|
|
244
244
|
process.stdout.write('\n');
|
|
245
245
|
if (runCheck('check-pi-prompts.mjs') !== 0) checkFailed = 1;
|
|
246
246
|
|
|
247
|
+
process.stdout.write('\n');
|
|
248
|
+
if (runCheck('check-cursor-adapter.mjs') !== 0) checkFailed = 1;
|
|
249
|
+
|
|
247
250
|
process.stdout.write('\n');
|
|
248
251
|
if (runCheck('check-session-plan-routing.mjs') !== 0) checkFailed = 1;
|
|
249
252
|
|
|
@@ -259,6 +262,9 @@ if (runCheck('check-owner-leakage.mjs') !== 0) checkFailed = 1;
|
|
|
259
262
|
process.stdout.write('\n');
|
|
260
263
|
if (runCheck('check-rules.mjs') !== 0) checkFailed = 1;
|
|
261
264
|
|
|
265
|
+
process.stdout.write('\n');
|
|
266
|
+
if (runCheck('check-auq-clarity.mjs') !== 0) checkFailed = 1;
|
|
267
|
+
|
|
262
268
|
process.stdout.write('\n');
|
|
263
269
|
if (runCheck('check-unicode-safety.mjs') !== 0) checkFailed = 1;
|
|
264
270
|
|
|
@@ -115,6 +115,11 @@ function die(msg, code = 1) {
|
|
|
115
115
|
* inherit it: a swallowed flag is silent (the mode never runs, and the caller
|
|
116
116
|
* believes it did), whereas this refusal is loud and one line long.
|
|
117
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
|
+
*
|
|
118
123
|
* @param {string[]} argv
|
|
119
124
|
* @param {number} i - index of the FLAG token
|
|
120
125
|
* @param {string} flag - the flag name, for the error message
|
|
@@ -122,7 +127,7 @@ function die(msg, code = 1) {
|
|
|
122
127
|
*/
|
|
123
128
|
function flagValue(argv, i, flag) {
|
|
124
129
|
const value = argv[i + 1];
|
|
125
|
-
if (value === undefined || value.startsWith('--')) {
|
|
130
|
+
if (value === undefined || value === '' || value.startsWith('--')) {
|
|
126
131
|
die(`${flag} requires a file-path argument`, 1);
|
|
127
132
|
}
|
|
128
133
|
return value;
|
|
@@ -247,6 +252,72 @@ function validateRequired(obj, errors) {
|
|
|
247
252
|
}
|
|
248
253
|
}
|
|
249
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Shape-check ONE optional identifier field: present ⇒ a non-empty string.
|
|
257
|
+
*
|
|
258
|
+
* Returns whether the key was PRESENT at all (regardless of validity), so the
|
|
259
|
+
* caller can distinguish "absent" from "present but malformed" — the two need
|
|
260
|
+
* different treatment and only the first is a warning.
|
|
261
|
+
*
|
|
262
|
+
* An EMPTY string is an error rather than a second flavour of absent, for the
|
|
263
|
+
* reason `sessionAttribution()` (scripts/lib/events.mjs) already states about
|
|
264
|
+
* omitting the key: an empty id satisfies a truthiness check while attributing
|
|
265
|
+
* to nothing. A reader comparing `manifest.session === <own session id>` would
|
|
266
|
+
* then treat the manifest as FOREIGN (ignore it) where the writer meant
|
|
267
|
+
* UNBOUND (enforce it) — the two dispositions are opposites, so the ambiguity
|
|
268
|
+
* is not cosmetic. Absence is the only honest encoding of "not session-bound".
|
|
269
|
+
*
|
|
270
|
+
* @param {Record<string, unknown>} obj
|
|
271
|
+
* @param {string} key
|
|
272
|
+
* @param {string[]} errors
|
|
273
|
+
* @returns {boolean} true when the key is present (valid or not)
|
|
274
|
+
*/
|
|
275
|
+
function validateOptionalSessionId(obj, key, errors) {
|
|
276
|
+
if (!(key in obj) || obj[key] === undefined) return false;
|
|
277
|
+
const value = obj[key];
|
|
278
|
+
const t = value === null ? 'null' : typeof value;
|
|
279
|
+
if (t !== 'string') {
|
|
280
|
+
errors.push(`${key} must be a non-empty string, got type: ${t}`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (/** @type {string} */ (value).length === 0) {
|
|
284
|
+
errors.push(
|
|
285
|
+
`${key} must be a non-empty string, got: "" — an empty id attributes to nothing; omit the key entirely to declare the manifest unbound`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validate the OPTIONAL session binding (#1123): `session` (the raw
|
|
293
|
+
* `session_id` of the session that WROTE this manifest) and its human-readable
|
|
294
|
+
* twin `semantic_session`. Both come from one `sessionAttribution(repoRoot)`
|
|
295
|
+
* call — see `skills/wave-executor/wave-loop.md` § Scope Manifest.
|
|
296
|
+
*
|
|
297
|
+
* Deliberately NOT part of {@link validateRequired}, and that is a compatibility
|
|
298
|
+
* constraint rather than a preference: `wave-scope.json` is a shared
|
|
299
|
+
* working-copy artefact, every manifest written before #1123 lacks the field,
|
|
300
|
+
* and the pre-union skeleton of § Scope Manifest 3.3 is fed through this very
|
|
301
|
+
* validator before the union exists. Requiring it would reject manifests the
|
|
302
|
+
* documented procedure itself produces. The absent case therefore WARNS — the
|
|
303
|
+
* flip to an error belongs to a later release, once no legacy writer remains.
|
|
304
|
+
*
|
|
305
|
+
* @param {Record<string, unknown>} obj
|
|
306
|
+
* @param {string[]} errors
|
|
307
|
+
* @param {string[]} warnings
|
|
308
|
+
*/
|
|
309
|
+
function validateSession(obj, errors, warnings) {
|
|
310
|
+
const present = validateOptionalSessionId(obj, 'session', errors);
|
|
311
|
+
validateOptionalSessionId(obj, 'semantic_session', errors);
|
|
312
|
+
if (!present) {
|
|
313
|
+
warnings.push(
|
|
314
|
+
'no session field — manifest is not session-bound (legacy, #1123), so every session sharing this ' +
|
|
315
|
+
'working copy is enforced against it. The writer derives it from sessionAttribution() — see ' +
|
|
316
|
+
'skills/wave-executor/wave-loop.md § Scope Manifest.',
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
250
321
|
/**
|
|
251
322
|
* Literal filesystem-root forms — POSIX "/" and the Windows equivalents "\"
|
|
252
323
|
* and a bare drive root ("C:\", "C:\\", ...). Checked independently of
|
|
@@ -678,6 +749,7 @@ function validate(
|
|
|
678
749
|
const warnings = [];
|
|
679
750
|
|
|
680
751
|
validateRequired(obj, errors);
|
|
752
|
+
validateSession(obj, errors, warnings);
|
|
681
753
|
validateAllowedPaths(obj, errors, warnings);
|
|
682
754
|
validateBlockedCommands(obj, errors);
|
|
683
755
|
validateGates(obj, errors);
|
|
@@ -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
|
@@ -56,6 +56,7 @@ import { createReadStream } from 'node:fs';
|
|
|
56
56
|
import { pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
58
|
import { processLearning, processSession, getMaskerStats } from './lib/vault-mirror/process.mjs';
|
|
59
|
+
import { emitMirrorEvent, emitMirrorRunEvent } from './lib/vault-mirror/telemetry.mjs';
|
|
59
60
|
import { emitEvent } from './lib/events.mjs';
|
|
60
61
|
import { autoCommitVaultMirror } from './lib/vault-mirror/auto-commit.mjs';
|
|
61
62
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
@@ -105,6 +106,43 @@ export function _normalizeRemote(url) {
|
|
|
105
106
|
.replace(/\/+$/, '');
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// ── Mirror telemetry (#1116) ──────────────────────────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// The mirror run itself used to be SILENT in `.orchestrator/metrics/events.jsonl`
|
|
112
|
+
// (measured 2026-08-23: `jq -r '.event' … | grep -icE 'board|mirror'` → 0 of
|
|
113
|
+
// 28 387 records; the only event this CLI emitted was
|
|
114
|
+
// `orchestrator.secret_masker.applied`). The expensive consequence is the
|
|
115
|
+
// `skipped-invalid` path below: a schema-invalid record is reported on stdout
|
|
116
|
+
// and the process still exits 0, so the affected session ends up WITHOUT a vault
|
|
117
|
+
// note and nothing durable records which one.
|
|
118
|
+
//
|
|
119
|
+
// Design constraints, all load-bearing:
|
|
120
|
+
// - Additive only. The stdout JSON protocol is untouched; consumers parse it.
|
|
121
|
+
// - Best-effort. A telemetry failure must never fail a mirror run, so every
|
|
122
|
+
// emit is wrapped and its rejection swallowed (same posture as the masker
|
|
123
|
+
// emit at the end of main()).
|
|
124
|
+
// - "Absent is not zero" (docs/events-schema.md): a field that was not
|
|
125
|
+
// measured is OMITTED, never written as 0/null. Hence no `path` key here —
|
|
126
|
+
// these emit sites are reached BEFORE any target path is resolved, so there
|
|
127
|
+
// is no path to report. `record_id` is likewise omitted when the record
|
|
128
|
+
// carries neither `id` nor `session_id`; `line` is the fallback locator that
|
|
129
|
+
// is always measured.
|
|
130
|
+
// - Same ledger as the masker emit: `emitEvent` is called 2-arg so both events
|
|
131
|
+
// from one run resolve the SAME destination (`SO_PROJECT_DIR`, i.e.
|
|
132
|
+
// `CLAUDE_PROJECT_DIR` or the CWD walk-up). This CLI has no repo-root flag
|
|
133
|
+
// and deriving one from `--source` would split a single run's telemetry
|
|
134
|
+
// across two ledgers.
|
|
135
|
+
//
|
|
136
|
+
// #1147 moved both emitters into `scripts/lib/vault-mirror/telemetry.mjs` and
|
|
137
|
+
// widened the coverage from "the two skipped-invalid branches" to "every entry,
|
|
138
|
+
// plus one run-level roll-up":
|
|
139
|
+
// - `emitMirrorEvent` is now also called from `process.mjs`'s 18 `emitAction`
|
|
140
|
+
// sites, so `created`/`updated`/every `skipped-*` gets a record too.
|
|
141
|
+
// - `emitMirrorRunEvent` adds the DENOMINATOR. Per-entry records alone cannot
|
|
142
|
+
// distinguish "healthy run over an empty source" from "the emitter is
|
|
143
|
+
// broken" — both write nothing (HR-105). The run event is emitted
|
|
144
|
+
// unconditionally, so `total: 0` is a measured zero.
|
|
145
|
+
|
|
108
146
|
// ── CLI argument parsing ──────────────────────────────────────────────────────
|
|
109
147
|
//
|
|
110
148
|
// Migrated to scripts/lib/cli-flags.mjs (#510). Behaviour changes vs prior
|
|
@@ -285,11 +323,115 @@ if (kind !== 'learning' && kind !== 'session') {
|
|
|
285
323
|
// imports) so they are import-safe and unit-testable; see _resolveCanonicalSuffix
|
|
286
324
|
// / _normalizeRemote there (#607 D2).
|
|
287
325
|
|
|
326
|
+
// ── Run-level accounting + run close-out (#1147) ──────────────────────────────
|
|
327
|
+
//
|
|
328
|
+
// Deliberately OUTSIDE main(): the run event's whole contract is that it is
|
|
329
|
+
// written ONCE PER RUN and that its ABSENCE is the broken-emitter signal
|
|
330
|
+
// (HR-105). Six exits bypass main's normal tail — the three PRE-LOOP aborts at
|
|
331
|
+
// the top of main (missing vault-dir, non-canonical vault, missing source), the
|
|
332
|
+
// malformed-JSON abort and the filesystem-error abort inside the loop (all five
|
|
333
|
+
// `process.exit`, which no `finally` and no `catch` can intercept), and the
|
|
334
|
+
// top-level `main().catch`, which runs in a scope where main's locals no longer
|
|
335
|
+
// exist. Keeping the counters and the emitter out here is what lets all six
|
|
336
|
+
// close the run out through ONE function instead of each re-deriving the
|
|
337
|
+
// payload.
|
|
338
|
+
const runState = {
|
|
339
|
+
/** Non-blank JSONL lines the run ATTEMPTED — the denominator. */
|
|
340
|
+
total: 0,
|
|
341
|
+
/** Entries that produced `skipped-invalid` (validation error or mapper crash). */
|
|
342
|
+
skippedInvalid: 0,
|
|
343
|
+
/** Per-`action` tally, keyed by the same strings the entries wrote to stdout. */
|
|
344
|
+
actions: new Map(),
|
|
345
|
+
/** Latch: the run may only be closed out once. */
|
|
346
|
+
finished: false,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** Count one entry action into {@link runState}. Ignores a non-string action. */
|
|
350
|
+
const tally = (action) => {
|
|
351
|
+
if (typeof action !== 'string' || action.length === 0) return;
|
|
352
|
+
runState.actions.set(action, (runState.actions.get(action) ?? 0) + 1);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Close the run out: emit the run-level roll-up AND the masking roll-up,
|
|
357
|
+
* exactly once. Never throws, never exits — the caller owns the exit code.
|
|
358
|
+
*
|
|
359
|
+
* Both emits are documented as unconditional, and both used to sit only on the
|
|
360
|
+
* happy tail: an abort skipped them, so the very runs an operator most wants
|
|
361
|
+
* counted were the ones that vanished from the ledger, in the one shape
|
|
362
|
+
* ("no record") that the docstring reserves for a broken emitter.
|
|
363
|
+
*
|
|
364
|
+
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'malformed-json'|'filesystem-error'|'unexpected-error'} [aborted]
|
|
365
|
+
* Omitted on a complete run. When present it LABELS the counters as partial:
|
|
366
|
+
* every line after the abort was never attempted, so the classes no longer
|
|
367
|
+
* partition `total`. On the three PRE-LOOP values (#1151) nothing was
|
|
368
|
+
* attempted at all — `total` is a measured 0 and the label is what separates
|
|
369
|
+
* "never started" from "ran over an empty source".
|
|
370
|
+
* @returns {Promise<void>}
|
|
371
|
+
*/
|
|
372
|
+
async function finishRun(aborted) {
|
|
373
|
+
if (runState.finished) return;
|
|
374
|
+
runState.finished = true;
|
|
375
|
+
|
|
376
|
+
const countOf = (action) => runState.actions.get(action) ?? 0;
|
|
377
|
+
const actionBreakdown = Object.fromEntries(runState.actions);
|
|
378
|
+
await emitMirrorRunEvent({
|
|
379
|
+
kind,
|
|
380
|
+
total: runState.total,
|
|
381
|
+
created: countOf('created'),
|
|
382
|
+
updated: countOf('updated'),
|
|
383
|
+
// Every non-failure skip class, summed from the SAME map the breakdown is
|
|
384
|
+
// built from — `skipped-invalid` is deliberately excluded and reported as
|
|
385
|
+
// `failed`, because those are the entries whose session silently ends up
|
|
386
|
+
// without a vault note.
|
|
387
|
+
skipped: [...runState.actions].reduce(
|
|
388
|
+
(sum, [action, n]) =>
|
|
389
|
+
action.startsWith('skipped-') && action !== 'skipped-invalid' ? sum + n : sum,
|
|
390
|
+
0,
|
|
391
|
+
),
|
|
392
|
+
failed: runState.skippedInvalid,
|
|
393
|
+
actionBreakdown,
|
|
394
|
+
dryRun,
|
|
395
|
+
...(aborted ? { aborted } : {}),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ── Masking telemetry (#1025) ───────────────────────────────────────────────
|
|
399
|
+
//
|
|
400
|
+
// Emitted here, at the END of the run, rather than at the lazy build site
|
|
401
|
+
// inside process.mjs. The build site is only reached once a record is actually
|
|
402
|
+
// processed, so a run over an empty/fully-skipped source would emit nothing and
|
|
403
|
+
// "the masker never ran" would be indistinguishable from "this channel has no
|
|
404
|
+
// masker wired".
|
|
405
|
+
//
|
|
406
|
+
// Counts only — never a needle, never a prefix of one, never masked text.
|
|
407
|
+
// Best-effort: a telemetry write must never be the reason a mirror run fails.
|
|
408
|
+
try {
|
|
409
|
+
const maskerStats = getMaskerStats();
|
|
410
|
+
await emitEvent('orchestrator.secret_masker.applied', {
|
|
411
|
+
channel: 'vault-mirror',
|
|
412
|
+
needle_count: maskerStats.needleCount,
|
|
413
|
+
records: maskerStats.records,
|
|
414
|
+
hits: maskerStats.hits,
|
|
415
|
+
dry_run: dryRun,
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
// Silent no-op — see the note above.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
288
422
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
289
423
|
|
|
290
424
|
async function main() {
|
|
425
|
+
// The three PRE-LOOP aborts below (#1151) close the run out through the same
|
|
426
|
+
// `finishRun` every other exit uses. They are the runs that never reached
|
|
427
|
+
// their first entry — a bad vault-dir, a wrong vault, a missing source — and
|
|
428
|
+
// until now they were the only outcomes that left NO record at all, which is
|
|
429
|
+
// the one shape the run event reserves for a broken emitter. Their counters
|
|
430
|
+
// are all `0`, and `aborted` is what makes that zero readable as "never
|
|
431
|
+
// started" rather than "ran over an empty source".
|
|
291
432
|
if (!existsSync(resolve(vaultDir))) {
|
|
292
433
|
process.stderr.write(`vault-mirror: vault-dir not found: ${vaultDir}\n`);
|
|
434
|
+
await finishRun('missing-vault-dir');
|
|
293
435
|
process.exit(2);
|
|
294
436
|
}
|
|
295
437
|
|
|
@@ -307,12 +449,14 @@ async function main() {
|
|
|
307
449
|
process.stderr.write(
|
|
308
450
|
`vault-mirror: refusing to mirror — "${vaultDir}" is not the canonical Meta-Vault (expected git origin ending in one of: ${canonicalSuffixes.join(', ')}; got ${got})\n`,
|
|
309
451
|
);
|
|
452
|
+
await finishRun('vault-not-canonical');
|
|
310
453
|
process.exit(2);
|
|
311
454
|
}
|
|
312
455
|
}
|
|
313
456
|
|
|
314
457
|
if (!existsSync(resolve(source))) {
|
|
315
458
|
process.stderr.write(`vault-mirror: source file not found: ${source}\n`);
|
|
459
|
+
await finishRun('missing-source');
|
|
316
460
|
process.exit(2);
|
|
317
461
|
}
|
|
318
462
|
|
|
@@ -328,7 +472,13 @@ async function main() {
|
|
|
328
472
|
}
|
|
329
473
|
|
|
330
474
|
let lineNum = 0;
|
|
331
|
-
|
|
475
|
+
// The run-level denominator lives in `runState` above: `runState.total` counts
|
|
476
|
+
// every NON-BLANK line the run attempted, so
|
|
477
|
+
// `created + updated + skipped + failed === runState.total` for any run that
|
|
478
|
+
// does not abort — and an aborted run says so with the `aborted` field rather
|
|
479
|
+
// than publishing a partial count as a complete one. The breakdown is keyed by
|
|
480
|
+
// the same `action` string the entry wrote to stdout, so no second vocabulary
|
|
481
|
+
// is introduced.
|
|
332
482
|
const ctx = {
|
|
333
483
|
vaultDir,
|
|
334
484
|
dryRun,
|
|
@@ -343,21 +493,30 @@ async function main() {
|
|
|
343
493
|
lineNum++;
|
|
344
494
|
const trimmed = line.trim();
|
|
345
495
|
if (!trimmed) continue;
|
|
496
|
+
runState.total++;
|
|
346
497
|
|
|
347
498
|
let entry;
|
|
348
499
|
try {
|
|
349
500
|
entry = JSON.parse(trimmed);
|
|
350
501
|
} catch (err) {
|
|
351
502
|
process.stderr.write(`vault-mirror: malformed JSON on line ${lineNum}: ${err.message}\n`);
|
|
503
|
+
// Close the run out BEFORE exiting: `process.exit` runs no `finally`, so
|
|
504
|
+
// without this the abort is the one outcome that leaves no run record —
|
|
505
|
+
// exactly the shape reserved for a broken emitter.
|
|
506
|
+
await finishRun('malformed-json');
|
|
352
507
|
process.exit(1);
|
|
353
508
|
}
|
|
354
509
|
|
|
355
510
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
511
|
+
// Both processors return the `action` string they emitted (every one of
|
|
512
|
+
// their exit paths is an `emitAction` call), so the tally needs no second
|
|
513
|
+
// census of the 18 call sites in process.mjs — a census that would go
|
|
514
|
+
// stale the first time a branch is added.
|
|
515
|
+
const action =
|
|
516
|
+
kind === 'learning'
|
|
517
|
+
? await processLearning(entry, lineNum, ctx)
|
|
518
|
+
: await processSession(entry, lineNum, ctx);
|
|
519
|
+
tally(action);
|
|
361
520
|
} catch (err) {
|
|
362
521
|
// Validation errors (missing required fields) → per-entry skip, not a global failure
|
|
363
522
|
if (err.message.startsWith('vault-mirror:')) {
|
|
@@ -366,7 +525,17 @@ async function main() {
|
|
|
366
525
|
process.stdout.write(
|
|
367
526
|
JSON.stringify({ action: 'skipped-invalid', path: null, kind, id: entryId }) + '\n',
|
|
368
527
|
);
|
|
369
|
-
|
|
528
|
+
runState.skippedInvalid++;
|
|
529
|
+
tally('skipped-invalid');
|
|
530
|
+
await emitMirrorEvent({
|
|
531
|
+
action: 'skipped-invalid',
|
|
532
|
+
kind,
|
|
533
|
+
line: lineNum,
|
|
534
|
+
recordId: entryId,
|
|
535
|
+
skipClass: 'validation',
|
|
536
|
+
reason: err.message,
|
|
537
|
+
dryRun,
|
|
538
|
+
});
|
|
370
539
|
continue;
|
|
371
540
|
}
|
|
372
541
|
// #718: discriminate genuine filesystem/system errors (which must still
|
|
@@ -395,47 +564,43 @@ async function main() {
|
|
|
395
564
|
reason: 'mapper-crash',
|
|
396
565
|
}) + '\n',
|
|
397
566
|
);
|
|
398
|
-
|
|
567
|
+
runState.skippedInvalid++;
|
|
568
|
+
tally('skipped-invalid');
|
|
569
|
+
await emitMirrorEvent({
|
|
570
|
+
action: 'skipped-invalid',
|
|
571
|
+
kind,
|
|
572
|
+
line: lineNum,
|
|
573
|
+
recordId: entryId,
|
|
574
|
+
skipClass: 'mapper-crash',
|
|
575
|
+
reason: err.message,
|
|
576
|
+
dryRun,
|
|
577
|
+
});
|
|
399
578
|
continue;
|
|
400
579
|
}
|
|
401
580
|
// Unexpected filesystem errors → fatal
|
|
402
581
|
process.stderr.write(`vault-mirror: filesystem error on line ${lineNum}: ${err.message}\n`);
|
|
582
|
+
await finishRun('filesystem-error');
|
|
403
583
|
process.exit(2);
|
|
404
584
|
}
|
|
405
585
|
}
|
|
406
586
|
|
|
407
|
-
// ──
|
|
587
|
+
// ── Run close-out (#1147) ───────────────────────────────────────────────────
|
|
408
588
|
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
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
|
-
}
|
|
589
|
+
// The happy tail. Both roll-ups live in `finishRun` above, which every abort
|
|
590
|
+
// path also calls, so "the run ended" is emitted from ONE place regardless of
|
|
591
|
+
// HOW it ended. Placed BEFORE the --strict-schema abort so a failing run still
|
|
592
|
+
// reports its denominator — that run is precisely the one an operator wants
|
|
593
|
+
// counted.
|
|
594
|
+
await finishRun();
|
|
430
595
|
|
|
431
596
|
// --strict-schema: abort with exit 1 when any entry was skipped-invalid.
|
|
432
597
|
// Useful in CI to catch producer-side schema drift early (issue #249).
|
|
433
|
-
if (strictSchema &&
|
|
598
|
+
if (strictSchema && runState.skippedInvalid > 0) {
|
|
434
599
|
process.stdout.write(
|
|
435
|
-
JSON.stringify({ action: 'strict-schema-abort', skipped:
|
|
600
|
+
JSON.stringify({ action: 'strict-schema-abort', skipped: runState.skippedInvalid, kind }) + '\n',
|
|
436
601
|
);
|
|
437
602
|
process.stderr.write(
|
|
438
|
-
`vault-mirror: --strict-schema: ${
|
|
603
|
+
`vault-mirror: --strict-schema: ${runState.skippedInvalid} entries failed validation — exiting 1\n`,
|
|
439
604
|
);
|
|
440
605
|
process.exit(1);
|
|
441
606
|
}
|
|
@@ -448,8 +613,12 @@ async function main() {
|
|
|
448
613
|
}
|
|
449
614
|
}
|
|
450
615
|
|
|
451
|
-
main().catch((err) => {
|
|
616
|
+
main().catch(async (err) => {
|
|
452
617
|
process.stderr.write(`vault-mirror: unexpected error: ${err.message}\n`);
|
|
618
|
+
// Same reason as the two in-loop aborts: an unexpected throw is a run that
|
|
619
|
+
// ENDED, and the ledger has to say so. `finishRun` never throws, so this
|
|
620
|
+
// cannot turn a diagnosable crash into a silent one.
|
|
621
|
+
await finishRun('unexpected-error');
|
|
453
622
|
process.exit(2);
|
|
454
623
|
});
|
|
455
624
|
}
|
|
@@ -36,7 +36,17 @@ 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
|
-
|
|
39
|
+
# Fallback carries an `error` key, NOT '{"jobs":[]}': an empty job list is a
|
|
40
|
+
# legitimate glab answer, so it makes a dead CLI read as "pipeline with no jobs"
|
|
41
|
+
# and the watch goes silent (LM-002: silence is not success).
|
|
42
|
+
s=$(glab ci status -R <OWNER>/<REPO> --output json 2>/dev/null || echo '{"error":"glab-ci-status-failed"}')
|
|
43
|
+
# Anything that is not a jobs-carrying object — the sentinel above, or raw
|
|
44
|
+
# non-JSON glab noise — becomes a visible `error:` line, never silence.
|
|
45
|
+
if ! jq -e 'type=="object" and has("jobs")' <<<"$s" >/dev/null 2>&1; then
|
|
46
|
+
echo "$(date -u +%H:%M:%SZ) error: glab ci status unusable — $(printf '%.60s' "$s")"
|
|
47
|
+
sleep 30
|
|
48
|
+
continue
|
|
49
|
+
fi
|
|
40
50
|
cur=$(jq -r '.jobs[] | select(.status!="running" and .status!="pending") | "\(.name): \(.status)"' <<<"$s" 2>/dev/null | sort)
|
|
41
51
|
comm -13 <(echo "$prev") <(echo "$cur")
|
|
42
52
|
prev=$cur
|
|
@@ -50,10 +60,16 @@ done
|
|
|
50
60
|
```
|
|
51
61
|
|
|
52
62
|
**Coverage.** Emits a line for each job transitioning out of `running` /
|
|
53
|
-
`pending
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
`pending`, **plus an `error:` line on every iteration where glab produced
|
|
64
|
+
nothing usable** — a dead CLI, an auth expiry, or a network drop surfaces as a
|
|
65
|
+
notification rather than as absence. Terminates when **all** jobs are in a
|
|
66
|
+
terminal state (`success`, `failed`, `canceled`, `skipped`). The final line
|
|
67
|
+
prints the GitHub mirror SHA. Silence now carries exactly one meaning: the
|
|
68
|
+
pipeline is running and no job has changed state.
|
|
69
|
+
|
|
70
|
+
If you feed this snippet's output through a downstream `grep -E` filter, the
|
|
71
|
+
alternation must include `error` alongside the job-status tokens — the failure
|
|
72
|
+
signature is a word, not an absence.
|
|
57
73
|
|
|
58
74
|
**Probed 2026-08-14 (glab 1.91.0), three corrections — #1022.** The snippet
|
|
59
75
|
above was silence-is-not-success in its own right until that date, and each
|
|
@@ -74,6 +90,16 @@ half failed into the next one's fallback:
|
|
|
74
90
|
terminal test against `{"jobs":[]}` (exit 1 = keep watching) versus the old
|
|
75
91
|
form against `[]` (exit 0 = break).
|
|
76
92
|
|
|
93
|
+
**Amended 2026-08-24 — #1077.** The `{"jobs":[]}` fallback the third bullet
|
|
94
|
+
describes was still *indistinguishable from a real answer*: a pipeline can
|
|
95
|
+
legitimately report zero jobs, so a dead glab produced a well-formed payload,
|
|
96
|
+
no `error`, and no output. Non-terminal is not the same as visible. The
|
|
97
|
+
fallback is now `{"error":"glab-ci-status-failed"}` plus the shape guard at the
|
|
98
|
+
top of the loop, and the `(.jobs|length) > 0` terminal guard stays as the
|
|
99
|
+
second line of defence for a genuine zero-job response. Probed: the sentinel
|
|
100
|
+
and raw non-JSON glab noise both emit the `error:` line;
|
|
101
|
+
`{"jobs":[],"pipeline":{}}` and a populated payload both take the normal path.
|
|
102
|
+
|
|
77
103
|
**GitHub-mirror equivalent.** When the pipeline is GitHub-Actions-native (PR
|
|
78
104
|
checks rather than a GitLab pipeline), use
|
|
79
105
|
`gh pr checks -R <OWNER>/<REPO> <pr> --watch --fail-fast`
|