session-orchestrator 3.24.0 → 4.0.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 +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
// scripts/lib/autopilot/dep-graph.mjs
|
|
2
|
-
//
|
|
3
|
-
// Issue dependency graph + topological-sort scheduler for autopilot --multi-story.
|
|
4
|
-
// Pure data layer — no I/O, no shell. Powers nextReady() lookups during multi-story
|
|
5
|
-
// pipeline scheduling per "Autopilot Phase D — Per-Story Worktree Pipelines"
|
|
6
|
-
// (#341; archived in the private Meta-Vault).
|
|
7
|
-
//
|
|
8
|
-
// Substrate: ADR-364 thin-slice MVP. Issue selection criterion: `status:ready` label
|
|
9
|
-
// only (OPEN-1 decision in PRD). Cross-loop wait semantics use commit-based deps
|
|
10
|
-
// (OPEN-4), but this module is pure-graph and doesn't enforce wait policy — the
|
|
11
|
-
// worktree-pipeline driver is responsible for that.
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @fileoverview Pure topological-sort dependency graph for GitLab/GitHub issues.
|
|
15
|
-
*
|
|
16
|
-
* Handles `blocks`/`blocked-by` relations between issues and exposes scheduling
|
|
17
|
-
* helpers for the `--multi-story` autopilot mode. All functions are pure data
|
|
18
|
-
* transformations with no I/O side effects.
|
|
19
|
-
*
|
|
20
|
-
* References:
|
|
21
|
-
* - ADR-364 thin-slice MVP
|
|
22
|
-
* - "Autopilot Phase D — Per-Story Worktree Pipelines" (#341; archived in the private Meta-Vault)
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} Issue
|
|
27
|
-
* @property {number} iid - Issue IID/number
|
|
28
|
-
* @property {string[]} blocks - Array of iids (as strings or numbers) this issue blocks
|
|
29
|
-
* @property {string[]} blockedBy - Array of iids (as strings or numbers) blocking this issue
|
|
30
|
-
* @property {string[]} labels - Issue labels (e.g., ["status:ready"])
|
|
31
|
-
* @property {string} title - Issue title (for diagnostics)
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {object} DepGraph
|
|
36
|
-
* @property {Map<number, Issue>} nodes - All known issues keyed by iid
|
|
37
|
-
* @property {Map<number, Set<number>>} edges - iid → set of iids it blocks (outgoing)
|
|
38
|
-
* @property {Map<number, Set<number>>} reverse - iid → set of iids blocking it (incoming)
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Internal helpers
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Coerce a raw iid value (string or number) to a number, or return null if
|
|
47
|
-
* the value is not a valid finite integer.
|
|
48
|
-
*
|
|
49
|
-
* @param {unknown} raw
|
|
50
|
-
* @returns {number|null}
|
|
51
|
-
*/
|
|
52
|
-
function toIid(raw) {
|
|
53
|
-
const n = Number(raw);
|
|
54
|
-
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Safely coerce a field that should be an array of iids.
|
|
59
|
-
* Returns an empty array when the field is null/undefined/non-array.
|
|
60
|
-
*
|
|
61
|
-
* @param {unknown} field
|
|
62
|
-
* @returns {number[]}
|
|
63
|
-
*/
|
|
64
|
-
function normalizeIidArray(field) {
|
|
65
|
-
if (!Array.isArray(field)) return [];
|
|
66
|
-
return field.map(toIid).filter((n) => n !== null);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// buildGraph
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Build a DepGraph from an array of issues.
|
|
75
|
-
*
|
|
76
|
-
* Reconciles both directions of the blocks/blockedBy relationship:
|
|
77
|
-
* - If A.blocks includes B's iid, then B.blockedBy is extended to include A.
|
|
78
|
-
* - If A.blockedBy includes B's iid, then B.blocks is extended to include A.
|
|
79
|
-
*
|
|
80
|
-
* Issues referenced in blocks/blockedBy that are not present in the input
|
|
81
|
-
* array are silently ignored after emitting a `console.warn`. The graph will
|
|
82
|
-
* only contain nodes for issues present in the `issues` parameter.
|
|
83
|
-
*
|
|
84
|
-
* @param {Issue[]} issues - Flat array of issues to build the graph from.
|
|
85
|
-
* @returns {DepGraph}
|
|
86
|
-
*/
|
|
87
|
-
export function buildGraph(issues) {
|
|
88
|
-
/** @type {Map<number, Issue>} */
|
|
89
|
-
const nodes = new Map();
|
|
90
|
-
/** @type {Map<number, Set<number>>} */
|
|
91
|
-
const edges = new Map();
|
|
92
|
-
/** @type {Map<number, Set<number>>} */
|
|
93
|
-
const reverse = new Map();
|
|
94
|
-
|
|
95
|
-
if (!Array.isArray(issues) || issues.length === 0) {
|
|
96
|
-
return { nodes, edges, reverse };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// First pass: intern all issues, normalizing iid arrays defensively.
|
|
100
|
-
for (const raw of issues) {
|
|
101
|
-
const iid = toIid(raw?.iid);
|
|
102
|
-
if (iid === null) {
|
|
103
|
-
console.warn('[dep-graph] buildGraph: skipping issue with invalid iid', raw);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
/** @type {Issue} */
|
|
107
|
-
const issue = {
|
|
108
|
-
iid,
|
|
109
|
-
blocks: normalizeIidArray(raw.blocks),
|
|
110
|
-
blockedBy: normalizeIidArray(raw.blockedBy),
|
|
111
|
-
labels: Array.isArray(raw.labels) ? [...raw.labels] : [],
|
|
112
|
-
title: typeof raw.title === 'string' ? raw.title : String(raw.title ?? ''),
|
|
113
|
-
};
|
|
114
|
-
nodes.set(iid, issue);
|
|
115
|
-
edges.set(iid, new Set());
|
|
116
|
-
reverse.set(iid, new Set());
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Second pass: reconcile both directions and populate edge maps.
|
|
120
|
-
// We first collect all declared relations, then validate both ends exist.
|
|
121
|
-
// Collect raw declarations: [blocker, blocked] pairs.
|
|
122
|
-
/** @type {Array<[number, number]>} */
|
|
123
|
-
const declared = [];
|
|
124
|
-
|
|
125
|
-
for (const issue of nodes.values()) {
|
|
126
|
-
for (const target of issue.blocks) {
|
|
127
|
-
declared.push([issue.iid, target]);
|
|
128
|
-
}
|
|
129
|
-
for (const source of issue.blockedBy) {
|
|
130
|
-
declared.push([source, issue.iid]);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Deduplicate and validate, then write into edge maps and normalised Issue fields.
|
|
135
|
-
const seen = new Set();
|
|
136
|
-
for (const [blocker, blocked] of declared) {
|
|
137
|
-
const key = `${blocker}:${blocked}`;
|
|
138
|
-
if (seen.has(key)) continue;
|
|
139
|
-
seen.add(key);
|
|
140
|
-
|
|
141
|
-
if (blocker === blocked) {
|
|
142
|
-
console.warn(`[dep-graph] buildGraph: self-loop on iid ${blocker} ignored`);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const blockerNode = nodes.get(blocker);
|
|
147
|
-
const blockedNode = nodes.get(blocked);
|
|
148
|
-
|
|
149
|
-
if (!blockerNode) {
|
|
150
|
-
console.warn(`[dep-graph] buildGraph: iid ${blocker} referenced but not in issues array — ignored`);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (!blockedNode) {
|
|
154
|
-
console.warn(`[dep-graph] buildGraph: iid ${blocked} referenced but not in issues array — ignored`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Sync normalised arrays on the Issue objects.
|
|
159
|
-
if (!blockerNode.blocks.includes(blocked)) blockerNode.blocks.push(blocked);
|
|
160
|
-
if (!blockedNode.blockedBy.includes(blocker)) blockedNode.blockedBy.push(blocker);
|
|
161
|
-
|
|
162
|
-
// Populate edge maps.
|
|
163
|
-
edges.get(blocker).add(blocked);
|
|
164
|
-
reverse.get(blocked).add(blocker);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { nodes, edges, reverse };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// topologicalSort
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Perform a topological sort on the graph using Kahn's BFS algorithm.
|
|
176
|
-
*
|
|
177
|
-
* Tie-breaking between nodes at the same depth is resolved by ascending iid
|
|
178
|
-
* (stable, deterministic output).
|
|
179
|
-
*
|
|
180
|
-
* When cycles are present, the `order` array contains only the nodes NOT
|
|
181
|
-
* involved in any cycle (processed in topological order). The `cycles` array
|
|
182
|
-
* contains each cycle as an array of iid numbers.
|
|
183
|
-
*
|
|
184
|
-
* @param {DepGraph} graph
|
|
185
|
-
* @returns {{ order: number[], cycles: number[][] }}
|
|
186
|
-
*/
|
|
187
|
-
export function topologicalSort(graph) {
|
|
188
|
-
const { nodes, edges, reverse } = graph;
|
|
189
|
-
|
|
190
|
-
if (nodes.size === 0) {
|
|
191
|
-
return { order: [], cycles: [] };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Build a mutable in-degree map.
|
|
195
|
-
/** @type {Map<number, number>} */
|
|
196
|
-
const inDegree = new Map();
|
|
197
|
-
for (const iid of nodes.keys()) {
|
|
198
|
-
inDegree.set(iid, reverse.get(iid)?.size ?? 0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Seed the queue with all zero-in-degree nodes, sorted by ascending iid.
|
|
202
|
-
/** @type {number[]} */
|
|
203
|
-
const queue = [...inDegree.entries()]
|
|
204
|
-
.filter(([, deg]) => deg === 0)
|
|
205
|
-
.map(([iid]) => iid)
|
|
206
|
-
.sort((a, b) => a - b);
|
|
207
|
-
|
|
208
|
-
/** @type {number[]} */
|
|
209
|
-
const order = [];
|
|
210
|
-
|
|
211
|
-
while (queue.length > 0) {
|
|
212
|
-
// Dequeue the smallest iid (queue is always kept sorted).
|
|
213
|
-
const iid = queue.shift();
|
|
214
|
-
order.push(iid);
|
|
215
|
-
|
|
216
|
-
// Decrement in-degree for all nodes this iid blocks.
|
|
217
|
-
const neighbours = [...(edges.get(iid) ?? [])].sort((a, b) => a - b);
|
|
218
|
-
for (const neighbour of neighbours) {
|
|
219
|
-
const newDeg = (inDegree.get(neighbour) ?? 0) - 1;
|
|
220
|
-
inDegree.set(neighbour, newDeg);
|
|
221
|
-
if (newDeg === 0) {
|
|
222
|
-
// Insert in sorted position to maintain ascending-iid stability.
|
|
223
|
-
insertSorted(queue, neighbour);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Any node not in `order` is part of a cycle.
|
|
229
|
-
const inOrderSet = new Set(order);
|
|
230
|
-
const cycleNodes = [...nodes.keys()].filter((iid) => !inOrderSet.has(iid));
|
|
231
|
-
|
|
232
|
-
const cycles = cycleNodes.length > 0 ? detectCyclesFromNodes(graph, cycleNodes) : [];
|
|
233
|
-
|
|
234
|
-
return { order, cycles };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Insert `value` into a sorted array (ascending) in-place using binary search.
|
|
239
|
-
*
|
|
240
|
-
* @param {number[]} arr
|
|
241
|
-
* @param {number} value
|
|
242
|
-
*/
|
|
243
|
-
function insertSorted(arr, value) {
|
|
244
|
-
let lo = 0;
|
|
245
|
-
let hi = arr.length;
|
|
246
|
-
while (lo < hi) {
|
|
247
|
-
const mid = (lo + hi) >>> 1;
|
|
248
|
-
if (arr[mid] < value) lo = mid + 1;
|
|
249
|
-
else hi = mid;
|
|
250
|
-
}
|
|
251
|
-
arr.splice(lo, 0, value);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// detectCycles
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Detect all strongly-connected components (SCCs) of size > 1 using an
|
|
260
|
-
* iterative implementation of Tarjan's SCC algorithm.
|
|
261
|
-
*
|
|
262
|
-
* Returns each cycle as an array of iid numbers (members of the SCC),
|
|
263
|
-
* sorted by ascending iid within each cycle. The outer array is sorted by
|
|
264
|
-
* the minimum iid of each cycle (ascending).
|
|
265
|
-
*
|
|
266
|
-
* @param {DepGraph} graph
|
|
267
|
-
* @returns {number[][]}
|
|
268
|
-
*/
|
|
269
|
-
export function detectCycles(graph) {
|
|
270
|
-
return detectCyclesFromNodes(graph, [...graph.nodes.keys()]);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Internal: run Tarjan's iterative SCC algorithm over a subset of nodes
|
|
275
|
-
* (or all nodes). Only SCCs of size > 1 are returned (true cycles).
|
|
276
|
-
*
|
|
277
|
-
* @param {DepGraph} graph
|
|
278
|
-
* @param {number[]} nodeSubset - iids to consider
|
|
279
|
-
* @returns {number[][]}
|
|
280
|
-
*/
|
|
281
|
-
function detectCyclesFromNodes(graph, nodeSubset) {
|
|
282
|
-
const { edges } = graph;
|
|
283
|
-
|
|
284
|
-
let index = 0;
|
|
285
|
-
/** @type {Map<number, number>} */
|
|
286
|
-
const indices = new Map();
|
|
287
|
-
/** @type {Map<number, number>} */
|
|
288
|
-
const lowlink = new Map();
|
|
289
|
-
/** @type {Set<number>} */
|
|
290
|
-
const onStack = new Set();
|
|
291
|
-
/** @type {number[]} */
|
|
292
|
-
const stack = [];
|
|
293
|
-
/** @type {number[][]} */
|
|
294
|
-
const sccs = [];
|
|
295
|
-
|
|
296
|
-
// Only process nodes in the subset.
|
|
297
|
-
const subsetSet = new Set(nodeSubset);
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Iterative Tarjan's SCC for a single root node.
|
|
301
|
-
*
|
|
302
|
-
* @param {number} root
|
|
303
|
-
*/
|
|
304
|
-
function strongconnect(root) {
|
|
305
|
-
// Explicit call-stack frames to avoid recursion limits.
|
|
306
|
-
// Each frame: { iid, neighbourIter, parentIid }
|
|
307
|
-
/** @type {Array<{ iid: number, neighbours: number[], ni: number }>} */
|
|
308
|
-
const callStack = [];
|
|
309
|
-
|
|
310
|
-
function visit(iid) {
|
|
311
|
-
indices.set(iid, index);
|
|
312
|
-
lowlink.set(iid, index);
|
|
313
|
-
index++;
|
|
314
|
-
stack.push(iid);
|
|
315
|
-
onStack.add(iid);
|
|
316
|
-
|
|
317
|
-
const neighbours = [...(edges.get(iid) ?? [])]
|
|
318
|
-
.filter((n) => subsetSet.has(n))
|
|
319
|
-
.sort((a, b) => a - b);
|
|
320
|
-
callStack.push({ iid, neighbours, ni: 0 });
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
visit(root);
|
|
324
|
-
|
|
325
|
-
while (callStack.length > 0) {
|
|
326
|
-
const frame = callStack[callStack.length - 1];
|
|
327
|
-
const { iid, neighbours } = frame;
|
|
328
|
-
|
|
329
|
-
if (frame.ni < neighbours.length) {
|
|
330
|
-
const w = neighbours[frame.ni++];
|
|
331
|
-
if (!indices.has(w)) {
|
|
332
|
-
visit(w);
|
|
333
|
-
} else if (onStack.has(w)) {
|
|
334
|
-
lowlink.set(iid, Math.min(lowlink.get(iid), indices.get(w)));
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
// Pop frame.
|
|
338
|
-
callStack.pop();
|
|
339
|
-
if (callStack.length > 0) {
|
|
340
|
-
const parent = callStack[callStack.length - 1];
|
|
341
|
-
lowlink.set(
|
|
342
|
-
parent.iid,
|
|
343
|
-
Math.min(lowlink.get(parent.iid), lowlink.get(iid))
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
// Check if this iid is an SCC root.
|
|
347
|
-
if (lowlink.get(iid) === indices.get(iid)) {
|
|
348
|
-
const scc = [];
|
|
349
|
-
let w;
|
|
350
|
-
do {
|
|
351
|
-
w = stack.pop();
|
|
352
|
-
onStack.delete(w);
|
|
353
|
-
scc.push(w);
|
|
354
|
-
} while (w !== iid);
|
|
355
|
-
if (scc.length > 1) {
|
|
356
|
-
sccs.push(scc.sort((a, b) => a - b));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
for (const iid of nodeSubset) {
|
|
364
|
-
if (!indices.has(iid)) {
|
|
365
|
-
strongconnect(iid);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Sort by minimum iid in each SCC ascending.
|
|
370
|
-
sccs.sort((a, b) => Math.min(...a) - Math.min(...b));
|
|
371
|
-
return sccs;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ---------------------------------------------------------------------------
|
|
375
|
-
// nextReady
|
|
376
|
-
// ---------------------------------------------------------------------------
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Return the set of issues that are ready to be started next.
|
|
380
|
-
*
|
|
381
|
-
* An issue is considered ready when ALL of the following hold:
|
|
382
|
-
* 1. It is NOT in `inFlight` (already being processed).
|
|
383
|
-
* 2. It is NOT in `completed` (already done).
|
|
384
|
-
* 3. Every iid in its `blockedBy` list is present in `completed`.
|
|
385
|
-
* 4. It carries the label `"status:ready"`.
|
|
386
|
-
*
|
|
387
|
-
* The returned array is sorted by ascending iid.
|
|
388
|
-
*
|
|
389
|
-
* @param {DepGraph} graph - The dependency graph.
|
|
390
|
-
* @param {Set<number>} inFlight - iids currently being processed.
|
|
391
|
-
* @param {Set<number>} completed - iids that have finished successfully.
|
|
392
|
-
* @returns {Issue[]}
|
|
393
|
-
*/
|
|
394
|
-
export function nextReady(graph, inFlight, completed) {
|
|
395
|
-
const safeInFlight = inFlight instanceof Set ? inFlight : new Set();
|
|
396
|
-
const safeCompleted = completed instanceof Set ? completed : new Set();
|
|
397
|
-
|
|
398
|
-
/** @type {Issue[]} */
|
|
399
|
-
const ready = [];
|
|
400
|
-
|
|
401
|
-
for (const [iid, issue] of graph.nodes) {
|
|
402
|
-
if (safeInFlight.has(iid)) continue;
|
|
403
|
-
if (safeCompleted.has(iid)) continue;
|
|
404
|
-
|
|
405
|
-
// All blockers must be completed.
|
|
406
|
-
const blockers = Array.isArray(issue.blockedBy) ? issue.blockedBy : [];
|
|
407
|
-
const allBlockersCompleted = blockers.every((b) => safeCompleted.has(b));
|
|
408
|
-
if (!allBlockersCompleted) continue;
|
|
409
|
-
|
|
410
|
-
// Must carry status:ready label.
|
|
411
|
-
if (!Array.isArray(issue.labels) || !issue.labels.includes('status:ready')) continue;
|
|
412
|
-
|
|
413
|
-
ready.push(issue);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return ready.sort((a, b) => a.iid - b.iid);
|
|
417
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// scripts/lib/autopilot/multi-killswitch.mjs
|
|
2
|
-
//
|
|
3
|
-
// 11th kill-switch (STALE_SUBAGENT_MIN) + cohort policy + concurrency cap for
|
|
4
|
-
// autopilot --multi-story. Pure decision logic; no I/O, no shell.
|
|
5
|
-
//
|
|
6
|
-
// References:
|
|
7
|
-
// - "Autopilot Phase D — Per-Story Worktree Pipelines" (#341; archived in the private Meta-Vault) (OPEN-2, OPEN-3, OPEN-5 decisions)
|
|
8
|
-
// - scripts/lib/autopilot/kill-switches.mjs (existing 10 switches; STALL_TIMEOUT precedent)
|
|
9
|
-
// - scripts/lib/resource-probe.mjs (snapshot shape)
|
|
10
|
-
|
|
11
|
-
import { KILL_SWITCHES } from './kill-switches.mjs';
|
|
12
|
-
|
|
13
|
-
// 11th kill-switch constant — additive to the existing 10 in KILL_SWITCHES
|
|
14
|
-
/** @type {string} */
|
|
15
|
-
export const STALE_SUBAGENT_MIN = 'stale-subagent-min';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Calculate concurrency cap per OPEN-2 decision from PRD §4.
|
|
19
|
-
* Formula: min(staticFloor, max(1, floor(ram_free_gb / ramPerLoopGb) - reserveSlots))
|
|
20
|
-
*
|
|
21
|
-
* Defensive: null/undefined ram_free_gb → 1; memory_pressure_pct_free < 15 → cap 1.
|
|
22
|
-
*
|
|
23
|
-
* @param {{ ram_free_gb: number, memory_pressure_pct_free?: number, claude_processes_count?: number }} snapshot
|
|
24
|
-
* @param {object} [opts]
|
|
25
|
-
* @param {number} [opts.staticFloor=3] - hard cap regardless of resources
|
|
26
|
-
* @param {number} [opts.ramPerLoopGb=4] - per-looper RAM budget
|
|
27
|
-
* @param {number} [opts.reserveSlots=1] - sessions to reserve for system + coordinator
|
|
28
|
-
* @returns {number} integer in [1, staticFloor]
|
|
29
|
-
*/
|
|
30
|
-
export function calculateConcurrencyCap(snapshot, opts = {}) {
|
|
31
|
-
const staticFloor = opts.staticFloor ?? 3;
|
|
32
|
-
const ramPerLoopGb = opts.ramPerLoopGb ?? 4;
|
|
33
|
-
const reserveSlots = opts.reserveSlots ?? 1;
|
|
34
|
-
|
|
35
|
-
// macOS memory pressure degraded guard
|
|
36
|
-
if (
|
|
37
|
-
typeof snapshot?.memory_pressure_pct_free === 'number' &&
|
|
38
|
-
snapshot.memory_pressure_pct_free < 15
|
|
39
|
-
) {
|
|
40
|
-
return 1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const ramFreeGb = snapshot?.ram_free_gb;
|
|
44
|
-
if (ramFreeGb === null || ramFreeGb === undefined || typeof ramFreeGb !== 'number' || !Number.isFinite(ramFreeGb)) {
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const computed = Math.floor(ramFreeGb / ramPerLoopGb) - reserveSlots;
|
|
49
|
-
return Math.min(staticFloor, Math.max(1, computed));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @typedef {object} LoopRegistration
|
|
54
|
-
* @property {string} loopId
|
|
55
|
-
* @property {number} pid
|
|
56
|
-
* @property {string} parentRunId
|
|
57
|
-
* @property {number} issueIid
|
|
58
|
-
* @property {'queued'|'running'|'complete'|'failed'} status
|
|
59
|
-
* @property {string|null} killSwitch
|
|
60
|
-
* @property {number} spiralRecoveryCount
|
|
61
|
-
* @property {number} startedAt - epoch ms
|
|
62
|
-
* @property {number} lastActivityAt - epoch ms
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Evaluate cross-loop kill-switches (STALE_SUBAGENT_MIN only — per-loop
|
|
67
|
-
* switches like SPIRAL are handled by postSessionKillSwitch upstream).
|
|
68
|
-
* Returns the FIRST kill condition that fires, or null.
|
|
69
|
-
*
|
|
70
|
-
* @param {LoopRegistration[]} loops
|
|
71
|
-
* @param {object} [opts]
|
|
72
|
-
* @param {number} [opts.staleSubagentMinSeconds=600]
|
|
73
|
-
* @param {() => number} [opts.nowMs=Date.now]
|
|
74
|
-
* @returns {{ kill: string, detail: string, loopId?: string }|null}
|
|
75
|
-
*/
|
|
76
|
-
export function evaluateMultiKillSwitches(loops, opts = {}) {
|
|
77
|
-
if (!Array.isArray(loops) || loops.length === 0) return null;
|
|
78
|
-
|
|
79
|
-
const staleThresholdSeconds = opts.staleSubagentMinSeconds ?? 600;
|
|
80
|
-
const now = (opts.nowMs ?? Date.now)();
|
|
81
|
-
|
|
82
|
-
for (const loop of loops) {
|
|
83
|
-
if (!loop || loop.status !== 'running') continue;
|
|
84
|
-
|
|
85
|
-
const lastActivity = loop.lastActivityAt;
|
|
86
|
-
if (typeof lastActivity !== 'number' || !Number.isFinite(lastActivity)) continue;
|
|
87
|
-
|
|
88
|
-
const idleMs = now - lastActivity;
|
|
89
|
-
const idleSeconds = Math.floor(idleMs / 1000);
|
|
90
|
-
|
|
91
|
-
if (idleMs > staleThresholdSeconds * 1000) {
|
|
92
|
-
return {
|
|
93
|
-
kill: STALE_SUBAGENT_MIN,
|
|
94
|
-
detail: `loop ${loop.loopId} stale > ${idleSeconds}s (threshold ${staleThresholdSeconds}s)`,
|
|
95
|
-
loopId: loop.loopId,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Decide whether a sibling loop's failure should abort the cohort.
|
|
105
|
-
* Per OPEN-5 hybrid policy:
|
|
106
|
-
* - First SPIRAL AND target loop spiralRecoveryCount < 1 → retry
|
|
107
|
-
* - Second SPIRAL OR target already retried → cohort-abort
|
|
108
|
-
*
|
|
109
|
-
* @param {LoopRegistration[]} loops
|
|
110
|
-
* @param {string} failedLoopId
|
|
111
|
-
* @returns {{ action: 'retry'|'cohort-abort', reason: string }}
|
|
112
|
-
*/
|
|
113
|
-
export function decideCohortAction(loops, failedLoopId) {
|
|
114
|
-
if (!Array.isArray(loops)) {
|
|
115
|
-
return { action: 'cohort-abort', reason: 'no loop registry provided' };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const spiralCount = loops.filter(
|
|
119
|
-
(l) => l && l.killSwitch === KILL_SWITCHES.SPIRAL,
|
|
120
|
-
).length;
|
|
121
|
-
|
|
122
|
-
const targetLoop = loops.find((l) => l && l.loopId === failedLoopId);
|
|
123
|
-
const targetSpiralRecoveries = targetLoop?.spiralRecoveryCount ?? 0;
|
|
124
|
-
|
|
125
|
-
if (spiralCount <= 1 && targetSpiralRecoveries < 1) {
|
|
126
|
-
return {
|
|
127
|
-
action: 'retry',
|
|
128
|
-
reason: 'first-strike spiral, recovering loop ' + failedLoopId,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
action: 'cohort-abort',
|
|
134
|
-
reason: 'second-strike spiral or cohort threshold',
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Check if the orchestrator stop condition is met (OPEN-3 layered policy).
|
|
140
|
-
* Priority: (1) cohort-abort → (2) backlog-empty → (3) inactivity-timeout
|
|
141
|
-
*
|
|
142
|
-
* @param {object} state
|
|
143
|
-
* @param {LoopRegistration[]} state.activeLoops
|
|
144
|
-
* @param {object[]} state.readyBacklog
|
|
145
|
-
* @param {number} state.lastCompletionAt
|
|
146
|
-
* @param {object} [opts]
|
|
147
|
-
* @param {number} [opts.inactivityTimeoutMs=300000]
|
|
148
|
-
* @param {() => number} [opts.nowMs=Date.now]
|
|
149
|
-
* @returns {{ stop: boolean, reason?: 'first-kill-switch'|'backlog-empty'|'inactivity-timeout' }}
|
|
150
|
-
*/
|
|
151
|
-
export function shouldStopOrchestrator(state, opts = {}) {
|
|
152
|
-
const inactivityTimeoutMs = opts.inactivityTimeoutMs ?? 300_000;
|
|
153
|
-
const now = (opts.nowMs ?? Date.now)();
|
|
154
|
-
|
|
155
|
-
const activeLoops = Array.isArray(state?.activeLoops) ? state.activeLoops : [];
|
|
156
|
-
const readyBacklog = Array.isArray(state?.readyBacklog) ? state.readyBacklog : [];
|
|
157
|
-
|
|
158
|
-
// Priority 1: cohort-abort triggered by spiral on an active loop
|
|
159
|
-
for (const loop of activeLoops) {
|
|
160
|
-
if (!loop || loop.killSwitch !== KILL_SWITCHES.SPIRAL) continue;
|
|
161
|
-
|
|
162
|
-
const decision = decideCohortAction(activeLoops, loop.loopId);
|
|
163
|
-
if (decision.action === 'cohort-abort') {
|
|
164
|
-
return { stop: true, reason: 'first-kill-switch' };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Priority 2: all work drained
|
|
169
|
-
if (activeLoops.length === 0 && readyBacklog.length === 0) {
|
|
170
|
-
return { stop: true, reason: 'backlog-empty' };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Priority 3: orchestrator inactivity timeout (only meaningful while loops are running)
|
|
174
|
-
if (
|
|
175
|
-
activeLoops.length > 0 &&
|
|
176
|
-
typeof state?.lastCompletionAt === 'number' &&
|
|
177
|
-
Number.isFinite(state.lastCompletionAt) &&
|
|
178
|
-
now - state.lastCompletionAt > inactivityTimeoutMs
|
|
179
|
-
) {
|
|
180
|
-
return { stop: true, reason: 'inactivity-timeout' };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return { stop: false };
|
|
184
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* webhook-url.mjs — Centralized webhook URL resolution for session-orchestrator.
|
|
3
|
-
*
|
|
4
|
-
* Replaces ad-hoc hardcoded personal-domain URL fallbacks with a single
|
|
5
|
-
* resolution function that reads from environment variables and Session Config.
|
|
6
|
-
* No personal-domain default exists — callers that need a URL must supply one
|
|
7
|
-
* explicitly via env or config.
|
|
8
|
-
*
|
|
9
|
-
* Resolution order (env wins over config):
|
|
10
|
-
* 1. Environment variable: SO_WEBHOOK_<KIND>_URL (e.g. SO_WEBHOOK_SLACK_URL)
|
|
11
|
-
* 2. Session Config field: webhooks.<kind>.url
|
|
12
|
-
* 3. Error: WebhookConfigError is thrown — no silent fallback to a personal domain.
|
|
13
|
-
*
|
|
14
|
-
* Supported kinds: 'slack' | 'discord' | 'generic' | 'gitlab-pipeline-status'
|
|
15
|
-
*
|
|
16
|
-
* Issue #228 — centralize webhook URL resolution + drop personal-domain default.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Named error
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Thrown by `resolveWebhookUrl` when no URL source (env or config) provides a
|
|
25
|
-
* webhook URL for the requested kind. Callers should catch this and surface an
|
|
26
|
-
* actionable message to the user.
|
|
27
|
-
*/
|
|
28
|
-
export class WebhookConfigError extends Error {
|
|
29
|
-
/**
|
|
30
|
-
* @param {string} kind - The webhook kind that was requested.
|
|
31
|
-
* @param {string} [message] - Optional override; defaults to a descriptive message.
|
|
32
|
-
*/
|
|
33
|
-
constructor(kind, message) {
|
|
34
|
-
super(
|
|
35
|
-
message ??
|
|
36
|
-
`No webhook URL configured for kind "${kind}". ` +
|
|
37
|
-
`Set SO_WEBHOOK_${kind.toUpperCase().replace(/-/g, '_')}_URL ` +
|
|
38
|
-
`or add webhooks.${kind}.url to Session Config.`,
|
|
39
|
-
);
|
|
40
|
-
this.name = 'WebhookConfigError';
|
|
41
|
-
this.kind = kind;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Supported kinds
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/** @type {ReadonlySet<string>} */
|
|
50
|
-
const SUPPORTED_KINDS = new Set([
|
|
51
|
-
'slack',
|
|
52
|
-
'discord',
|
|
53
|
-
'generic',
|
|
54
|
-
'gitlab-pipeline-status',
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Public API
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Resolve a webhook URL for the given kind.
|
|
63
|
-
*
|
|
64
|
-
* Resolution order:
|
|
65
|
-
* 1. `SO_WEBHOOK_<KIND>_URL` environment variable (e.g. `SO_WEBHOOK_SLACK_URL`)
|
|
66
|
-
* — kind is upper-cased and hyphens become underscores.
|
|
67
|
-
* 2. `config.webhooks.<kind>.url` — when a Session Config object is provided.
|
|
68
|
-
* 3. Throws {@link WebhookConfigError} — no silent personal-domain fallback.
|
|
69
|
-
*
|
|
70
|
-
* @param {{ kind: string, config?: object }} options
|
|
71
|
-
* - `kind` — One of the supported webhook kinds (see SUPPORTED_KINDS).
|
|
72
|
-
* - `config` — Parsed Session Config object (optional). Must have a `webhooks`
|
|
73
|
-
* sub-object if a config-based URL is desired.
|
|
74
|
-
* @returns {string} The resolved webhook URL (guaranteed non-empty string).
|
|
75
|
-
* @throws {WebhookConfigError} When kind is unsupported or no URL is found.
|
|
76
|
-
* @throws {TypeError} When `kind` is not a string.
|
|
77
|
-
*/
|
|
78
|
-
export function resolveWebhookUrl({ kind, config } = {}) {
|
|
79
|
-
if (typeof kind !== 'string' || kind.length === 0) {
|
|
80
|
-
throw new TypeError('resolveWebhookUrl: "kind" must be a non-empty string');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!SUPPORTED_KINDS.has(kind)) {
|
|
84
|
-
throw new WebhookConfigError(
|
|
85
|
-
kind,
|
|
86
|
-
`Unsupported webhook kind "${kind}". Supported kinds: ${[...SUPPORTED_KINDS].join(', ')}.`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 1. Environment variable: SO_WEBHOOK_<KIND>_URL
|
|
91
|
-
const envKey = `SO_WEBHOOK_${kind.toUpperCase().replace(/-/g, '_')}_URL`;
|
|
92
|
-
const envUrl = process.env[envKey];
|
|
93
|
-
if (typeof envUrl === 'string' && envUrl.length > 0) {
|
|
94
|
-
return envUrl;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 2. Session Config: webhooks.<kind>.url
|
|
98
|
-
const configUrl = config?.webhooks?.[kind]?.url;
|
|
99
|
-
if (typeof configUrl === 'string' && configUrl.length > 0) {
|
|
100
|
-
return configUrl;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3. No source provided a URL — throw rather than fall back to a personal domain.
|
|
104
|
-
throw new WebhookConfigError(kind);
|
|
105
|
-
}
|