session-orchestrator 5.3.0 → 5.4.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/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- 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/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- 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 +70 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +57 -4
- package/docs/components.md +12 -12
- package/docs/events-schema.md +11 -8
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +55 -6
- package/docs/session-config-template.md +11 -2
- package/hooks/_lib/hook-import-set.json +27 -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 +53 -0
- package/hooks/on-stop.mjs +3 -1
- package/hooks/post-tool-batch-wave-signal.mjs +3 -1
- package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +559 -72
- package/hooks/subagent-telemetry.mjs +96 -19
- package/package.json +3 -3
- package/pi/prompts/navigator.md +12 -0
- 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 +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +49 -53
- package/scripts/backfill-abandoned-sessions.mjs +1 -1
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/emit-session.mjs +296 -2
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -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 +10 -6
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- 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-schema.mjs +13 -0
- package/scripts/lib/config.mjs +71 -10
- package/scripts/lib/events.mjs +21 -2
- package/scripts/lib/fleet-protocol.mjs +270 -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/instruction-budget-guard.mjs +69 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- 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 +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +43 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +62 -11
- 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 +3 -4
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- 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 +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- 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/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +10 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +323 -302
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +72 -46
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/evolve/SKILL.md +2 -2
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- 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 +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- 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 +22 -60
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- 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 +2 -2
- 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/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +3 -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
|
}
|
|
@@ -27,20 +27,32 @@ import { createHash } from 'node:crypto';
|
|
|
27
27
|
import {
|
|
28
28
|
appendFileSync,
|
|
29
29
|
existsSync,
|
|
30
|
+
lstatSync,
|
|
30
31
|
mkdirSync,
|
|
31
32
|
readFileSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
unlinkSync,
|
|
32
35
|
writeFileSync,
|
|
33
36
|
} from 'node:fs';
|
|
34
37
|
import path from 'node:path';
|
|
35
38
|
|
|
36
|
-
import { DEFAULT_KILL_GRACE_MS } from './wave-executor/dispatch-common.mjs';
|
|
37
|
-
|
|
38
39
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* Grace period between SIGTERM and SIGKILL, in ms — the repo's one definition,
|
|
41
|
+
* used by the gate's group ladder here and by `wave-executor/foreign-dispatch.mjs`
|
|
42
|
+
* and `remote-dispatch.mjs` through `runChild` (`dispatch-common.mjs`).
|
|
43
|
+
*
|
|
44
|
+
* `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
|
|
45
|
+
* ignores it never emits `close`, and `runChild`'s only resolve paths are
|
|
46
|
+
* `close` and `error` — so without escalation a wedged `cursor-agent` hangs
|
|
47
|
+
* `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
|
|
48
|
+
* catchable, so the escalation always terminates.
|
|
49
|
+
*
|
|
50
|
+
* Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
|
|
51
|
+
* stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
|
|
52
|
+
* measured needing longer than that to shut down — raise the constant, never
|
|
53
|
+
* drop the escalation.
|
|
42
54
|
*/
|
|
43
|
-
export
|
|
55
|
+
export const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
44
56
|
|
|
45
57
|
/** Append-only ledger of every gate process THIS repo's orchestrator started.
|
|
46
58
|
* Gitignored via `.gitignore:125` (`.orchestrator/runtime/`), verified with
|
|
@@ -572,7 +584,42 @@ export function readGateProcessLedger(repoRoot, {
|
|
|
572
584
|
}
|
|
573
585
|
|
|
574
586
|
/**
|
|
575
|
-
*
|
|
587
|
+
* Replace a regular file's contents atomically: write `<target>.tmp-<pid>` in
|
|
588
|
+
* the same directory, then rename it over the target. Throws instead of
|
|
589
|
+
* writing when the target is a symlink or not a regular file — the caller turns
|
|
590
|
+
* that into its one WARN line.
|
|
591
|
+
*
|
|
592
|
+
* Why not `writeFileSync(target)`: that truncates first, so a kill between the
|
|
593
|
+
* truncate and the write leaves the file empty, and it FOLLOWS a symlink — a
|
|
594
|
+
* ledger linked to `events.jsonl` emptied the link's target (CWE-59, reproduced
|
|
595
|
+
* 2026-10-01). `rename` replaces the directory entry itself, never a link's
|
|
596
|
+
* target, and the `wx` flag refuses a pre-planted file or link at the tmp name.
|
|
597
|
+
* Kept local, not imported, because this module deliberately has no local
|
|
598
|
+
* imports; `orphan-reaper/reaper-audit.mjs` carries the same few lines.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} target
|
|
601
|
+
* @param {string} body
|
|
602
|
+
* @param {{lstatSync: Function, writeFileSync: Function, renameSync: Function, unlinkSync: Function}} io
|
|
603
|
+
*/
|
|
604
|
+
function replaceRegularFile(target, body, io) {
|
|
605
|
+
if (!io.lstatSync(target).isFile()) {
|
|
606
|
+
throw new Error('not a regular file (a symlink is never written through) — left untouched');
|
|
607
|
+
}
|
|
608
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
609
|
+
try {
|
|
610
|
+
io.writeFileSync(tmp, body, { encoding: 'utf8', flag: 'wx' });
|
|
611
|
+
io.renameSync(tmp, target);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
// EEXIST: the tmp name belongs to someone else — never remove it.
|
|
614
|
+
if (err?.code !== 'EEXIST') {
|
|
615
|
+
try { io.unlinkSync(tmp); } catch { /* nothing was created */ }
|
|
616
|
+
}
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Drop expired and unusable lines from the ledger, rewriting it atomically.
|
|
576
623
|
* "Unusable" is {@link isValidLedgerRecord}'s verdict — the same one
|
|
577
624
|
* {@link readGateProcessLedger} applies, so nothing the reader skips survives
|
|
578
625
|
* the pruner.
|
|
@@ -583,6 +630,8 @@ export function readGateProcessLedger(repoRoot, {
|
|
|
583
630
|
*
|
|
584
631
|
* In-process fs only — never a shell `rm`/`mv` (PSA-003, and
|
|
585
632
|
* `.orchestrator/metrics/**` deletions are a blocked-command rule for a reason).
|
|
633
|
+
* The rewrite goes through {@link replaceRegularFile}: a symlinked or
|
|
634
|
+
* non-regular ledger is refused with one WARN and left untouched.
|
|
586
635
|
*
|
|
587
636
|
* Named ceiling (BV-004): read-filter-write is not atomic against a concurrent
|
|
588
637
|
* append, so a line appended between the read and the write is lost. Acceptable
|
|
@@ -595,16 +644,18 @@ export function readGateProcessLedger(repoRoot, {
|
|
|
595
644
|
* @param {object} [opts]
|
|
596
645
|
* @param {number} [opts.nowMs]
|
|
597
646
|
* @param {number} [opts.maxAgeMs]
|
|
598
|
-
* @param {{existsSync: Function, readFileSync: Function, writeFileSync: Function
|
|
647
|
+
* @param {{existsSync: Function, readFileSync: Function, writeFileSync: Function,
|
|
648
|
+
* lstatSync: Function, renameSync: Function, unlinkSync: Function}} [opts.fs] fs seam;
|
|
649
|
+
* replaces `node:fs` wholesale, so a partial seam fails closed (WARN, 0).
|
|
599
650
|
* @returns {number} Number of lines removed (expired + malformed). 0 when the
|
|
600
|
-
* ledger is absent, empty, or
|
|
651
|
+
* ledger is absent, empty, unreadable, or not a regular file.
|
|
601
652
|
*/
|
|
602
653
|
export function pruneGateProcessLedger(repoRoot, {
|
|
603
654
|
nowMs = Date.now(),
|
|
604
655
|
maxAgeMs = DEFAULT_LEDGER_MAX_AGE_MS,
|
|
605
656
|
fs: fsSeam,
|
|
606
657
|
} = {}) {
|
|
607
|
-
const io = fsSeam ?? { existsSync, readFileSync, writeFileSync };
|
|
658
|
+
const io = fsSeam ?? { existsSync, readFileSync, writeFileSync, lstatSync, renameSync, unlinkSync };
|
|
608
659
|
const target = ledgerPathFor(repoRoot);
|
|
609
660
|
let raw;
|
|
610
661
|
try {
|
|
@@ -634,7 +685,7 @@ export function pruneGateProcessLedger(repoRoot, {
|
|
|
634
685
|
|
|
635
686
|
if (removed === 0) return 0;
|
|
636
687
|
try {
|
|
637
|
-
|
|
688
|
+
replaceRegularFile(target, kept.length > 0 ? `${kept.join('\n')}\n` : '', io);
|
|
638
689
|
} catch (err) {
|
|
639
690
|
process.stderr.write(
|
|
640
691
|
`process-group: could not prune ${GATE_PROCESS_LEDGER_RELPATH}: ${err?.message ?? String(err)}\n`,
|
|
@@ -462,7 +462,9 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
|
|
|
462
462
|
* H4 — CI configuration hygiene.
|
|
463
463
|
*
|
|
464
464
|
* Measured 5/6. Checks locally declared executable commands without evaluating
|
|
465
|
-
* pipeline conditions or external includes
|
|
465
|
+
* pipeline conditions or external includes; when no local audit step is found
|
|
466
|
+
* but `.gitlab-ci.yml` declares `include:`, the finding is downgraded to an
|
|
467
|
+
* unverifiable, non-fixable one. The dependency-audit gap is the load-bearing one: three
|
|
466
468
|
* of the tested repos carried known-vulnerable dependencies that no pipeline
|
|
467
469
|
* would ever surface.
|
|
468
470
|
*
|
|
@@ -506,6 +508,24 @@ export function checkCiConfig(repoRoot) {
|
|
|
506
508
|
}
|
|
507
509
|
|
|
508
510
|
if (ciConfigs.some(({ text }) => text) && !ciConfigs.some(hasAuditStep)) {
|
|
511
|
+
// Jobs supplied via include: (component/local/project/remote/template) are
|
|
512
|
+
// never resolved, so an audit may exist there — report it as unverifiable.
|
|
513
|
+
let includes = 0;
|
|
514
|
+
try {
|
|
515
|
+
const gitlabText = hasGitlab ? ciConfigs[0].text : '';
|
|
516
|
+
const include = yaml.loadAll(gitlabText, undefined, { schema: GITLAB_SCHEMA }).filter(isConfigMap).at(-1)?.include;
|
|
517
|
+
includes = Array.isArray(include) ? include.length : include ? 1 : 0;
|
|
518
|
+
} catch {
|
|
519
|
+
/* unparseable config — keep the plain missing-audit finding */
|
|
520
|
+
}
|
|
521
|
+
if (includes > 0) {
|
|
522
|
+
findings.push({
|
|
523
|
+
check: 'ci-audit-job',
|
|
524
|
+
fixable: false,
|
|
525
|
+
message: `CI dependency-audit step could not be verified: ${includes} include(s) not evaluated (.gitlab-ci.yml include:)`,
|
|
526
|
+
});
|
|
527
|
+
return findings;
|
|
528
|
+
}
|
|
509
529
|
findings.push({
|
|
510
530
|
check: 'ci-audit-job',
|
|
511
531
|
fixable: true,
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
import { spawnSync } from 'node:child_process';
|
|
65
65
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
66
|
-
import { dirname, join } from 'node:path';
|
|
66
|
+
import { dirname, join, resolve } from 'node:path';
|
|
67
67
|
import { fileURLToPath } from 'node:url';
|
|
68
68
|
|
|
69
69
|
import { emitEvent, sessionAttribution } from './events.mjs';
|
|
@@ -120,7 +120,8 @@ const GATE_TIMEOUT_MS = DEFAULT_GATE_TIMEOUT_MS;
|
|
|
120
120
|
* @returns {string}
|
|
121
121
|
*/
|
|
122
122
|
function resolveRepoRoot(explicit) {
|
|
123
|
-
|
|
123
|
+
// Absolute on purpose: emitEvent refuses a relative root (#1468).
|
|
124
|
+
if (typeof explicit === 'string' && explicit.trim()) return resolve(explicit);
|
|
124
125
|
return process.cwd();
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -773,13 +774,44 @@ function coerceMaxRetries(n) {
|
|
|
773
774
|
// Public API
|
|
774
775
|
// ---------------------------------------------------------------------------
|
|
775
776
|
|
|
777
|
+
/**
|
|
778
|
+
* The kill-ladder fields of a `quality_gate.passed/.failed` record (#1439).
|
|
779
|
+
*
|
|
780
|
+
* ONE builder for BOTH emitters — this library's {@link emitGateEvent} and the
|
|
781
|
+
* `scripts/run-quality-gate.mjs` CLI — so the two can never publish different
|
|
782
|
+
* shapes under the same keys. From THIS builder all three keys are always
|
|
783
|
+
* present: `timed_out` `false`, `survivors` `0` and `kill_signals` `[]` are
|
|
784
|
+
* measurements ("not killed"), unlike `counts`, whose absence means "not
|
|
785
|
+
* measured". The CLI then passes the result through `joinGateKillFields()`
|
|
786
|
+
* (`scripts/lib/gates/gate-helpers.mjs`, #1457), which OR-joins the inner
|
|
787
|
+
* per-command ladders and MAY omit the three keys when that inner side was
|
|
788
|
+
* not measured and the outer ladder did not fire.
|
|
789
|
+
*
|
|
790
|
+
* - `timed_out` — the run hit its wall-clock ceiling and was killed (exit 124).
|
|
791
|
+
* - `survivors` — how many pids were still alive after the SIGKILL step; the
|
|
792
|
+
* pids themselves stay in the gate output / diagnostics bundle.
|
|
793
|
+
* - `kill_signals` — the signals actually sent, in order (`SIGTERM`, `SIGKILL`).
|
|
794
|
+
* Copied, because `spawnInGroup` records them at send time into a live array.
|
|
795
|
+
*
|
|
796
|
+
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]}|null|undefined} run
|
|
797
|
+
* @returns {{timed_out: boolean, survivors: number, kill_signals: string[]}}
|
|
798
|
+
*/
|
|
799
|
+
export function gateKillFields(run) {
|
|
800
|
+
return {
|
|
801
|
+
timed_out: run?.timedOut === true,
|
|
802
|
+
survivors: Array.isArray(run?.survivors) ? run.survivors.length : 0,
|
|
803
|
+
kill_signals: Array.isArray(run?.killSignals) ? [...run.killSignals] : [],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
776
807
|
/**
|
|
777
808
|
* Emit exactly one `orchestrator.quality_gate.{passed,failed}` event per
|
|
778
809
|
* `runQualityGateWithRetry` CALL (#928b).
|
|
779
810
|
*
|
|
780
811
|
* NO DOUBLE-COUNTING. The two gate paths never nest:
|
|
781
812
|
* - `scripts/run-quality-gate.mjs` spawns `scripts/lib/gates/gate-*.mjs`
|
|
782
|
-
* directly
|
|
813
|
+
* directly; it imports only the pure `gateKillFields` helper from this
|
|
814
|
+
* module, never `runQualityGateWithRetry`, so it never reaches this emitter;
|
|
783
815
|
* - this module spawns the resolved gate COMMANDS (and `parse-config.mjs`)
|
|
784
816
|
* directly and does not invoke that wrapper.
|
|
785
817
|
* A single run therefore passes through exactly one emitter.
|
|
@@ -846,8 +878,13 @@ function coerceMaxRetries(n) {
|
|
|
846
878
|
* @param {number} attempts
|
|
847
879
|
* @param {string|null} gate
|
|
848
880
|
* @param {{passed: number, failed: number, total: number}|null} [counts]
|
|
881
|
+
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]}|null} [killRun]
|
|
882
|
+
* the run that decided the verdict — the last attempt's failing gate; `null`
|
|
883
|
+
* on the passing path, where every gate exited 0 and so none was killed
|
|
884
|
+
* (`spawnInGroup` only starts its kill ladder on timeout or overflow, and
|
|
885
|
+
* both force a non-zero exit code).
|
|
849
886
|
*/
|
|
850
|
-
async function emitGateEvent(repoRoot, ok, attempts, gate, counts) {
|
|
887
|
+
async function emitGateEvent(repoRoot, ok, attempts, gate, counts, killRun = null) {
|
|
851
888
|
try {
|
|
852
889
|
await emitEvent(
|
|
853
890
|
`orchestrator.quality_gate.${ok ? 'passed' : 'failed'}`,
|
|
@@ -857,6 +894,7 @@ async function emitGateEvent(repoRoot, ok, attempts, gate, counts) {
|
|
|
857
894
|
attempts,
|
|
858
895
|
...(gate ? { gate } : {}),
|
|
859
896
|
...(counts ? { counts } : {}),
|
|
897
|
+
...gateKillFields(killRun),
|
|
860
898
|
...sessionAttribution(repoRoot),
|
|
861
899
|
},
|
|
862
900
|
{ repoRoot },
|
|
@@ -985,6 +1023,8 @@ export async function runQualityGateWithRetry(opts) {
|
|
|
985
1023
|
exitCode: result.exitCode,
|
|
986
1024
|
output: result.output,
|
|
987
1025
|
timedOut: result.timedOut,
|
|
1026
|
+
killSignals: result.killSignals,
|
|
1027
|
+
survivors: result.survivors,
|
|
988
1028
|
command: cmd,
|
|
989
1029
|
attempt,
|
|
990
1030
|
};
|
|
@@ -1063,7 +1103,7 @@ export async function runQualityGateWithRetry(opts) {
|
|
|
1063
1103
|
`❌ quality-gate exhausted retries (${attempt}), writing diagnostics to ${bundlePath ?? '<unwritable>'}\n`,
|
|
1064
1104
|
);
|
|
1065
1105
|
|
|
1066
|
-
await emitGateEvent(repoRoot, false, attempt, lastFailure?.gate ?? null, testCounts);
|
|
1106
|
+
await emitGateEvent(repoRoot, false, attempt, lastFailure?.gate ?? null, testCounts, lastFailure);
|
|
1067
1107
|
|
|
1068
1108
|
const out = {
|
|
1069
1109
|
ok: false,
|
|
@@ -49,7 +49,7 @@ import { ramSnapshot, cpuSnapshot, processCounts, swapUsedMb, memoryPressurePctF
|
|
|
49
49
|
|
|
50
50
|
export { evaluate, PROCESSES_PER_SESSION, PRESSURE_HARD_PCT, PRESSURE_SOFT_PCT, PRESSURE_HEALTHY_PCT, DEFAULT_RESOURCE_THRESHOLDS } from './resource-probe/evaluate.mjs';
|
|
51
51
|
export { peerSessionsCount } from './resource-probe/probe-platform.mjs';
|
|
52
|
-
export { parseEtimeToMinutes, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
|
|
52
|
+
export { parseEtimeToMinutes, parseEtimeToSeconds, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
|
|
53
53
|
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
55
|
// Public API — probe()
|