session-orchestrator 3.22.0 → 3.24.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 +8 -0
- 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/050-plan.mdc +2 -2
- 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/remote-offload/SKILL.md +13 -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 +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- 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/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- 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 +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -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 +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- 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/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- 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/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- 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/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- 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/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- 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 +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- 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 +220 -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-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -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 +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -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-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- 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-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- 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/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- 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 +54 -17
- 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/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
package/scripts/vault-mirror.mjs
CHANGED
|
@@ -56,12 +56,14 @@ 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';
|
|
62
63
|
import { resolveRepoNamespace } from './lib/vault-mirror/namespace.mjs';
|
|
63
64
|
import { resolveCanonicalSuffixes } from './lib/named-vault-resolver.mjs';
|
|
64
65
|
import { loadOwnerConfig } from './lib/owner-yaml.mjs';
|
|
66
|
+
import { canonicalizeSessions } from './lib/sessions-canonical.mjs';
|
|
65
67
|
|
|
66
68
|
// ── Canonical-vault helpers (#600 D2 / #607 D2) ────────────────────────────────
|
|
67
69
|
// These are module-level (above the CLI bootstrap) so the module is import-safe
|
|
@@ -105,6 +107,43 @@ export function _normalizeRemote(url) {
|
|
|
105
107
|
.replace(/\/+$/, '');
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// ── Mirror telemetry (#1116) ──────────────────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// The mirror run itself used to be SILENT in `.orchestrator/metrics/events.jsonl`
|
|
113
|
+
// (measured 2026-08-23: `jq -r '.event' … | grep -icE 'board|mirror'` → 0 of
|
|
114
|
+
// 28 387 records; the only event this CLI emitted was
|
|
115
|
+
// `orchestrator.secret_masker.applied`). The expensive consequence is the
|
|
116
|
+
// `skipped-invalid` path below: a schema-invalid record is reported on stdout
|
|
117
|
+
// and the process still exits 0, so the affected session ends up WITHOUT a vault
|
|
118
|
+
// note and nothing durable records which one.
|
|
119
|
+
//
|
|
120
|
+
// Design constraints, all load-bearing:
|
|
121
|
+
// - Additive only. The stdout JSON protocol is untouched; consumers parse it.
|
|
122
|
+
// - Best-effort. A telemetry failure must never fail a mirror run, so every
|
|
123
|
+
// emit is wrapped and its rejection swallowed (same posture as the masker
|
|
124
|
+
// emit at the end of main()).
|
|
125
|
+
// - "Absent is not zero" (docs/events-schema.md): a field that was not
|
|
126
|
+
// measured is OMITTED, never written as 0/null. Hence no `path` key here —
|
|
127
|
+
// these emit sites are reached BEFORE any target path is resolved, so there
|
|
128
|
+
// is no path to report. `record_id` is likewise omitted when the record
|
|
129
|
+
// carries neither `id` nor `session_id`; `line` is the fallback locator that
|
|
130
|
+
// is always measured.
|
|
131
|
+
// - Same ledger as the masker emit: `emitEvent` is called 2-arg so both events
|
|
132
|
+
// from one run resolve the SAME destination (`SO_PROJECT_DIR`, i.e.
|
|
133
|
+
// `CLAUDE_PROJECT_DIR` or the CWD walk-up). This CLI has no repo-root flag
|
|
134
|
+
// and deriving one from `--source` would split a single run's telemetry
|
|
135
|
+
// across two ledgers.
|
|
136
|
+
//
|
|
137
|
+
// #1147 moved both emitters into `scripts/lib/vault-mirror/telemetry.mjs` and
|
|
138
|
+
// widened the coverage from "the two skipped-invalid branches" to "every entry,
|
|
139
|
+
// plus one run-level roll-up":
|
|
140
|
+
// - `emitMirrorEvent` is now also called from `process.mjs`'s 18 `emitAction`
|
|
141
|
+
// sites, so `created`/`updated`/every `skipped-*` gets a record too.
|
|
142
|
+
// - `emitMirrorRunEvent` adds the DENOMINATOR. Per-entry records alone cannot
|
|
143
|
+
// distinguish "healthy run over an empty source" from "the emitter is
|
|
144
|
+
// broken" — both write nothing (HR-105). The run event is emitted
|
|
145
|
+
// unconditionally, so `total: 0` is a measured zero.
|
|
146
|
+
|
|
108
147
|
// ── CLI argument parsing ──────────────────────────────────────────────────────
|
|
109
148
|
//
|
|
110
149
|
// Migrated to scripts/lib/cli-flags.mjs (#510). Behaviour changes vs prior
|
|
@@ -285,11 +324,115 @@ if (kind !== 'learning' && kind !== 'session') {
|
|
|
285
324
|
// imports) so they are import-safe and unit-testable; see _resolveCanonicalSuffix
|
|
286
325
|
// / _normalizeRemote there (#607 D2).
|
|
287
326
|
|
|
327
|
+
// ── Run-level accounting + run close-out (#1147) ──────────────────────────────
|
|
328
|
+
//
|
|
329
|
+
// Deliberately OUTSIDE main(): the run event's whole contract is that it is
|
|
330
|
+
// written ONCE PER RUN and that its ABSENCE is the broken-emitter signal
|
|
331
|
+
// (HR-105). Six exits bypass main's normal tail — the three PRE-LOOP aborts at
|
|
332
|
+
// the top of main (missing vault-dir, non-canonical vault, missing source), the
|
|
333
|
+
// malformed-JSON abort and the filesystem-error abort inside the loop (all five
|
|
334
|
+
// `process.exit`, which no `finally` and no `catch` can intercept), and the
|
|
335
|
+
// top-level `main().catch`, which runs in a scope where main's locals no longer
|
|
336
|
+
// exist. Keeping the counters and the emitter out here is what lets all six
|
|
337
|
+
// close the run out through ONE function instead of each re-deriving the
|
|
338
|
+
// payload.
|
|
339
|
+
const runState = {
|
|
340
|
+
/** Non-blank JSONL lines the run ATTEMPTED — the denominator. */
|
|
341
|
+
total: 0,
|
|
342
|
+
/** Entries that produced `skipped-invalid` (validation error or mapper crash). */
|
|
343
|
+
skippedInvalid: 0,
|
|
344
|
+
/** Per-`action` tally, keyed by the same strings the entries wrote to stdout. */
|
|
345
|
+
actions: new Map(),
|
|
346
|
+
/** Latch: the run may only be closed out once. */
|
|
347
|
+
finished: false,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/** Count one entry action into {@link runState}. Ignores a non-string action. */
|
|
351
|
+
const tally = (action) => {
|
|
352
|
+
if (typeof action !== 'string' || action.length === 0) return;
|
|
353
|
+
runState.actions.set(action, (runState.actions.get(action) ?? 0) + 1);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Close the run out: emit the run-level roll-up AND the masking roll-up,
|
|
358
|
+
* exactly once. Never throws, never exits — the caller owns the exit code.
|
|
359
|
+
*
|
|
360
|
+
* Both emits are documented as unconditional, and both used to sit only on the
|
|
361
|
+
* happy tail: an abort skipped them, so the very runs an operator most wants
|
|
362
|
+
* counted were the ones that vanished from the ledger, in the one shape
|
|
363
|
+
* ("no record") that the docstring reserves for a broken emitter.
|
|
364
|
+
*
|
|
365
|
+
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'malformed-json'|'filesystem-error'|'unexpected-error'} [aborted]
|
|
366
|
+
* Omitted on a complete run. When present it LABELS the counters as partial:
|
|
367
|
+
* every line after the abort was never attempted, so the classes no longer
|
|
368
|
+
* partition `total`. On the three PRE-LOOP values (#1151) nothing was
|
|
369
|
+
* attempted at all — `total` is a measured 0 and the label is what separates
|
|
370
|
+
* "never started" from "ran over an empty source".
|
|
371
|
+
* @returns {Promise<void>}
|
|
372
|
+
*/
|
|
373
|
+
async function finishRun(aborted) {
|
|
374
|
+
if (runState.finished) return;
|
|
375
|
+
runState.finished = true;
|
|
376
|
+
|
|
377
|
+
const countOf = (action) => runState.actions.get(action) ?? 0;
|
|
378
|
+
const actionBreakdown = Object.fromEntries(runState.actions);
|
|
379
|
+
await emitMirrorRunEvent({
|
|
380
|
+
kind,
|
|
381
|
+
total: runState.total,
|
|
382
|
+
created: countOf('created'),
|
|
383
|
+
updated: countOf('updated'),
|
|
384
|
+
// Every non-failure skip class, summed from the SAME map the breakdown is
|
|
385
|
+
// built from — `skipped-invalid` is deliberately excluded and reported as
|
|
386
|
+
// `failed`, because those are the entries whose session silently ends up
|
|
387
|
+
// without a vault note.
|
|
388
|
+
skipped: [...runState.actions].reduce(
|
|
389
|
+
(sum, [action, n]) =>
|
|
390
|
+
action.startsWith('skipped-') && action !== 'skipped-invalid' ? sum + n : sum,
|
|
391
|
+
0,
|
|
392
|
+
),
|
|
393
|
+
failed: runState.skippedInvalid,
|
|
394
|
+
actionBreakdown,
|
|
395
|
+
dryRun,
|
|
396
|
+
...(aborted ? { aborted } : {}),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ── Masking telemetry (#1025) ───────────────────────────────────────────────
|
|
400
|
+
//
|
|
401
|
+
// Emitted here, at the END of the run, rather than at the lazy build site
|
|
402
|
+
// inside process.mjs. The build site is only reached once a record is actually
|
|
403
|
+
// processed, so a run over an empty/fully-skipped source would emit nothing and
|
|
404
|
+
// "the masker never ran" would be indistinguishable from "this channel has no
|
|
405
|
+
// masker wired".
|
|
406
|
+
//
|
|
407
|
+
// Counts only — never a needle, never a prefix of one, never masked text.
|
|
408
|
+
// Best-effort: a telemetry write must never be the reason a mirror run fails.
|
|
409
|
+
try {
|
|
410
|
+
const maskerStats = getMaskerStats();
|
|
411
|
+
await emitEvent('orchestrator.secret_masker.applied', {
|
|
412
|
+
channel: 'vault-mirror',
|
|
413
|
+
needle_count: maskerStats.needleCount,
|
|
414
|
+
records: maskerStats.records,
|
|
415
|
+
hits: maskerStats.hits,
|
|
416
|
+
dry_run: dryRun,
|
|
417
|
+
});
|
|
418
|
+
} catch {
|
|
419
|
+
// Silent no-op — see the note above.
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
288
423
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
289
424
|
|
|
290
425
|
async function main() {
|
|
426
|
+
// The three PRE-LOOP aborts below (#1151) close the run out through the same
|
|
427
|
+
// `finishRun` every other exit uses. They are the runs that never reached
|
|
428
|
+
// their first entry — a bad vault-dir, a wrong vault, a missing source — and
|
|
429
|
+
// until now they were the only outcomes that left NO record at all, which is
|
|
430
|
+
// the one shape the run event reserves for a broken emitter. Their counters
|
|
431
|
+
// are all `0`, and `aborted` is what makes that zero readable as "never
|
|
432
|
+
// started" rather than "ran over an empty source".
|
|
291
433
|
if (!existsSync(resolve(vaultDir))) {
|
|
292
434
|
process.stderr.write(`vault-mirror: vault-dir not found: ${vaultDir}\n`);
|
|
435
|
+
await finishRun('missing-vault-dir');
|
|
293
436
|
process.exit(2);
|
|
294
437
|
}
|
|
295
438
|
|
|
@@ -307,12 +450,14 @@ async function main() {
|
|
|
307
450
|
process.stderr.write(
|
|
308
451
|
`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
452
|
);
|
|
453
|
+
await finishRun('vault-not-canonical');
|
|
310
454
|
process.exit(2);
|
|
311
455
|
}
|
|
312
456
|
}
|
|
313
457
|
|
|
314
458
|
if (!existsSync(resolve(source))) {
|
|
315
459
|
process.stderr.write(`vault-mirror: source file not found: ${source}\n`);
|
|
460
|
+
await finishRun('missing-source');
|
|
316
461
|
process.exit(2);
|
|
317
462
|
}
|
|
318
463
|
|
|
@@ -328,7 +473,13 @@ async function main() {
|
|
|
328
473
|
}
|
|
329
474
|
|
|
330
475
|
let lineNum = 0;
|
|
331
|
-
|
|
476
|
+
// The run-level denominator lives in `runState` above: `runState.total` counts
|
|
477
|
+
// every NON-BLANK line the run attempted, so
|
|
478
|
+
// `created + updated + skipped + failed === runState.total` for any run that
|
|
479
|
+
// does not abort — and an aborted run says so with the `aborted` field rather
|
|
480
|
+
// than publishing a partial count as a complete one. The breakdown is keyed by
|
|
481
|
+
// the same `action` string the entry wrote to stdout, so no second vocabulary
|
|
482
|
+
// is introduced.
|
|
332
483
|
const ctx = {
|
|
333
484
|
vaultDir,
|
|
334
485
|
dryRun,
|
|
@@ -339,25 +490,34 @@ async function main() {
|
|
|
339
490
|
qualityMinConfidence,
|
|
340
491
|
};
|
|
341
492
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Dispatch one already-parsed entry to its processor and account for the
|
|
495
|
+
* result. Extracted (#1186c) so the `--kind session` path below can call it
|
|
496
|
+
* AFTER a whole-file dedup pass instead of once per raw line; every branch
|
|
497
|
+
* is byte-identical to the pre-#1186c per-line loop body.
|
|
498
|
+
* @param {unknown} entry — parsed JSONL value (usually an object; a bare
|
|
499
|
+
* `null`/primitive line is a real shape this must keep handling, see the
|
|
500
|
+
* #1186c session branch below for why it is never filtered out here).
|
|
501
|
+
* @param {number} entryLineNum — 1-based JSONL line number, or (for the
|
|
502
|
+
* `--kind session` dedup path) the line of the record that WON the
|
|
503
|
+
* collapse. `_lineNum` is read ONLY for telemetry (process.mjs
|
|
504
|
+
* `emitEntryAction` → `line:` on the per-entry ledger event), never to
|
|
505
|
+
* derive content — process.mjs itself tolerates a non-finite value by
|
|
506
|
+
* suppressing just that one ledger record, but every call site here
|
|
507
|
+
* always supplies a real line number.
|
|
508
|
+
* @returns {Promise<void>}
|
|
509
|
+
*/
|
|
510
|
+
async function dispatchEntry(entry, entryLineNum) {
|
|
348
511
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
process.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
} else {
|
|
359
|
-
await processSession(entry, lineNum, ctx);
|
|
360
|
-
}
|
|
512
|
+
// Both processors return the `action` string they emitted (every one of
|
|
513
|
+
// their exit paths is an `emitAction` call), so the tally needs no second
|
|
514
|
+
// census of the 18 call sites in process.mjs — a census that would go
|
|
515
|
+
// stale the first time a branch is added.
|
|
516
|
+
const action =
|
|
517
|
+
kind === 'learning'
|
|
518
|
+
? await processLearning(entry, entryLineNum, ctx)
|
|
519
|
+
: await processSession(entry, entryLineNum, ctx);
|
|
520
|
+
tally(action);
|
|
361
521
|
} catch (err) {
|
|
362
522
|
// Validation errors (missing required fields) → per-entry skip, not a global failure
|
|
363
523
|
if (err.message.startsWith('vault-mirror:')) {
|
|
@@ -366,8 +526,18 @@ async function main() {
|
|
|
366
526
|
process.stdout.write(
|
|
367
527
|
JSON.stringify({ action: 'skipped-invalid', path: null, kind, id: entryId }) + '\n',
|
|
368
528
|
);
|
|
369
|
-
|
|
370
|
-
|
|
529
|
+
runState.skippedInvalid++;
|
|
530
|
+
tally('skipped-invalid');
|
|
531
|
+
await emitMirrorEvent({
|
|
532
|
+
action: 'skipped-invalid',
|
|
533
|
+
kind,
|
|
534
|
+
line: entryLineNum,
|
|
535
|
+
recordId: entryId,
|
|
536
|
+
skipClass: 'validation',
|
|
537
|
+
reason: err.message,
|
|
538
|
+
dryRun,
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
371
541
|
}
|
|
372
542
|
// #718: discriminate genuine filesystem/system errors (which must still
|
|
373
543
|
// abort the whole run — a partially-written vault is worse than a loud
|
|
@@ -383,7 +553,7 @@ async function main() {
|
|
|
383
553
|
(typeof err.code === 'string' && err.code.length > 0) || Boolean(err.syscall);
|
|
384
554
|
if (!isSystemError) {
|
|
385
555
|
process.stderr.write(
|
|
386
|
-
`vault-mirror: mapper crash on line ${
|
|
556
|
+
`vault-mirror: mapper crash on line ${entryLineNum} (${err.message}) — record skipped\n`,
|
|
387
557
|
);
|
|
388
558
|
const entryId = entry?.id ?? entry?.session_id ?? null;
|
|
389
559
|
process.stdout.write(
|
|
@@ -395,47 +565,127 @@ async function main() {
|
|
|
395
565
|
reason: 'mapper-crash',
|
|
396
566
|
}) + '\n',
|
|
397
567
|
);
|
|
398
|
-
|
|
399
|
-
|
|
568
|
+
runState.skippedInvalid++;
|
|
569
|
+
tally('skipped-invalid');
|
|
570
|
+
await emitMirrorEvent({
|
|
571
|
+
action: 'skipped-invalid',
|
|
572
|
+
kind,
|
|
573
|
+
line: entryLineNum,
|
|
574
|
+
recordId: entryId,
|
|
575
|
+
skipClass: 'mapper-crash',
|
|
576
|
+
reason: err.message,
|
|
577
|
+
dryRun,
|
|
578
|
+
});
|
|
579
|
+
return;
|
|
400
580
|
}
|
|
401
581
|
// Unexpected filesystem errors → fatal
|
|
402
|
-
process.stderr.write(`vault-mirror: filesystem error on line ${
|
|
582
|
+
process.stderr.write(`vault-mirror: filesystem error on line ${entryLineNum}: ${err.message}\n`);
|
|
583
|
+
await finishRun('filesystem-error');
|
|
403
584
|
process.exit(2);
|
|
404
585
|
}
|
|
405
586
|
}
|
|
406
587
|
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
|
|
588
|
+
// #1186c: for `--kind session`, every parsed entry is buffered here instead
|
|
589
|
+
// of dispatched inline — the dedup pass below needs the WHOLE file before it
|
|
590
|
+
// can tell which of several same-`session_id` lines is the winner. `--kind
|
|
591
|
+
// learning` is unaffected: it still dispatches per line, inline, immediately
|
|
592
|
+
// below (a malformed line further down the file must not undo an already-
|
|
593
|
+
// dispatched learning — pinned by the existing `total: 2, created: 1` abort
|
|
594
|
+
// test in tests/unit/vault-mirror.test.mjs).
|
|
595
|
+
const sessionEntries = [];
|
|
596
|
+
const sessionLineNums = [];
|
|
597
|
+
|
|
598
|
+
for (const line of lines) {
|
|
599
|
+
lineNum++;
|
|
600
|
+
const trimmed = line.trim();
|
|
601
|
+
if (!trimmed) continue;
|
|
602
|
+
runState.total++;
|
|
603
|
+
|
|
604
|
+
let entry;
|
|
605
|
+
try {
|
|
606
|
+
entry = JSON.parse(trimmed);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
process.stderr.write(`vault-mirror: malformed JSON on line ${lineNum}: ${err.message}\n`);
|
|
609
|
+
// Close the run out BEFORE exiting: `process.exit` runs no `finally`, so
|
|
610
|
+
// without this the abort is the one outcome that leaves no run record —
|
|
611
|
+
// exactly the shape reserved for a broken emitter.
|
|
612
|
+
await finishRun('malformed-json');
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (kind === 'session') {
|
|
617
|
+
sessionEntries.push(entry);
|
|
618
|
+
sessionLineNums.push(lineNum);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
await dispatchEntry(entry, lineNum);
|
|
429
623
|
}
|
|
430
624
|
|
|
625
|
+
if (kind === 'session') {
|
|
626
|
+
// Only an OBJECT entry carrying a non-empty `session_id` is eligible for
|
|
627
|
+
// the dedup collapse — the same predicate canonicalizeSessions itself uses
|
|
628
|
+
// internally (sessions-canonical.mjs `isRecordObject` + `isNonEmptyString`,
|
|
629
|
+
// not exported, so re-stated here rather than reached into). Everything
|
|
630
|
+
// else — a bare `null`/primitive JSONL line, or a legacy record with no
|
|
631
|
+
// `session_id` field — is dispatched EXACTLY as before: unaffected, in
|
|
632
|
+
// original file order, through the SAME validation/mapper-crash paths
|
|
633
|
+
// process.mjs already has for those shapes. Two regression-guard tests in
|
|
634
|
+
// tests/unit/vault-mirror.test.mjs depend on this (a bare `null` line and a
|
|
635
|
+
// legacy `session`-keyed record both still reach processSession() and its
|
|
636
|
+
// existing error handling, never silently vanish into the collapse).
|
|
637
|
+
const isIdentifiable = (e) =>
|
|
638
|
+
e !== null &&
|
|
639
|
+
typeof e === 'object' &&
|
|
640
|
+
!Array.isArray(e) &&
|
|
641
|
+
typeof e.session_id === 'string' &&
|
|
642
|
+
e.session_id.length > 0;
|
|
643
|
+
const identifiable = sessionEntries.filter(isIdentifiable);
|
|
644
|
+
// canonicalizeSessions never clones — the survivors are the SAME object
|
|
645
|
+
// references as in `sessionEntries`, so reference identity below is exact,
|
|
646
|
+
// never a guess (scripts/lib/sessions-canonical.mjs header, "RULE ORDER").
|
|
647
|
+
const survivors = new Set(canonicalizeSessions(identifiable));
|
|
648
|
+
|
|
649
|
+
for (let i = 0; i < sessionEntries.length; i++) {
|
|
650
|
+
const entry = sessionEntries[i];
|
|
651
|
+
if (isIdentifiable(entry) && !survivors.has(entry)) {
|
|
652
|
+
// A losing duplicate: an earlier line whose `session_id` a LATER line
|
|
653
|
+
// in this same batch supersedes or overwrites (crash-recovery
|
|
654
|
+
// re-append, #1068 stub/supersede pair). No dispatch, no stdout line,
|
|
655
|
+
// no tally for it — the winning occurrence (dispatched below, at its
|
|
656
|
+
// own position) already produces the ONE note this physical session
|
|
657
|
+
// gets. BV-004 ceiling: `runState.total` still counts this raw line,
|
|
658
|
+
// so `created+updated+skipped+failed` no longer partitions `total`
|
|
659
|
+
// for a `--kind session` run that collapsed at least one duplicate —
|
|
660
|
+
// no test pins that invariant for session kind (only for `learning`,
|
|
661
|
+
// where duplicates are not collapsed), and a partially-written vault
|
|
662
|
+
// from a batch that could not be fully deduped is the worse failure
|
|
663
|
+
// mode. Revisit with a dedicated telemetry action if an operator ever
|
|
664
|
+
// needs to name WHICH lines were collapsed, not just how many notes
|
|
665
|
+
// were written.
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
await dispatchEntry(entry, sessionLineNums[i]);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── Run close-out (#1147) ───────────────────────────────────────────────────
|
|
673
|
+
//
|
|
674
|
+
// The happy tail. Both roll-ups live in `finishRun` above, which every abort
|
|
675
|
+
// path also calls, so "the run ended" is emitted from ONE place regardless of
|
|
676
|
+
// HOW it ended. Placed BEFORE the --strict-schema abort so a failing run still
|
|
677
|
+
// reports its denominator — that run is precisely the one an operator wants
|
|
678
|
+
// counted.
|
|
679
|
+
await finishRun();
|
|
680
|
+
|
|
431
681
|
// --strict-schema: abort with exit 1 when any entry was skipped-invalid.
|
|
432
682
|
// Useful in CI to catch producer-side schema drift early (issue #249).
|
|
433
|
-
if (strictSchema &&
|
|
683
|
+
if (strictSchema && runState.skippedInvalid > 0) {
|
|
434
684
|
process.stdout.write(
|
|
435
|
-
JSON.stringify({ action: 'strict-schema-abort', skipped:
|
|
685
|
+
JSON.stringify({ action: 'strict-schema-abort', skipped: runState.skippedInvalid, kind }) + '\n',
|
|
436
686
|
);
|
|
437
687
|
process.stderr.write(
|
|
438
|
-
`vault-mirror: --strict-schema: ${
|
|
688
|
+
`vault-mirror: --strict-schema: ${runState.skippedInvalid} entries failed validation — exiting 1\n`,
|
|
439
689
|
);
|
|
440
690
|
process.exit(1);
|
|
441
691
|
}
|
|
@@ -448,8 +698,12 @@ async function main() {
|
|
|
448
698
|
}
|
|
449
699
|
}
|
|
450
700
|
|
|
451
|
-
main().catch((err) => {
|
|
701
|
+
main().catch(async (err) => {
|
|
452
702
|
process.stderr.write(`vault-mirror: unexpected error: ${err.message}\n`);
|
|
703
|
+
// Same reason as the two in-loop aborts: an unexpected throw is a run that
|
|
704
|
+
// ENDED, and the ledger has to say so. `finishRun` never throws, so this
|
|
705
|
+
// cannot turn a diagnosable crash into a silent one.
|
|
706
|
+
await finishRun('unexpected-error');
|
|
453
707
|
process.exit(2);
|
|
454
708
|
});
|
|
455
709
|
}
|
|
@@ -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`
|
|
@@ -101,10 +101,39 @@ The two slots are `parallelPeer.mode` and `parallelPeer.pid`; `<ageHours>` is th
|
|
|
101
101
|
|
|
102
102
|
### Outcome handling
|
|
103
103
|
|
|
104
|
-
- **Worktree anlegen + starten** → invoke `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. The helper creates a sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, runs idempotency + boundary checks, and logs a WARN line to stderr on fresh creation.
|
|
104
|
+
- **Worktree anlegen + starten** → invoke `enterWorktree({ basePath, sessionId, branch, repoRoot, rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. The helper creates a sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, runs idempotency + boundary checks, and logs a WARN line to stderr on fresh creation. When `<branch>` is already checked out by another worktree — the normal case, since Phase 0.5 passes the current HEAD — the worktree lands on a fresh `so/<sessionId>` branch created at `<branch>` and the helper returns `{ branch: 'so/<sessionId>', promotedFrom: '<branch>' }` (#1067); the new worktree's STATE.md `branch` MUST record `so/<sessionId>` and note `promoted from <branch>@<repoRoot>`. Since #1170, `enterWorktree` releases the source root ITSELF once the destination worktree provably exists — it calls `leaveSourceRoot({ repoRoot, sessionId: rawSessionId, semanticSessionId: sessionId, reason })` from `scripts/lib/session-transition.mjs` internally, on BOTH success exits, so this AUQ handler makes no separate `leaveSourceRoot` call. `rawSessionId` — **read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })`, never the semantic label, and never `current-session.json`, which may describe a peer session (#863)** — is the RAW physical `session_id` owning this root's lock/registry entry; a wrong id aborts the teardown with `left.ok: false, reason: 'lock-session-mismatch:<owner>'` and removes nothing. In detail: the promotion is a PROCESS BOUNDARY, not a live migration (#1069) — the old root is deregistered and its `session.lock` released BEFORE the new worktree's own Phase 1.2 acquires, so the two roots never both own a live claim at once. `enterWorktree`'s return value carries the outcome as `left: { ok, steps, reason? }`; `leaveSourceRoot()` never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless (the destination worktree already exists — aborting here would leave the two-live-roots state the call prevents). Then exit the current preamble flow — the new worktree's own session-start runs from scratch (Phase 1 onwards). On failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit a stderr warning `parallel-aware: enterWorktree failed: <error>; falling back to Manuell` and proceed via the Manuell path.
|
|
105
105
|
- **Manuell** → append a Deviation via `appendDeviationOnDisk()`:
|
|
106
106
|
`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>, mode=<peer.mode>, pid=<peer.pid>. PSA-001/PSA-002/PSA-004 discipline applies.`
|
|
107
|
-
Continue Phase-0.
|
|
107
|
+
Continue Phase-0 — and run the **Peer-Scope-Union** protocol below before the first write.
|
|
108
|
+
|
|
109
|
+
### Peer-Scope-Union (Manuell only, #1195)
|
|
110
|
+
|
|
111
|
+
In-place beside a peer is survivable when the two scopes are DECLARED to each other rather than discovered by collision. Measured 2026-09-02 in a consumer repo: a 4-subagent session ran beside a deep session (wave 4, `enforcement: strict`) in ONE checkout, no worktree, zero collisions — the peer's paths were carried in the deep session's `allowedPaths` union (19 → 41, `--assert-subset` green) across a wave rollover.
|
|
112
|
+
|
|
113
|
+
Four steps, in order. Steps 1 and 2 are the protocol; 3 and 4 are what keeps it honest.
|
|
114
|
+
|
|
115
|
+
1. **Declare the COMPLETE path list.** The arriving session sends the peer every path it will write — including the ones a script produces (fixtures, snapshots, result files, temp helpers), not only the ones it plans to edit by hand. A path omitted here is a path the peer's guard reports as a violation. In the same message it adopts the peer's resource rules.
|
|
116
|
+
2. **The peer unions.** The receiving coordinator adds those paths to its `allowedPaths` as ONE record `peer-session-<id>` in the wave's scope manifest, re-asserts subset/disjointness, and re-materializes them on every wave rollover (`skills/wave-executor/wave-loop.md` § Scope Manifest).
|
|
117
|
+
3. **Probe with ONE real write.** Before dispatching any agent, make one small PLANNED Edit from the declared list. A denial here costs one edit; the same denial found after a fan-out costs the wave.
|
|
118
|
+
4. **Announce before committing.** The arriving session sends its final file list, the peer sequences its own push behind it, and the SHAs come back. The git index is shared (PSA-007) — sequencing it is the only thing that makes two sessions in one checkout committable.
|
|
119
|
+
|
|
120
|
+
Message template for step 1 (`SendMessage`, first line self-contained per `.claude/rules/cross-session-messaging.md`):
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Scope-union request: I will write exactly these paths in <repo> — please add them to your allowedPaths.
|
|
124
|
+
|
|
125
|
+
Paths (complete, incl. files my scripts write):
|
|
126
|
+
- <path>
|
|
127
|
+
- <path>
|
|
128
|
+
|
|
129
|
+
Resource rules I adopt from you: no build, no dev-server ports, no service stop/restart,
|
|
130
|
+
commit only via `git commit --only <my files>` after announcing, no push, no tag.
|
|
131
|
+
|
|
132
|
+
I will probe with ONE planned edit before dispatching, announce my final file list before
|
|
133
|
+
committing, and send you the SHAs afterwards.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Delivery is never guaranteed (CSM-004): an unanswered request establishes nothing. Without a confirmed union, do not write beside the peer — take the worktree instead.
|
|
108
137
|
- **Abbrechen** → exit Phase-0 immediately. No file writes.
|
|
109
138
|
|
|
110
139
|
## Always-OK Pass-Through (no AUQ)
|
|
@@ -134,14 +134,23 @@ async function runParallelAwarePreamble({ repoRoot, callerMode, callerSessionId
|
|
|
134
134
|
return { outcome: 'EXCLUSIVE_BLOCKED', callerClass, blockingSession: exclusiveActive, active: classifiedActive };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// GH#67: a registry-sourced peer whose lock is SUPERSEDED (a LIVE lock at
|
|
138
|
+
// this repoRoot is owned by a DIFFERENT raw session_id) is likely a
|
|
139
|
+
// finished-but-still-fresh task on a platform without SessionEnd. It stays
|
|
140
|
+
// visible in `active` (never a filter — the lock is advisory, #1085
|
|
141
|
+
// contract), but it is not eligible to trigger the Promotion AUQ. Split it
|
|
142
|
+
// out as advisory before the parallelPeer lookup.
|
|
143
|
+
const supersededPeers = classifiedActive.filter((e) => e.lockSuperseded === true);
|
|
144
|
+
const promotionEligible = classifiedActive.filter((e) => e.lockSuperseded !== true);
|
|
145
|
+
|
|
137
146
|
const parallelPeer = callerClass === 'parallel-ok'
|
|
138
|
-
?
|
|
147
|
+
? promotionEligible.find((e) => e._class === 'parallel-ok' && e.sessionId !== callerSessionId)
|
|
139
148
|
: null;
|
|
140
149
|
if (parallelPeer) {
|
|
141
150
|
return { outcome: 'PROMOTION_OFFER', callerClass, parallelPeer, active: classifiedActive };
|
|
142
151
|
}
|
|
143
152
|
|
|
144
|
-
return { outcome: 'PASS_THROUGH', callerClass, active: classifiedActive };
|
|
153
|
+
return { outcome: 'PASS_THROUGH', callerClass, active: classifiedActive, advisory: supersededPeers };
|
|
145
154
|
}
|
|
146
155
|
```
|
|
147
156
|
|
|
@@ -153,7 +162,9 @@ The skill consuming the preamble translates the outcome:
|
|
|
153
162
|
|---------|--------|
|
|
154
163
|
| `PASS_THROUGH` | Continue immediately. No AUQ. Pre-P1.3 behavior. |
|
|
155
164
|
| `EXCLUSIVE_BLOCKED` | Fire Exclusive-Conflict AUQ from `parallel-aware-auq.md`. Block until user response. On "Abbrechen": exit cleanly. On "Andere Session beenden": surface to user (preamble does NOT kill other session). On "Warten": pause Phase 0; re-run preamble on user retry. |
|
|
156
|
-
| `PROMOTION_OFFER` | Fire Promotion AUQ from `parallel-aware-auq.md`. On "Worktree anlegen": call enterWorktree() from worktree-pipeline.mjs (see parallel-aware-auq.md outcome-handling). On "Manuell": append Deviation (`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>`) and continue. On "Abbrechen": exit. |
|
|
165
|
+
| `PROMOTION_OFFER` | Fire Promotion AUQ from `parallel-aware-auq.md`. On "Worktree anlegen": call `enterWorktree({ ..., rawSessionId, reason: 'worktree-promotion' })` from worktree-pipeline.mjs (see parallel-aware-auq.md outcome-handling) — since #1170 this ONE call also releases the source root: it calls `leaveSourceRoot({ repoRoot, sessionId: rawSessionId, semanticSessionId, reason })` from `session-transition.mjs` internally, on BOTH success exits, so no separate call is made here. `rawSessionId` is the RAW physical `session_id` from this root's `.orchestrator/session.lock` (`readLock({ repoRoot })`), never the semantic label and never `current-session.json` (may describe a peer, #863). The promotion is a process boundary, not a live migration (#1069): the old root is deregistered and its lock released BEFORE the new worktree's own Phase 1.2 acquires — never both roots owning at once. The return value's `left` field carries the outcome; `leaveSourceRoot()` never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless. On "Manuell": append Deviation (`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>`) and continue. On "Abbrechen": exit. |
|
|
166
|
+
|
|
167
|
+
**Superseded-lock advisory (GH#67).** A `discovered` peer with `lockSuperseded: true` never fires the Promotion AUQ — it is downgraded to the `advisory` array on the `PASS_THROUGH` result instead (see the `runParallelAwarePreamble` reference above), because a live lock at this repoRoot is owned by a different raw session_id and the entry is likely a finished-but-still-fresh task on a platform without SessionEnd, not a live collision (#1085 advisory-lock contract — the entry is never filtered, only downgraded). The consuming skill prints ONE advisory line per entry: `parallel-aware: registry entry <sessionId> (last heartbeat <N> min ago) is superseded by this root's live lock <lockOwnerId> — likely a finished task on a platform without SessionEnd (GH#67); still counted for PSA-001 awareness`, then continues. `lockSuperseded: false` with `lockOwnerId: null` means "no live lock here" — distinct from "own lock". The same session id remains PSA-002-relevant if it also shows up in STATE.md (`source: 'state-md'`, handled unchanged by Phase 1.2.1/Phase 1b below).
|
|
157
168
|
|
|
158
169
|
## Phase 1b Peer-Guard (defense-in-depth)
|
|
159
170
|
|
|
@@ -169,7 +180,11 @@ The guard is a SOFT-GATE — operator can override, but the warning is mandatory
|
|
|
169
180
|
findPeers(repoRoot, { mySessionId }) → peer = peers.find((p) => p.source === 'state-md') →
|
|
170
181
|
peer === null → safe to write STATE.md; continue Phase 1b normally.
|
|
171
182
|
peer !== null → fire Promotion AUQ (parallel-aware-auq.md "Promotion" block).
|
|
172
|
-
On "Worktree anlegen": enterWorktree()
|
|
183
|
+
On "Worktree anlegen": enterWorktree(..., rawSessionId) — releases
|
|
184
|
+
the source root internally (#1170; no
|
|
185
|
+
separate leaveSourceRoot call needed)
|
|
186
|
+
→ continue in sibling (process boundary,
|
|
187
|
+
old root released before the new acquire).
|
|
173
188
|
On "Manuell": appendDeviationOnDisk() + continue in-place.
|
|
174
189
|
On "Abbrechen": exit cleanly.
|
|
175
190
|
```
|