session-orchestrator 3.22.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 +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/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 +168 -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/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 +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -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 +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- 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 +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- 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-bash-issue-budget.mjs +117 -4
- 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 +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 +5 -0
- 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/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- 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/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/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 +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- 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 +281 -13
- 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 +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 +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/sync.mjs +46 -8
- 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 +16 -32
- 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-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-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/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- 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/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- 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 +11 -3
- 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 +30 -16
- 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 +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
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`
|
|
@@ -101,7 +101,7 @@ 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. Once the worktree exists, 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.
|
|
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. 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>`. Once the worktree exists, and BEFORE exiting the preamble flow, call `leaveSourceRoot({ repoRoot, sessionId: <the raw physical session_id owning this root's lock/registry entry>, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs`. **Read that `sessionId` 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); a wrong id aborts the teardown with `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. The call never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue (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
107
|
Continue Phase-0.
|
|
@@ -153,7 +153,7 @@ The skill consuming the preamble translates the outcome:
|
|
|
153
153
|
|---------|--------|
|
|
154
154
|
| `PASS_THROUGH` | Continue immediately. No AUQ. Pre-P1.3 behavior. |
|
|
155
155
|
| `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. |
|
|
156
|
+
| `PROMOTION_OFFER` | Fire Promotion AUQ from `parallel-aware-auq.md`. On "Worktree anlegen": call enterWorktree() from worktree-pipeline.mjs, then — BEFORE exiting Phase 0 — `leaveSourceRoot({ repoRoot, sessionId, semanticSessionId, reason: 'worktree-promotion' })` from `session-transition.mjs` (see parallel-aware-auq.md outcome-handling). `sessionId` 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. It never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue. On "Manuell": append Deviation (`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>`) and continue. On "Abbrechen": exit. |
|
|
157
157
|
|
|
158
158
|
## Phase 1b Peer-Guard (defense-in-depth)
|
|
159
159
|
|
|
@@ -169,7 +169,9 @@ The guard is a SOFT-GATE — operator can override, but the warning is mandatory
|
|
|
169
169
|
findPeers(repoRoot, { mySessionId }) → peer = peers.find((p) => p.source === 'state-md') →
|
|
170
170
|
peer === null → safe to write STATE.md; continue Phase 1b normally.
|
|
171
171
|
peer !== null → fire Promotion AUQ (parallel-aware-auq.md "Promotion" block).
|
|
172
|
-
On "Worktree anlegen": enterWorktree() →
|
|
172
|
+
On "Worktree anlegen": enterWorktree() → leaveSourceRoot(this root)
|
|
173
|
+
→ continue in sibling (process boundary,
|
|
174
|
+
old root released before the new acquire).
|
|
173
175
|
On "Manuell": appendDeviationOnDisk() + continue in-place.
|
|
174
176
|
On "Abbrechen": exit cleanly.
|
|
175
177
|
```
|
|
@@ -20,10 +20,12 @@ These tools have the same name and behavior on all platforms. Cursor IDE uses eq
|
|
|
20
20
|
|
|
21
21
|
## Platform-Specific Tool Mapping
|
|
22
22
|
|
|
23
|
+
> **Every statement about a foreign platform carries a measurement date and the tool version it was measured on** (PSA-006). An undated capability claim about Codex CLI, Cursor IDE, or Pi is unverified — re-measure before relying on it, and stamp what you find.
|
|
24
|
+
|
|
23
25
|
| Function | Claude Code | Codex CLI | Cursor IDE | Pi |
|
|
24
26
|
|----------|------------|-----------|------------|----|
|
|
25
27
|
| Present choices to user | `AskUserQuestion` tool with structured options | Numbered Markdown list as plain text, wait for user reply | Numbered Markdown list (same as Codex) | Numbered Markdown list v1; native UI adapter planned |
|
|
26
|
-
| Dispatch subagent | `Agent({ description, prompt, subagent_type })` |
|
|
28
|
+
| Dispatch subagent | `Agent({ description, prompt, subagent_type })` | Native multi-agent collaboration namespace: `spawn_agent`, `list_agents`, `wait_agent`, `send_message`, `followup_task`, `interrupt_agent`, `close_agent`. Measured 2026-08-25 on `codex-cli 0.141.0` — `codex features list` → `multi_agent stable true`. | No native in-session Agent tool. Coordinator-side foreign dispatch via the headless `cursor-agent` CLI exists (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150) — measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. | Sequential execution v1. Do not assume native subagents until the Pi SDK dispatcher exists. |
|
|
27
29
|
| Track tasks | `TaskCreate` / `TaskUpdate` / `TaskList` | Plain-text checklist in response context | Plain-text checklist (same as Codex) | Plain-text checklist (same as Codex) |
|
|
28
30
|
| Enter plan mode | `EnterPlanMode` / `ExitPlanMode` tools | `/plan` slash command (prompt-level, not tool-based) | Instruction-based: "Focus on analysis and planning. Do not modify files until the user approves." | `/plan` prompt template; use instruction-based planning when tool mode is unavailable |
|
|
29
31
|
| Web search | `WebSearch` tool | Built-in web search (invoke via instruction) | `@web` in Cursor chat | Use Pi's available web/search tools if configured; otherwise Bash curl or browser handoff |
|
|
@@ -53,18 +55,22 @@ Agent({
|
|
|
53
55
|
description: "3-5 word summary",
|
|
54
56
|
prompt: "full task context...",
|
|
55
57
|
subagent_type: "general-purpose",
|
|
56
|
-
run_in_background:
|
|
58
|
+
run_in_background: true // RECOMMENDED for wave dispatch since 2026-08-25 (FA-6);
|
|
59
|
+
// verify the started set via meta.json sidecars, never via the
|
|
60
|
+
// launch ack — see skills/wave-executor/wave-loop.md § Started-Set Verification
|
|
57
61
|
})
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
**On Codex CLI / Codex Desktop:**
|
|
61
|
-
|
|
65
|
+
Codex has a native multi-agent collaboration namespace — use it; do not fall back to sequential in-session execution. Measured 2026-08-25 on `codex-cli 0.141.0`: `codex features list` reports `multi_agent stable true`, and a tool-call census over `~/.codex/sessions` (`grep -rhoE '"(spawn_agent|send_message|wait_agent|list_agents|followup_task|interrupt_agent|close_agent)"' . | sort | uniq -c`) returns wait_agent 6041 · send_message 2233 · spawn_agent 1748 · list_agents 836 · close_agent 781 · followup_task 656 · interrupt_agent 109 — i.e. the toolset is in routine production use, not a preview.
|
|
66
|
+
|
|
67
|
+
Lifecycle: `spawn_agent` (launch, one per wave task) → `list_agents` (enumerate the started set) → `wait_agent` (block on completion) → `send_message` / `followup_task` (steer a running agent) → `interrupt_agent` / `close_agent` (abort / reap). Verify the started set via `list_agents`, never via the `spawn_agent` return alone. Map wave work to these prompt-level roles:
|
|
62
68
|
- **explorer** — read-only evidence gathering (maps to Claude Code's `Explore` subagent)
|
|
63
69
|
- **worker** — implementation tasks (maps to Claude Code's `general-purpose` subagent)
|
|
64
|
-
- **session-reviewer** — quality review
|
|
70
|
+
- **session-reviewer** — quality review; spawn as a separate agent rather than reviewing in the main session
|
|
65
71
|
|
|
66
72
|
**On Cursor IDE:**
|
|
67
|
-
No Agent() tool or typed agent roles
|
|
73
|
+
No native Agent() tool or typed agent roles inside the Composer session — wave tasks run sequentially there, and `agents-per-wave` is ignored. This is a statement about the IDE session only: coordinator-side foreign dispatch to Cursor models via the headless `cursor-agent` CLI does exist (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150 — detached worktree, filesystem-measured result, `orchestrator.foreign_dispatch.completed` telemetry, mandatory Claude review, `NEVER_FOREIGN_ROLES` lock). Measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. Slash commands live in `.cursor/commands/`. There is no Skill tool — Read `skills/<name>/SKILL.md` instead.
|
|
68
74
|
|
|
69
75
|
**On Pi:**
|
|
70
76
|
No Session Orchestrator Pi subagent dispatcher exists in v1. Execute wave tasks sequentially in the active Pi session. Treat `agents-per-wave` as advisory until the SDK-based dispatcher lands.
|
|
@@ -143,6 +143,20 @@ The Ownership Model above resolves *STATE.md* specifically, but the same discipl
|
|
|
143
143
|
|
|
144
144
|
This is the wave-plan-time analog of PSA-007 (subagents never race the shared git index) applied one layer up, to shared *files* rather than the git index — see [`../../.claude/rules/parallel-sessions.md`](../../.claude/rules/parallel-sessions.md) § PSA-007.
|
|
145
145
|
|
|
146
|
+
### `wave-scope.json` Session Binding (#1123)
|
|
147
|
+
|
|
148
|
+
The rule above deconflicts writers *inside one wave*. The same working copy is also shared across SESSIONS, and `<state-dir>/wave-scope.json` is the one control artefact that constrains writes rather than describing them. It lives in the working copy, not in the session — so before #1123 a manifest written by session A governed session B's every Edit. Measured 2026-08-22 (#1082): a Discovery wave's `allowedPaths: []` — prescribed for every Discovery wave — denied all writes of an unrelated parallel session, with a deny reason that could only tell it to fix a wave plan it does not own.
|
|
149
|
+
|
|
150
|
+
**The manifest is SESSION-BOUND since #1123.** The coordinator that writes it names itself in two optional fields, `session` (raw `session_id`) and `semantic_session`, both from ONE `sessionAttribution(repoRoot)` call (`scripts/lib/events.mjs`) — see `skills/wave-executor/wave-loop.md` § Scope Manifest 1. `hooks/enforce-scope.mjs` Gate 3b classifies the manifest with `readOwnSessionIds()` + `classifyManifestSession()` (`scripts/lib/session-identity/own-session.mjs`):
|
|
151
|
+
|
|
152
|
+
- **`foreign`** (ids present, none of them ours) → the gate ALLOWS the write and emits one `orchestrator.scope.foreign_session_ignored` event. A foreign manifest is somebody else's wave plan; it never had authority here, and the event keeps the skip counted rather than silent.
|
|
153
|
+
- **`own`** → enforce, unchanged.
|
|
154
|
+
- **`unknown`** — no id in the manifest (legacy, pre-#1123) or our own identity unresolvable → enforce, unchanged. Only what is PROVABLY foreign is treated as foreign; a guess would turn "cannot tell" into a silent enforcement-off.
|
|
155
|
+
|
|
156
|
+
Two consequences for anyone touching this artefact. A stale manifest left by a crashed or finished PEER session no longer scopes this session out of its own writes — but a stale manifest of THIS session still does, so the § Scope Manifest lifecycle (delete `wave-scope.json` with `filescopes/` at session end) remains the operator's job. And an empty id is never an honest "unbound": `scripts/validate-wave-scope.mjs` (`validateSession()`) rejects `"session": ""` as an ERROR while an ABSENT key is only a warning, because an empty id matches nobody and would make every reader treat the manifest as foreign where the writer meant "binds everyone".
|
|
157
|
+
|
|
158
|
+
Mechanism, disposition table and named limits: [`../../docs/scope-collision-guard.md`](../../docs/scope-collision-guard.md) § 2.3.
|
|
159
|
+
|
|
146
160
|
## Guards
|
|
147
161
|
|
|
148
162
|
### Branch Validation
|
|
@@ -237,11 +251,24 @@ This replaces the v1 PID-liveness check (`process.kill(pid, 0)`) which was funda
|
|
|
237
251
|
Despite the v2 liveness rule above existing since Epic #583, `acquire()`'s conflict classifier (`scripts/lib/session-lock.mjs`, the `classifyExisting()` closure) and `checkStale()` still let `pidAlive`/TTL-age act as an independent veto — which let an external `/close` observe the lock's recorded `pid` (the ephemeral hook subprocess / `node -e acquire()` PID, routinely dead within <1s) as dead and misclassify a live, actively-heartbeating session as `stale-pid-dead`, hijacking it mid-wave. Fixed in #744:
|
|
238
252
|
|
|
239
253
|
- `classifyExisting()` now checks `isLockLive(existing)` **first** and unconditionally returns `{ reason: 'active' }` when true — a dead recorded `pid` can never veto a fresh `last_heartbeat`.
|
|
240
|
-
- Only once `isLockLive()` is false
|
|
241
|
-
- `checkStale()` surfaces the same `isLockLive()` result as an additive `isLive` field alongside the legacy `ttlExpired
|
|
254
|
+
- Only once `isLockLive()` is false is the lock classified stale — see the #1137 follow-up below for the single reason it now returns.
|
|
255
|
+
- `checkStale()` surfaces the same `isLockLive()` result as an additive `isLive` field alongside the legacy `ttlExpired` signal, so recovery-flow diagnostics can observe when the two diverge.
|
|
242
256
|
|
|
243
257
|
Net: `pid` (field notes above) stays forensic-only; `last_heartbeat` freshness is the sole determinant of "is this session still active" everywhere in `session-lock.mjs`.
|
|
244
258
|
|
|
259
|
+
### #1137 — one stale reason, `stale-heartbeat`
|
|
260
|
+
|
|
261
|
+
#744 left the *stale* half still keyed on `pidAlive`: `stale-pid-dead` when the recorded pid was confirmed dead, `stale-pid-alive` otherwise. Measured 2026-08-23 across the fleet's live locks: **7 of 7 recorded pids were dead**, including the lock of the session that was heartbeating at that very moment. The pid on a lock is the `node -e` / hook subprocess that wrote it, and it exits within about a second of genesis. Two consequences, both live defects:
|
|
262
|
+
|
|
263
|
+
- `stale-pid-alive` was **structurally unreachable** same-host — nothing could produce it except a pid-number collision.
|
|
264
|
+
- The Phase-1.2 recovery AUQ rendered "pid=… is confirmed dead" for **every** same-host stale lock, presenting a measurement it had not made as the operator's reason to reclaim.
|
|
265
|
+
|
|
266
|
+
The fix removes the question rather than re-answering it. `classifyExisting()` returns exactly one stale reason, `stale-heartbeat`, carrying `ageHours` (age from `started_at`, unchanged) and `heartbeatAgeMinutes` (age from `last_heartbeat`) — the quantity the liveness rule actually thresholds against, so a recovery prompt states the measured heartbeat age instead of a liveness verdict. `checkStale()` gains the same `heartbeatAgeMinutes` field.
|
|
267
|
+
|
|
268
|
+
**#1151 follow-up — the `pidAlive` stub is GONE.** #1137 left `checkStale()` returning `pidAlive: null` as a shape-compatible placeholder. It was removed outright: measured @ `f0766e1`, zero production readers repo-wide, so the field only invited a reader to treat `null` as "unknown liveness" — a question this code no longer asks. `checkStale()` now returns `isLive` (the verdict) and `heartbeatAgeMinutes` (the magnitude behind it); anything reasoning about a lock's liveness reads those two. `isPidAliveOnHost` stays exported for `file-lock.mjs` and `lock-reaper.mjs`, where the pid IS the process being asked about.
|
|
269
|
+
|
|
270
|
+
`isPidAliveOnHost` remains exported from `session-lock.mjs` and is unaffected — `file-lock.mjs` and `lock-reaper.mjs` are legitimate callers, because there the pid IS the process being asked about.
|
|
271
|
+
|
|
245
272
|
### Schema v1 → v2 backward-compat
|
|
246
273
|
|
|
247
274
|
Readers (e.g., `readLock()` in `session-lock.mjs`, `discoverActiveSessions()`) MUST tolerate absent `last_heartbeat` and `semantic_session_id` fields (v1 locks written before Epic #583). When `last_heartbeat` is absent, fall back to TTL-based expiry from `started_at`. When `semantic_session_id` is absent, treat as unknown.
|
|
@@ -206,7 +206,11 @@ const modeSelector = async () => {
|
|
|
206
206
|
// Each iteration rebuilds signals from current disk state. STATE.md will be
|
|
207
207
|
// freshly idle-reset by the previous /close, sessions.jsonl will have the
|
|
208
208
|
// new tail entry, etc. This is the contract: live signals every iteration.
|
|
209
|
-
|
|
209
|
+
// `repoRoot` is passed explicitly (#1071) rather than left to the helper's
|
|
210
|
+
// cwd default. Do NOT hand-write a `backlogLimit` here — the window default
|
|
211
|
+
// lives once, in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`); see
|
|
212
|
+
// skills/session-start/phase-7-5-mode-selector.md for the same contract.
|
|
213
|
+
const signals = await buildLiveSignals({ repoRoot: process.cwd() });
|
|
210
214
|
return selectMode(signals);
|
|
211
215
|
};
|
|
212
216
|
|
|
@@ -19,7 +19,7 @@ description: >
|
|
|
19
19
|
|
|
20
20
|
This skill runs when the Bootstrap Gate is closed (missing CLAUDE.md, Session Config, or `.orchestrator/bootstrap.lock`) or when the user invokes `/bootstrap` directly. It scaffolds the minimum structure required by all session-orchestrator skills, commits it, and writes the lock file that opens the gate for all future invocations.
|
|
21
21
|
|
|
22
|
-
**Anti-bureaucracy contract:**
|
|
22
|
+
**Anti-bureaucracy contract:** On a first-time full bootstrap (no tier flags, no `--no-interview`), expect **7–9** `AskUserQuestion` prompts in three fixed blocks — not an open-ended wizard. (1) **Tier/stack** (Phase 2): one tier-confirmation question, plus an optional second archetype question when `PATH_TYPE = public` and archetype confidence is low (Standard/Deep only). (2) **Owner persona** (Phase 3.5): five questions from `scripts/lib/owner-interview.mjs` (first-run only). (3) **Dispatcher autonomy** (Phase 3.5.1): one question from `scripts/lib/config/dispatcher-autonomy-capture.mjs`. Flagged flows (`--upgrade`, `--retroactive`, `--sync-rules`, `--ecosystem-health`) and `--no-interview` skip some or all of these blocks.
|
|
23
23
|
|
|
24
24
|
## Invocation Context
|
|
25
25
|
|
|
@@ -120,7 +120,7 @@ AskUserQuestion({
|
|
|
120
120
|
})
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Store as `CONFIRMED_ARCHETYPE`.
|
|
123
|
+
Store as `CONFIRMED_ARCHETYPE`. The tier/stack block contributes **1–2** questions; a first-run full bootstrap adds **6 more** from the owner interview (Phase 3.5, five questions) and dispatcher-autonomy capture (Phase 3.5.1, one question) — **7–9 total**.
|
|
124
124
|
|
|
125
125
|
## Upgrade Flow (`--upgrade <tier>`)
|
|
126
126
|
|
|
@@ -586,7 +586,7 @@ If invoked directly via `/bootstrap`: report the created files list and stop.
|
|
|
586
586
|
|
|
587
587
|
- **NEVER create application code during bootstrap** — only structural files (CLAUDE.md, .gitignore, README.md, manifests, CI). The feature that follows brings its own implementation.
|
|
588
588
|
- **NEVER skip the lock file write** — `.orchestrator/bootstrap.lock` is the gate's mechanical truth. Bootstrap without a lock file is incomplete.
|
|
589
|
-
- **
|
|
589
|
+
- **Fixed question budget, no ad-hoc prompts** — tier/stack (1–2), owner interview (5, Phase 3.5), dispatcher-autonomy capture (1, Phase 3.5.1) sum to **7–9** on a first-run full bootstrap; `--no-interview` and flag short-circuits reduce this. Make a best-effort tier recommendation and let the user correct via `/bootstrap --upgrade` later — do not add prompts beyond these blocks.
|
|
590
590
|
- **ALWAYS commit** — bootstrap ends with a git commit. The lock file is part of that commit.
|
|
591
591
|
- **ALWAYS check for retroactive flag** — if `--retroactive` is in `$ARGUMENTS`, skip all scaffolding and jump directly to writing `bootstrap.lock` (tier inferred from existing file inventory, fallback: `fast`).
|
|
592
592
|
- **NEVER abort bootstrap on rules-fetch failure** — rules-fetch is opt-in and best-effort. The legacy Clank sync path is the safety net.
|