session-orchestrator 5.3.0 → 5.5.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/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -36,6 +36,16 @@
|
|
|
36
36
|
* dispatcher: (optional; host-local cross-repo dispatcher autonomy override — #679)
|
|
37
37
|
* autonomy: string ('' = no override; resolver enum off | advisory | autonomous-gated;
|
|
38
38
|
* precedence env SO_DISPATCHER_AUTONOMY > this > committed > off)
|
|
39
|
+
* vault-integration: (optional; host-local switch for the vault-integration gate — SO#1448)
|
|
40
|
+
* enabled: boolean (false = off on this host; true is ignored — the host may not enable)
|
|
41
|
+
* mode: 'off' | 'warn' | 'strict' (applied only when weaker than the committed mode;
|
|
42
|
+
* precedence env SO_VAULT_INTEGRATION > this > committed.
|
|
43
|
+
* An invalid value drops ONLY this section, see loadOwnerConfig)
|
|
44
|
+
* vault-dirs: (optional list; per-directory vault-dir — agents/vault#319)
|
|
45
|
+
* - path: string (the vault directory)
|
|
46
|
+
* match: { path-prefix: string } (required; cwd directory prefix, first match wins)
|
|
47
|
+
* name: string (optional label, defaults to path)
|
|
48
|
+
* precedence env SO_VAULT_DIR > this > paths.vault-dir > committed
|
|
39
49
|
*
|
|
40
50
|
* ── Exports ───────────────────────────────────────────────────────────────────
|
|
41
51
|
*
|
|
@@ -44,7 +54,8 @@
|
|
|
44
54
|
* validateOwnerSections(obj) — pure validation, no I/O; bucketed per section (#820)
|
|
45
55
|
* validateOwnerConfig(obj) — pure validation, no I/O; thin wrapper over validateOwnerSections
|
|
46
56
|
* loadOwnerConfig({path?}) — reads file; per-section tolerance for OPTIONAL
|
|
47
|
-
* sections (paths/dispatcher/vaults/
|
|
57
|
+
* sections (paths/dispatcher/vault-integration/vaults/
|
|
58
|
+
* baselines/vault-dirs) — #820
|
|
48
59
|
* writeOwnerConfig(config, {path?}) — validates, writes YAML, creates dir
|
|
49
60
|
* getDefaults() — returns sensible default config object
|
|
50
61
|
*
|
|
@@ -202,9 +213,12 @@ const REQUIRED_SECTIONS = /** @type {const} */ (['owner', 'tone', 'efficiency',
|
|
|
202
213
|
* sections — currently `scripts/lib/owner-config-banner.mjs`, which derived the
|
|
203
214
|
* #1244 "survived a whole-file discard" set from a hand-mirrored local copy.
|
|
204
215
|
* Frozen so a consumer cannot mutate the shared array.
|
|
216
|
+
*
|
|
217
|
+
* `vault-integration` has no `getDefaults()` entry: absent means "no host
|
|
218
|
+
* override", so a dropped section simply leaves the committed gate in force.
|
|
205
219
|
*/
|
|
206
220
|
export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
|
|
207
|
-
Object.freeze(['paths', 'dispatcher'])
|
|
221
|
+
Object.freeze(['paths', 'dispatcher', 'vault-integration'])
|
|
208
222
|
);
|
|
209
223
|
|
|
210
224
|
/**
|
|
@@ -213,7 +227,10 @@ export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
|
|
|
213
227
|
* `parseNamedVaults`/`parseBaselines` — that already drops bad entries with
|
|
214
228
|
* its own WARN). Surfaced here only via `sectionWarnings`, never dropped.
|
|
215
229
|
*/
|
|
216
|
-
const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines']);
|
|
230
|
+
const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines', 'vault-dirs']);
|
|
231
|
+
|
|
232
|
+
/** Host-settable vault-integration modes (mirrors VAULT_MODE_VALUES in config-schema.mjs). */
|
|
233
|
+
const VALID_VAULT_INTEGRATION_MODES = ['off', 'warn', 'strict'];
|
|
217
234
|
|
|
218
235
|
/** Canonical validation-order — MUST match the pre-#820 inline validation order. */
|
|
219
236
|
const SECTION_ORDER = /** @type {const} */ ([
|
|
@@ -421,6 +438,35 @@ export function validateOwnerSections(obj) {
|
|
|
421
438
|
sections.dispatcher = { valid: errors.length === 0, errors };
|
|
422
439
|
}
|
|
423
440
|
|
|
441
|
+
// ── vault-integration (optional; host-local gate switch — SO#1448) ──────────
|
|
442
|
+
// STRICT enum validation, unlike `dispatcher`: a typo like `mode: of` must not
|
|
443
|
+
// fall through to the committed `strict` silently while the operator believes
|
|
444
|
+
// the host is off. An invalid section is dropped (and reported) on its own —
|
|
445
|
+
// `paths` and every other section survive (#820 per-section tolerance).
|
|
446
|
+
{
|
|
447
|
+
const errors = [];
|
|
448
|
+
const vi = obj['vault-integration'];
|
|
449
|
+
if (vi !== undefined && vi !== null) {
|
|
450
|
+
if (!isPlainObject(vi)) {
|
|
451
|
+
errors.push('vault-integration must be an object when present');
|
|
452
|
+
} else {
|
|
453
|
+
if (vi.enabled !== undefined && vi.enabled !== null && typeof vi.enabled !== 'boolean') {
|
|
454
|
+
errors.push(`vault-integration.enabled must be a boolean, got: ${typeof vi.enabled}`);
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
vi.mode !== undefined &&
|
|
458
|
+
vi.mode !== null &&
|
|
459
|
+
!VALID_VAULT_INTEGRATION_MODES.includes(vi.mode)
|
|
460
|
+
) {
|
|
461
|
+
errors.push(
|
|
462
|
+
`vault-integration.mode must be one of ${VALID_VAULT_INTEGRATION_MODES.join(', ')}, got: ${JSON.stringify(vi.mode)}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
sections['vault-integration'] = { valid: errors.length === 0, errors };
|
|
468
|
+
}
|
|
469
|
+
|
|
424
470
|
// ── vaults (optional; N named vaults for walk-up resolution — #700) ──────────
|
|
425
471
|
// Drop-and-WARN on malformed entries is handled by parseNamedVaults() in
|
|
426
472
|
// named-vault-resolver.mjs. Here we only validate the container shape to
|
|
@@ -512,6 +558,47 @@ export function validateOwnerSections(obj) {
|
|
|
512
558
|
sections.baselines = { valid: errors.length === 0, errors };
|
|
513
559
|
}
|
|
514
560
|
|
|
561
|
+
// ── vault-dirs (optional; per-directory vault-dir — agents/vault#319) ────────
|
|
562
|
+
// Same entry shape as baselines: (and parsed by the same parseBaselines() with
|
|
563
|
+
// section 'vault-dirs'), except `name` is optional. List section: surfaced via
|
|
564
|
+
// sectionWarnings, never dropped — the resolver drops bad entries itself.
|
|
565
|
+
{
|
|
566
|
+
const errors = [];
|
|
567
|
+
const vaultDirs = obj['vault-dirs'];
|
|
568
|
+
if (vaultDirs !== undefined && vaultDirs !== null) {
|
|
569
|
+
if (!Array.isArray(vaultDirs)) {
|
|
570
|
+
errors.push('vault-dirs must be an array when present');
|
|
571
|
+
} else {
|
|
572
|
+
for (let i = 0; i < vaultDirs.length; i++) {
|
|
573
|
+
const entry = vaultDirs[i];
|
|
574
|
+
if (!isPlainObject(entry)) {
|
|
575
|
+
errors.push(`vault-dirs[${i}] must be an object`);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (typeof entry.path !== 'string' || entry.path.trim() === '') {
|
|
579
|
+
errors.push(`vault-dirs[${i}].path must be a non-empty string`);
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
entry.name !== undefined &&
|
|
583
|
+
entry.name !== null &&
|
|
584
|
+
(typeof entry.name !== 'string' || entry.name.trim() === '')
|
|
585
|
+
) {
|
|
586
|
+
errors.push(`vault-dirs[${i}].name must be a non-empty string when present`);
|
|
587
|
+
}
|
|
588
|
+
if (!isPlainObject(entry.match)) {
|
|
589
|
+
errors.push(`vault-dirs[${i}].match must be an object`);
|
|
590
|
+
} else if (
|
|
591
|
+
typeof entry.match['path-prefix'] !== 'string' ||
|
|
592
|
+
entry.match['path-prefix'].trim() === ''
|
|
593
|
+
) {
|
|
594
|
+
errors.push(`vault-dirs[${i}].match.path-prefix must be a non-empty string`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
sections['vault-dirs'] = { valid: errors.length === 0, errors };
|
|
600
|
+
}
|
|
601
|
+
|
|
515
602
|
const errors = SECTION_ORDER.flatMap((name) => sections[name].errors);
|
|
516
603
|
return { sections, errors };
|
|
517
604
|
}
|
|
@@ -540,10 +627,10 @@ export function validateOwnerConfig(obj) {
|
|
|
540
627
|
* Per-section tolerance (#820): an invalid REQUIRED section (owner, tone,
|
|
541
628
|
* efficiency, hardware-sharing) still discards the whole file (legacy
|
|
542
629
|
* behaviour, unchanged). An invalid OPTIONAL object section (paths,
|
|
543
|
-
* dispatcher) is instead replaced by its default value — the rest of the
|
|
630
|
+
* dispatcher, vault-integration) is instead replaced by its default value — the rest of the
|
|
544
631
|
* file survives, `source` becomes `'partial'`, and the drop is reported via
|
|
545
632
|
* `droppedSections` + a stderr WARN. OPTIONAL list sections (vaults,
|
|
546
|
-
* baselines) are passed through UNTOUCHED even when strict-invalid — their
|
|
633
|
+
* baselines, vault-dirs) are passed through UNTOUCHED even when strict-invalid — their
|
|
547
634
|
* consumers already run a lenient parse-at-point-of-use pass — and are
|
|
548
635
|
* surfaced only via `sectionWarnings` (never dropped, never counted towards
|
|
549
636
|
* `'partial'`).
|
|
@@ -658,6 +745,14 @@ export function loadOwnerConfig(opts = {}) {
|
|
|
658
745
|
discardDropped.push({ section: name, errors: sec.errors });
|
|
659
746
|
}
|
|
660
747
|
}
|
|
748
|
+
// `vault-dirs:` (agents/vault#319) survives the discard too, raw — exactly as
|
|
749
|
+
// on the required-valid branch below; parseBaselines() drops bad entries at
|
|
750
|
+
// point-of-use. Dropping it would hand a repo under a declared prefix the
|
|
751
|
+
// host-wide `paths.vault-dir` (which DOES survive above): the silent vault
|
|
752
|
+
// override #319 exists to prevent, triggered by an unrelated `tone:` typo.
|
|
753
|
+
if (parsed['vault-dirs'] !== undefined && parsed['vault-dirs'] !== null) {
|
|
754
|
+
discardConfig['vault-dirs'] = parsed['vault-dirs'];
|
|
755
|
+
}
|
|
661
756
|
const discardResult = { config: discardConfig, source: 'defaults', errors: allErrors };
|
|
662
757
|
if (discardDropped.length > 0) discardResult.droppedSections = discardDropped;
|
|
663
758
|
return discardResult;
|
|
@@ -673,7 +768,10 @@ export function loadOwnerConfig(opts = {}) {
|
|
|
673
768
|
for (const name of OPTIONAL_OBJECT_SECTIONS) {
|
|
674
769
|
const sec = sections[name];
|
|
675
770
|
if (sec && !sec.valid) {
|
|
676
|
-
|
|
771
|
+
// A section without a default (vault-integration) is removed, not set to
|
|
772
|
+
// `undefined`: js-yaml's dump rejects undefined values on a write-back.
|
|
773
|
+
if (defaults[name] === undefined) delete config[name];
|
|
774
|
+
else config[name] = defaults[name];
|
|
677
775
|
droppedSections.push({ section: name, errors: sec.errors });
|
|
678
776
|
const firstError = sec.errors[0] ?? 'invalid section';
|
|
679
777
|
console.warn(
|
|
@@ -261,7 +261,7 @@ export function validatePersonaSpec(rawSpec, sourcePath) {
|
|
|
261
261
|
pushError(
|
|
262
262
|
'model',
|
|
263
263
|
'enum',
|
|
264
|
-
`model must be one of ${[...ALLOWED_MODEL_ALIASES].join('|')} or a full model ID like "claude-opus-
|
|
264
|
+
`model must be one of ${[...ALLOWED_MODEL_ALIASES].join('|')} or a full model ID like "claude-opus-5-5" (got ${JSON.stringify(m)})`,
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
267
|
}
|
package/scripts/lib/platform.mjs
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* - plain constants (no filesystem work): SO_SHARED_DIR, SO_OS, SO_IS_WINDOWS,
|
|
9
9
|
* SO_IS_WSL, SO_PATH_SEP
|
|
10
10
|
* - pure resolvers: detectPlatform, resolvePluginRoot, resolveProjectDir,
|
|
11
|
-
* resolveStateDir, resolveConfigFile
|
|
11
|
+
* resolveSessionRoot, resolveStateDir, resolveConfigFile
|
|
12
12
|
*
|
|
13
13
|
* NOTHING in this module touches the filesystem at import time.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, statSync } from 'node:fs';
|
|
16
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { resolvePluginRoot as _resolvePluginRootRobust } from './plugin-root.mjs';
|
|
@@ -249,6 +249,210 @@ export function resolveProjectDir(platform) {
|
|
|
249
249
|
return cwd;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// resolveSessionRoot (#1492)
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Basename of a worktree the HARNESS creates for an `isolation: "worktree"`
|
|
258
|
+
* subagent: `agent-` + the agent id. Measured 2026-10-02 over this host's
|
|
259
|
+
* transcripts: 13481 `.claude/worktrees/agent-<hex>` hits, every agent id of the
|
|
260
|
+
* form `a` + 16 hex (`agent-a00828908cdd83bd8`). A worktree a SESSION enters
|
|
261
|
+
* (`EnterWorktree`, `claude --worktree <name>`) lives in the same directory under
|
|
262
|
+
* a chosen name (`landing-produktschau`, `s71-politur`), so the name is the only
|
|
263
|
+
* discriminator the path offers.
|
|
264
|
+
*/
|
|
265
|
+
const HARNESS_AGENT_WORKTREE_RE = /^agent-[0-9a-f]{8,}$/;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The first non-blank launch-dir env var, in `resolveProjectDir()` order, or `''`.
|
|
269
|
+
* Trimmed: a whitespace-only value is truthy and would otherwise win
|
|
270
|
+
* (`development.md` § Error Handling, env-var fallback whitespace trap).
|
|
271
|
+
*
|
|
272
|
+
* The ONE definition of "the launch dir the harness states": the clamp in
|
|
273
|
+
* {@link resolveSessionRoot} and the relocation trace in `hooks/enforce-scope.mjs`
|
|
274
|
+
* must read the same chain, so a second copy of it is a second definition.
|
|
275
|
+
*
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
export function launchDirFromEnv() {
|
|
279
|
+
for (const name of ['CLAUDE_PROJECT_DIR', 'CODEX_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'PI_PROJECT_DIR']) {
|
|
280
|
+
const value = (process.env[name] || '').trim();
|
|
281
|
+
if (value !== '') return value;
|
|
282
|
+
}
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The nearest directory at or above `cwd` holding a `.git` entry — a directory,
|
|
288
|
+
* or the FILE a linked worktree carries — or `''` when none does. Git's own repo
|
|
289
|
+
* discovery walks up looking for exactly this entry, so where git answers, both
|
|
290
|
+
* agree (measured 2026-10-02: same answer, 0.017 ms here vs 4.57 ms for a
|
|
291
|
+
* `git rev-parse --show-toplevel` spawn). Existence only — the entry is not
|
|
292
|
+
* validated, which git would do.
|
|
293
|
+
*
|
|
294
|
+
* Not `walkUpFor(cwd, '.git', 'any')`: its home-directory boundary exists for
|
|
295
|
+
* PLATFORM markers (#1139); a repo whose root is `$HOME` is still a repo root.
|
|
296
|
+
*
|
|
297
|
+
* @param {string} cwd
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function dotGitAncestor(cwd) {
|
|
301
|
+
for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
|
|
302
|
+
if (existsSync(path.join(dir, '.git'))) return dir;
|
|
303
|
+
if (path.dirname(dir) === dir) return '';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* `<P>` when `root` is a harness subagent worktree `<P>/.claude/worktrees/agent-<hex>`
|
|
309
|
+
* whose parent `<P>` is itself a working copy; otherwise `root` unchanged.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} root
|
|
312
|
+
* @returns {string}
|
|
313
|
+
*/
|
|
314
|
+
function _liftHarnessAgentWorktree(root) {
|
|
315
|
+
if (!HARNESS_AGENT_WORKTREE_RE.test(path.basename(root))) return root;
|
|
316
|
+
const worktreesDir = path.dirname(root);
|
|
317
|
+
if (path.basename(worktreesDir) !== 'worktrees') return root;
|
|
318
|
+
const stateDir = path.dirname(worktreesDir);
|
|
319
|
+
if (path.basename(stateDir) !== '.claude') return root;
|
|
320
|
+
const parent = path.dirname(stateDir);
|
|
321
|
+
return existsSync(path.join(parent, '.git')) ? parent : root;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The NATIVE realpath, or the resolved spelling when the path does not exist.
|
|
326
|
+
*
|
|
327
|
+
* `realpathSync.native`, not the JS `realpathSync`: on a case-insensitive
|
|
328
|
+
* volume (APFS default) the JS form resolves symlinks but keeps the CALLER's
|
|
329
|
+
* letter case, so `/users/me/repo` stayed `/users/…` beside the git-found
|
|
330
|
+
* `/Users/…`, `path.relative` answered `../../users/…` and the clamp was
|
|
331
|
+
* skipped (#1504 point 8, measured 2026-10-03 on a home dir typed lowercase:
|
|
332
|
+
* JS `/users/<me>`, native `/Users/<me>`). Every comparison in the clamp chain
|
|
333
|
+
* goes through this one function — mixing the two forms re-opens the split.
|
|
334
|
+
*
|
|
335
|
+
* @param {string} p
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
function _canonical(p) {
|
|
339
|
+
try { return realpathSync.native(p); } catch { return path.resolve(p); }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Is `child` a STRICT descendant of `parent` (never `parent` itself)? Both sides
|
|
344
|
+
* canonical, so macOS `/tmp` vs `/private/tmp` cannot split one directory in two.
|
|
345
|
+
*
|
|
346
|
+
* @param {string} child
|
|
347
|
+
* @param {string} parent
|
|
348
|
+
* @returns {boolean}
|
|
349
|
+
*/
|
|
350
|
+
function _isStrictlyInside(child, parent) {
|
|
351
|
+
const rel = path.relative(_canonical(parent), _canonical(child));
|
|
352
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The SESSION ROOT (#1492) — the working copy whose control files belong to the
|
|
357
|
+
* session a hook fires in: `wave-scope.json`, the `.orchestrator/` ledgers, the
|
|
358
|
+
* event records. One resolver for every hook that reads them (enforce-scope,
|
|
359
|
+
* enforce-commands, post-bash-write-verify, pre-task-scope-disjoint).
|
|
360
|
+
*
|
|
361
|
+
* NOT the launch dir whenever the session works elsewhere: `$CLAUDE_PROJECT_DIR`
|
|
362
|
+
* stays on the dir the session was LAUNCHED in after it enters a worktree
|
|
363
|
+
* (docs/en/hooks § "Worktrees are different"), while the coordinator writes
|
|
364
|
+
* `wave-scope.json` into that worktree. Read from the launch dir, the manifest
|
|
365
|
+
* was absent and every scope gate allowed everything for the whole session,
|
|
366
|
+
* silently (#1492, measured in session main-2026-10-02-session-21); preferred
|
|
367
|
+
* for the dispatch ledger it also gave every worktree session of one launch dir
|
|
368
|
+
* ONE shared ledger and the wave key `w?` (measured on be6a2e3e).
|
|
369
|
+
*
|
|
370
|
+
* NOT the payload `cwd` itself: it follows the session's `cd` (docs/en/hooks
|
|
371
|
+
* § "cwd follows Claude"), and a `cd sub` must not move the session's state into
|
|
372
|
+
* `<root>/sub` — a second, empty ledger whose collisions were allowed (#1489 Pkt 6).
|
|
373
|
+
*
|
|
374
|
+
* NEVER ABOVE the launch dir: a session launched in a repo SUBDIRECTORY (a
|
|
375
|
+
* monorepo package, a project under a git-tracked `$HOME`) keeps its manifest
|
|
376
|
+
* there. The repo root above it holds none, so resolving to it switched every
|
|
377
|
+
* scope gate off (review HIGH-1 on 4fdbc469, measured DENY → ALLOW).
|
|
378
|
+
*
|
|
379
|
+
* Precedence:
|
|
380
|
+
* 1. no payload `cwd` → `resolveProjectDir()`, the pre-#1492 answer unchanged;
|
|
381
|
+
* 2. the repo root of `cwd` — `toplevel` when the caller already holds
|
|
382
|
+
* `git rev-parse --show-toplevel` of it, else the nearest `.git` ancestor
|
|
383
|
+
* (`dotGitAncestor`), which answers the same without a spawn, so neither a
|
|
384
|
+
* timed-out git (review MED on 63f35e8c) nor a hot-path budget can send a
|
|
385
|
+
* worktree session back to the launch dir — lifted to `<P>` when it is a
|
|
386
|
+
* harness subagent worktree `<P>/.claude/worktrees/agent-<hex>` (below),
|
|
387
|
+
* then replaced by the launch dir from env (`launchDirFromEnv`) when it
|
|
388
|
+
* lies strictly ABOVE it;
|
|
389
|
+
* 3. the launch dir from env (`CLAUDE_PROJECT_DIR` → `CODEX_PROJECT_DIR` →
|
|
390
|
+
* `CURSOR_PROJECT_DIR` → `PI_PROJECT_DIR`), only when `cwd` is in no repo;
|
|
391
|
+
* 4. `cwd`.
|
|
392
|
+
*
|
|
393
|
+
* The clamp reads the SAME launch-dir chain as rung 3, not `$CLAUDE_PROJECT_DIR`
|
|
394
|
+
* alone. The Cursor and Pi bridges delete `CLAUDE_PROJECT_DIR` and set their
|
|
395
|
+
* own `*_PROJECT_DIR` to the payload `cwd` on every call (`cursor-hook-bridge.mjs`,
|
|
396
|
+
* `pi-hook-bridge.mjs`); a clamp on `$CLAUDE_PROJECT_DIR` alone therefore never
|
|
397
|
+
* fired there, and a Cursor/Pi workspace in a repo SUBDIRECTORY climbed to the
|
|
398
|
+
* repo root, found no manifest and allowed every write (review HIGH-1 on
|
|
399
|
+
* b57e572c, measured end-to-end through `cursor-hook-bridge.mjs`: DENY at
|
|
400
|
+
* ff3e43b5 → ALLOW). For a Claude session nothing changes — `$CLAUDE_PROJECT_DIR`
|
|
401
|
+
* is first in the chain. For a bridge session the answer is the payload `cwd`
|
|
402
|
+
* whenever it sits in a repo, which is exactly what the bridges resolved before
|
|
403
|
+
* #1492 (`resolveProjectDir()` → their `*_PROJECT_DIR` → the payload `cwd`).
|
|
404
|
+
* The clamp does not ask whether `cwd` lies inside the launch dir: an
|
|
405
|
+
* `isolation: "worktree"` agent's `cwd` never does, and it must clamp too (see
|
|
406
|
+
* THE LIFT).
|
|
407
|
+
*
|
|
408
|
+
* THE LIFT. An `isolation: "worktree"` subagent's hook payload carries the
|
|
409
|
+
* worktree the harness made for it, `<P>/.claude/worktrees/agent-<hex>`. That
|
|
410
|
+
* worktree is a repo root of its own and holds no manifest, so rung 2 alone
|
|
411
|
+
* would disarm scope enforcement for every such agent. The harness puts it under
|
|
412
|
+
* the LAUNCH checkout, so `<P>` is what `$CLAUDE_PROJECT_DIR` resolved to before
|
|
413
|
+
* #1492 — the lift keeps exactly that, and the clamp keeps it for a launch in a
|
|
414
|
+
* subdirectory of `<P>`. It is keyed on the PATH SHAPE, never on which candidate
|
|
415
|
+
* holds a manifest: a root picked because a control file is readable there
|
|
416
|
+
* would let any directory that lacks one become the deciding root.
|
|
417
|
+
*
|
|
418
|
+
* Ceilings (BV-004):
|
|
419
|
+
* - An agent of a session that ENTERED a worktree is still unenforced, as before
|
|
420
|
+
* #1492: its worktree sits under the launch checkout (measured: session
|
|
421
|
+
* 0d5e4fc3, 2026-09-25 — cwd `<main>/.claude/worktrees/landing-produktschau`,
|
|
422
|
+
* agent worktreePath `<main>/.claude/worktrees/agent-acd5bfd0aa590e2e8`), so
|
|
423
|
+
* the lift lands on the launch root, where that session has no manifest.
|
|
424
|
+
* Fixing it needs a design (e.g. find the manifest bound to the payload
|
|
425
|
+
* `session_id` among the repo's worktrees). Revisit when an entered-worktree
|
|
426
|
+
* session dispatches `isolation: "worktree"` agents under a manifest.
|
|
427
|
+
* - Rung 2 is `cd`-relocatable: a session whose `cwd` sits in ANOTHER repo (a
|
|
428
|
+
* submodule, a nested or sibling checkout) is checked against that repo's
|
|
429
|
+
* manifest — usually none, so its gates allow — where the launch dir's
|
|
430
|
+
* manifest used to apply. Same relocation `pre-task-scope-disjoint` has
|
|
431
|
+
* keyed its ledger on since #1489. Revisit if an `orchestrator.scope.*`
|
|
432
|
+
* record or a review ever shows an out-of-scope write made after such a `cd`.
|
|
433
|
+
* - A session worktree NAMED like a harness one (`agent-` + 8 or more hex) is
|
|
434
|
+
* lifted to its parent — the pre-#1492 launch-dir behaviour for that one
|
|
435
|
+
* session, not a new failure. Revisit if the harness changes its naming.
|
|
436
|
+
* - A Cursor/Pi bridge session follows its payload `cwd`, as it did before
|
|
437
|
+
* #1492: the bridges hand over no launch dir distinct from that `cwd`, so a
|
|
438
|
+
* bridge session whose `cwd` moves into a subdirectory without a manifest is
|
|
439
|
+
* unenforced there. Not a new failure — the pre-#1492 bridge answer. Revisit
|
|
440
|
+
* when a bridge hands over a stable workspace root separately from the
|
|
441
|
+
* payload `cwd` — then clamp on that instead.
|
|
442
|
+
*
|
|
443
|
+
* @param {string|undefined} cwd the hook payload's `cwd`
|
|
444
|
+
* @param {string} [toplevel] `git rev-parse --show-toplevel` of `cwd`, `''` when unknown
|
|
445
|
+
* @returns {string} Absolute path
|
|
446
|
+
*/
|
|
447
|
+
export function resolveSessionRoot(cwd, toplevel = '') {
|
|
448
|
+
if (typeof cwd !== 'string' || cwd.trim() === '') return resolveProjectDir();
|
|
449
|
+
const repoRoot = (typeof toplevel === 'string' ? toplevel : '') || dotGitAncestor(cwd);
|
|
450
|
+
if (!repoRoot) return launchDirFromEnv() || cwd;
|
|
451
|
+
const root = _liftHarnessAgentWorktree(repoRoot);
|
|
452
|
+
const launch = launchDirFromEnv();
|
|
453
|
+
return launch !== '' && _isStrictlyInside(launch, root) ? launch : root;
|
|
454
|
+
}
|
|
455
|
+
|
|
252
456
|
// ---------------------------------------------------------------------------
|
|
253
457
|
// resolveStateDir
|
|
254
458
|
// ---------------------------------------------------------------------------
|
|
@@ -260,7 +260,9 @@ async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
|
|
|
260
260
|
* @type {Record<"claude"|"codex"|"cursor"|"pi", string>}
|
|
261
261
|
*/
|
|
262
262
|
const PLATFORM_UPDATE_INSTRUCTIONS = {
|
|
263
|
-
claude:
|
|
263
|
+
claude:
|
|
264
|
+
'/plugin update session-orchestrator@kanevry, then restart Claude Code ' +
|
|
265
|
+
'(from a clone: `npm run update:local` also installs the deps Claude Code refuses — docs/install.md § Upgrade)',
|
|
264
266
|
codex:
|
|
265
267
|
'run `codex plugin marketplace upgrade kanevry && codex plugin add session-orchestrator@kanevry` ' +
|
|
266
268
|
'(docs/codex-setup.md), then restart Codex',
|