session-orchestrator 5.2.0 → 5.3.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/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -1812,6 +1812,32 @@ export function redirectRuleMatches(rule, command, opts = {}) {
|
|
|
1812
1812
|
// an existing repo-relative rule, and a repo-relative entry still cannot
|
|
1813
1813
|
// escape the root. An unexpandable `~` (no usable `home`) matches NOTHING —
|
|
1814
1814
|
// fail-closed for the new class, since an unexpanded `~` names no file.
|
|
1815
|
+
const compiled = compileTargetDenylist(denylist, home);
|
|
1816
|
+
if (compiled.relRegexes.length === 0 && compiled.absRegexes.length === 0) return false;
|
|
1817
|
+
|
|
1818
|
+
for (const entry of extractRedirectTargets(command)) {
|
|
1819
|
+
if (entry.unresolved) continue;
|
|
1820
|
+
if (!modes.has(entry.mode)) continue;
|
|
1821
|
+
if (targetMatchesDenylist(entry.target, compiled, repoRoot, home)) return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Compile a rule's `target-denylist` into the two glob classes described in
|
|
1828
|
+
* `redirectRuleMatches` (#1362): repo-relative entries and home-anchored (`~/…`)
|
|
1829
|
+
* entries. Extracted so a SECOND rule class can reuse the exact same compilation
|
|
1830
|
+
* instead of growing a third copy of the glob logic (#1401) — the file already
|
|
1831
|
+
* carries that warning at `findMatchedRedirectEntry` in the consuming hook.
|
|
1832
|
+
*
|
|
1833
|
+
* An unexpandable `~` (no usable `home`) contributes NOTHING — fail-closed for
|
|
1834
|
+
* the home-anchored class, since an unexpanded `~` names no file.
|
|
1835
|
+
*
|
|
1836
|
+
* @param {string[]} denylist
|
|
1837
|
+
* @param {string|undefined} home
|
|
1838
|
+
* @returns {{ relRegexes: RegExp[], absRegexes: RegExp[] }}
|
|
1839
|
+
*/
|
|
1840
|
+
function compileTargetDenylist(denylist, home) {
|
|
1815
1841
|
const relRegexes = [];
|
|
1816
1842
|
const absRegexes = [];
|
|
1817
1843
|
for (const pattern of denylist) {
|
|
@@ -1824,38 +1850,48 @@ export function redirectRuleMatches(rule, command, opts = {}) {
|
|
|
1824
1850
|
relRegexes.push(redirectGlobToRegExp(text));
|
|
1825
1851
|
}
|
|
1826
1852
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
for (const entry of extractRedirectTargets(command)) {
|
|
1830
|
-
if (entry.unresolved) continue;
|
|
1831
|
-
if (!modes.has(entry.mode)) continue;
|
|
1853
|
+
return { relRegexes, absRegexes };
|
|
1854
|
+
}
|
|
1832
1855
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1856
|
+
/**
|
|
1857
|
+
* Test ONE resolved target text against a compiled denylist.
|
|
1858
|
+
*
|
|
1859
|
+
* A RELATIVE target is repo-root-relative in BOTH classes, exactly as the
|
|
1860
|
+
* repo-relative class already treats it (#994 R1) — otherwise
|
|
1861
|
+
* `../../.claude/projects/<enc>/memory/MEMORY.md` from the repo root names the
|
|
1862
|
+
* very file the `~` spelling blocks, and only the spelling would be denied
|
|
1863
|
+
* (measured 2026-09-13: relative `>`/`>>` ALLOW vs tilde/absolute BLOCK on one
|
|
1864
|
+
* and the same resolved path). This resolves TARGETS, never ENTRIES: a
|
|
1865
|
+
* repo-relative denylist entry still goes through `repoRelativeRedirectTarget`
|
|
1866
|
+
* and still cannot reach out of the root.
|
|
1867
|
+
*
|
|
1868
|
+
* @param {string} rawTarget — resolved target text (quotes already stripped)
|
|
1869
|
+
* @param {{ relRegexes: RegExp[], absRegexes: RegExp[] }} compiled
|
|
1870
|
+
* @param {string|null} repoRoot
|
|
1871
|
+
* @param {string|undefined} home
|
|
1872
|
+
* @returns {boolean}
|
|
1873
|
+
*/
|
|
1874
|
+
function targetMatchesDenylist(rawTarget, compiled, repoRoot, home) {
|
|
1875
|
+
const { relRegexes, absRegexes } = compiled;
|
|
1876
|
+
|
|
1877
|
+
if (absRegexes.length > 0) {
|
|
1878
|
+
const expandedTarget = expandLeadingHome(rawTarget, home);
|
|
1879
|
+
const absTarget = path.isAbsolute(expandedTarget)
|
|
1880
|
+
? expandedTarget
|
|
1881
|
+
: repoRoot && path.isAbsolute(repoRoot)
|
|
1882
|
+
? path.resolve(repoRoot, expandedTarget)
|
|
1883
|
+
: null;
|
|
1884
|
+
if (absTarget !== null) {
|
|
1885
|
+
const folded = foldAbsoluteTarget(absTarget);
|
|
1886
|
+
if (absRegexes.some((re) => re.test(folded))) return true;
|
|
1852
1887
|
}
|
|
1888
|
+
}
|
|
1853
1889
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
}
|
|
1890
|
+
if (relRegexes.length > 0) {
|
|
1891
|
+
const target = repoRelativeRedirectTarget(rawTarget, repoRoot, home);
|
|
1892
|
+
if (target !== null && relRegexes.some((re) => re.test(target))) return true;
|
|
1858
1893
|
}
|
|
1894
|
+
|
|
1859
1895
|
return false;
|
|
1860
1896
|
}
|
|
1861
1897
|
|
|
@@ -1875,6 +1911,217 @@ function foldAbsoluteTarget(abs) {
|
|
|
1875
1911
|
);
|
|
1876
1912
|
}
|
|
1877
1913
|
|
|
1914
|
+
/**
|
|
1915
|
+
* Verbs that REMOVE or RELOCATE a file named as a bare operand (#1401).
|
|
1916
|
+
*
|
|
1917
|
+
* `mv` is here for BOTH of its operand roles: the SOURCE disappears from its
|
|
1918
|
+
* path, and the DESTINATION is overwritten when it already exists — either one
|
|
1919
|
+
* loses the file that was there. So every operand of these verbs is a candidate,
|
|
1920
|
+
* with no source/destination distinction to get wrong.
|
|
1921
|
+
*
|
|
1922
|
+
* NAMED CEILING (BV-004) — measured 2026-09-19 against `splitChainSegments` +
|
|
1923
|
+
* `resolveSegmentVerb`. COVERED, because the verb resolves through the existing
|
|
1924
|
+
* machinery: bare `rm`/`mv`/`unlink`; `env X=1 rm …`, `sudo rm …`, `timeout 5 rm
|
|
1925
|
+
* …`, `nohup`/`nice`/`stdbuf`/`command`/`exec` (WRAPPER_UNWRAP); every chain
|
|
1926
|
+
* position (`;`, `&&`, `||`, `|`, `&`, newline) and compound-statement head
|
|
1927
|
+
* (`do rm …`, `{ rm …; }` — COMMAND_POSITION_KEYWORDS); and the literal payload
|
|
1928
|
+
* of `bash -c '…'` / `env -S '…'` / `eval '…'` via the shared payload recursion.
|
|
1929
|
+
*
|
|
1930
|
+
* NOT covered, deliberately, because the deleted path is not lexically an
|
|
1931
|
+
* operand of a resolvable verb:
|
|
1932
|
+
* - `find … -delete` / `find … -exec rm {} \;` — the verb is `find`; its path
|
|
1933
|
+
* operand is a SEARCH ROOT, and treating a search root as a delete target
|
|
1934
|
+
* would also flag a pure `find … -print` over the same directory.
|
|
1935
|
+
* - `… | xargs rm` — the targets arrive on stdin and do not exist as command
|
|
1936
|
+
* text at hook time. `xargs` is deliberately NOT in WRAPPER_UNWRAP (see
|
|
1937
|
+
* there: unwrapping it could only LOOSEN the guard).
|
|
1938
|
+
* - `git rm <path>` — the verb is `git`; a git-subverb grammar is a separate
|
|
1939
|
+
* table. PSA-007 already bans `git rm` for dispatched subagents.
|
|
1940
|
+
* - in-process deletion (`node -e 'fs.unlinkSync(…)'`) — not redirect- or
|
|
1941
|
+
* operand-shaped; same class boundary the `redirect-harness-memory`
|
|
1942
|
+
* rationale draws for `tee -a` and `fs.appendFileSync`.
|
|
1943
|
+
* REVISIT TRIGGER: any of those four shapes showing up in a real deletion
|
|
1944
|
+
* incident or a transcript census — extend this set or add the sibling grammar,
|
|
1945
|
+
* never special-case one command.
|
|
1946
|
+
*/
|
|
1947
|
+
const PATH_DESTRUCTIVE_VERBS = new Set(['rm', 'mv', 'unlink']);
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Recursive collector behind {@link extractDeleteTargets}. Mirrors
|
|
1951
|
+
* collectRedirectTargets' structure — same segment walk, same dual-reading
|
|
1952
|
+
* union, same depth/budget caps and the same fail-VISIBLE cut-off markers.
|
|
1953
|
+
*
|
|
1954
|
+
* @param {Array<Array<{ text: string, quoted: boolean, redirect?: object }>>} segments
|
|
1955
|
+
* @param {Array<object>} out — accumulator
|
|
1956
|
+
* @param {number} depth
|
|
1957
|
+
* @param {{ remaining: number }} budget
|
|
1958
|
+
*/
|
|
1959
|
+
function collectDeleteTargets(segments, out, depth, budget) {
|
|
1960
|
+
for (const segment of segments) {
|
|
1961
|
+
const resolved = resolveSegmentVerb(segment);
|
|
1962
|
+
|
|
1963
|
+
// BOTH readings of an ambiguous unknown WRAPPER flag contribute (#1000),
|
|
1964
|
+
// unioned and never replaced: `env -Q x rm .orchestrator/metrics/events.jsonl`
|
|
1965
|
+
// resolves to the non-destructive verb `x` in parse A and to `rm` in parse B,
|
|
1966
|
+
// while `sudo -n rm <file>` is the mirror case (parse B swallows `rm` as
|
|
1967
|
+
// `-n`'s operand). Picking either reading alone drops one of the two.
|
|
1968
|
+
const readings = resolved.alt ? [resolved, resolved.alt] : [resolved];
|
|
1969
|
+
const seen = new Set();
|
|
1970
|
+
|
|
1971
|
+
for (const reading of readings) {
|
|
1972
|
+
if (!reading.verb || reading.index < 0) continue;
|
|
1973
|
+
if (!PATH_DESTRUCTIVE_VERBS.has(reading.verb)) continue;
|
|
1974
|
+
// A verb that only exists inside a quoted token is inert literal text for
|
|
1975
|
+
// this segment — same rule commandHasRecursiveForceRm applies. An
|
|
1976
|
+
// interpreter's quoted payload is reached through the recursion below
|
|
1977
|
+
// instead, where it is re-tokenized as the command it really is.
|
|
1978
|
+
if (segment[reading.index].quoted) continue;
|
|
1979
|
+
|
|
1980
|
+
let seenDashDash = false;
|
|
1981
|
+
for (let i = reading.index + 1; i < segment.length; i++) {
|
|
1982
|
+
const tok = segment[i];
|
|
1983
|
+
// A redirect operand is a filename the SHELL consumes, not an operand of
|
|
1984
|
+
// the verb — `rm -f x > out.log` deletes `x`, it does not delete
|
|
1985
|
+
// `out.log` (the redirect surface judges that one).
|
|
1986
|
+
if (tok.redirect) { i = redirectSpanEnd(segment, i); continue; }
|
|
1987
|
+
if (!seenDashDash && tok.text === '--') { seenDashDash = true; continue; }
|
|
1988
|
+
// FLAG SKIPPING, and why this is the UNION of both flag readings rather
|
|
1989
|
+
// than a pick (#1000 applied to the VERB's own options): an option table
|
|
1990
|
+
// cannot enumerate every flag `rm`/`mv` may grow, and the two possible
|
|
1991
|
+
// readings of an unknown one are "boolean" (the next word is an operand)
|
|
1992
|
+
// and "value-taking" (the next word is the flag's value). The
|
|
1993
|
+
// value-taking reading is a strict SUBSET of the boolean one here — it
|
|
1994
|
+
// can only HIDE a word, never reveal one — so collecting under the
|
|
1995
|
+
// boolean reading yields exactly the union of both. That is also the
|
|
1996
|
+
// fail-closed direction: `mv -t .orchestrator/metrics/ x` reports the
|
|
1997
|
+
// `-t` operand as a candidate (it IS a destination that can overwrite),
|
|
1998
|
+
// where a value-taking reading would have swallowed it silently.
|
|
1999
|
+
if (!seenDashDash && !tok.quoted && tok.text.startsWith('-') && tok.text !== '-') continue;
|
|
2000
|
+
|
|
2001
|
+
const key = `${reading.verb}\u0000${tok.text}`;
|
|
2002
|
+
if (seen.has(key)) continue;
|
|
2003
|
+
seen.add(key);
|
|
2004
|
+
|
|
2005
|
+
if (/[$`]/.test(tok.text)) {
|
|
2006
|
+
// Variable indirection (`rm -f "$LEDGER"`) or command substitution:
|
|
2007
|
+
// fail-visible, never guess (#983 rule, same direction).
|
|
2008
|
+
out.push({
|
|
2009
|
+
target: null, verb: reading.verb, unresolved: true, reason: 'variable/substitution',
|
|
2010
|
+
});
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
out.push({ target: tok.text, verb: reading.verb });
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Literal `-c` / `env -S` / `eval` payload recursion — the same deduped
|
|
2018
|
+
// union helper and the same shared budget the match and redirect surfaces
|
|
2019
|
+
// use, so this traversal cannot be starved any differently than those.
|
|
2020
|
+
const payloads = dedupedSegmentPayloads(segment, resolved);
|
|
2021
|
+
if (payloads.length === 0) continue;
|
|
2022
|
+
|
|
2023
|
+
// A cap that drops payloads SILENTLY is a bypass, not a cap (#988 T2) —
|
|
2024
|
+
// emit a marker so the consuming hook can surface it.
|
|
2025
|
+
if (depth >= MAX_PAYLOAD_DEPTH) {
|
|
2026
|
+
out.push({ target: null, verb: null, unresolved: true, reason: 'depth-exceeded' });
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
for (const payload of payloads) {
|
|
2030
|
+
if (budget.remaining <= 0) {
|
|
2031
|
+
out.push({ target: null, verb: null, unresolved: true, reason: 'budget-exhausted' });
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
budget.remaining -= 1;
|
|
2035
|
+
const subTokens = tokenizeCommand(
|
|
2036
|
+
normalizeShellWhitespaceExpansions(payload, { expandSingleQuoted: true }),
|
|
2037
|
+
);
|
|
2038
|
+
collectDeleteTargets(splitSegments(subTokens), out, depth + 1, budget);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Extract every path a command names as an operand of a file-destroying verb
|
|
2045
|
+
* (`rm`, `mv`, `unlink` — see {@link PATH_DESTRUCTIVE_VERBS} for the covered and
|
|
2046
|
+
* uncovered shapes), across all chain segments and literal shell payloads (#1401).
|
|
2047
|
+
*
|
|
2048
|
+
* This is the DELETE/RENAME sibling of `extractRedirectTargets`: that one answers
|
|
2049
|
+
* "which file does this command WRITE", this one "which file does it REMOVE or
|
|
2050
|
+
* MOVE". They are separate surfaces because a redirect target and a verb operand
|
|
2051
|
+
* are different grammar positions — `rm -f x > out.log` names one of each, and
|
|
2052
|
+
* the guard's 53,896-line telemetry loss (#1401) happened because only the write
|
|
2053
|
+
* surface was ever consulted.
|
|
2054
|
+
*
|
|
2055
|
+
* Entry shapes:
|
|
2056
|
+
* - `{ target: string, verb: 'rm'|'mv'|'unlink' }` — resolved operand, quotes
|
|
2057
|
+
* stripped by the tokenizer.
|
|
2058
|
+
* - `{ target: null, verb, unresolved: true, reason: 'variable/substitution' }`
|
|
2059
|
+
* — the operand carries a variable or a command substitution. Reported
|
|
2060
|
+
* fail-visible; never a match candidate (#641 FP class).
|
|
2061
|
+
* - `{ target: null, verb: null, unresolved: true,
|
|
2062
|
+
* reason: 'budget-exhausted'|'depth-exceeded' }` — a payload subtree was NOT
|
|
2063
|
+
* traversed because a recursion cap cut it off (#988 T2).
|
|
2064
|
+
*
|
|
2065
|
+
* @param {string} command
|
|
2066
|
+
* @returns {Array<{ target: string|null, verb: string|null, unresolved?: boolean, reason?: string }>}
|
|
2067
|
+
*/
|
|
2068
|
+
export function extractDeleteTargets(command) {
|
|
2069
|
+
if (typeof command !== 'string' || command.length === 0) return [];
|
|
2070
|
+
const out = [];
|
|
2071
|
+
const segments = splitSegments(tokenizeCommand(normalizeShellWhitespaceExpansions(command)));
|
|
2072
|
+
collectDeleteTargets(segments, out, 0, { remaining: MAX_PAYLOAD_EVALUATIONS });
|
|
2073
|
+
return out;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Return the FIRST collected delete/rename target a `path-delete` policy rule's
|
|
2078
|
+
* `target-denylist` matches, or `null` when none does (#1401).
|
|
2079
|
+
*
|
|
2080
|
+
* Returns the ENTRY, not a boolean, so the consuming hook can name the verb and
|
|
2081
|
+
* the target in its deny reason without re-probing through a boolean matcher —
|
|
2082
|
+
* the workaround `findMatchedRedirectEntry` needs, and which its own docblock
|
|
2083
|
+
* flags as one copy too many.
|
|
2084
|
+
*
|
|
2085
|
+
* Takes pre-collected `entries` rather than the raw command so the hook
|
|
2086
|
+
* tokenizes ONCE (it needs the same array for the unresolved-marker notices).
|
|
2087
|
+
*
|
|
2088
|
+
* PER-TARGET, never per-command: every entry is judged on its own, so a sibling
|
|
2089
|
+
* operand that is NOT on the denylist can never lift the verdict for one that
|
|
2090
|
+
* is. `rm -f .orchestrator/tmp/ok .orchestrator/metrics/events.jsonl` matches on
|
|
2091
|
+
* the second operand — the exemption is the ABSENCE of a denylist entry for
|
|
2092
|
+
* `.orchestrator/tmp/**`, never a whole-command escape clause. That shape is the
|
|
2093
|
+
* #1106 hole (`guard-design.md` § "Widening a matcher without narrowing its
|
|
2094
|
+
* bypass"), where an appended exempt statement lifted the verdict for the whole
|
|
2095
|
+
* command; it is structurally unreachable here.
|
|
2096
|
+
*
|
|
2097
|
+
* Denylist semantics (globs, the two classes, `~` handling, case-folding,
|
|
2098
|
+
* repo-root relativization) are IDENTICAL to `redirectRuleMatches` — both call
|
|
2099
|
+
* `compileTargetDenylist` / `targetMatchesDenylist`, so the two rule classes can
|
|
2100
|
+
* never drift apart on what a pattern means.
|
|
2101
|
+
*
|
|
2102
|
+
* @param {{ 'target-denylist'?: string[] }} rule
|
|
2103
|
+
* @param {Array<{ target: string|null, verb: string|null, unresolved?: boolean }>} entries
|
|
2104
|
+
* @param {{ repoRoot?: string|null, home?: string|undefined }} [opts]
|
|
2105
|
+
* @returns {{ target: string, verb: string }|null}
|
|
2106
|
+
*/
|
|
2107
|
+
export function firstDeniedDeleteTarget(rule, entries, opts = {}) {
|
|
2108
|
+
const { repoRoot = null, home = process.env.HOME } = opts;
|
|
2109
|
+
if (!rule || !Array.isArray(entries) || entries.length === 0) return null;
|
|
2110
|
+
const denylist = Array.isArray(rule['target-denylist']) ? rule['target-denylist'] : [];
|
|
2111
|
+
if (denylist.length === 0) return null;
|
|
2112
|
+
|
|
2113
|
+
const compiled = compileTargetDenylist(denylist, home);
|
|
2114
|
+
if (compiled.relRegexes.length === 0 && compiled.absRegexes.length === 0) return null;
|
|
2115
|
+
|
|
2116
|
+
for (const entry of entries) {
|
|
2117
|
+
if (entry.unresolved || typeof entry.target !== 'string') continue;
|
|
2118
|
+
if (targetMatchesDenylist(entry.target, compiled, repoRoot, home)) {
|
|
2119
|
+
return { target: entry.target, verb: entry.verb };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1878
2125
|
/**
|
|
1879
2126
|
* Test whether a command string contains a blocked pattern with shell-aware
|
|
1880
2127
|
* boundaries AND a quoted-payload guard (#641).
|
|
@@ -14,6 +14,15 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
14
14
|
|
|
15
15
|
const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Default `dialectic.budget-tokens` — the INPUT ceiling of one derivation call
|
|
19
|
+
* (SSOT; `scripts/dialectic-deriver.mjs` `DEFAULT_BUDGET.input` imports it).
|
|
20
|
+
* It is an upper bound the pre-dispatch estimate aborts on, not a spend: card
|
|
21
|
+
* bodies + steering alone estimated ~12k tokens here and ~30k in a consumer
|
|
22
|
+
* repo, so the former default of 8000 aborted real runs (#1380).
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_BUDGET_TOKENS = 32000;
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* Parse the top-level `dialectic:` YAML block from markdown content.
|
|
19
28
|
* Independent of the `## Session Config` section boundary.
|
|
@@ -21,7 +30,7 @@ const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
|
21
30
|
* Defaults:
|
|
22
31
|
* cadence: 5 (integer ≥ 0 — session count between auto-runs; 0 = disabled)
|
|
23
32
|
* model: 'haiku'
|
|
24
|
-
* budget-tokens:
|
|
33
|
+
* budget-tokens: DEFAULT_BUDGET_TOKENS (32000, integer ≥ 0)
|
|
25
34
|
*
|
|
26
35
|
* Throws if `model` is present but not one of haiku|sonnet|opus.
|
|
27
36
|
*
|
|
@@ -29,7 +38,7 @@ const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
|
29
38
|
* @returns {{ cadence: number, model: string, "budget-tokens": number }}
|
|
30
39
|
*/
|
|
31
40
|
export function _parseDialectic(content) {
|
|
32
|
-
const defaults = { cadence: 5, model: 'haiku', 'budget-tokens':
|
|
41
|
+
const defaults = { cadence: 5, model: 'haiku', 'budget-tokens': DEFAULT_BUDGET_TOKENS };
|
|
33
42
|
|
|
34
43
|
const lines = preprocessBlockLines(content);
|
|
35
44
|
let inBlock = false;
|
|
@@ -49,7 +58,7 @@ export function _parseDialectic(content) {
|
|
|
49
58
|
|
|
50
59
|
let cadence = 5;
|
|
51
60
|
let model = 'haiku';
|
|
52
|
-
let budgetTokens =
|
|
61
|
+
let budgetTokens = DEFAULT_BUDGET_TOKENS;
|
|
53
62
|
|
|
54
63
|
for (const rawLine of blockLines) {
|
|
55
64
|
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gate.mjs — Parser for the top-level `gate:` YAML block
|
|
6
|
+
* (PRD `docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md` §4 A3,
|
|
7
|
+
* Epic #1425 / issue #1432).
|
|
8
|
+
*
|
|
9
|
+
* Holds the wall-clock ceiling for GATE PATH B — the
|
|
10
|
+
* `scripts/run-quality-gate.mjs` → `scripts/lib/gates/gate-*.mjs` route, which
|
|
11
|
+
* had NO timeout at all before #1425 A3. Path A (`scripts/lib/quality-gate.mjs`)
|
|
12
|
+
* keeps its own `SO_GATE_TIMEOUT_MS` operator override; this block sets the
|
|
13
|
+
* COMMITTED default for path B so both paths may run equally long.
|
|
14
|
+
*
|
|
15
|
+
* Precedence in `run-quality-gate.mjs`: `SO_GATE_TIMEOUT_MS` (operator, wins) >
|
|
16
|
+
* this committed value > `DEFAULT_GATE_TIMEOUT_MS` (900 000).
|
|
17
|
+
*
|
|
18
|
+
* Tolerant parser: malformed values silently fall back to the default.
|
|
19
|
+
*
|
|
20
|
+
* Returns `{ 'timeout-path-b-ms': number }`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 900 000 ms (15 min) — same ceiling as `GATE_TIMEOUT_MS` on path A (PRD §4). */
|
|
24
|
+
const DEFAULT_TIMEOUT_PATH_B_MS = 900_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse the top-level `gate:` YAML block from markdown content.
|
|
28
|
+
* Independent of the `## Session Config` section boundary.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} content — full file contents
|
|
31
|
+
* @returns {{'timeout-path-b-ms': number}}
|
|
32
|
+
*/
|
|
33
|
+
export function _parseGate(content) {
|
|
34
|
+
let timeoutPathBMs = DEFAULT_TIMEOUT_PATH_B_MS;
|
|
35
|
+
|
|
36
|
+
const lines = preprocessBlockLines(content);
|
|
37
|
+
let inBlock = false;
|
|
38
|
+
const blockLines = [];
|
|
39
|
+
|
|
40
|
+
for (const rawLine of lines) {
|
|
41
|
+
const line = rawLine.replace(/\r$/, '');
|
|
42
|
+
if (!inBlock) {
|
|
43
|
+
if (matchBlockHeader(line, 'gate')) inBlock = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
47
|
+
blockLines.push(line);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const rawLine of blockLines) {
|
|
51
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
52
|
+
if (!clean.trim()) continue;
|
|
53
|
+
|
|
54
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
55
|
+
if (!kvMatch) continue;
|
|
56
|
+
|
|
57
|
+
const k = kvMatch[1];
|
|
58
|
+
let v = kvMatch[2].trim();
|
|
59
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
60
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
61
|
+
|
|
62
|
+
if (k === 'timeout-path-b-ms') {
|
|
63
|
+
// A zero or negative ceiling would kill every command instantly — the
|
|
64
|
+
// failure mode is worse than the uncapped state this key exists to fix,
|
|
65
|
+
// so it falls back to the default rather than being honoured.
|
|
66
|
+
if (/^-?\d+$/.test(v)) {
|
|
67
|
+
const n = Number.parseInt(v, 10);
|
|
68
|
+
if (Number.isFinite(n) && n > 0) timeoutPathBMs = n;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { 'timeout-path-b-ms': timeoutPathBMs };
|
|
74
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* reaper.mjs — Parser for the top-level `reaper:` YAML block
|
|
6
|
+
* (PRD `docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md` §4,
|
|
7
|
+
* Epic #1425 / issue #1432 B4).
|
|
8
|
+
*
|
|
9
|
+
* Configures the orphan watchdog that `hooks/post-tool-batch-wave-signal.mjs`
|
|
10
|
+
* (PostToolBatch) and `hooks/on-stop.mjs` (Stop + SubagentStop) trigger as a
|
|
11
|
+
* detached fire-and-forget scan. The scan itself lives in
|
|
12
|
+
* `scripts/lib/orphan-reaper.mjs`; this module only reads the numbers.
|
|
13
|
+
*
|
|
14
|
+
* DEFAULT OFF. `enabled` defaults to `false` — unlike `loop-guard`, this signal
|
|
15
|
+
* can SEND SIGNALS to processes, so it ships inert and is armed per repo only
|
|
16
|
+
* after its firing rate has been measured (`.claude/rules/host-resources.md`
|
|
17
|
+
* HR-101: a class above ~10 % is a broken instrument; HR-105: a rule nothing
|
|
18
|
+
* records is unfalsifiable).
|
|
19
|
+
*
|
|
20
|
+
* Tolerant parser: malformed values silently fall back to defaults.
|
|
21
|
+
*
|
|
22
|
+
* Returns `{ enabled, mode, 'min-age-seconds', 'min-scan-interval-seconds',
|
|
23
|
+
* 'kill-grace-ms', 'verify-wait-ms', 'max-hook-latency-ms', 'false-alarm-window' }`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Startwerte für Stufe 1 — NOT calibrated. Every number carries its provenance
|
|
28
|
+
* from the PRD §4 parameter table ("keine Zahl ohne ihre Population").
|
|
29
|
+
*
|
|
30
|
+
* RUNTIME SSOT is `REAPER_DEFAULTS` in `scripts/lib/orphan-reaper.mjs`; these
|
|
31
|
+
* literals mirror it deliberately rather than importing it, because that module
|
|
32
|
+
* pulls `process-group.mjs` + `resource-probe/parsers.mjs` into the config
|
|
33
|
+
* import graph, which every `parseSessionConfig()` caller would then pay for.
|
|
34
|
+
* The parity is held by a test, not by an import
|
|
35
|
+
* (`tests/lib/config/reaper.test.mjs` § defaults mirror REAPER_DEFAULTS).
|
|
36
|
+
*/
|
|
37
|
+
/** 300 s — DevWatchdog's hard limit for `tsgo`; the 2026-09-20 orphans were 7–17 min old. */
|
|
38
|
+
const DEFAULT_MIN_AGE_SECONDS = 300;
|
|
39
|
+
/** 30 s — DevWatchdog's normal scan cadence; keeps a PostToolBatch storm from taxing every tool call. */
|
|
40
|
+
const DEFAULT_MIN_SCAN_INTERVAL_SECONDS = 30;
|
|
41
|
+
/** 10 000 ms — `DEFAULT_KILL_GRACE_MS` from `dispatch-common.mjs:61`; repo convention, not newly invented. */
|
|
42
|
+
const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
43
|
+
/** 500 ms — wait before reading the effect back; without it the 2026-09-20 check falsely reported "still alive". */
|
|
44
|
+
const DEFAULT_VERIFY_WAIT_MS = 500;
|
|
45
|
+
/** 50 ms — ceiling a scan may delay a hook by; above that the scan counts as too expensive. */
|
|
46
|
+
const DEFAULT_MAX_HOOK_LATENCY_MS = 50;
|
|
47
|
+
/** 50 decisions — rolling window over the JSONL audit (B5), so the rate has a population on quiet hosts too. */
|
|
48
|
+
const DEFAULT_FALSE_ALARM_WINDOW = 50;
|
|
49
|
+
|
|
50
|
+
/** `report` = decide + audit only (dryRun); `kill` = send signals (PRD Stufe 2, after calibration). */
|
|
51
|
+
const MODE_VALUES = new Set(['report', 'kill']);
|
|
52
|
+
const DEFAULT_MODE = 'report';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Coerce a raw string into a finite integer >= `min`, else `fallback`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} raw
|
|
58
|
+
* @param {number} fallback
|
|
59
|
+
* @param {number} min
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function parseBoundedInt(raw, fallback, min) {
|
|
63
|
+
if (!/^-?\d+$/.test(raw)) return fallback;
|
|
64
|
+
const n = Number.parseInt(raw, 10);
|
|
65
|
+
if (!Number.isFinite(n) || n < min) return fallback;
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse the top-level `reaper:` YAML block from markdown content.
|
|
71
|
+
* Independent of the `## Session Config` section boundary.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} content — full file contents
|
|
74
|
+
* @returns {{enabled: boolean, mode: string, 'min-age-seconds': number,
|
|
75
|
+
* 'min-scan-interval-seconds': number, 'kill-grace-ms': number,
|
|
76
|
+
* 'verify-wait-ms': number, 'max-hook-latency-ms': number,
|
|
77
|
+
* 'false-alarm-window': number}}
|
|
78
|
+
*/
|
|
79
|
+
export function _parseReaper(content) {
|
|
80
|
+
let enabled = false;
|
|
81
|
+
let mode = DEFAULT_MODE;
|
|
82
|
+
let minAgeSeconds = DEFAULT_MIN_AGE_SECONDS;
|
|
83
|
+
let minScanIntervalSeconds = DEFAULT_MIN_SCAN_INTERVAL_SECONDS;
|
|
84
|
+
let killGraceMs = DEFAULT_KILL_GRACE_MS;
|
|
85
|
+
let verifyWaitMs = DEFAULT_VERIFY_WAIT_MS;
|
|
86
|
+
let maxHookLatencyMs = DEFAULT_MAX_HOOK_LATENCY_MS;
|
|
87
|
+
let falseAlarmWindow = DEFAULT_FALSE_ALARM_WINDOW;
|
|
88
|
+
|
|
89
|
+
const lines = preprocessBlockLines(content);
|
|
90
|
+
let inBlock = false;
|
|
91
|
+
const blockLines = [];
|
|
92
|
+
|
|
93
|
+
for (const rawLine of lines) {
|
|
94
|
+
const line = rawLine.replace(/\r$/, '');
|
|
95
|
+
if (!inBlock) {
|
|
96
|
+
if (matchBlockHeader(line, 'reaper')) inBlock = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
100
|
+
blockLines.push(line);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const rawLine of blockLines) {
|
|
104
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
105
|
+
if (!clean.trim()) continue;
|
|
106
|
+
|
|
107
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
108
|
+
if (!kvMatch) continue;
|
|
109
|
+
|
|
110
|
+
const k = kvMatch[1];
|
|
111
|
+
let v = kvMatch[2].trim();
|
|
112
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
113
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
114
|
+
|
|
115
|
+
switch (k) {
|
|
116
|
+
case 'enabled':
|
|
117
|
+
// Default is false → only an explicit "true" arms the watchdog.
|
|
118
|
+
enabled = v.toLowerCase() === 'true';
|
|
119
|
+
break;
|
|
120
|
+
case 'mode':
|
|
121
|
+
// An unknown mode falls back to the INERT one — never to `kill`.
|
|
122
|
+
mode = MODE_VALUES.has(v.toLowerCase()) ? v.toLowerCase() : DEFAULT_MODE;
|
|
123
|
+
break;
|
|
124
|
+
case 'min-age-seconds':
|
|
125
|
+
minAgeSeconds = parseBoundedInt(v, DEFAULT_MIN_AGE_SECONDS, 0);
|
|
126
|
+
break;
|
|
127
|
+
case 'min-scan-interval-seconds':
|
|
128
|
+
minScanIntervalSeconds = parseBoundedInt(v, DEFAULT_MIN_SCAN_INTERVAL_SECONDS, 1);
|
|
129
|
+
break;
|
|
130
|
+
case 'kill-grace-ms':
|
|
131
|
+
killGraceMs = parseBoundedInt(v, DEFAULT_KILL_GRACE_MS, 0);
|
|
132
|
+
break;
|
|
133
|
+
case 'verify-wait-ms':
|
|
134
|
+
verifyWaitMs = parseBoundedInt(v, DEFAULT_VERIFY_WAIT_MS, 0);
|
|
135
|
+
break;
|
|
136
|
+
case 'max-hook-latency-ms':
|
|
137
|
+
maxHookLatencyMs = parseBoundedInt(v, DEFAULT_MAX_HOOK_LATENCY_MS, 1);
|
|
138
|
+
break;
|
|
139
|
+
case 'false-alarm-window':
|
|
140
|
+
falseAlarmWindow = parseBoundedInt(v, DEFAULT_FALSE_ALARM_WINDOW, 1);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Self-healing clamp: a `min-age-seconds` below the scan interval is dead
|
|
146
|
+
// config — a process younger than one scan period can be born and reaped
|
|
147
|
+
// between two scans, so the watchdog would fire on processes it has never
|
|
148
|
+
// seen before. Widen the age floor to at least one scan period (the same
|
|
149
|
+
// shape as loop-guard's window >= threshold clamp).
|
|
150
|
+
if (minAgeSeconds < minScanIntervalSeconds) minAgeSeconds = minScanIntervalSeconds;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
enabled,
|
|
154
|
+
mode,
|
|
155
|
+
'min-age-seconds': minAgeSeconds,
|
|
156
|
+
'min-scan-interval-seconds': minScanIntervalSeconds,
|
|
157
|
+
'kill-grace-ms': killGraceMs,
|
|
158
|
+
'verify-wait-ms': verifyWaitMs,
|
|
159
|
+
'max-hook-latency-ms': maxHookLatencyMs,
|
|
160
|
+
'false-alarm-window': falseAlarmWindow,
|
|
161
|
+
};
|
|
162
|
+
}
|
package/scripts/lib/config.mjs
CHANGED
|
@@ -65,6 +65,8 @@ import { _parseHealthEndpoints, _parseEcosystemHealthBlockEnabled } from './conf
|
|
|
65
65
|
import { _parseBrokenWindow } from './config/broken-window.mjs';
|
|
66
66
|
import { _parseSlopcheck } from './config/slopcheck.mjs';
|
|
67
67
|
import { _parseDiscoveryValidator } from './config/discovery-validator.mjs';
|
|
68
|
+
import { _parseReaper } from './config/reaper.mjs';
|
|
69
|
+
import { _parseGate } from './config/gate.mjs';
|
|
68
70
|
import { _parseTemplatesFirst } from './config/templates-first.mjs';
|
|
69
71
|
import { _parseVerificationAutoFix } from './config/verification-auto-fix.mjs';
|
|
70
72
|
import { _parseDialectic } from './config/dialectic.mjs';
|
|
@@ -417,6 +419,16 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
417
419
|
// discovery-validator: parsed from full content (PSA-006 enforcement / issue #567)
|
|
418
420
|
const discoveryValidator = _parseDiscoveryValidator(mdContent);
|
|
419
421
|
|
|
422
|
+
// reaper: orphan-process watchdog, default OFF (PRD 2026-09-20 Prozessgruppen-Kill
|
|
423
|
+
// und Waisen-Wächter, Epic #1425 / #1432 B4). Consumed by the two hooks that
|
|
424
|
+
// trigger the detached scan — post-tool-batch-wave-signal.mjs and on-stop.mjs.
|
|
425
|
+
const reaper = _parseReaper(mdContent);
|
|
426
|
+
|
|
427
|
+
// gate: wall-clock ceiling for gate path B (run-quality-gate.mjs →
|
|
428
|
+
// gate-*.mjs), which had no timeout at all before #1425 A3. The operator
|
|
429
|
+
// override SO_GATE_TIMEOUT_MS still wins over this committed value.
|
|
430
|
+
const gate = _parseGate(mdContent);
|
|
431
|
+
|
|
420
432
|
// templates-first: parsed from full content (PRD gsd Pattern 3 / issues #517, #519)
|
|
421
433
|
const templatesFirst = _parseTemplatesFirst(mdContent);
|
|
422
434
|
|
|
@@ -555,6 +567,8 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
555
567
|
'skill-evolution': skillEvolution,
|
|
556
568
|
'dispatcher-autonomy': dispatcherAutonomy,
|
|
557
569
|
'discovery-validator': discoveryValidator,
|
|
570
|
+
'reaper': reaper,
|
|
571
|
+
'gate': gate,
|
|
558
572
|
'templates-first': templatesFirst,
|
|
559
573
|
'verification-auto-fix': verificationAutoFix,
|
|
560
574
|
'dialectic': dialectic,
|