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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* atomic-json.mjs — shared atomic read-modify-write helper for JSON hook state.
|
|
3
|
+
*
|
|
4
|
+
* Extracted (issue #1197) from four byte-identical private copies of
|
|
5
|
+
* `atomicMutateJson` that had accumulated independently in
|
|
6
|
+
* `hooks/cwd-change-restore.mjs`, `hooks/on-session-end.mjs`,
|
|
7
|
+
* `hooks/post-tool-batch-wave-signal.mjs`, and
|
|
8
|
+
* `hooks/post-tool-failure-corrective-context.mjs` (measured 2026-09-02 @
|
|
9
|
+
* a019d5a4 via `rg -n "function atomicMutateJson" hooks/` — 4 hits, one per
|
|
10
|
+
* file, logic byte-identical modulo the per-file tmp-suffix and the
|
|
11
|
+
* `fs.`-namespace-vs-named-import style `on-session-end.mjs` uses).
|
|
12
|
+
*
|
|
13
|
+
* F-H fix (W3-reviewer finding, #1197): the pre-extraction copies treated
|
|
14
|
+
* EVERY read/parse failure as "file absent" and silently fell back to
|
|
15
|
+
* `defaultValue` — so an unparsable file, `EISDIR`, `EACCES`, or a file
|
|
16
|
+
* truncated mid-write by a concurrent writer was overwritten with
|
|
17
|
+
* `defaultValue`-derived content instead of being left alone. Only `ENOENT`
|
|
18
|
+
* (file genuinely does not exist yet) is a legitimate "start fresh" case;
|
|
19
|
+
* every other failure now aborts BEFORE the tmp-write/rename stage and
|
|
20
|
+
* reports `{ ok: false, reason }` — the original file is never touched.
|
|
21
|
+
*
|
|
22
|
+
* Return-object over throw (deliberate — see #1197 task note): all four
|
|
23
|
+
* callers already run under `main().catch(() => {}).finally(() =>
|
|
24
|
+
* process.exit(0))` — informational, never-deny hooks (verified 2026-09-02:
|
|
25
|
+
* none of the four match a deny/block pattern) — so a thrown error would be
|
|
26
|
+
* swallowed safely too. But in `post-tool-batch-wave-signal.mjs` a throw at
|
|
27
|
+
* the FIRST call site (line ~278, the `last_batch` write) would abort
|
|
28
|
+
* `main()` before it reaches the independent heartbeat-refresh block that
|
|
29
|
+
* follows — a real behavioural loss the thrown-error path would introduce
|
|
30
|
+
* silently. A result object lets every caller decide locally whether an RMW
|
|
31
|
+
* failure should short-circuit the rest of `main()` or just get logged and
|
|
32
|
+
* ignored, so no caller loses unrelated post-call behaviour by construction.
|
|
33
|
+
*
|
|
34
|
+
* NAMED CEILING (BV-004): tmp-file + rename makes each individual write
|
|
35
|
+
* atomic, but not the full read-modify-write — two concurrent callers can
|
|
36
|
+
* still interleave (both read the same `current`, both compute an update
|
|
37
|
+
* from it, the second rename wins and silently drops the first mutation).
|
|
38
|
+
* This module does not defend against that race. Revisit with a per-file
|
|
39
|
+
* lock (e.g. an flock-style sidecar or `session-lock.mjs`'s lease pattern)
|
|
40
|
+
* if `.orchestrator/current-session.json` writes start dropping fields
|
|
41
|
+
* under concurrent-session load — no such loss has been measured yet.
|
|
42
|
+
*
|
|
43
|
+
* @module hooks/_lib/atomic-json
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Atomic read-modify-write of a JSON file via temp-file + rename.
|
|
51
|
+
*
|
|
52
|
+
* Reads the existing file and parses it as JSON. When the file does not
|
|
53
|
+
* exist (`ENOENT`), starts from `defaultValue` — the only case in which
|
|
54
|
+
* "file absent" is legitimate. Any OTHER read or parse failure (directory
|
|
55
|
+
* at `filePath`, permission denied, truncated/corrupt JSON, …) aborts
|
|
56
|
+
* WITHOUT calling `mutate` and WITHOUT writing anything — the original file
|
|
57
|
+
* (if any) is left exactly as it was.
|
|
58
|
+
*
|
|
59
|
+
* On success, applies the synchronous `mutate` transformer, writes the
|
|
60
|
+
* result to a `${filePath}.tmp-<suffix>-<pid>-<ts>` sibling, then renames it
|
|
61
|
+
* over `filePath` (atomic on POSIX same-filesystem rename; best-effort on
|
|
62
|
+
* Windows). If the write/rename stage itself fails, the tmp file is
|
|
63
|
+
* best-effort unlinked so a failure never leaves an orphaned `.tmp-*`
|
|
64
|
+
* artifact behind.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} filePath — absolute path to the JSON file.
|
|
67
|
+
* @param {object} defaultValue — starting value used ONLY when the file does
|
|
68
|
+
* not exist yet (`ENOENT`). Never applied on top of an unreadable-but-
|
|
69
|
+
* present file.
|
|
70
|
+
* @param {function(object): object} mutate — pure synchronous transformer;
|
|
71
|
+
* receives the parsed current value (or `defaultValue`), returns the next
|
|
72
|
+
* value to persist.
|
|
73
|
+
* @param {string} [tmpTag] — short tag folded into the tmp filename so
|
|
74
|
+
* concurrent callers targeting the same `filePath` from different hooks
|
|
75
|
+
* don't collide on the same tmp path (mirrors the per-caller suffixes the
|
|
76
|
+
* four pre-extraction copies used: `-cwd-`, `-ose-`, `-ptb-`, `-ptf-`).
|
|
77
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, reason: string }>}
|
|
78
|
+
*/
|
|
79
|
+
export async function atomicMutateJson(filePath, defaultValue, mutate, tmpTag = 'ajs') {
|
|
80
|
+
let current = defaultValue;
|
|
81
|
+
try {
|
|
82
|
+
const raw = await readFile(filePath, 'utf8');
|
|
83
|
+
current = JSON.parse(raw);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err && err.code === 'ENOENT') {
|
|
86
|
+
// File genuinely does not exist yet — the only legitimate "fresh
|
|
87
|
+
// file" case. `current` already holds `defaultValue`.
|
|
88
|
+
} else {
|
|
89
|
+
// EISDIR, EACCES, a JSON.parse SyntaxError (unparsable/truncated
|
|
90
|
+
// content), or anything else — never silently treat as "absent".
|
|
91
|
+
// Abort before mutate/write; the original file is untouched.
|
|
92
|
+
return { ok: false, reason: (err && err.code) || 'parse-error' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const updated = mutate(current);
|
|
97
|
+
const tmp = `${filePath}.tmp-${tmpTag}-${process.pid}-${Date.now()}`;
|
|
98
|
+
try {
|
|
99
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
100
|
+
await writeFile(tmp, JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
101
|
+
await rename(tmp, filePath);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
try {
|
|
104
|
+
await unlink(tmp);
|
|
105
|
+
} catch {
|
|
106
|
+
// tmp was never created, or is already gone — nothing to clean up.
|
|
107
|
+
}
|
|
108
|
+
return { ok: false, reason: (err && err.code) || 'write-error' };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, value: updated };
|
|
111
|
+
}
|
|
@@ -89,8 +89,9 @@ export async function bootstrapLock({
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Step 1: try to acquire. If a fresh acquire succeeds, we are done.
|
|
92
|
-
// If a stale-
|
|
93
|
-
//
|
|
92
|
+
// If a stale-heartbeat lock exists, force-overwrite it (the prior session
|
|
93
|
+
// stopped heartbeating past its ttl; the current raw owner can take the
|
|
94
|
+
// worktree).
|
|
94
95
|
// Only an exact match of the existing physical raw sessionId permits the
|
|
95
96
|
// same-session force-refresh. semantic_session_id, STATE.md `session`, and
|
|
96
97
|
// an owner proof never make a different raw id the same owner.
|
|
@@ -107,8 +108,11 @@ export async function bootstrapLock({
|
|
|
107
108
|
|
|
108
109
|
const shouldForce =
|
|
109
110
|
acquireResult.ok !== true && (
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
// #1137: 'stale-heartbeat' replaced the former 'stale-pid-dead' /
|
|
112
|
+
// 'stale-pid-alive' pair. Both legacy spellings are gone from
|
|
113
|
+
// session-lock.mjs; matching only the new one keeps this force-branch
|
|
114
|
+
// reachable.
|
|
115
|
+
acquireResult.reason === 'stale-heartbeat' ||
|
|
112
116
|
(acquireResult.reason === 'active' &&
|
|
113
117
|
acquireResult.existingLock &&
|
|
114
118
|
acquireResult.existingLock.session_id === sessionId)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-paths.mjs — consolidated subagent sidecar-path derivation (#1196).
|
|
3
|
+
*
|
|
4
|
+
* Four call sites independently derived the SAME shape — the stopping
|
|
5
|
+
* subagent's own `agent-<id>.jsonl` transcript + `agent-<id>.meta.json`
|
|
6
|
+
* sidecar, from the PARENT transcript path the harness sends on stdin — and
|
|
7
|
+
* diverged on strictness. Measured 2026-09-02 @ a019d5a4 (this consolidation):
|
|
8
|
+
*
|
|
9
|
+
* | copy (file:line) | honours `agent_transcript_path` | id bound | rejects `'unknown'` | containment check |
|
|
10
|
+
* |------------------------------------------------------------|:---:|:---:|:---:|:---:|
|
|
11
|
+
* | hooks/subagent-telemetry.mjs:513 `resolveSubagentTranscriptPath` | no | unbounded (`+`) | yes (:517) | n/a (no override) |
|
|
12
|
+
* | hooks/on-stop.mjs:666 `resolveSidecarBase` | no | `{1,64}` (:566) | **no** | n/a (no override) |
|
|
13
|
+
* | hooks/post-subagent-discovery-validator.mjs:538 `resolveAgentTranscriptPath` | yes (:539) | `{1,64}` (:547) | yes (:549) | **no** — `explicit` returned unchecked (:539) |
|
|
14
|
+
* | scripts/lib/wave-transcript-tail.mjs:610 `readAgentType` | no | **none** | no | n/a (no override) |
|
|
15
|
+
*
|
|
16
|
+
* `resolveSubagentSidecar()` below applies the STRICTEST rule found in ANY
|
|
17
|
+
* copy to every caller: the `{1,64}` bound, the `'unknown'` rejection (closes
|
|
18
|
+
* the on-stop.mjs / wave-transcript-tail.mjs gap), AND a path-containment
|
|
19
|
+
* check on the `agentTranscriptPath` override that NONE of the four copies
|
|
20
|
+
* performed — post-subagent-discovery-validator.mjs returned the harness
|
|
21
|
+
* override completely unvalidated. That containment check is a NEW rule this
|
|
22
|
+
* consolidation introduces, not a preserved behaviour.
|
|
23
|
+
*
|
|
24
|
+
* @module hooks/_lib/subagent-paths
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Real agent ids are hex-ish tokens (e.g. `a60348a01ca982b4c`); anything else
|
|
31
|
+
* is rejected, not sanitised, so no payload value can traverse out of the
|
|
32
|
+
* `subagents/` directory. `{1,64}` is the strictest bound found across the
|
|
33
|
+
* four prior copies (hooks/subagent-telemetry.mjs used an unbounded `+`;
|
|
34
|
+
* scripts/lib/wave-transcript-tail.mjs applied no charset check at all).
|
|
35
|
+
*/
|
|
36
|
+
export const AGENT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate a candidate agent id against the strictest rule of the four prior
|
|
40
|
+
* copies: the `{1,64}` charset+length bound, AND explicit rejection of the
|
|
41
|
+
* literal `'unknown'` — the no-usable-id fallback value that several callers
|
|
42
|
+
* (`firstNonEmptyString(..., 'unknown')`) substitute when the harness sends no
|
|
43
|
+
* id at all. `'unknown'` matches `AGENT_ID_RE` (it's plain lowercase letters),
|
|
44
|
+
* so a charset check alone does not catch it — hooks/on-stop.mjs's
|
|
45
|
+
* `resolveSidecarBase()` had exactly this gap (AGENT_ID_RE-only, no explicit
|
|
46
|
+
* `'unknown'` rejection) before this consolidation.
|
|
47
|
+
*
|
|
48
|
+
* @param {unknown} agentId
|
|
49
|
+
* @returns {agentId is string}
|
|
50
|
+
*/
|
|
51
|
+
export function isValidAgentId(agentId) {
|
|
52
|
+
return typeof agentId === 'string' && AGENT_ID_RE.test(agentId) && agentId !== 'unknown';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the sidecar pair (`.jsonl` transcript + `.meta.json`) for a stopping
|
|
57
|
+
* subagent (#949, #1190, #1191, #1196).
|
|
58
|
+
*
|
|
59
|
+
* Two paths to the same result:
|
|
60
|
+
* - **Override.** `agentTranscriptPath` (the harness's `agent_transcript_path`,
|
|
61
|
+
* honoured today only by hooks/post-subagent-discovery-validator.mjs:539)
|
|
62
|
+
* wins when present — but ONLY when, after `path.resolve`, it sits inside
|
|
63
|
+
* `dirname(transcriptPath)`. An override outside that tree is REJECTED
|
|
64
|
+
* (returns null), never silently accepted and never falls through to
|
|
65
|
+
* derivation — a bad override must not resolve to an unrelated agent's
|
|
66
|
+
* sidecar.
|
|
67
|
+
* - **Derivation.** `<dir>/<base>/subagents/agent-<agentId>` from the PARENT
|
|
68
|
+
* transcript path — the shape all four prior copies agree on
|
|
69
|
+
* (subagent-telemetry.mjs:513, on-stop.mjs:666,
|
|
70
|
+
* post-subagent-discovery-validator.mjs:552-553, and the subagentsDir
|
|
71
|
+
* scripts/lib/wave-transcript-tail.mjs's tailLoop() already builds by hand).
|
|
72
|
+
*
|
|
73
|
+
* Returns null — never a guessed or partial path — on ANY invalid input: an
|
|
74
|
+
* empty/non-string `transcriptPath`, an `agentId` failing `isValidAgentId()`
|
|
75
|
+
* (only checked in the derivation branch — the override branch does not need
|
|
76
|
+
* a valid agentId, matching all four prior copies' precedence), or an override
|
|
77
|
+
* that fails containment.
|
|
78
|
+
*
|
|
79
|
+
* NAMED CEILING (BV-004): the meta path is always derived as
|
|
80
|
+
* `transcript.replace(/\.jsonl$/i, '.meta.json')` (or `${transcript}.meta.json`
|
|
81
|
+
* when the transcript has no `.jsonl` suffix to replace — an override the
|
|
82
|
+
* harness contract says never happens; see the header comments of
|
|
83
|
+
* hooks/post-subagent-discovery-validator.mjs). REVISIT TRIGGER: if a real
|
|
84
|
+
* override without a `.jsonl` suffix is ever observed, this needs its own
|
|
85
|
+
* validation branch instead of the current always-well-formed assumption.
|
|
86
|
+
*
|
|
87
|
+
* NAMED CEILING (BV-004, LOW, qa review): the override's containment check
|
|
88
|
+
* (`path.relative(resolvedDir, resolvedOverride)` below) is LEXICAL, not
|
|
89
|
+
* filesystem-real — it runs on `path.resolve()`'d strings, never on
|
|
90
|
+
* `fs.realpathSync()`'d ones. A symlink SITTING INSIDE `dirname(transcriptPath)`
|
|
91
|
+
* whose TARGET points outside that tree therefore passes containment: the
|
|
92
|
+
* lexical path looks contained, the file it actually resolves to is not.
|
|
93
|
+
* Deliberately not fixed with a realpath call here — every current consumer
|
|
94
|
+
* of this override branch (hooks/post-subagent-discovery-validator.mjs) only
|
|
95
|
+
* READS the resulting sidecar files, and `agentTranscriptPath` is a value the
|
|
96
|
+
* HARNESS itself supplies on stdin, not one an untrusted external actor can
|
|
97
|
+
* inject — so the realpath syscalls would buy nothing against the live
|
|
98
|
+
* threat model. REVISIT TRIGGER: if `agentTranscriptPath` (or any future
|
|
99
|
+
* override this module accepts) is ever sourced from a producer this repo
|
|
100
|
+
* does NOT trust as much as the harness's own stdin payload, resolve both
|
|
101
|
+
* `resolvedOverride` and `resolvedDir` through `fs.realpathSync()` before the
|
|
102
|
+
* `path.relative()` comparison, so a symlink cannot smuggle the effective
|
|
103
|
+
* path outside the allowed tree.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} args
|
|
106
|
+
* @param {string|undefined|null} args.transcriptPath — parent transcript path
|
|
107
|
+
* (stdin `transcript_path`, or an equivalent parent-transcript path)
|
|
108
|
+
* @param {string|undefined|null} args.agentId
|
|
109
|
+
* @param {string|undefined|null} [args.agentTranscriptPath] — harness override
|
|
110
|
+
* (`agent_transcript_path`); takes precedence over derivation when present
|
|
111
|
+
* and contained under `dirname(transcriptPath)`
|
|
112
|
+
* @returns {{base: string, transcript: string, meta: string}|null}
|
|
113
|
+
*/
|
|
114
|
+
export function resolveSubagentSidecar({ transcriptPath, agentId, agentTranscriptPath = null }) {
|
|
115
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return null;
|
|
116
|
+
|
|
117
|
+
const dir = path.dirname(transcriptPath);
|
|
118
|
+
const base = path.basename(transcriptPath).replace(/\.jsonl$/i, '');
|
|
119
|
+
if (!base || base === '.' || base === '..') return null;
|
|
120
|
+
|
|
121
|
+
let transcript;
|
|
122
|
+
if (typeof agentTranscriptPath === 'string' && agentTranscriptPath.trim()) {
|
|
123
|
+
const resolvedOverride = path.resolve(agentTranscriptPath);
|
|
124
|
+
const resolvedDir = path.resolve(dir);
|
|
125
|
+
const rel = path.relative(resolvedDir, resolvedOverride);
|
|
126
|
+
const contained = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
127
|
+
if (!contained) return null;
|
|
128
|
+
transcript = resolvedOverride;
|
|
129
|
+
} else {
|
|
130
|
+
if (!isValidAgentId(agentId)) return null;
|
|
131
|
+
transcript = path.join(dir, base, 'subagents', `agent-${agentId}.jsonl`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const meta = /\.jsonl$/i.test(transcript)
|
|
135
|
+
? transcript.replace(/\.jsonl$/i, '.meta.json')
|
|
136
|
+
: `${transcript}.meta.json`;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
base: transcript.replace(/\.jsonl$/i, ''),
|
|
140
|
+
transcript,
|
|
141
|
+
meta,
|
|
142
|
+
};
|
|
143
|
+
}
|