@wrongstack/core 0.313.1 → 0.316.1
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/dist/chronicle/index.js +31 -31
- package/dist/chronicle/legacy-journal-import.d.ts +0 -6
- package/dist/chronicle/metrics-ingest.d.ts +14 -0
- package/dist/chronicle/metrics-schema.d.ts +10 -1
- package/dist/chronicle/metrics-store.d.ts +20 -2
- package/dist/chronicle/project-server.js +39 -31
- package/dist/chronicle/sink.d.ts +13 -0
- package/dist/chronicle/sqlite-journal.d.ts +20 -2
- package/dist/{chunk-WOEZV3Q4.js → chunk-25LSLC3G.js} +2 -1
- package/dist/{chunk-AI4LLQ2U.js → chunk-27QIURLN.js} +229 -12
- package/dist/{chunk-I45KAW2N.js → chunk-3AGWVSS7.js} +38 -450
- package/dist/chunk-3BDV7RL3.js +211 -0
- package/dist/{chunk-NFAYHPRH.js → chunk-3ECENVHL.js} +10 -8
- package/dist/{chunk-FW5LDDMJ.js → chunk-45WI72ZB.js} +1576 -1156
- package/dist/{chunk-AT4V4XGI.js → chunk-4BMVPOHO.js} +539 -313
- package/dist/{chunk-YBUJCZ57.js → chunk-4I7HGVVA.js} +17 -5
- package/dist/{chunk-JVGPR3TS.js → chunk-4LCZVJVF.js} +7 -55
- package/dist/{chunk-4ZHTKJHM.js → chunk-4SJNS7OB.js} +36 -2
- package/dist/{chunk-AD7ZFZCS.js → chunk-5BMIE7QG.js} +3 -3
- package/dist/{chunk-IQ35HMQL.js → chunk-5YPSXQXM.js} +71 -24
- package/dist/chunk-6FHWXLPT.js +10 -0
- package/dist/{chunk-FIV4IJL7.js → chunk-6H5GEOET.js} +163 -20
- package/dist/{chunk-VB3H5WWB.js → chunk-6NWEVFEL.js} +2 -2
- package/dist/{chunk-E6I5RXVK.js → chunk-AYW5YINX.js} +2 -2
- package/dist/{chunk-DEIQMBD2.js → chunk-B7F4WVP4.js} +2 -2
- package/dist/{chunk-XQ2XGIBF.js → chunk-BPBKKBUK.js} +6 -7
- package/dist/{chunk-K4YUNKU7.js → chunk-BYD345OX.js} +2 -2
- package/dist/{chunk-LZDDLNCZ.js → chunk-C27KDLOX.js} +4 -4
- package/dist/{chunk-6AKOTGLM.js → chunk-CU546ESW.js} +7 -7
- package/dist/{chunk-ZZ4V3WIZ.js → chunk-DLHHXUAM.js} +10 -1
- package/dist/{chunk-BAKVJLEV.js → chunk-DOTHQSJ2.js} +28 -16
- package/dist/{chunk-KBZGE6LN.js → chunk-DYLPMV4Q.js} +4 -3
- package/dist/{chunk-52RTVH4M.js → chunk-F3JWNST7.js} +2 -2
- package/dist/{chunk-WEBMSQP2.js → chunk-FC2ZGRF3.js} +21 -4
- package/dist/{chunk-FHAMQS7Q.js → chunk-FCBTO76Z.js} +76 -146
- package/dist/{chunk-4LJJYBAU.js → chunk-FHZVFYSL.js} +9 -1
- package/dist/{chunk-W4A6QA3D.js → chunk-FWNLEZYG.js} +4 -3
- package/dist/{chunk-OJZEX73F.js → chunk-GDQ7CZRQ.js} +5 -1
- package/dist/{chunk-QVOVMXJR.js → chunk-GJJGJB2R.js} +32 -9
- package/dist/{chunk-4XV6IR37.js → chunk-HI3JGZR2.js} +7 -6
- package/dist/{chunk-FSWCWSTS.js → chunk-HMLIGAU7.js} +26 -26
- package/dist/{chunk-PFXFQQRT.js → chunk-HXKZ7OBQ.js} +7 -43
- package/dist/{chunk-2FQL4PDC.js → chunk-I7DALG7J.js} +5 -5
- package/dist/{chunk-GM54GJBU.js → chunk-IRUTEHCE.js} +3 -3
- package/dist/{chunk-TYW2GI5I.js → chunk-J2WRWR37.js} +40 -13
- package/dist/{chunk-ZEHUCJHY.js → chunk-JFPJ6LLQ.js} +2 -2
- package/dist/chunk-JMULFFLN.js +9 -0
- package/dist/{chunk-RD6DEE36.js → chunk-JO4MGB6Z.js} +94 -8
- package/dist/{chunk-5DSMEVA4.js → chunk-K6BXGITA.js} +7 -7
- package/dist/{chunk-O74MJHXU.js → chunk-KZMSZV32.js} +2 -2
- package/dist/{chunk-GFJOMSHG.js → chunk-LA7CLDRW.js} +6 -2
- package/dist/{chunk-M2WF6AXG.js → chunk-LSMMOS2Z.js} +7 -3
- package/dist/{chunk-ASX6AW36.js → chunk-LXWCMQNJ.js} +3 -3
- package/dist/{chunk-WZO23AWO.js → chunk-M27LIEKV.js} +72 -22
- package/dist/{chunk-JTHKOYWU.js → chunk-MJPHJRAL.js} +15 -15
- package/dist/{chunk-G5AAKEEZ.js → chunk-MTL4O5YR.js} +5 -5
- package/dist/chunk-NHJ75HXY.js +1 -0
- package/dist/{chunk-ZYU2YOJ6.js → chunk-NIOAGFPB.js} +29 -15
- package/dist/{chunk-Y2GE5TSI.js → chunk-NS52ISWS.js} +2 -2
- package/dist/{chunk-UIWONWVG.js → chunk-NSOFPKGT.js} +541 -148
- package/dist/{chunk-RCQYT2I4.js → chunk-NVIZG63R.js} +2 -2
- package/dist/{chunk-EPC3GZD4.js → chunk-OOYZPOXM.js} +2 -2
- package/dist/{chunk-7ZHZE5EO.js → chunk-OSGLYW5J.js} +2 -2
- package/dist/{chunk-CE4ZOCOI.js → chunk-PFCMSSUF.js} +9 -9
- package/dist/{chunk-GFXZTHNI.js → chunk-PGRVXIMY.js} +5 -5
- package/dist/{chunk-JJPMWPB6.js → chunk-PISBFBJN.js} +6 -6
- package/dist/chunk-POOOIWE5.js +9 -0
- package/dist/{chunk-5RP3QTRL.js → chunk-PQNS3U7Z.js} +20 -3
- package/dist/{chunk-3E244UDA.js → chunk-QSTSJOCK.js} +52 -6
- package/dist/{chunk-YUPMXOGP.js → chunk-RFQMOKWK.js} +3 -3
- package/dist/chunk-RTOTNK3P.js +484 -0
- package/dist/{chunk-SN2DQRL6.js → chunk-RXKSDUAU.js} +7 -7
- package/dist/{chunk-T3KGZBU5.js → chunk-S45JPKQN.js} +189 -43
- package/dist/{chunk-YNCVSFGF.js → chunk-SAFHACFA.js} +4 -1
- package/dist/{chunk-KI4L2DUL.js → chunk-TCCHINEF.js} +114 -19
- package/dist/{chunk-AWNQ6ED6.js → chunk-THEGTRAG.js} +7 -2
- package/dist/{chunk-2KKODIQU.js → chunk-TR4LQE7L.js} +15 -15
- package/dist/{chunk-GPQ3NJPR.js → chunk-TWJMGXCI.js} +3 -3
- package/dist/{chunk-H72H4XDU.js → chunk-U45AXT5D.js} +9 -9
- package/dist/{chunk-62V2AFVG.js → chunk-U4FWBSO3.js} +160 -73
- package/dist/{chunk-A455ATZB.js → chunk-UBGDX6FI.js} +8 -1
- package/dist/chunk-UJILKYES.js +75 -0
- package/dist/{chunk-KSJ5UTP7.js → chunk-ULMAQV3T.js} +84 -4
- package/dist/{chunk-CB2HFKTP.js → chunk-VVBP6KGT.js} +1 -1
- package/dist/{chunk-RPOA7IAE.js → chunk-W5VITDM2.js} +43 -25
- package/dist/{chunk-QRTG4XTI.js → chunk-WAHQVNWT.js} +7 -1
- package/dist/{chunk-SQP64RCS.js → chunk-WGGYRBXH.js} +3019 -2976
- package/dist/{chunk-K3G6JBUC.js → chunk-WLYKU45P.js} +679 -141
- package/dist/chunk-WP7BVANJ.js +53 -0
- package/dist/{chunk-GAZEWQEZ.js → chunk-XF4VUMKW.js} +2 -2
- package/dist/{chunk-PAAWLLZX.js → chunk-Y5YXDM7D.js} +2 -2
- package/dist/{chunk-65RDIOR2.js → chunk-YOPAUVU4.js} +162 -44
- package/dist/{chunk-FFSX2QDQ.js → chunk-YPT4A46F.js} +3 -3
- package/dist/chunk-YWXT3JNM.js +425 -0
- package/dist/{chunk-EFEHDQGY.js → chunk-ZHZ2Y2DH.js} +64 -10
- package/dist/{chunk-CQQL4ZOQ.js → chunk-ZT7JNIGX.js} +4 -3
- package/dist/coordination/agent-monitor.d.ts +9 -1
- package/dist/coordination/agent-subagent-runner.d.ts +1 -1
- package/dist/coordination/agents/agent-prompts.d.ts +8 -0
- package/dist/coordination/agents/index.d.ts +33 -1
- package/dist/coordination/agents/index.js +52 -22
- package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
- package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
- package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
- package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
- package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
- package/dist/coordination/agents/types.d.ts +13 -2
- package/dist/coordination/brain-monitor.d.ts +30 -7
- package/dist/coordination/collab-debug.d.ts +1 -1
- package/dist/coordination/collab-pause.d.ts +1 -1
- package/dist/coordination/delegate-tool.d.ts +7 -5
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
- package/dist/coordination/director-options.d.ts +7 -0
- package/dist/coordination/director-spawn-model.d.ts +17 -0
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/director.d.ts +12 -0
- package/dist/coordination/dispatcher.d.ts +8 -0
- package/dist/coordination/fleet-bus.d.ts +35 -4
- package/dist/coordination/fleet-supervisor.d.ts +10 -2
- package/dist/coordination/fleet.d.ts +16 -7
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.js +86 -49
- package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
- package/dist/coordination/mailbox-http-router.d.ts +4 -3
- package/dist/coordination/mailbox-http-validation.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +15 -15
- package/dist/coordination/mailbox-types.d.ts +24 -6
- package/dist/coordination/model-tier-leader.d.ts +122 -0
- package/dist/coordination/model-tier.d.ts +122 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
- package/dist/coordination/origin-session.d.ts +16 -0
- package/dist/coordination/provider-status-tracker.d.ts +37 -5
- package/dist/coordination/session-note-hub.d.ts +44 -0
- package/dist/coordination/session-note-tool.d.ts +10 -0
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/context.d.ts +22 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +130 -38
- package/dist/core/request-conversation-binding.d.ts +37 -0
- package/dist/core/session-notes.d.ts +25 -0
- package/dist/core/system-prompt-builder.d.ts +11 -2
- package/dist/core/system-prompt-variants.d.ts +78 -0
- package/dist/design/index.js +3 -3
- package/dist/execution/auto-compaction-middleware.d.ts +11 -0
- package/dist/execution/autonomy-brain.d.ts +2 -1
- package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
- package/dist/execution/compaction-core.d.ts +4 -4
- package/dist/execution/eternal-autonomy.d.ts +1 -1
- package/dist/execution/index.js +138 -129
- package/dist/execution/model-runtime.d.ts +0 -7
- package/dist/execution/parallel-eternal-engine.d.ts +7 -7
- package/dist/extension/index.js +3 -3
- package/dist/goal/index.js +6 -6
- package/dist/hooks/index.js +3 -3
- package/dist/hq/index.js +14 -12
- package/dist/hq/persistence/event-log.d.ts +0 -1
- package/dist/hq/persistence.d.ts +4 -4
- package/dist/hq/publisher.d.ts +2 -0
- package/dist/index.d.ts +14 -11
- package/dist/index.js +728 -635
- package/dist/infrastructure/index.d.ts +4 -4
- package/dist/infrastructure/index.js +281 -260
- package/dist/infrastructure/token-counter.d.ts +9 -0
- package/dist/kernel/events/agent-events.d.ts +77 -3
- package/dist/kernel/events/brain-events.d.ts +12 -0
- package/dist/kernel/events/provider-events.d.ts +7 -1
- package/dist/kernel/events/session-events.d.ts +14 -0
- package/dist/kernel/events/tool-events.d.ts +13 -0
- package/dist/kernel/index.js +4 -4
- package/dist/mailbox-attach.d.ts +1 -1
- package/dist/models/index.js +10 -9
- package/dist/observability/event-bridge.d.ts +25 -5
- package/dist/observability/index.d.ts +9 -8
- package/dist/observability/index.js +459 -351
- package/dist/observability/tool-usage-source.d.ts +65 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +66 -50
- package/dist/plugins/auto-review-plugin.d.ts +3 -2
- package/dist/plugins/review-finding-commands.d.ts +0 -8
- package/dist/{review-finding-store-DKAV3AS6.js → plugins/review-finding-store.js} +7 -6
- package/dist/plugins/review-finding-types.js +19 -0
- package/dist/plugins/review-report-integration.js +21 -0
- package/dist/{review-report-store-OWY3EC3N.js → plugins/review-report-store.js} +7 -6
- package/dist/plugins/review-report-types.js +13 -0
- package/dist/prompts/index.js +5 -5
- package/dist/registry/index.js +3 -3
- package/dist/registry/tool-registry.d.ts +59 -9
- package/dist/{registry-R23UBE7H.js → registry-7N4O3E7W.js} +5 -5
- package/dist/security/capabilities.d.ts +6 -0
- package/dist/security/file-permissions.d.ts +1 -1
- package/dist/security/index.js +47 -33
- package/dist/security/permission-policy.d.ts +12 -1
- package/dist/security/secret-scrubber.d.ts +15 -0
- package/dist/session-catalog/index.d.ts +1 -0
- package/dist/session-catalog/index.js +19 -14
- package/dist/session-catalog/project-server.js +30 -13
- package/dist/session-catalog/protocol.d.ts +32 -1
- package/dist/session-catalog/registry.d.ts +17 -1
- package/dist/session-catalog/session-agents.d.ts +56 -0
- package/dist/session-catalog/store-rebuild.d.ts +14 -0
- package/dist/session-catalog/store-schema.d.ts +26 -0
- package/dist/session-catalog/store.d.ts +27 -1
- package/dist/session-note-attach.d.ts +9 -0
- package/dist/skills/index.js +5 -5
- package/dist/statusline/index.d.ts +43 -0
- package/dist/statusline/index.js +102 -0
- package/dist/storage/board-store-port.d.ts +2 -2
- package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
- package/dist/storage/config-loader.d.ts +3 -2
- package/dist/storage/file-session-writer.d.ts +4 -2
- package/dist/storage/goal-coordination.d.ts +6 -1
- package/dist/storage/goal-kanban.d.ts +4 -1
- package/dist/storage/index.d.ts +12 -4
- package/dist/storage/index.js +76 -61
- package/dist/storage/input-history-store.d.ts +2 -4
- package/dist/storage/memory-curator.d.ts +133 -0
- package/dist/storage/session-agent-attribution.d.ts +39 -0
- package/dist/storage/session-doctor.d.ts +109 -0
- package/dist/storage/session-event-bridge.d.ts +5 -7
- package/dist/storage/session-outcome.d.ts +42 -0
- package/dist/storage/session-recovery.d.ts +55 -0
- package/dist/storage/session-store/load-session-data.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +0 -2
- package/dist/storage/session-store/types.d.ts +8 -0
- package/dist/storage/session-store.d.ts +3 -3
- package/dist/storage/session-summary-tracker.d.ts +8 -1
- package/dist/storage/session-write-buffer.d.ts +18 -0
- package/dist/storage/session-writer-truncate.d.ts +2 -0
- package/dist/tasking/index.js +2 -2
- package/dist/tools/fallback-manage-tools.d.ts +6 -4
- package/dist/tools/index.js +21 -21
- package/dist/tools/model-tier-set-tool.d.ts +66 -0
- package/dist/types/agent-bridge.d.ts +2 -1
- package/dist/types/config/mcp-features.d.ts +7 -0
- package/dist/types/config/model-tiers.d.ts +148 -0
- package/dist/types/config/root.d.ts +9 -0
- package/dist/types/config/tools.d.ts +73 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/context.d.ts +2 -0
- package/dist/types/default-config.d.ts +6 -0
- package/dist/types/errors.d.ts +2 -1
- package/dist/types/index.d.ts +11 -9
- package/dist/types/index.js +25 -15
- package/dist/types/multi-agent.d.ts +43 -0
- package/dist/types/plugin.d.ts +1 -1
- package/dist/types/provider.d.ts +15 -0
- package/dist/types/runtime-capability-manifest.d.ts +1 -1
- package/dist/types/secret-scrubber.d.ts +15 -0
- package/dist/types/session-markers.d.ts +37 -10
- package/dist/types/session-markers.js +1 -1
- package/dist/types/session-timeline.d.ts +197 -0
- package/dist/types/session-timeline.js +12 -0
- package/dist/types/session.d.ts +172 -11
- package/dist/types/system-prompt.d.ts +27 -0
- package/dist/utils/config-backup.d.ts +9 -1
- package/dist/utils/heap-watchdog.js +2 -2
- package/dist/utils/index.js +50 -48
- package/dist/utils/message-invariants.d.ts +4 -1
- package/dist/utils/regex-guard.d.ts +1 -1
- package/dist/utils/socket-path.d.ts +1 -1
- package/dist/utils/token-estimate.d.ts +6 -2
- package/dist/utils/wstack-paths.d.ts +2 -0
- package/dist/wiring/proxy-rewrite.d.ts +28 -0
- package/dist/wiring/proxy-rewrite.js +7 -1
- package/dist/worktree/index.js +2 -2
- package/instructions/agents/explore-companion.md +45 -18
- package/instructions/agents/memory-curator.md +34 -22
- package/instructions/coordination/director-preamble.md +12 -7
- package/instructions/coordination/subagent-baseline.md +26 -5
- package/instructions/leader-after-task.md +4 -1
- package/instructions/llm/chimera-review.md +30 -9
- package/instructions/llm/memory-curator.md +31 -0
- package/instructions/sections/tool/delegation-compact.md +2 -0
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/sections/tool/mailbox-compact.md +5 -1
- package/instructions/sections/tool/mailbox-full.md +3 -0
- package/instructions/sections/tool/session-note-compact.md +3 -0
- package/instructions/sections/tool/session-note-full.md +15 -0
- package/instructions/system-lite.md +9 -4
- package/instructions/system-pro.md +66 -143
- package/instructions/system.md +73 -175
- package/package.json +32 -4
- package/dist/chunk-2BUUIMQB.js +0 -225
- package/dist/chunk-FAYRX2AD.js +0 -13
- package/dist/chunk-H5E77KUJ.js +0 -15
- package/dist/chunk-M7BS2K6U.js +0 -1
- package/dist/coordination/agents.d.ts +0 -8
- package/dist/coordination/transport.d.ts +0 -2
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DefaultSessionStore,
|
|
3
3
|
DirectorStateCheckpoint
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NSOFPKGT.js";
|
|
5
5
|
import {
|
|
6
|
+
applyTierToSubagentConfig,
|
|
7
|
+
classifyTier,
|
|
8
|
+
listTierIds,
|
|
6
9
|
resolveModelMatrixResolution,
|
|
10
|
+
resolveTier,
|
|
7
11
|
roleNeedsIndependentReviewModel
|
|
8
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-RTOTNK3P.js";
|
|
9
13
|
import {
|
|
10
14
|
DefaultMultiAgentCoordinator,
|
|
11
15
|
FLEET_ROSTER_BUDGETS,
|
|
@@ -14,23 +18,34 @@ import {
|
|
|
14
18
|
dispatchAgent,
|
|
15
19
|
formatSubagentStructuredReport,
|
|
16
20
|
nicknameKeyFromDisplay
|
|
17
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-S45JPKQN.js";
|
|
18
22
|
import {
|
|
19
23
|
getAgentDefinition
|
|
20
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-FCBTO76Z.js";
|
|
25
|
+
import {
|
|
26
|
+
expandGlob
|
|
27
|
+
} from "./chunk-J2WRWR37.js";
|
|
28
|
+
import {
|
|
29
|
+
safeParse,
|
|
30
|
+
safeStringify
|
|
31
|
+
} from "./chunk-W5VITDM2.js";
|
|
32
|
+
import {
|
|
33
|
+
readBundledInstructionText
|
|
34
|
+
} from "./chunk-5S437PRC.js";
|
|
21
35
|
import {
|
|
22
36
|
applyMailboxSendPolicy,
|
|
23
37
|
defaultResolveProjectDir,
|
|
24
38
|
getSharedProjectMailbox,
|
|
39
|
+
postSessionNote,
|
|
25
40
|
resolveMailboxIdentity
|
|
26
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-JO4MGB6Z.js";
|
|
27
42
|
import {
|
|
28
43
|
MAILBOX_MAX_ACK_BATCH,
|
|
29
44
|
MAILBOX_MAX_QUERY_LIMIT,
|
|
30
45
|
UNREAD_CHECK_MIN_INTERVAL_MS,
|
|
31
46
|
resolveSendType,
|
|
32
47
|
resolveSendTypeSafe
|
|
33
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-IRUTEHCE.js";
|
|
34
49
|
import {
|
|
35
50
|
MAILBOX_TYPE_PROPERTIES,
|
|
36
51
|
hasMailboxCapability,
|
|
@@ -41,57 +56,51 @@ import {
|
|
|
41
56
|
mailboxIdentityBase,
|
|
42
57
|
normalizeRecipient,
|
|
43
58
|
sessionRecipient
|
|
44
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-FWNLEZYG.js";
|
|
45
60
|
import {
|
|
46
61
|
scrubErrorText
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import {
|
|
49
|
-
ToolCapabilities
|
|
50
|
-
} from "./chunk-A455ATZB.js";
|
|
62
|
+
} from "./chunk-Y5YXDM7D.js";
|
|
51
63
|
import {
|
|
52
64
|
SECRET_FILE_MODE
|
|
53
|
-
} from "./chunk-
|
|
54
|
-
import {
|
|
55
|
-
QUOTA_EXHAUSTED_RE,
|
|
56
|
-
ROUTE_SCOPED_QUOTA_RE,
|
|
57
|
-
parseResetHintMs
|
|
58
|
-
} from "./chunk-NFAYHPRH.js";
|
|
59
|
-
import {
|
|
60
|
-
expandGlob
|
|
61
|
-
} from "./chunk-TYW2GI5I.js";
|
|
65
|
+
} from "./chunk-6FHWXLPT.js";
|
|
62
66
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} from "./chunk-RPOA7IAE.js";
|
|
66
|
-
import {
|
|
67
|
-
readBundledInstructionText
|
|
68
|
-
} from "./chunk-5S437PRC.js";
|
|
67
|
+
ToolCapabilities
|
|
68
|
+
} from "./chunk-UBGDX6FI.js";
|
|
69
69
|
import {
|
|
70
70
|
renderInstructionLayer
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-M27LIEKV.js";
|
|
72
72
|
import {
|
|
73
73
|
buildChildEnv
|
|
74
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-6NWEVFEL.js";
|
|
75
|
+
import {
|
|
76
|
+
resolveOwningSessionId
|
|
77
|
+
} from "./chunk-ZHZ2Y2DH.js";
|
|
75
78
|
import {
|
|
76
79
|
isPidAlive
|
|
77
80
|
} from "./chunk-TRT2HRWZ.js";
|
|
78
81
|
import {
|
|
79
82
|
atomicWrite,
|
|
80
83
|
withFileLock
|
|
81
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-OSGLYW5J.js";
|
|
82
85
|
import {
|
|
83
86
|
resolveWstackPaths
|
|
84
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-SAFHACFA.js";
|
|
88
|
+
import {
|
|
89
|
+
MAX_RESET_HINT_MS,
|
|
90
|
+
QUOTA_EXHAUSTED_RE,
|
|
91
|
+
ROUTE_SCOPED_QUOTA_RE,
|
|
92
|
+
parseResetHintMs
|
|
93
|
+
} from "./chunk-3ECENVHL.js";
|
|
94
|
+
import {
|
|
95
|
+
expectDefined
|
|
96
|
+
} from "./chunk-PFZO3C6H.js";
|
|
85
97
|
import {
|
|
86
98
|
AgentError,
|
|
87
99
|
ERROR_CODES
|
|
88
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-25LSLC3G.js";
|
|
89
101
|
import {
|
|
90
102
|
toErrorMessage
|
|
91
103
|
} from "./chunk-TBRL2RCC.js";
|
|
92
|
-
import {
|
|
93
|
-
expectDefined
|
|
94
|
-
} from "./chunk-PFZO3C6H.js";
|
|
95
104
|
|
|
96
105
|
// src/coordination/agent-bridge.ts
|
|
97
106
|
import { randomUUID } from "node:crypto";
|
|
@@ -302,6 +311,10 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
302
311
|
Math.ceil(limit * (1 + factor)),
|
|
303
312
|
ceiling.timeoutMs ?? DEFAULT_CEILING.timeoutMs
|
|
304
313
|
);
|
|
314
|
+
if (next2 <= limit) {
|
|
315
|
+
deny();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
305
318
|
extend({ timeoutMs: next2 });
|
|
306
319
|
} else {
|
|
307
320
|
deny();
|
|
@@ -317,6 +330,10 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
317
330
|
const field = FIELD_BY_KIND[kind];
|
|
318
331
|
const cap = ceiling[field] ?? DEFAULT_CEILING[field];
|
|
319
332
|
const next = Math.min(Math.ceil(limit * (1 + factor)), cap);
|
|
333
|
+
if (next <= limit) {
|
|
334
|
+
deny();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
320
337
|
extend({ [field]: next });
|
|
321
338
|
})
|
|
322
339
|
];
|
|
@@ -341,20 +358,16 @@ function editedPath(input) {
|
|
|
341
358
|
const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
|
|
342
359
|
return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
|
|
343
360
|
}
|
|
361
|
+
var UNATTRIBUTED_SESSION = "";
|
|
362
|
+
var MAX_TRACKED_SESSIONS = 16;
|
|
344
363
|
var BrainMonitor = class {
|
|
345
364
|
constructor(opts) {
|
|
346
365
|
this.opts = opts;
|
|
347
366
|
this.applyTunables(opts);
|
|
348
367
|
}
|
|
349
368
|
opts;
|
|
350
|
-
|
|
351
|
-
errorTimestamps = [];
|
|
352
|
-
editTimestamps = /* @__PURE__ */ new Map();
|
|
353
|
-
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
369
|
+
bySession = /* @__PURE__ */ new Map();
|
|
354
370
|
unsubscribers = [];
|
|
355
|
-
engaging = false;
|
|
356
|
-
activeRuns = 0;
|
|
357
|
-
lastProgressAt = 0;
|
|
358
371
|
stallTimer;
|
|
359
372
|
// Mutable, not readonly: `reconfigure()` re-applies these live. Every knob
|
|
360
373
|
// on the Brain except these used to be live-editable, so `/brain monitor …`
|
|
@@ -385,6 +398,69 @@ var BrainMonitor = class {
|
|
|
385
398
|
const id = this.opts.leaderSessionId;
|
|
386
399
|
return typeof id === "function" ? id() : id;
|
|
387
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The signal state for one session, created on first sight.
|
|
403
|
+
*
|
|
404
|
+
* A host that pins `leaderSessionId` (the CLI, one session per process) only
|
|
405
|
+
* ever reaches one bucket. A host that does not (the WebUI, four tabs on one
|
|
406
|
+
* runtime) gets one bucket per tab, which is the point.
|
|
407
|
+
*/
|
|
408
|
+
stateFor(sessionId) {
|
|
409
|
+
const key = sessionId || UNATTRIBUTED_SESSION;
|
|
410
|
+
let state = this.bySession.get(key);
|
|
411
|
+
if (!state) {
|
|
412
|
+
this.pruneIdleSessions();
|
|
413
|
+
state = {
|
|
414
|
+
failStreaks: /* @__PURE__ */ new Map(),
|
|
415
|
+
errorTimestamps: [],
|
|
416
|
+
editTimestamps: /* @__PURE__ */ new Map(),
|
|
417
|
+
lastEngagedAt: /* @__PURE__ */ new Map(),
|
|
418
|
+
engaging: false,
|
|
419
|
+
activeRuns: 0,
|
|
420
|
+
lastProgressAt: 0,
|
|
421
|
+
touchedAt: Date.now()
|
|
422
|
+
};
|
|
423
|
+
this.bySession.set(key, state);
|
|
424
|
+
}
|
|
425
|
+
state.touchedAt = Date.now();
|
|
426
|
+
return state;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Drop buckets for sessions that have been quiet longer than any signal
|
|
430
|
+
* window. Without this a long-lived host accumulates one bucket per session
|
|
431
|
+
* it has ever seen — cheap individually, unbounded in aggregate.
|
|
432
|
+
*
|
|
433
|
+
* Cooldowns are the one thing worth losing sleep over here: pruning a bucket
|
|
434
|
+
* resets its rate limit. That is acceptable only because the prune horizon is
|
|
435
|
+
* strictly longer than the cooldown, so a pruned session could have engaged
|
|
436
|
+
* again anyway.
|
|
437
|
+
*/
|
|
438
|
+
pruneIdleSessions() {
|
|
439
|
+
if (this.bySession.size < MAX_TRACKED_SESSIONS) return;
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
const horizon = Math.max(
|
|
442
|
+
this.fileChurnWindowMs,
|
|
443
|
+
this.errorStormWindowMs,
|
|
444
|
+
this.stallMs,
|
|
445
|
+
this.cooldownMs
|
|
446
|
+
);
|
|
447
|
+
for (const [key, state] of this.bySession) {
|
|
448
|
+
if (state.engaging) continue;
|
|
449
|
+
if (now - state.touchedAt > horizon) this.bySession.delete(key);
|
|
450
|
+
}
|
|
451
|
+
if (this.bySession.size >= MAX_TRACKED_SESSIONS) {
|
|
452
|
+
let oldestKey;
|
|
453
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
454
|
+
for (const [key, state] of this.bySession) {
|
|
455
|
+
if (state.engaging) continue;
|
|
456
|
+
if (state.touchedAt < oldestAt) {
|
|
457
|
+
oldestAt = state.touchedAt;
|
|
458
|
+
oldestKey = key;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (oldestKey !== void 0) this.bySession.delete(oldestKey);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
388
464
|
/** Resolve the live-tunable subset onto the instance. Shared by the constructor and `reconfigure`. */
|
|
389
465
|
applyTunables(next) {
|
|
390
466
|
this.toolFailureStreak = next.toolFailureStreak ?? 3;
|
|
@@ -469,18 +545,19 @@ var BrainMonitor = class {
|
|
|
469
545
|
this.opts.events.on("tool.executed", (e) => {
|
|
470
546
|
const leaderSid = this.resolveLeaderSessionId();
|
|
471
547
|
if (leaderSid && e.sessionId && e.sessionId !== leaderSid) return;
|
|
472
|
-
|
|
473
|
-
|
|
548
|
+
const state = this.stateFor(e.sessionId);
|
|
549
|
+
state.lastProgressAt = Date.now();
|
|
550
|
+
this.trackFileChurn(state, e.sessionId, e.name, e.ok, e.input);
|
|
474
551
|
if (!this.signals.toolFailureStreak) return;
|
|
475
552
|
if (e.ok) {
|
|
476
|
-
|
|
553
|
+
state.failStreaks.delete(e.name);
|
|
477
554
|
return;
|
|
478
555
|
}
|
|
479
|
-
const streak = (
|
|
480
|
-
|
|
556
|
+
const streak = (state.failStreaks.get(e.name) ?? 0) + 1;
|
|
557
|
+
state.failStreaks.set(e.name, streak);
|
|
481
558
|
if (streak >= this.toolFailureStreak) {
|
|
482
|
-
|
|
483
|
-
void this.engage("tool_failure_streak", {
|
|
559
|
+
state.failStreaks.delete(e.name);
|
|
560
|
+
void this.engage("tool_failure_streak", e.sessionId, {
|
|
484
561
|
question: `The tool "${e.name}" has failed ${streak} times in a row. Should the agent be steered to a different approach?`,
|
|
485
562
|
context: [
|
|
486
563
|
`Tool: ${e.name}`,
|
|
@@ -496,8 +573,9 @@ var BrainMonitor = class {
|
|
|
496
573
|
this.opts.events.on("agent.run.started", (e) => {
|
|
497
574
|
const lsid = this.resolveLeaderSessionId();
|
|
498
575
|
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
499
|
-
this.
|
|
500
|
-
|
|
576
|
+
const state = this.stateFor(e.sessionId);
|
|
577
|
+
state.activeRuns += 1;
|
|
578
|
+
state.lastProgressAt = Date.now();
|
|
501
579
|
}),
|
|
502
580
|
// `agent.run.completed` is the only terminator. `Agent.run` emits it on
|
|
503
581
|
// every exit path — the success path and, unconditionally, the catch
|
|
@@ -512,27 +590,30 @@ var BrainMonitor = class {
|
|
|
512
590
|
this.opts.events.on("agent.run.completed", (e) => {
|
|
513
591
|
const lsid = this.resolveLeaderSessionId();
|
|
514
592
|
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
515
|
-
|
|
593
|
+
const state = this.stateFor(e.sessionId);
|
|
594
|
+
state.activeRuns = Math.max(0, state.activeRuns - 1);
|
|
516
595
|
}),
|
|
517
596
|
this.opts.events.on("iteration.started", (e) => {
|
|
518
597
|
const lsid = this.resolveLeaderSessionId();
|
|
519
598
|
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
520
|
-
this.lastProgressAt = Date.now();
|
|
599
|
+
this.stateFor(e.sessionId).lastProgressAt = Date.now();
|
|
521
600
|
})
|
|
522
601
|
);
|
|
523
602
|
this.stallTimer = setInterval(() => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
603
|
+
for (const [key, state] of this.bySession) {
|
|
604
|
+
if (state.activeRuns === 0 || state.lastProgressAt === 0) continue;
|
|
605
|
+
const idleMs = Date.now() - state.lastProgressAt;
|
|
606
|
+
if (idleMs < this.stallMs) continue;
|
|
607
|
+
state.lastProgressAt = Date.now();
|
|
608
|
+
void this.engage("agent_stall", key || void 0, {
|
|
609
|
+
question: `An active run has made no observable progress (no tool call or iteration) for ${Math.round(idleMs / 6e4)} minute(s). Should the agent be steered before more time is wasted?`,
|
|
610
|
+
context: [
|
|
611
|
+
`Active runs: ${state.activeRuns}`,
|
|
612
|
+
`Idle for: ${Math.round(idleMs / 1e3)}s`,
|
|
613
|
+
`Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
|
|
614
|
+
].join("\n")
|
|
615
|
+
});
|
|
616
|
+
}
|
|
536
617
|
}, this.stallCheckIntervalMs);
|
|
537
618
|
this.stallTimer.unref?.();
|
|
538
619
|
}
|
|
@@ -542,15 +623,16 @@ var BrainMonitor = class {
|
|
|
542
623
|
const lsid = this.resolveLeaderSessionId();
|
|
543
624
|
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
544
625
|
const now = Date.now();
|
|
545
|
-
this.
|
|
546
|
-
|
|
626
|
+
const state = this.stateFor(e.sessionId);
|
|
627
|
+
state.errorTimestamps.push(now);
|
|
628
|
+
state.errorTimestamps = state.errorTimestamps.filter(
|
|
547
629
|
(t) => now - t <= this.errorStormWindowMs
|
|
548
630
|
);
|
|
549
|
-
if (
|
|
550
|
-
const count =
|
|
551
|
-
|
|
631
|
+
if (state.errorTimestamps.length >= this.errorStormCount) {
|
|
632
|
+
const count = state.errorTimestamps.length;
|
|
633
|
+
state.errorTimestamps = [];
|
|
552
634
|
const message = e.err instanceof Error ? e.err.message : String(e.err);
|
|
553
|
-
void this.engage("error_storm", {
|
|
635
|
+
void this.engage("error_storm", e.sessionId, {
|
|
554
636
|
question: `${count} errors occurred within ${Math.round(this.errorStormWindowMs / 1e3)}s (phase: ${e.phase}). Should the agent be steered before more work is wasted?`,
|
|
555
637
|
context: `Latest error: ${message.slice(0, 400)}`
|
|
556
638
|
});
|
|
@@ -572,30 +654,32 @@ var BrainMonitor = class {
|
|
|
572
654
|
this.running = false;
|
|
573
655
|
for (const off of this.unsubscribers) off();
|
|
574
656
|
this.unsubscribers.length = 0;
|
|
575
|
-
this.
|
|
576
|
-
|
|
577
|
-
|
|
657
|
+
for (const state of this.bySession.values()) {
|
|
658
|
+
state.failStreaks.clear();
|
|
659
|
+
state.errorTimestamps = [];
|
|
660
|
+
state.editTimestamps.clear();
|
|
661
|
+
state.activeRuns = 0;
|
|
662
|
+
state.lastProgressAt = 0;
|
|
663
|
+
}
|
|
578
664
|
if (this.stallTimer) {
|
|
579
665
|
clearInterval(this.stallTimer);
|
|
580
666
|
this.stallTimer = void 0;
|
|
581
667
|
}
|
|
582
|
-
this.activeRuns = 0;
|
|
583
|
-
this.lastProgressAt = 0;
|
|
584
668
|
}
|
|
585
669
|
/** Sliding-window count of successful edits per file → churn signal. */
|
|
586
|
-
trackFileChurn(toolName, ok, input) {
|
|
670
|
+
trackFileChurn(state, sessionId, toolName, ok, input) {
|
|
587
671
|
if (!this.signals.fileChurn) return;
|
|
588
672
|
if (!ok || !this.fileEditTools.has(toolName.toLowerCase())) return;
|
|
589
673
|
const path12 = editedPath(input);
|
|
590
674
|
if (!path12) return;
|
|
591
675
|
const now = Date.now();
|
|
592
|
-
const stamps = (
|
|
676
|
+
const stamps = (state.editTimestamps.get(path12) ?? []).filter(
|
|
593
677
|
(t) => now - t <= this.fileChurnWindowMs
|
|
594
678
|
);
|
|
595
679
|
stamps.push(now);
|
|
596
680
|
if (stamps.length >= this.fileChurnThreshold) {
|
|
597
|
-
|
|
598
|
-
void this.engage("file_churn", {
|
|
681
|
+
state.editTimestamps.delete(path12);
|
|
682
|
+
void this.engage("file_churn", sessionId, {
|
|
599
683
|
question: `The file "${path12}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
|
|
600
684
|
context: [
|
|
601
685
|
`File: ${path12}`,
|
|
@@ -605,28 +689,32 @@ var BrainMonitor = class {
|
|
|
605
689
|
});
|
|
606
690
|
return;
|
|
607
691
|
}
|
|
608
|
-
if (
|
|
609
|
-
for (const [p, times] of
|
|
692
|
+
if (state.editTimestamps.size >= 500 && !state.editTimestamps.has(path12)) {
|
|
693
|
+
for (const [p, times] of state.editTimestamps) {
|
|
610
694
|
if (times.every((t) => now - t > this.fileChurnWindowMs)) {
|
|
611
|
-
|
|
695
|
+
state.editTimestamps.delete(p);
|
|
612
696
|
}
|
|
613
697
|
}
|
|
614
|
-
if (
|
|
615
|
-
const oldest =
|
|
616
|
-
if (oldest !== void 0)
|
|
698
|
+
if (state.editTimestamps.size >= 500) {
|
|
699
|
+
const oldest = state.editTimestamps.keys().next().value;
|
|
700
|
+
if (oldest !== void 0) state.editTimestamps.delete(oldest);
|
|
617
701
|
}
|
|
618
702
|
}
|
|
619
|
-
|
|
703
|
+
state.editTimestamps.set(path12, stamps);
|
|
620
704
|
}
|
|
621
|
-
async engage(kind, input) {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
705
|
+
async engage(kind, sessionId, input) {
|
|
706
|
+
const state = this.stateFor(sessionId);
|
|
707
|
+
const last = state.lastEngagedAt.get(kind) ?? 0;
|
|
708
|
+
if (state.engaging || Date.now() - last < this.cooldownMs) return;
|
|
709
|
+
state.engaging = true;
|
|
710
|
+
state.lastEngagedAt.set(kind, Date.now());
|
|
626
711
|
try {
|
|
627
712
|
const request = {
|
|
628
713
|
id: `brainmon-${randomUUID2()}`,
|
|
629
|
-
|
|
714
|
+
// The session whose evidence triggered this — NOT the host's current
|
|
715
|
+
// one. They differ the moment two sessions run under one host, and
|
|
716
|
+
// taking the host's sent the steer to the wrong leader.
|
|
717
|
+
sessionId: sessionId ?? this.opts.sessionId?.(),
|
|
630
718
|
source: "system",
|
|
631
719
|
question: input.question,
|
|
632
720
|
context: input.context,
|
|
@@ -684,7 +772,7 @@ var BrainMonitor = class {
|
|
|
684
772
|
});
|
|
685
773
|
} catch {
|
|
686
774
|
} finally {
|
|
687
|
-
|
|
775
|
+
state.engaging = false;
|
|
688
776
|
}
|
|
689
777
|
}
|
|
690
778
|
async maybeIntervene(kind, request, decision) {
|
|
@@ -695,6 +783,7 @@ var BrainMonitor = class {
|
|
|
695
783
|
const guidance = decision.rationale?.trim() || decision.text.trim();
|
|
696
784
|
try {
|
|
697
785
|
await this.opts.intervene({
|
|
786
|
+
sessionId: request.sessionId,
|
|
698
787
|
subject: `Brain intervention: ${kind.replace(/_/g, " ")}`,
|
|
699
788
|
body: [
|
|
700
789
|
`The Brain engaged after detecting: ${request.question}`,
|
|
@@ -1846,10 +1935,10 @@ async function compactLog(opts, keepCount = 2e3) {
|
|
|
1846
1935
|
const archived = log.entries.slice(0, log.entries.length - keepCount);
|
|
1847
1936
|
const kept = log.entries.slice(-keepCount);
|
|
1848
1937
|
const archivePath = path.join(opts.storageDir, `file-authors-archive-${Date.now()}.json`);
|
|
1849
|
-
await
|
|
1938
|
+
await atomicWrite(
|
|
1850
1939
|
archivePath,
|
|
1851
|
-
JSON.stringify({ projectRoot: opts.projectRoot, entries: archived }, null, 2)
|
|
1852
|
-
|
|
1940
|
+
`${JSON.stringify({ projectRoot: opts.projectRoot, entries: archived }, null, 2)}
|
|
1941
|
+
`
|
|
1853
1942
|
);
|
|
1854
1943
|
log.entries = kept;
|
|
1855
1944
|
log.lastCompactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2034,6 +2123,16 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
2034
2123
|
import * as fsp2 from "node:fs/promises";
|
|
2035
2124
|
import * as path3 from "node:path";
|
|
2036
2125
|
|
|
2126
|
+
// src/coordination/origin-session.ts
|
|
2127
|
+
function callerSessionId(ctx, fallback) {
|
|
2128
|
+
const c = ctx;
|
|
2129
|
+
const pinned = c?.activeRunSessionId;
|
|
2130
|
+
if (typeof pinned === "string" && pinned.length > 0) return pinned;
|
|
2131
|
+
const live = c?.session?.id;
|
|
2132
|
+
if (typeof live === "string" && live.length > 0) return live;
|
|
2133
|
+
return fallback;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2037
2136
|
// src/coordination/task-boundary.ts
|
|
2038
2137
|
var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
|
|
2039
2138
|
"n/a",
|
|
@@ -2112,6 +2211,14 @@ var taskBoundarySchemaProperties = {
|
|
|
2112
2211
|
function createDelegateTool(opts) {
|
|
2113
2212
|
const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
|
|
2114
2213
|
const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
|
|
2214
|
+
const emitDelegateCompleted = (payload) => {
|
|
2215
|
+
opts.events?.emit("delegate.completed", payload);
|
|
2216
|
+
opts.events?.emit("subagent.done", {
|
|
2217
|
+
sessionId: payload.sessionId,
|
|
2218
|
+
summary: payload.summary,
|
|
2219
|
+
ok: payload.ok
|
|
2220
|
+
});
|
|
2221
|
+
};
|
|
2115
2222
|
const inputSchema = {
|
|
2116
2223
|
type: "object",
|
|
2117
2224
|
properties: {
|
|
@@ -2136,6 +2243,10 @@ function createDelegateTool(opts) {
|
|
|
2136
2243
|
type: "string",
|
|
2137
2244
|
description: "Model id within the provider. Defaults to host model."
|
|
2138
2245
|
},
|
|
2246
|
+
tier: {
|
|
2247
|
+
type: "string",
|
|
2248
|
+
description: "Cost/capability level for this worker: 'budget' (cheap + fast, for mechanical or well-specified work), 'standard' (the default), or 'premium' (expensive + most capable, for work where being wrong is costly). Resolved deterministically into a model, a failover chain, and a spend budget from `modelTiers` config, so you do NOT need to know any model id. Omit to let the routing table decide by role. An explicit `model` always wins over the tier.\n\nA CHEAPER TIER DOES NOT MAKE `delegate` CHEAPER FOR YOU. The leader stays fully blocked for the entire run either way \u2014 a cheap model is usually a SLOWER one, so `tier: 'budget'` on `delegate` can cost more of your wall-clock than it saves in dollars. Tier is about the worker's spend; `delegate` vs `spawn_subagent` is about your own time, and they are separate decisions. If the job is anything but short, pass the tier to `spawn_subagent` instead and keep working."
|
|
2249
|
+
},
|
|
2139
2250
|
systemPromptOverride: {
|
|
2140
2251
|
type: "string",
|
|
2141
2252
|
description: "Extra prompt text appended to the role baseline."
|
|
@@ -2181,7 +2292,7 @@ function createDelegateTool(opts) {
|
|
|
2181
2292
|
};
|
|
2182
2293
|
return {
|
|
2183
2294
|
name: "delegate",
|
|
2184
|
-
description: "Hand a piece of work to a subagent and block until it returns. This call is synchronous: the leader's iteration pauses for the full duration of the subagent's run. (Multiple `delegate` calls fired in the same assistant turn still parallelize through the provider's parallel-tool-call surface, but each one eats wall-clock time \u2014 so for fan-out you actually control, reach for the async path below.) Use `delegate` when your next step genuinely needs the subagent's verdict \u2014 a review, a fact-check, a sign-off. Has own context, own LLM call, auto-extending budget, and a partial-completion handoff path (maxHandoffs, default 1). Workers cannot recursively spawn.\n\n**Do NOT use `delegate` for long-running work.** While `delegate` is in flight, the leader is fully blocked \u2014 it cannot act on other tools, read mail, or react to the user. If the work might run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), the blocking call wastes the leader's time. Use the async tool family instead: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then `await_tasks` to retrieve results later. The leader keeps doing other work while the worker churns, and a worker that realizes its task will run long can
|
|
2295
|
+
description: "Hand a piece of work to a subagent and block until it returns. This call is synchronous: the leader's iteration pauses for the full duration of the subagent's run. (Multiple `delegate` calls fired in the same assistant turn still parallelize through the provider's parallel-tool-call surface, but each one eats wall-clock time \u2014 so for fan-out you actually control, reach for the async path below.) Use `delegate` when your next step genuinely needs the subagent's verdict \u2014 a review, a fact-check, a sign-off. Has own context, own LLM call, auto-extending budget, and a partial-completion handoff path (maxHandoffs, default 1). Workers cannot recursively spawn.\n\n**Do NOT use `delegate` for long-running work.** While `delegate` is in flight, the leader is fully blocked \u2014 it cannot act on other tools, read mail, or react to the user. If the work might run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), the blocking call wastes the leader's time. Use the async tool family instead: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then `await_tasks` to retrieve results later. The leader keeps doing other work while the worker churns, and a worker that realizes its task will run long can tell the leader (type `steer` or `ask` via `session_note`, otherwise `mail_send`) saying *\"my task is going to run long, please spawn a subagent instead\"* so the leader re-dispatches asynchronously instead of waiting.\n\n**Do NOT use `delegate` for fan-out you control.** Multiple sequential `delegate` calls each block the leader, wasting wall-clock time. For independent investigations you want to run in parallel \u2014 security scan + bug hunt + perf review on the same PR \u2014 use the async tool family: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then the `await_tasks` tool with `{mode: 'any'}` to fold the first useful result into the next decision while the rest keep churning. Reach for `delegate` only when the result gates your next move AND the work is short enough that blocking the leader is acceptable.",
|
|
2185
2296
|
usageHint: "Set `task` to the objective, then make the edges explicit: `scope` (what the work covers) and `outOfScope` (at least one concrete non-goal) are REQUIRED \u2014 the call is rejected without them, and the worker treats the rendered boundary block as a hard contract. Pick `role` from roster or pass `name` for free-form. Reach for `delegate` only when the result gates your next move AND the work is short enough that blocking the leader is acceptable (minutes, not hours). For long-running work or fan-out you control, use `spawn_subagent` + `assign_task` + `await_tasks` instead. Raise `maxHandoffs` (default 1, cap 8) for multi-day or multi-refactor tasks; pass larger `timeoutMs`/`maxIterations`/`maxToolCalls` only when needed.",
|
|
2186
2297
|
permission: "auto",
|
|
2187
2298
|
mutating: false,
|
|
@@ -2189,7 +2300,7 @@ function createDelegateTool(opts) {
|
|
|
2189
2300
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
2190
2301
|
inputSchema,
|
|
2191
2302
|
async execute(input, _ctx, execOpts) {
|
|
2192
|
-
const sessionId = opts.directorRunId;
|
|
2303
|
+
const sessionId = callerSessionId(_ctx, opts.directorRunId) ?? opts.directorRunId;
|
|
2193
2304
|
const abortSignal = execOpts?.signal;
|
|
2194
2305
|
const i = input ?? {};
|
|
2195
2306
|
if (typeof i.task !== "string" || !i.task.trim()) {
|
|
@@ -2214,7 +2325,7 @@ function createDelegateTool(opts) {
|
|
|
2214
2325
|
const launchModePreface = [
|
|
2215
2326
|
"Launch-mode guidance (delegate): you were launched via the synchronous `delegate` tool, so the leader is blocked on this call for the full duration of your run.",
|
|
2216
2327
|
"If, after inspecting the task, you judge it will run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), do NOT silently grind through it under the blocking call.",
|
|
2217
|
-
'Escalate to the leader
|
|
2328
|
+
'Escalate to the leader with `session_note to="leader"` when that tool is registered (same-session, next iteration). Fall back to `mail_send` or `mailbox action=send` only for cross-session mail. Send a `steer` or `ask`, e.g. *"my task is going to run long, please spawn a subagent instead"*, so the leader can re-dispatch asynchronously via `spawn_subagent` + `assign_task`.',
|
|
2218
2329
|
'Then return a clean checkpoint with `completion:"partial"` and a concrete `remaining_work`.',
|
|
2219
2330
|
"If the task is short and bounded, just do it end-to-end \u2014 do not over-trigger the escalation for normal work."
|
|
2220
2331
|
].join("\n\n");
|
|
@@ -2258,6 +2369,13 @@ function createDelegateTool(opts) {
|
|
|
2258
2369
|
};
|
|
2259
2370
|
cfg = applyRosterBudget({ ...cfg, name: i.name });
|
|
2260
2371
|
}
|
|
2372
|
+
if (i.tier) cfg.tier = i.tier;
|
|
2373
|
+
const budgetPins = [];
|
|
2374
|
+
if (typeof i.maxIterations === "number") budgetPins.push("maxIterations");
|
|
2375
|
+
if (typeof i.maxToolCalls === "number") budgetPins.push("maxToolCalls");
|
|
2376
|
+
if (typeof i.maxTokens === "number") budgetPins.push("maxTokens");
|
|
2377
|
+
if (typeof i.maxCostUsd === "number") budgetPins.push("maxCostUsd");
|
|
2378
|
+
if (budgetPins.length) cfg.budgetPins = budgetPins;
|
|
2261
2379
|
if (typeof i.maxIterations === "number") {
|
|
2262
2380
|
cfg.maxIterations = i.maxIterations;
|
|
2263
2381
|
}
|
|
@@ -2301,7 +2419,7 @@ function createDelegateTool(opts) {
|
|
|
2301
2419
|
systemPromptOverride: segments.join("\n\n")
|
|
2302
2420
|
};
|
|
2303
2421
|
})();
|
|
2304
|
-
const subagentId = await dir.spawn(attemptConfig);
|
|
2422
|
+
const subagentId = await dir.spawn({ ...attemptConfig, originSessionId: sessionId });
|
|
2305
2423
|
if (handoffCount === 0) {
|
|
2306
2424
|
opts.events?.emit("delegate.started", {
|
|
2307
2425
|
sessionId,
|
|
@@ -2329,7 +2447,7 @@ function createDelegateTool(opts) {
|
|
|
2329
2447
|
} catch {
|
|
2330
2448
|
}
|
|
2331
2449
|
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
2332
|
-
|
|
2450
|
+
emitDelegateCompleted({
|
|
2333
2451
|
sessionId,
|
|
2334
2452
|
target,
|
|
2335
2453
|
task: i.task,
|
|
@@ -2357,7 +2475,7 @@ function createDelegateTool(opts) {
|
|
|
2357
2475
|
} catch {
|
|
2358
2476
|
}
|
|
2359
2477
|
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
2360
|
-
|
|
2478
|
+
emitDelegateCompleted({
|
|
2361
2479
|
sessionId,
|
|
2362
2480
|
target,
|
|
2363
2481
|
task: i.task,
|
|
@@ -2382,7 +2500,7 @@ function createDelegateTool(opts) {
|
|
|
2382
2500
|
}
|
|
2383
2501
|
if ("__emptyResult" in result) {
|
|
2384
2502
|
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
2385
|
-
|
|
2503
|
+
emitDelegateCompleted({
|
|
2386
2504
|
sessionId,
|
|
2387
2505
|
target,
|
|
2388
2506
|
task: i.task,
|
|
@@ -2436,7 +2554,7 @@ function createDelegateTool(opts) {
|
|
|
2436
2554
|
} catch {
|
|
2437
2555
|
costUsd = void 0;
|
|
2438
2556
|
}
|
|
2439
|
-
|
|
2557
|
+
emitDelegateCompleted({
|
|
2440
2558
|
sessionId,
|
|
2441
2559
|
target,
|
|
2442
2560
|
task: i.task,
|
|
@@ -2474,7 +2592,7 @@ function createDelegateTool(opts) {
|
|
|
2474
2592
|
}
|
|
2475
2593
|
} catch (err) {
|
|
2476
2594
|
const message = toErrorMessage(err);
|
|
2477
|
-
|
|
2595
|
+
emitDelegateCompleted({
|
|
2478
2596
|
sessionId,
|
|
2479
2597
|
target,
|
|
2480
2598
|
task: i.task,
|
|
@@ -2570,7 +2688,7 @@ function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs)
|
|
|
2570
2688
|
`Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
|
|
2571
2689
|
"Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
|
|
2572
2690
|
'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
|
|
2573
|
-
|
|
2691
|
+
'If parallel help would materially improve the outcome, `session_note to="leader" kind="ask"` (or `mail_send` if you must reach another session) naming the exact helper task; do not spawn agents yourself.',
|
|
2574
2692
|
`Original task:
|
|
2575
2693
|
${originalTask}`,
|
|
2576
2694
|
`Prior checkpoint summary:
|
|
@@ -2733,12 +2851,16 @@ var DEFAULT_EXPLORE_EDIT_TOOLS = [
|
|
|
2733
2851
|
"str_replace"
|
|
2734
2852
|
];
|
|
2735
2853
|
var DEFAULT_EXPLORE_SEARCH_TOOLS = [
|
|
2736
|
-
"search",
|
|
2737
2854
|
"grep",
|
|
2738
2855
|
"codebase-search"
|
|
2739
2856
|
];
|
|
2740
2857
|
function buildProbeTaskText(probe) {
|
|
2741
|
-
const payload = {
|
|
2858
|
+
const payload = {
|
|
2859
|
+
probe: probe.probe,
|
|
2860
|
+
// Repeated on every assign so a long-lived resident cannot treat a
|
|
2861
|
+
// later probe as permission to keep mapping the previous subject.
|
|
2862
|
+
scope: "Help the leader, then stop. Answer only this probe with codebase-* tools first. Do not map adjacent files, features, tests, or docs unless named in probe/hint. Do not reindex. Deliver via submit_result only."
|
|
2863
|
+
};
|
|
2742
2864
|
if (probe.hint) payload.hint = probe.hint;
|
|
2743
2865
|
if (probe.context) payload.context = probe.context;
|
|
2744
2866
|
return JSON.stringify(payload, null, 2);
|
|
@@ -2903,7 +3025,7 @@ var ExploreCompanion = class {
|
|
|
2903
3025
|
if (!this.readSet.has(path12)) {
|
|
2904
3026
|
this.engage({
|
|
2905
3027
|
id: randomUUID5(),
|
|
2906
|
-
probe: `Map file ${path12}: role, exports,
|
|
3028
|
+
probe: `Map file ${path12} for the leader: role, exports, incoming/outgoing calls, and blast radius if they edit it.`,
|
|
2907
3029
|
hint: { file: path12 },
|
|
2908
3030
|
context: `Leader edited ${path12} without reading it first.`,
|
|
2909
3031
|
source: "edit_unread_file",
|
|
@@ -2918,7 +3040,7 @@ var ExploreCompanion = class {
|
|
|
2918
3040
|
this.readSet.add(path12);
|
|
2919
3041
|
this.engage({
|
|
2920
3042
|
id: randomUUID5(),
|
|
2921
|
-
probe: `
|
|
3043
|
+
probe: `Give the leader a skeleton of ${path12} plus callers and dependents \u2014 what it exports, who imports it.`,
|
|
2922
3044
|
hint: { file: path12 },
|
|
2923
3045
|
context: `Leader read unfamiliar file ${path12}.`,
|
|
2924
3046
|
source: "unfamiliar_read",
|
|
@@ -2933,7 +3055,7 @@ var ExploreCompanion = class {
|
|
|
2933
3055
|
const query = typeof input["query"] === "string" ? input["query"] : typeof input["pattern"] === "string" ? input["pattern"] : "";
|
|
2934
3056
|
this.engage({
|
|
2935
3057
|
id: randomUUID5(),
|
|
2936
|
-
probe: query ? `Locate "${query}"
|
|
3058
|
+
probe: query ? `Locate "${query}" for the leader \u2014 ${e.name} returned no hits. Try codebase-search synonyms, then grep/glob. Do not reindex.` : `The leader's ${e.name} returned no results. Find where the concept actually lives via codebase-search, then grep/glob. Do not reindex.`,
|
|
2937
3059
|
hint: query ? { symbol: query } : void 0,
|
|
2938
3060
|
context: `${e.name} for "${query}" returned zero results.`,
|
|
2939
3061
|
source: "search_zero_hits",
|
|
@@ -2954,7 +3076,7 @@ var ExploreCompanion = class {
|
|
|
2954
3076
|
const first = mentions[0];
|
|
2955
3077
|
this.engage({
|
|
2956
3078
|
id: randomUUID5(),
|
|
2957
|
-
probe: `Pre-map
|
|
3079
|
+
probe: `Pre-map files/symbols for the leader's in-progress todo so they can start already oriented: "${todo.content.slice(0, 160)}".`,
|
|
2958
3080
|
hint: first ? { [first.kind]: first.value } : void 0,
|
|
2959
3081
|
context: `Todo "${todo.content.slice(0, 120)}" flipped to in_progress.`,
|
|
2960
3082
|
source: "todo_in_progress",
|
|
@@ -2970,7 +3092,7 @@ var ExploreCompanion = class {
|
|
|
2970
3092
|
for (const token of tokens.slice(0, 2)) {
|
|
2971
3093
|
this.engage({
|
|
2972
3094
|
id: randomUUID5(),
|
|
2973
|
-
probe: `What is ${token.value}, where does it live, and who uses it?
|
|
3095
|
+
probe: `What is ${token.value}, where does it live, and who uses it? Answer so the leader can recover from the error that named it.`,
|
|
2974
3096
|
hint: { [token.kind]: token.value },
|
|
2975
3097
|
context: `Error: ${err.message.slice(0, 300)}`,
|
|
2976
3098
|
source: "error_symbol",
|
|
@@ -3339,11 +3461,7 @@ var DirectorBudgetPolicy = class {
|
|
|
3339
3461
|
}
|
|
3340
3462
|
const payload = event.payload;
|
|
3341
3463
|
if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
|
|
3342
|
-
this.handleTimeoutThreshold(
|
|
3343
|
-
event.subagentId,
|
|
3344
|
-
event.taskId,
|
|
3345
|
-
payload
|
|
3346
|
-
);
|
|
3464
|
+
this.handleTimeoutThreshold(event.subagentId, event.taskId, payload);
|
|
3347
3465
|
return;
|
|
3348
3466
|
}
|
|
3349
3467
|
const guardKey = `${event.subagentId}:${payload.kind}`;
|
|
@@ -3360,13 +3478,7 @@ var DirectorBudgetPolicy = class {
|
|
|
3360
3478
|
return;
|
|
3361
3479
|
}
|
|
3362
3480
|
}
|
|
3363
|
-
const grant = () => this.grantBoundedExtension(
|
|
3364
|
-
event.subagentId,
|
|
3365
|
-
event.taskId,
|
|
3366
|
-
payload,
|
|
3367
|
-
guardKey,
|
|
3368
|
-
prior
|
|
3369
|
-
);
|
|
3481
|
+
const grant = () => this.grantBoundedExtension(event.subagentId, event.taskId, payload, guardKey, prior);
|
|
3370
3482
|
if (payload.kind !== "cost" || !this.deps.brain) {
|
|
3371
3483
|
grant();
|
|
3372
3484
|
return;
|
|
@@ -3490,12 +3602,18 @@ var DirectorBudgetPolicy = class {
|
|
|
3490
3602
|
recordExtension(subagentId, taskId, kind, newLimit) {
|
|
3491
3603
|
const total = (this.extendTotals.get(subagentId) ?? 0) + 1;
|
|
3492
3604
|
this.extendTotals.set(subagentId, total);
|
|
3605
|
+
const sessionId = this.deps.currentSessionId?.();
|
|
3493
3606
|
this.deps.fleet.emit({
|
|
3494
3607
|
subagentId,
|
|
3495
3608
|
taskId,
|
|
3496
3609
|
ts: Date.now(),
|
|
3497
3610
|
type: "budget.extended",
|
|
3498
|
-
payload: {
|
|
3611
|
+
payload: {
|
|
3612
|
+
...sessionId ? { sessionId } : {},
|
|
3613
|
+
kind,
|
|
3614
|
+
newLimit,
|
|
3615
|
+
totalExtensions: total
|
|
3616
|
+
}
|
|
3499
3617
|
});
|
|
3500
3618
|
}
|
|
3501
3619
|
isCollabAgent(subagentId) {
|
|
@@ -3909,21 +4027,6 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
3909
4027
|
// src/coordination/director-tools.ts
|
|
3910
4028
|
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
3911
4029
|
|
|
3912
|
-
// src/coordination/kanban-dispatch-port.ts
|
|
3913
|
-
var notWired = () => {
|
|
3914
|
-
throw new Error(
|
|
3915
|
-
"Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
|
|
3916
|
-
);
|
|
3917
|
-
};
|
|
3918
|
-
var port = void 0;
|
|
3919
|
-
function setKanbanDispatch(impl) {
|
|
3920
|
-
port = impl;
|
|
3921
|
-
}
|
|
3922
|
-
function kanbanDispatch() {
|
|
3923
|
-
if (!port) notWired();
|
|
3924
|
-
return port;
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
4030
|
// src/coordination/director-input-helpers.ts
|
|
3928
4031
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3929
4032
|
function stringArray(value) {
|
|
@@ -3944,17 +4047,17 @@ function instantiateRosterConfig2(role, base) {
|
|
|
3944
4047
|
}
|
|
3945
4048
|
|
|
3946
4049
|
// src/coordination/kanban-ops-port.ts
|
|
3947
|
-
var
|
|
4050
|
+
var notWired = () => {
|
|
3948
4051
|
throw new Error(
|
|
3949
4052
|
"KanbanBoundaryOpsPort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanBoundaryOps)."
|
|
3950
4053
|
);
|
|
3951
4054
|
};
|
|
3952
|
-
var
|
|
4055
|
+
var port;
|
|
3953
4056
|
function setKanbanBoundaryOps(impl) {
|
|
3954
|
-
|
|
4057
|
+
port = impl;
|
|
3955
4058
|
}
|
|
3956
4059
|
function kanbanBoundaryOps() {
|
|
3957
|
-
return
|
|
4060
|
+
return port ?? notWired();
|
|
3958
4061
|
}
|
|
3959
4062
|
|
|
3960
4063
|
// src/coordination/director-kanban-queue-helpers.ts
|
|
@@ -3974,6 +4077,7 @@ function normalizeKanbanQueueInput(input) {
|
|
|
3974
4077
|
role: typeof raw.role === "string" ? raw.role : void 0,
|
|
3975
4078
|
provider: typeof raw.provider === "string" ? raw.provider : void 0,
|
|
3976
4079
|
model: typeof raw.model === "string" ? raw.model : void 0,
|
|
4080
|
+
tier: typeof raw.tier === "string" ? raw.tier : void 0,
|
|
3977
4081
|
fallbackModels: stringArray(raw.fallbackModels),
|
|
3978
4082
|
tools: stringArray(raw.tools),
|
|
3979
4083
|
allowedCapabilities: stringArray(raw.allowedCapabilities),
|
|
@@ -3998,7 +4102,10 @@ function buildKanbanSubagentConfig(task, input, roster, instantiateRosterConfig3
|
|
|
3998
4102
|
...tools ? { tools: ensureKanbanTool(tools) } : {},
|
|
3999
4103
|
...input.allowedCapabilities ?? assignment?.allowedCapabilities ? { allowedCapabilities: input.allowedCapabilities ?? assignment?.allowedCapabilities } : {},
|
|
4000
4104
|
...input.worktree !== void 0 ? { worktree: input.worktree } : {},
|
|
4001
|
-
...assignment?.
|
|
4105
|
+
...input.tier ?? assignment?.tier ? { tier: input.tier ?? assignment?.tier } : {},
|
|
4106
|
+
// A board's cost ceiling is an explicit decision by whoever queued the task,
|
|
4107
|
+
// so it is pinned: the tier layer may tighten a roster default, never this.
|
|
4108
|
+
...assignment?.costCeilingUsd !== void 0 ? { maxCostUsd: assignment.costCeilingUsd, budgetPins: ["maxCostUsd"] } : {}
|
|
4002
4109
|
};
|
|
4003
4110
|
}
|
|
4004
4111
|
function matchesKanbanQueueQuery(task, query) {
|
|
@@ -4125,6 +4232,21 @@ function ensureKanbanTool(tools) {
|
|
|
4125
4232
|
return tools.includes("kanban") ? [...tools] : [...tools, "kanban"];
|
|
4126
4233
|
}
|
|
4127
4234
|
|
|
4235
|
+
// src/coordination/kanban-dispatch-port.ts
|
|
4236
|
+
var notWired2 = () => {
|
|
4237
|
+
throw new Error(
|
|
4238
|
+
"Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
|
|
4239
|
+
);
|
|
4240
|
+
};
|
|
4241
|
+
var port2;
|
|
4242
|
+
function setKanbanDispatch(impl) {
|
|
4243
|
+
port2 = impl;
|
|
4244
|
+
}
|
|
4245
|
+
function kanbanDispatch() {
|
|
4246
|
+
if (!port2) notWired2();
|
|
4247
|
+
return port2;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4128
4250
|
// src/coordination/director-basic-tools.ts
|
|
4129
4251
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
4130
4252
|
function makeAssignTool(director) {
|
|
@@ -4584,998 +4706,1008 @@ function makeWorkCompleteTool(director) {
|
|
|
4584
4706
|
};
|
|
4585
4707
|
}
|
|
4586
4708
|
|
|
4587
|
-
// src/coordination/director-
|
|
4709
|
+
// src/coordination/director-mutation-test-tool.ts
|
|
4588
4710
|
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
description: "Reviewer worktree override. Default off because reviewer is read-only."
|
|
4653
|
-
},
|
|
4654
|
-
verifierWorktree: {
|
|
4655
|
-
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
4656
|
-
description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
|
|
4711
|
+
import { readFileSync } from "node:fs";
|
|
4712
|
+
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
4713
|
+
|
|
4714
|
+
// src/coordination/mutation-engine.ts
|
|
4715
|
+
var TOKEN_PATTERNS = [
|
|
4716
|
+
{
|
|
4717
|
+
kind: "relax-boundary",
|
|
4718
|
+
// `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
|
|
4719
|
+
// context on both sides so generic text (JSX, strings) is not touched.
|
|
4720
|
+
regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>>(?!=|>))/g,
|
|
4721
|
+
replace: () => ">="
|
|
4722
|
+
},
|
|
4723
|
+
{
|
|
4724
|
+
kind: "tighten-boundary",
|
|
4725
|
+
regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>>=)/g,
|
|
4726
|
+
replace: () => ">"
|
|
4727
|
+
},
|
|
4728
|
+
{
|
|
4729
|
+
kind: "arith-plus-to-minus",
|
|
4730
|
+
// `+` between operands (binary), not `++`, unary `+x`, or `+=`.
|
|
4731
|
+
regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
|
|
4732
|
+
replace: () => "-"
|
|
4733
|
+
},
|
|
4734
|
+
{
|
|
4735
|
+
kind: "arith-minus-to-plus",
|
|
4736
|
+
// Binary `-` between operands, not `--`, `-=` or negative-number literal.
|
|
4737
|
+
regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>-(?!-|=))/g,
|
|
4738
|
+
replace: () => "+"
|
|
4739
|
+
},
|
|
4740
|
+
{
|
|
4741
|
+
kind: "negate-boolean",
|
|
4742
|
+
// Standalone boolean literals used as values, not property names.
|
|
4743
|
+
regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
|
|
4744
|
+
replace: (m) => m === "true" ? "false" : "true"
|
|
4745
|
+
},
|
|
4746
|
+
{
|
|
4747
|
+
kind: "return-null",
|
|
4748
|
+
// `return <expr>;` where expr is not already null/undefined/void.
|
|
4749
|
+
regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
|
|
4750
|
+
replace: () => "return null;",
|
|
4751
|
+
endpointsInCode: true
|
|
4752
|
+
}
|
|
4753
|
+
];
|
|
4754
|
+
function planMutations(file, source, opts = {}) {
|
|
4755
|
+
const maxPerFile = opts.maxPerFile ?? 25;
|
|
4756
|
+
const out = [];
|
|
4757
|
+
const lines = source.split("\n");
|
|
4758
|
+
const masks = computeLineMasks(source);
|
|
4759
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
4760
|
+
const line = lines[lineIdx];
|
|
4761
|
+
const t = line.trim();
|
|
4762
|
+
if (t.startsWith("//")) continue;
|
|
4763
|
+
const codeRanges = masks[lineIdx];
|
|
4764
|
+
const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
|
|
4765
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
4766
|
+
pattern.regex.lastIndex = 0;
|
|
4767
|
+
let m = pattern.regex.exec(line);
|
|
4768
|
+
while (m !== null) {
|
|
4769
|
+
const token = m.groups?.["op"] ?? m[0];
|
|
4770
|
+
const tokenStart = m.index + m[0].indexOf(token);
|
|
4771
|
+
if (!inCode(tokenStart)) {
|
|
4772
|
+
m = pattern.regex.exec(line);
|
|
4773
|
+
continue;
|
|
4657
4774
|
}
|
|
4775
|
+
if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) {
|
|
4776
|
+
m = pattern.regex.exec(line);
|
|
4777
|
+
continue;
|
|
4778
|
+
}
|
|
4779
|
+
const original = line.slice(tokenStart, tokenStart + token.length);
|
|
4780
|
+
const replacement = pattern.replace(token);
|
|
4781
|
+
if (replacement === original) {
|
|
4782
|
+
m = pattern.regex.exec(line);
|
|
4783
|
+
continue;
|
|
4784
|
+
}
|
|
4785
|
+
out.push({
|
|
4786
|
+
id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
|
|
4787
|
+
kind: pattern.kind,
|
|
4788
|
+
file,
|
|
4789
|
+
line: lineIdx + 1,
|
|
4790
|
+
column: tokenStart + 1,
|
|
4791
|
+
original,
|
|
4792
|
+
replacement
|
|
4793
|
+
});
|
|
4794
|
+
m = pattern.regex.exec(line);
|
|
4658
4795
|
}
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4796
|
+
}
|
|
4797
|
+
if (out.length >= maxPerFile) break;
|
|
4798
|
+
}
|
|
4799
|
+
return out.slice(0, maxPerFile);
|
|
4800
|
+
}
|
|
4801
|
+
function computeLineMasks(source) {
|
|
4802
|
+
const lines = source.split("\n");
|
|
4803
|
+
const masks = lines.map(() => []);
|
|
4804
|
+
const stack = [{ kind: "code", depth: 0, parens: [] }];
|
|
4805
|
+
let inBlockComment = false;
|
|
4806
|
+
let lastToken = null;
|
|
4807
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
4808
|
+
const line = lines[lineIdx];
|
|
4809
|
+
const ranges = masks[lineIdx];
|
|
4810
|
+
let runStart = null;
|
|
4811
|
+
const closeRun = (end) => {
|
|
4812
|
+
if (runStart !== null && end > runStart) ranges.push([runStart, end]);
|
|
4813
|
+
runStart = null;
|
|
4814
|
+
};
|
|
4815
|
+
let i = 0;
|
|
4816
|
+
if (inBlockComment) {
|
|
4817
|
+
const close = line.indexOf("*/");
|
|
4818
|
+
if (close === -1) continue;
|
|
4819
|
+
inBlockComment = false;
|
|
4820
|
+
i = close + 2;
|
|
4821
|
+
}
|
|
4822
|
+
while (i < line.length) {
|
|
4823
|
+
const top = stack[stack.length - 1];
|
|
4824
|
+
const c = line[i];
|
|
4825
|
+
if (top.kind === "template") {
|
|
4826
|
+
if (c === "\\") {
|
|
4827
|
+
i += 2;
|
|
4828
|
+
continue;
|
|
4829
|
+
}
|
|
4830
|
+
if (c === "`") {
|
|
4831
|
+
stack.pop();
|
|
4832
|
+
lastToken = "`";
|
|
4833
|
+
i++;
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
4836
|
+
if (c === "$" && line[i + 1] === "{") {
|
|
4837
|
+
stack.push({ kind: "code", depth: 0, parens: [] });
|
|
4838
|
+
lastToken = "${";
|
|
4839
|
+
i += 2;
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
i++;
|
|
4843
|
+
continue;
|
|
4670
4844
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
)
|
|
4684
|
-
|
|
4685
|
-
id: randomUUID9(),
|
|
4686
|
-
subagentId,
|
|
4687
|
-
description: buildVerifierTask(i, {
|
|
4688
|
-
attempt,
|
|
4689
|
-
implementerResults,
|
|
4690
|
-
repairResults,
|
|
4691
|
-
priorAttempts: attempts
|
|
4692
|
-
}),
|
|
4693
|
-
timeoutMs: i.timeoutMs
|
|
4694
|
-
});
|
|
4695
|
-
gateTaskIds.push(taskId);
|
|
4696
|
-
taskRoleById.set(taskId, "verifier");
|
|
4845
|
+
if (/[\w$]/.test(c)) {
|
|
4846
|
+
let j = i + 1;
|
|
4847
|
+
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
4848
|
+
lastToken = line.slice(i, j);
|
|
4849
|
+
if (runStart === null) runStart = i;
|
|
4850
|
+
i = j;
|
|
4851
|
+
continue;
|
|
4852
|
+
}
|
|
4853
|
+
if (c === "'" || c === '"') {
|
|
4854
|
+
closeRun(i);
|
|
4855
|
+
i++;
|
|
4856
|
+
while (i < line.length && line[i] !== c) {
|
|
4857
|
+
if (line[i] === "\\") i++;
|
|
4858
|
+
i++;
|
|
4697
4859
|
}
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4860
|
+
i++;
|
|
4861
|
+
lastToken = c;
|
|
4862
|
+
continue;
|
|
4863
|
+
}
|
|
4864
|
+
if (c === "`") {
|
|
4865
|
+
closeRun(i);
|
|
4866
|
+
stack.push({ kind: "template", depth: 0, parens: [] });
|
|
4867
|
+
i++;
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4870
|
+
if (c === "/" && line[i + 1] === "/") {
|
|
4871
|
+
closeRun(i);
|
|
4872
|
+
break;
|
|
4873
|
+
}
|
|
4874
|
+
if (c === "/" && line[i + 1] === "*") {
|
|
4875
|
+
closeRun(i);
|
|
4876
|
+
const close = line.indexOf("*/", i + 2);
|
|
4877
|
+
if (close === -1) {
|
|
4878
|
+
inBlockComment = true;
|
|
4879
|
+
break;
|
|
4715
4880
|
}
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
if (
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
4727
|
-
nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
|
|
4728
|
-
};
|
|
4881
|
+
i = close + 2;
|
|
4882
|
+
continue;
|
|
4883
|
+
}
|
|
4884
|
+
if (c === "/") {
|
|
4885
|
+
if (!tokenCanEndOperand(lastToken)) {
|
|
4886
|
+
closeRun(i);
|
|
4887
|
+
const next = skipRegexLiteral(line, i);
|
|
4888
|
+
lastToken = next > i + 1 ? "regex" : "/";
|
|
4889
|
+
i = next;
|
|
4890
|
+
continue;
|
|
4729
4891
|
}
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4892
|
+
}
|
|
4893
|
+
if (c === "(") {
|
|
4894
|
+
top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
|
|
4895
|
+
lastToken = c;
|
|
4896
|
+
} else if (c === ")") {
|
|
4897
|
+
const kind = top.parens.pop() ?? "expr";
|
|
4898
|
+
lastToken = kind === "control" ? "control-paren-close" : ")";
|
|
4899
|
+
} else if (c === "{") {
|
|
4900
|
+
top.depth++;
|
|
4901
|
+
lastToken = c;
|
|
4902
|
+
} else if (c === "}") {
|
|
4903
|
+
if (top.depth > 0) {
|
|
4904
|
+
top.depth--;
|
|
4905
|
+
lastToken = c;
|
|
4906
|
+
} else if (stack.length > 1) {
|
|
4907
|
+
closeRun(i);
|
|
4908
|
+
stack.pop();
|
|
4909
|
+
i++;
|
|
4910
|
+
continue;
|
|
4911
|
+
} else {
|
|
4912
|
+
lastToken = c;
|
|
4748
4913
|
}
|
|
4914
|
+
} else if (c !== " " && c !== " " && c !== "\r") {
|
|
4915
|
+
lastToken = c;
|
|
4749
4916
|
}
|
|
4917
|
+
if (runStart === null) runStart = i;
|
|
4918
|
+
i++;
|
|
4750
4919
|
}
|
|
4751
|
-
|
|
4752
|
-
}
|
|
4753
|
-
|
|
4754
|
-
const raw = input ?? {};
|
|
4755
|
-
return {
|
|
4756
|
-
task: typeof raw.task === "string" ? raw.task : void 0,
|
|
4757
|
-
implementerTaskIds: stringArray(raw.implementerTaskIds),
|
|
4758
|
-
repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
|
|
4759
|
-
maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
|
|
4760
|
-
targets: stringArray(raw.targets),
|
|
4761
|
-
commands: stringArray(raw.commands),
|
|
4762
|
-
expected: typeof raw.expected === "string" ? raw.expected : void 0,
|
|
4763
|
-
evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
|
|
4764
|
-
reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
|
|
4765
|
-
verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
|
|
4766
|
-
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
|
|
4767
|
-
reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
|
|
4768
|
-
verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
|
|
4769
|
-
};
|
|
4770
|
-
}
|
|
4771
|
-
function clampRepairAttempts(value) {
|
|
4772
|
-
if (!Number.isFinite(value)) return 0;
|
|
4773
|
-
return Math.max(0, Math.min(5, Math.floor(value)));
|
|
4920
|
+
closeRun(line.length);
|
|
4921
|
+
}
|
|
4922
|
+
return masks;
|
|
4774
4923
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4924
|
+
var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
|
|
4925
|
+
"return",
|
|
4926
|
+
"typeof",
|
|
4927
|
+
"instanceof",
|
|
4928
|
+
"in",
|
|
4929
|
+
"of",
|
|
4930
|
+
"new",
|
|
4931
|
+
"delete",
|
|
4932
|
+
"void",
|
|
4933
|
+
"throw",
|
|
4934
|
+
"case",
|
|
4935
|
+
"do",
|
|
4936
|
+
"else",
|
|
4937
|
+
"yield",
|
|
4938
|
+
"await"
|
|
4939
|
+
]);
|
|
4940
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
4941
|
+
function tokenCanEndOperand(token) {
|
|
4942
|
+
if (token === null) return false;
|
|
4943
|
+
if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
|
|
4944
|
+
return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
|
|
4781
4945
|
}
|
|
4782
|
-
function
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
""
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
"Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
|
|
4808
|
-
"Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
|
|
4809
|
-
"",
|
|
4810
|
-
`Gate attempt: ${state.attempt}`,
|
|
4811
|
-
input.task ? `Original task:
|
|
4812
|
-
${input.task}` : void 0,
|
|
4813
|
-
input.targets?.length ? `Targets:
|
|
4814
|
-
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
4815
|
-
input.expected ? `Expected behavior:
|
|
4816
|
-
${input.expected}` : void 0,
|
|
4817
|
-
input.evidence ? `Known evidence:
|
|
4818
|
-
${input.evidence}` : void 0,
|
|
4819
|
-
taskResultsBlock("Implementer results", state.implementerResults),
|
|
4820
|
-
taskResultsBlock("Repair results so far", state.repairResults),
|
|
4821
|
-
priorAttemptsBlock(state.priorAttempts)
|
|
4822
|
-
].filter((part) => !!part).join("\n\n");
|
|
4823
|
-
}
|
|
4824
|
-
function buildRepairTask(input, attempt, attemptNumber) {
|
|
4825
|
-
return [
|
|
4826
|
-
`Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
|
|
4827
|
-
"Address every must-fix item. Run relevant checks before returning.",
|
|
4828
|
-
"Do not claim done unless verifier/reviewer feedback is resolved.",
|
|
4829
|
-
"",
|
|
4830
|
-
input.task ? `Original task:
|
|
4831
|
-
${input.task}` : void 0,
|
|
4832
|
-
input.targets?.length ? `Targets:
|
|
4833
|
-
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
4834
|
-
input.commands?.length ? `Commands expected to pass:
|
|
4835
|
-
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
4836
|
-
input.expected ? `Expected behavior:
|
|
4837
|
-
${input.expected}` : void 0,
|
|
4838
|
-
attempt.mustFix.length ? `Must fix:
|
|
4839
|
-
${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
4840
|
-
attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
|
|
4841
|
-
${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
4842
|
-
`Reviewer/verifier reports:
|
|
4843
|
-
${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
|
|
4844
|
-
${r.summary}`).join("\n\n")}`
|
|
4845
|
-
].filter((part) => !!part).join("\n\n");
|
|
4846
|
-
}
|
|
4847
|
-
function taskResultsBlock(title, results) {
|
|
4848
|
-
if (results.length === 0) return void 0;
|
|
4849
|
-
return `${title}:
|
|
4850
|
-
${results.map((r) => `### ${r.subagentId}/${r.taskId}
|
|
4851
|
-
${summarizeTaskResult(r).summary}`).join("\n\n")}`;
|
|
4852
|
-
}
|
|
4853
|
-
function priorAttemptsBlock(attempts) {
|
|
4854
|
-
if (attempts.length === 0) return void 0;
|
|
4855
|
-
return `Prior quality gate attempts:
|
|
4856
|
-
${attempts.map(
|
|
4857
|
-
(a) => `### Attempt ${a.attempt}
|
|
4858
|
-
${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
|
|
4859
|
-
).join("\n\n")}`;
|
|
4860
|
-
}
|
|
4861
|
-
function summarizeTaskResult(result) {
|
|
4862
|
-
const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
|
|
4863
|
-
const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
|
|
4864
|
-
return {
|
|
4865
|
-
taskId: result.taskId,
|
|
4866
|
-
subagentId: result.subagentId,
|
|
4867
|
-
status: result.status,
|
|
4868
|
-
summary: excerpt(text || error || "(no output)", 4e3),
|
|
4869
|
-
error
|
|
4870
|
-
};
|
|
4871
|
-
}
|
|
4872
|
-
function assessRoleResult(role, result) {
|
|
4873
|
-
const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
|
|
4874
|
-
const summary = summarizeTaskResult(result);
|
|
4875
|
-
const text = summary.summary;
|
|
4876
|
-
const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
|
|
4877
|
-
if (result.status !== "success") {
|
|
4878
|
-
return {
|
|
4879
|
-
role: resolvedRole,
|
|
4880
|
-
subagentId: result.subagentId,
|
|
4881
|
-
taskId: result.taskId,
|
|
4882
|
-
status: result.status,
|
|
4883
|
-
verdict: "fail",
|
|
4884
|
-
summary: text,
|
|
4885
|
-
uncertaintyFlags,
|
|
4886
|
-
error: summary.error ?? result.status
|
|
4887
|
-
};
|
|
4946
|
+
function skipRegexLiteral(line, start) {
|
|
4947
|
+
let i = start + 1;
|
|
4948
|
+
let inClass = false;
|
|
4949
|
+
while (i < line.length) {
|
|
4950
|
+
const ch = line[i];
|
|
4951
|
+
if (ch === "\\") {
|
|
4952
|
+
i += 2;
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (inClass) {
|
|
4956
|
+
if (ch === "]") inClass = false;
|
|
4957
|
+
i++;
|
|
4958
|
+
continue;
|
|
4959
|
+
}
|
|
4960
|
+
if (ch === "[") {
|
|
4961
|
+
inClass = true;
|
|
4962
|
+
i++;
|
|
4963
|
+
continue;
|
|
4964
|
+
}
|
|
4965
|
+
if (ch === "/") {
|
|
4966
|
+
i++;
|
|
4967
|
+
break;
|
|
4968
|
+
}
|
|
4969
|
+
if (ch === "\n" || ch === "\r") return line.length;
|
|
4970
|
+
i++;
|
|
4888
4971
|
}
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
subagentId: result.subagentId,
|
|
4892
|
-
taskId: result.taskId,
|
|
4893
|
-
status: result.status,
|
|
4894
|
-
verdict: parseQualityVerdict(resolvedRole, text),
|
|
4895
|
-
summary: text,
|
|
4896
|
-
uncertaintyFlags
|
|
4897
|
-
};
|
|
4972
|
+
while (i < line.length && /[a-z]/.test(line[i])) i++;
|
|
4973
|
+
return i;
|
|
4898
4974
|
}
|
|
4899
|
-
function
|
|
4900
|
-
const
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
if (
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4975
|
+
function parseMutationReport(text) {
|
|
4976
|
+
const candidates = [];
|
|
4977
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
4978
|
+
if (fence?.[1]) candidates.push(fence[1].trim());
|
|
4979
|
+
const firstBrace = text.indexOf("{");
|
|
4980
|
+
if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
|
|
4981
|
+
for (const candidate of candidates) {
|
|
4982
|
+
if (!candidate) continue;
|
|
4983
|
+
try {
|
|
4984
|
+
const parsed = JSON.parse(candidate);
|
|
4985
|
+
if (!Array.isArray(parsed.mutants)) continue;
|
|
4986
|
+
return {
|
|
4987
|
+
mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
|
|
4988
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : void 0
|
|
4989
|
+
};
|
|
4990
|
+
} catch {
|
|
4911
4991
|
}
|
|
4912
|
-
if (report.error) mustFix.push(`${report.role}: ${report.error}`);
|
|
4913
|
-
}
|
|
4914
|
-
if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
|
|
4915
|
-
if (hasInconclusive || reports.length === 0) {
|
|
4916
|
-
return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
|
|
4917
4992
|
}
|
|
4918
|
-
return
|
|
4993
|
+
return void 0;
|
|
4919
4994
|
}
|
|
4920
|
-
function
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
)
|
|
4927
|
-
|
|
4995
|
+
function extractBalancedObject(text, start) {
|
|
4996
|
+
let depth = 0;
|
|
4997
|
+
let inString = false;
|
|
4998
|
+
let escaped = false;
|
|
4999
|
+
for (let i = start; i < text.length; i++) {
|
|
5000
|
+
const c = text[i];
|
|
5001
|
+
if (escaped) {
|
|
5002
|
+
escaped = false;
|
|
5003
|
+
continue;
|
|
4928
5004
|
}
|
|
4929
|
-
if (
|
|
4930
|
-
|
|
4931
|
-
|
|
5005
|
+
if (c === "\\") {
|
|
5006
|
+
escaped = true;
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
if (c === '"') inString = !inString;
|
|
5010
|
+
if (inString) continue;
|
|
5011
|
+
if (c === "{") depth++;
|
|
5012
|
+
else if (c === "}") {
|
|
5013
|
+
depth--;
|
|
5014
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
4932
5015
|
}
|
|
4933
|
-
return "inconclusive";
|
|
4934
5016
|
}
|
|
4935
|
-
|
|
4936
|
-
if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
|
|
4937
|
-
if (sectionHasBlockingContent(text, "Failures")) return "fail";
|
|
4938
|
-
return "inconclusive";
|
|
4939
|
-
}
|
|
4940
|
-
function sectionHasBlockingContent(text, heading) {
|
|
4941
|
-
const section = extractSection(text, heading);
|
|
4942
|
-
if (!section) return false;
|
|
4943
|
-
return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
|
|
4944
|
-
}
|
|
4945
|
-
function extractSection(text, heading) {
|
|
4946
|
-
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4947
|
-
const pattern = new RegExp(
|
|
4948
|
-
`(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
|
|
4949
|
-
"i"
|
|
4950
|
-
);
|
|
4951
|
-
const match = text.match(pattern);
|
|
4952
|
-
const body = match?.[1]?.trim();
|
|
4953
|
-
return body ? body : void 0;
|
|
5017
|
+
return text.slice(start);
|
|
4954
5018
|
}
|
|
4955
|
-
function
|
|
4956
|
-
if (
|
|
4957
|
-
|
|
4958
|
-
|
|
5019
|
+
function normalizeMutantEntry(value) {
|
|
5020
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
5021
|
+
const rec = value;
|
|
5022
|
+
const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
|
|
5023
|
+
const status = rec["status"];
|
|
5024
|
+
if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
|
|
5025
|
+
return void 0;
|
|
5026
|
+
}
|
|
5027
|
+
return {
|
|
5028
|
+
id,
|
|
5029
|
+
file: typeof rec["file"] === "string" ? rec["file"] : "",
|
|
5030
|
+
line: typeof rec["line"] === "number" ? rec["line"] : 0,
|
|
5031
|
+
kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
|
|
5032
|
+
status,
|
|
5033
|
+
evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
|
|
5034
|
+
};
|
|
4959
5035
|
}
|
|
4960
5036
|
|
|
4961
5037
|
// src/coordination/director-mutation-test-tool.ts
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
const masks = computeLineMasks(source);
|
|
5011
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
5012
|
-
const line = lines[lineIdx];
|
|
5013
|
-
const t = line.trim();
|
|
5014
|
-
if (t.startsWith("//")) continue;
|
|
5015
|
-
const codeRanges = masks[lineIdx];
|
|
5016
|
-
const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
|
|
5017
|
-
for (const pattern of TOKEN_PATTERNS) {
|
|
5018
|
-
pattern.regex.lastIndex = 0;
|
|
5019
|
-
let m;
|
|
5020
|
-
while ((m = pattern.regex.exec(line)) !== null) {
|
|
5021
|
-
const token = m.groups?.["op"] ?? m[0];
|
|
5022
|
-
const tokenStart = m.index + m[0].indexOf(token);
|
|
5023
|
-
if (!inCode(tokenStart)) continue;
|
|
5024
|
-
if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
|
|
5025
|
-
const original = line.slice(tokenStart, tokenStart + token.length);
|
|
5026
|
-
const replacement = pattern.replace(token);
|
|
5027
|
-
if (replacement === original) continue;
|
|
5028
|
-
out.push({
|
|
5029
|
-
id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
|
|
5030
|
-
kind: pattern.kind,
|
|
5031
|
-
file,
|
|
5032
|
-
line: lineIdx + 1,
|
|
5033
|
-
column: tokenStart + 1,
|
|
5034
|
-
original,
|
|
5035
|
-
replacement
|
|
5036
|
-
});
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
if (out.length >= maxPerFile) break;
|
|
5040
|
-
}
|
|
5041
|
-
return out.slice(0, maxPerFile);
|
|
5042
|
-
}
|
|
5043
|
-
function computeLineMasks(source) {
|
|
5044
|
-
const lines = source.split("\n");
|
|
5045
|
-
const masks = lines.map(() => []);
|
|
5046
|
-
const stack = [{ kind: "code", depth: 0, parens: [] }];
|
|
5047
|
-
let inBlockComment = false;
|
|
5048
|
-
let lastToken = null;
|
|
5049
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
5050
|
-
const line = lines[lineIdx];
|
|
5051
|
-
const ranges = masks[lineIdx];
|
|
5052
|
-
let runStart = null;
|
|
5053
|
-
const closeRun = (end) => {
|
|
5054
|
-
if (runStart !== null && end > runStart) ranges.push([runStart, end]);
|
|
5055
|
-
runStart = null;
|
|
5056
|
-
};
|
|
5057
|
-
let i = 0;
|
|
5058
|
-
if (inBlockComment) {
|
|
5059
|
-
const close = line.indexOf("*/");
|
|
5060
|
-
if (close === -1) continue;
|
|
5061
|
-
inBlockComment = false;
|
|
5062
|
-
i = close + 2;
|
|
5063
|
-
}
|
|
5064
|
-
while (i < line.length) {
|
|
5065
|
-
const top = stack[stack.length - 1];
|
|
5066
|
-
const c = line[i];
|
|
5067
|
-
if (top.kind === "template") {
|
|
5068
|
-
if (c === "\\") {
|
|
5069
|
-
i += 2;
|
|
5070
|
-
continue;
|
|
5071
|
-
}
|
|
5072
|
-
if (c === "`") {
|
|
5073
|
-
stack.pop();
|
|
5074
|
-
lastToken = "`";
|
|
5075
|
-
i++;
|
|
5076
|
-
continue;
|
|
5077
|
-
}
|
|
5078
|
-
if (c === "$" && line[i + 1] === "{") {
|
|
5079
|
-
stack.push({ kind: "code", depth: 0, parens: [] });
|
|
5080
|
-
lastToken = "${";
|
|
5081
|
-
i += 2;
|
|
5082
|
-
continue;
|
|
5083
|
-
}
|
|
5084
|
-
i++;
|
|
5085
|
-
continue;
|
|
5086
|
-
}
|
|
5087
|
-
if (/[\w$]/.test(c)) {
|
|
5088
|
-
let j = i + 1;
|
|
5089
|
-
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
5090
|
-
lastToken = line.slice(i, j);
|
|
5091
|
-
if (runStart === null) runStart = i;
|
|
5092
|
-
i = j;
|
|
5093
|
-
continue;
|
|
5094
|
-
}
|
|
5095
|
-
if (c === "'" || c === '"') {
|
|
5096
|
-
closeRun(i);
|
|
5097
|
-
i++;
|
|
5098
|
-
while (i < line.length && line[i] !== c) {
|
|
5099
|
-
if (line[i] === "\\") i++;
|
|
5100
|
-
i++;
|
|
5038
|
+
var DEFAULT_MAX_PER_FILE = 10;
|
|
5039
|
+
var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
|
|
5040
|
+
var CHAOS_ROLE = "chaos-monkey";
|
|
5041
|
+
function makeMutationTestTool(director, roster, opts = {}) {
|
|
5042
|
+
return {
|
|
5043
|
+
name: "mutation_test",
|
|
5044
|
+
description: "Chaos Monkey mutation testing: deterministically sabotage boundary conditions in the target code (> to >=, + to -, boolean flips, return null), re-run the tests per mutant, and report which mutants were killed. Surviving mutants mean the tests are weak \u2014 optionally loop a strengthen-tests repair until they die.",
|
|
5045
|
+
usageHint: "Use after writing new code AND its tests, before delivering. Pass targets (files) and testCommand. Provide repairSubagentId to auto-strengthen weak tests. Survivors that persist are reported as suspected-equivalent.",
|
|
5046
|
+
permission: "auto",
|
|
5047
|
+
mutating: false,
|
|
5048
|
+
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
5049
|
+
inputSchema: {
|
|
5050
|
+
type: "object",
|
|
5051
|
+
properties: {
|
|
5052
|
+
targets: {
|
|
5053
|
+
type: "array",
|
|
5054
|
+
items: { type: "string" },
|
|
5055
|
+
description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
|
|
5056
|
+
},
|
|
5057
|
+
testCommand: {
|
|
5058
|
+
type: "string",
|
|
5059
|
+
description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
|
|
5060
|
+
},
|
|
5061
|
+
cwd: { type: "string", description: "Working directory for the test command." },
|
|
5062
|
+
maxPerFile: {
|
|
5063
|
+
type: "number",
|
|
5064
|
+
minimum: 1,
|
|
5065
|
+
maximum: 25,
|
|
5066
|
+
description: "Mutant cap per file per pass. Default 10."
|
|
5067
|
+
},
|
|
5068
|
+
maxStrengthenAttempts: {
|
|
5069
|
+
type: "number",
|
|
5070
|
+
minimum: 0,
|
|
5071
|
+
maximum: 5,
|
|
5072
|
+
description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
|
|
5073
|
+
},
|
|
5074
|
+
repairSubagentId: {
|
|
5075
|
+
type: "string",
|
|
5076
|
+
description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
|
|
5077
|
+
},
|
|
5078
|
+
chaosWorktree: {
|
|
5079
|
+
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
5080
|
+
description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
|
|
5081
|
+
},
|
|
5082
|
+
timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
|
|
5083
|
+
reportOnly: {
|
|
5084
|
+
type: "boolean",
|
|
5085
|
+
description: "Skip the strengthen loop even when survivors exist. Default false."
|
|
5101
5086
|
}
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5087
|
+
},
|
|
5088
|
+
required: ["targets", "testCommand"],
|
|
5089
|
+
additionalProperties: false
|
|
5090
|
+
},
|
|
5091
|
+
async execute(input, ctx) {
|
|
5092
|
+
const i = normalizeMutationTestInput(input);
|
|
5093
|
+
const root = opts.projectRoot ?? ctx.projectRoot;
|
|
5094
|
+
const plan = buildPlan(i, root);
|
|
5095
|
+
if (plan.length === 0) {
|
|
5096
|
+
return {
|
|
5097
|
+
verdict: "inconclusive",
|
|
5098
|
+
passed: false,
|
|
5099
|
+
error: "No mutable sites found in the given targets (after comment/string filtering)."
|
|
5100
|
+
};
|
|
5111
5101
|
}
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5102
|
+
const chaosBase = roster?.[CHAOS_ROLE];
|
|
5103
|
+
if (!chaosBase) {
|
|
5104
|
+
return {
|
|
5105
|
+
verdict: "inconclusive",
|
|
5106
|
+
passed: false,
|
|
5107
|
+
error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
|
|
5108
|
+
};
|
|
5115
5109
|
}
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5110
|
+
const chaosSubagentId = await director.spawn(
|
|
5111
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
5112
|
+
);
|
|
5113
|
+
const chaosTaskId = await director.assign({
|
|
5114
|
+
id: randomUUID9(),
|
|
5115
|
+
subagentId: chaosSubagentId,
|
|
5116
|
+
description: buildChaosTask(plan, i, 1, []),
|
|
5117
|
+
timeoutMs: i.timeoutMs
|
|
5118
|
+
});
|
|
5119
|
+
const [chaosResult] = await director.awaitTasks([chaosTaskId]);
|
|
5120
|
+
const pass1 = collectOutcomes(chaosResult, plan);
|
|
5121
|
+
const survivors = pass1.filter((m) => m.status === "survived");
|
|
5122
|
+
const maxAttempts = clamp(
|
|
5123
|
+
i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
|
|
5124
|
+
0,
|
|
5125
|
+
5
|
|
5126
|
+
);
|
|
5127
|
+
const attempts = [];
|
|
5128
|
+
let current = survivors;
|
|
5129
|
+
let rerunUnknowns = [];
|
|
5130
|
+
while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
|
|
5131
|
+
const attemptNo = attempts.length + 1;
|
|
5132
|
+
const strengthenTaskId = await director.assign({
|
|
5133
|
+
id: randomUUID9(),
|
|
5134
|
+
subagentId: i.repairSubagentId,
|
|
5135
|
+
description: buildStrengthenTask(current, i, attemptNo),
|
|
5136
|
+
timeoutMs: i.timeoutMs
|
|
5137
|
+
});
|
|
5138
|
+
const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
|
|
5139
|
+
if (strengthenResult?.status !== "success") {
|
|
5140
|
+
attempts.push({
|
|
5141
|
+
attempt: attemptNo,
|
|
5142
|
+
survivorsBefore: current,
|
|
5143
|
+
strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
|
|
5144
|
+
survivorsAfter: current,
|
|
5145
|
+
suspectedEquivalent: []
|
|
5146
|
+
});
|
|
5121
5147
|
break;
|
|
5122
5148
|
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
i
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
top.depth--;
|
|
5147
|
-
lastToken = c;
|
|
5148
|
-
} else if (stack.length > 1) {
|
|
5149
|
-
closeRun(i);
|
|
5150
|
-
stack.pop();
|
|
5151
|
-
i++;
|
|
5152
|
-
continue;
|
|
5153
|
-
} else {
|
|
5154
|
-
lastToken = c;
|
|
5155
|
-
}
|
|
5156
|
-
} else if (c !== " " && c !== " " && c !== "\r") {
|
|
5157
|
-
lastToken = c;
|
|
5149
|
+
const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
|
|
5150
|
+
const rerunSubagentId = await director.spawn(
|
|
5151
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
5152
|
+
);
|
|
5153
|
+
const rerunTaskId = await director.assign({
|
|
5154
|
+
id: randomUUID9(),
|
|
5155
|
+
subagentId: rerunSubagentId,
|
|
5156
|
+
description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
|
|
5157
|
+
timeoutMs: i.timeoutMs
|
|
5158
|
+
});
|
|
5159
|
+
const [rerunResult] = await director.awaitTasks([rerunTaskId]);
|
|
5160
|
+
const passN = collectOutcomes(rerunResult, survivorPlan);
|
|
5161
|
+
const stillSurviving = passN.filter((m) => !isKill(m.status));
|
|
5162
|
+
rerunUnknowns = passN.filter((m) => m.status === "skipped");
|
|
5163
|
+
attempts.push({
|
|
5164
|
+
attempt: attemptNo,
|
|
5165
|
+
survivorsBefore: current,
|
|
5166
|
+
strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
|
|
5167
|
+
rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
|
|
5168
|
+
survivorsAfter: stillSurviving,
|
|
5169
|
+
suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
5170
|
+
});
|
|
5171
|
+
current = stillSurviving;
|
|
5158
5172
|
}
|
|
5159
|
-
|
|
5160
|
-
|
|
5173
|
+
const finalSurvivors = current.filter((m) => m.status === "survived");
|
|
5174
|
+
const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
|
|
5175
|
+
const skippedCount = pass1.filter((m) => m.status === "skipped").length;
|
|
5176
|
+
const rerunUnknownCount = rerunUnknowns.length;
|
|
5177
|
+
const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
|
|
5178
|
+
const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
|
|
5179
|
+
return {
|
|
5180
|
+
verdict,
|
|
5181
|
+
passed: verdict === "pass",
|
|
5182
|
+
mutationScore: Number.parseFloat(score.toFixed(3)),
|
|
5183
|
+
planned: plan.length,
|
|
5184
|
+
killed: pass1.filter((m) => isKill(m.status)).length,
|
|
5185
|
+
// Breakout of `killed`: how many kills were detected by the test
|
|
5186
|
+
// command hanging rather than by a failing assertion. A subset of
|
|
5187
|
+
// `killed`, surfaced so a director can distinguish a hang-heavy
|
|
5188
|
+
// suite (mutants breaking termination, not assertions) from an
|
|
5189
|
+
// assertion-strong one. hangHeavy = killedByHang === killed.
|
|
5190
|
+
killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
|
|
5191
|
+
survived: pass1.filter((m) => m.status === "survived").length,
|
|
5192
|
+
skipped: pass1.filter((m) => m.status === "skipped").length,
|
|
5193
|
+
finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
5194
|
+
suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
|
|
5195
|
+
strengthenAttempts: attempts.length,
|
|
5196
|
+
attempts,
|
|
5197
|
+
chaosTaskId,
|
|
5198
|
+
// Unverified leftovers from the strengthen loop: surfaced so the
|
|
5199
|
+
// caller can see WHICH mutants lack kill evidence, and counted by
|
|
5200
|
+
// the verdict gate above.
|
|
5201
|
+
unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
5202
|
+
nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
|
|
5203
|
+
};
|
|
5161
5204
|
}
|
|
5162
|
-
|
|
5163
|
-
}
|
|
5164
|
-
return masks;
|
|
5205
|
+
};
|
|
5165
5206
|
}
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
"
|
|
5169
|
-
"
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
]);
|
|
5182
|
-
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
5183
|
-
function tokenCanEndOperand(token) {
|
|
5184
|
-
if (token === null) return false;
|
|
5185
|
-
if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
|
|
5186
|
-
return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
|
|
5207
|
+
function normalizeMutationTestInput(input) {
|
|
5208
|
+
const raw = input ?? {};
|
|
5209
|
+
const targets = stringArray(raw["targets"]) ?? [];
|
|
5210
|
+
const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
|
|
5211
|
+
return {
|
|
5212
|
+
targets: targets.filter(Boolean),
|
|
5213
|
+
testCommand,
|
|
5214
|
+
cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
|
|
5215
|
+
maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
|
|
5216
|
+
maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
|
|
5217
|
+
repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
|
|
5218
|
+
chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
|
|
5219
|
+
timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
|
|
5220
|
+
reportOnly: raw["reportOnly"] === true
|
|
5221
|
+
};
|
|
5187
5222
|
}
|
|
5188
|
-
function
|
|
5189
|
-
|
|
5190
|
-
let inClass = false;
|
|
5191
|
-
while (i < line.length) {
|
|
5192
|
-
const ch = line[i];
|
|
5193
|
-
if (ch === "\\") {
|
|
5194
|
-
i += 2;
|
|
5195
|
-
continue;
|
|
5196
|
-
}
|
|
5197
|
-
if (inClass) {
|
|
5198
|
-
if (ch === "]") inClass = false;
|
|
5199
|
-
i++;
|
|
5200
|
-
continue;
|
|
5201
|
-
}
|
|
5202
|
-
if (ch === "[") {
|
|
5203
|
-
inClass = true;
|
|
5204
|
-
i++;
|
|
5205
|
-
continue;
|
|
5206
|
-
}
|
|
5207
|
-
if (ch === "/") {
|
|
5208
|
-
i++;
|
|
5209
|
-
break;
|
|
5210
|
-
}
|
|
5211
|
-
if (ch === "\n" || ch === "\r") return line.length;
|
|
5212
|
-
i++;
|
|
5213
|
-
}
|
|
5214
|
-
while (i < line.length && /[a-z]/.test(line[i])) i++;
|
|
5215
|
-
return i;
|
|
5223
|
+
function clamp(n, lo, hi) {
|
|
5224
|
+
return Math.min(hi, Math.max(lo, n));
|
|
5216
5225
|
}
|
|
5217
|
-
function
|
|
5218
|
-
const
|
|
5219
|
-
const
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
|
|
5223
|
-
for (const candidate of candidates) {
|
|
5224
|
-
if (!candidate) continue;
|
|
5226
|
+
function buildPlan(i, projectRoot) {
|
|
5227
|
+
const plan = [];
|
|
5228
|
+
for (const target of i.targets) {
|
|
5229
|
+
const abs = isAbsolute2(target) ? target : join3(projectRoot ?? process.cwd(), target);
|
|
5230
|
+
let source;
|
|
5225
5231
|
try {
|
|
5226
|
-
|
|
5227
|
-
if (!Array.isArray(parsed.mutants)) continue;
|
|
5228
|
-
return {
|
|
5229
|
-
mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
|
|
5230
|
-
summary: typeof parsed.summary === "string" ? parsed.summary : void 0
|
|
5231
|
-
};
|
|
5232
|
+
source = readFileSync(abs, "utf8");
|
|
5232
5233
|
} catch {
|
|
5234
|
+
continue;
|
|
5233
5235
|
}
|
|
5236
|
+
plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
|
|
5234
5237
|
}
|
|
5235
|
-
return
|
|
5238
|
+
return plan;
|
|
5236
5239
|
}
|
|
5237
|
-
function
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5240
|
+
function makeChaosConfig(base, worktree) {
|
|
5241
|
+
return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
|
|
5242
|
+
}
|
|
5243
|
+
function buildChaosTask(plan, i, pass, priorSurvivors) {
|
|
5244
|
+
const mutants = plan.map(
|
|
5245
|
+
(m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
|
|
5246
|
+
).join("\n");
|
|
5247
|
+
const prior = priorSurvivors.length > 0 ? `
|
|
5248
|
+
These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
|
|
5249
|
+
${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
|
|
5250
|
+
return [
|
|
5251
|
+
"Execute this deterministic mutation plan against the current checkout.",
|
|
5252
|
+
"",
|
|
5253
|
+
"For each mutant, in order:",
|
|
5254
|
+
"1. Apply ONLY that mutation at its exact (file, line, column).",
|
|
5255
|
+
`2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
|
|
5256
|
+
"3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
|
|
5257
|
+
"4. Restore the file byte-for-byte before the next mutant.",
|
|
5258
|
+
"",
|
|
5259
|
+
"Mutants:",
|
|
5260
|
+
mutants,
|
|
5261
|
+
prior,
|
|
5262
|
+
"",
|
|
5263
|
+
"Rules: one mutation at a time; never stack; if the anchored token no longer matches, mark skipped with the drift as evidence; do not fix or refactor anything; stay inside the plan.",
|
|
5264
|
+
"Finish with submit_result, then repeat the same JSON as your final text."
|
|
5265
|
+
].join("\n");
|
|
5266
|
+
}
|
|
5267
|
+
function buildStrengthenTask(survivors, i, attempt) {
|
|
5268
|
+
const confirmed = survivors.filter((s) => s.status === "survived");
|
|
5269
|
+
const unverified = survivors.filter((s) => s.status === "skipped");
|
|
5270
|
+
const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
|
|
5271
|
+
return [
|
|
5272
|
+
`Strengthen the tests so the mutants below die (attempt ${attempt}).`,
|
|
5273
|
+
"",
|
|
5274
|
+
...confirmed.length > 0 ? [
|
|
5275
|
+
"CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
|
|
5276
|
+
...confirmed.map(row),
|
|
5277
|
+
""
|
|
5278
|
+
] : [],
|
|
5279
|
+
...unverified.length > 0 ? [
|
|
5280
|
+
"UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
|
|
5281
|
+
...unverified.map(row),
|
|
5282
|
+
""
|
|
5283
|
+
] : [],
|
|
5284
|
+
`Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
|
|
5285
|
+
"",
|
|
5286
|
+
"For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
5287
|
+
].join("\n");
|
|
5288
|
+
}
|
|
5289
|
+
function collectOutcomes(result, plan) {
|
|
5290
|
+
const fromText = parseTextOutcomes(result);
|
|
5291
|
+
if (fromText.length > 0) {
|
|
5292
|
+
const remaining = [...plan];
|
|
5293
|
+
const matched = [];
|
|
5294
|
+
for (const m of fromText) {
|
|
5295
|
+
const idx = remaining.findIndex((p) => p.id === m.id);
|
|
5296
|
+
if (idx === -1) continue;
|
|
5297
|
+
remaining.splice(idx, 1);
|
|
5298
|
+
matched.push(m);
|
|
5250
5299
|
}
|
|
5251
|
-
if (
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5300
|
+
if (matched.length > 0) {
|
|
5301
|
+
const missing = remaining.map((p) => ({
|
|
5302
|
+
id: p.id,
|
|
5303
|
+
file: p.file,
|
|
5304
|
+
line: p.line,
|
|
5305
|
+
kind: p.kind,
|
|
5306
|
+
status: "skipped",
|
|
5307
|
+
evidence: "not reported by chaos task"
|
|
5308
|
+
}));
|
|
5309
|
+
return [...matched, ...missing];
|
|
5257
5310
|
}
|
|
5258
5311
|
}
|
|
5259
|
-
return
|
|
5312
|
+
return plan.map((p) => ({
|
|
5313
|
+
id: p.id,
|
|
5314
|
+
file: p.file,
|
|
5315
|
+
line: p.line,
|
|
5316
|
+
kind: p.kind,
|
|
5317
|
+
status: "skipped",
|
|
5318
|
+
evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
|
|
5319
|
+
}));
|
|
5260
5320
|
}
|
|
5261
|
-
function
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
const
|
|
5266
|
-
if (!
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
return {
|
|
5270
|
-
id,
|
|
5271
|
-
file:
|
|
5272
|
-
line:
|
|
5273
|
-
kind:
|
|
5274
|
-
status,
|
|
5275
|
-
evidence:
|
|
5276
|
-
};
|
|
5321
|
+
function isKill(status) {
|
|
5322
|
+
return status === "killed" || status === "killed-by-hang";
|
|
5323
|
+
}
|
|
5324
|
+
function parseTextOutcomes(result) {
|
|
5325
|
+
const text = typeof result?.result === "string" ? result.result : void 0;
|
|
5326
|
+
if (!text) return [];
|
|
5327
|
+
const parsed = parseMutationReport(text);
|
|
5328
|
+
if (!parsed) return [];
|
|
5329
|
+
return parsed.mutants.map((m) => ({
|
|
5330
|
+
id: m.id,
|
|
5331
|
+
file: m.file,
|
|
5332
|
+
line: m.line,
|
|
5333
|
+
kind: m.kind,
|
|
5334
|
+
status: m.status,
|
|
5335
|
+
evidence: m.evidence
|
|
5336
|
+
}));
|
|
5277
5337
|
}
|
|
5278
5338
|
|
|
5279
|
-
// src/coordination/director-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
var CHAOS_ROLE = "chaos-monkey";
|
|
5283
|
-
function makeMutationTestTool(director, roster, opts = {}) {
|
|
5339
|
+
// src/coordination/director-quality-gate-tool.ts
|
|
5340
|
+
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
5341
|
+
function makeQualityGateTool(director, roster) {
|
|
5284
5342
|
return {
|
|
5285
|
-
name: "
|
|
5286
|
-
description: "
|
|
5287
|
-
usageHint: "Use after
|
|
5343
|
+
name: "quality_gate",
|
|
5344
|
+
description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
|
|
5345
|
+
usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
|
|
5288
5346
|
permission: "auto",
|
|
5289
5347
|
mutating: false,
|
|
5290
5348
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
5291
5349
|
inputSchema: {
|
|
5292
5350
|
type: "object",
|
|
5293
5351
|
properties: {
|
|
5352
|
+
task: {
|
|
5353
|
+
type: "string",
|
|
5354
|
+
description: "Original implementation task or acceptance goal being gated."
|
|
5355
|
+
},
|
|
5356
|
+
implementerTaskIds: {
|
|
5357
|
+
type: "array",
|
|
5358
|
+
items: { type: "string" },
|
|
5359
|
+
description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
|
|
5360
|
+
},
|
|
5361
|
+
repairSubagentId: {
|
|
5362
|
+
type: "string",
|
|
5363
|
+
description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
|
|
5364
|
+
},
|
|
5365
|
+
maxRepairAttempts: {
|
|
5366
|
+
type: "number",
|
|
5367
|
+
minimum: 0,
|
|
5368
|
+
maximum: 5,
|
|
5369
|
+
description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
|
|
5370
|
+
},
|
|
5294
5371
|
targets: {
|
|
5295
5372
|
type: "array",
|
|
5296
5373
|
items: { type: "string" },
|
|
5297
|
-
description: "
|
|
5374
|
+
description: "Files, packages, or paths that reviewer/verifier should focus on."
|
|
5298
5375
|
},
|
|
5299
|
-
|
|
5376
|
+
commands: {
|
|
5377
|
+
type: "array",
|
|
5378
|
+
items: { type: "string" },
|
|
5379
|
+
description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
|
|
5380
|
+
},
|
|
5381
|
+
expected: {
|
|
5300
5382
|
type: "string",
|
|
5301
|
-
description:
|
|
5383
|
+
description: "Expected behavior or acceptance criteria."
|
|
5302
5384
|
},
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
minimum: 1,
|
|
5307
|
-
maximum: 25,
|
|
5308
|
-
description: "Mutant cap per file per pass. Default 10."
|
|
5385
|
+
evidence: {
|
|
5386
|
+
type: "string",
|
|
5387
|
+
description: "Known implementation notes, diff summary, or commands already run."
|
|
5309
5388
|
},
|
|
5310
|
-
|
|
5311
|
-
type: "
|
|
5312
|
-
|
|
5313
|
-
maximum: 5,
|
|
5314
|
-
description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
|
|
5389
|
+
reviewer: {
|
|
5390
|
+
type: "boolean",
|
|
5391
|
+
description: "Whether to run the reviewer lane. Default true."
|
|
5315
5392
|
},
|
|
5316
|
-
|
|
5317
|
-
type: "
|
|
5318
|
-
description: "
|
|
5393
|
+
verifier: {
|
|
5394
|
+
type: "boolean",
|
|
5395
|
+
description: "Whether to run the verifier lane. Default true."
|
|
5319
5396
|
},
|
|
5320
|
-
|
|
5397
|
+
timeoutMs: {
|
|
5398
|
+
type: "number",
|
|
5399
|
+
minimum: 1,
|
|
5400
|
+
description: "Optional per reviewer/verifier/repair task timeout."
|
|
5401
|
+
},
|
|
5402
|
+
reviewerWorktree: {
|
|
5321
5403
|
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
5322
|
-
description: "
|
|
5404
|
+
description: "Reviewer worktree override. Default off because reviewer is read-only."
|
|
5323
5405
|
},
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
description: "Skip the strengthen loop even when survivors exist. Default false."
|
|
5406
|
+
verifierWorktree: {
|
|
5407
|
+
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
5408
|
+
description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
|
|
5328
5409
|
}
|
|
5329
|
-
},
|
|
5330
|
-
required: ["targets", "testCommand"],
|
|
5331
|
-
additionalProperties: false
|
|
5332
|
-
},
|
|
5333
|
-
async execute(input, ctx) {
|
|
5334
|
-
const i = normalizeMutationTestInput(input);
|
|
5335
|
-
const root = opts.projectRoot ?? ctx.projectRoot;
|
|
5336
|
-
const plan = buildPlan(i, root);
|
|
5337
|
-
if (plan.length === 0) {
|
|
5338
|
-
return {
|
|
5339
|
-
verdict: "inconclusive",
|
|
5340
|
-
passed: false,
|
|
5341
|
-
error: "No mutable sites found in the given targets (after comment/string filtering)."
|
|
5342
|
-
};
|
|
5343
5410
|
}
|
|
5344
|
-
|
|
5345
|
-
|
|
5411
|
+
},
|
|
5412
|
+
async execute(input) {
|
|
5413
|
+
const i = normalizeQualityGateInput(input);
|
|
5414
|
+
const runReviewer = i.reviewer !== false;
|
|
5415
|
+
const runVerifier = i.verifier !== false;
|
|
5416
|
+
if (!runReviewer && !runVerifier) {
|
|
5346
5417
|
return {
|
|
5347
5418
|
verdict: "inconclusive",
|
|
5348
5419
|
passed: false,
|
|
5349
|
-
error: "
|
|
5420
|
+
error: "quality_gate requires reviewer, verifier, or both."
|
|
5350
5421
|
};
|
|
5351
5422
|
}
|
|
5352
|
-
const
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
const chaosTaskId = await director.assign({
|
|
5356
|
-
id: randomUUID10(),
|
|
5357
|
-
subagentId: chaosSubagentId,
|
|
5358
|
-
description: buildChaosTask(plan, i, 1, []),
|
|
5359
|
-
timeoutMs: i.timeoutMs
|
|
5360
|
-
});
|
|
5361
|
-
const [chaosResult] = await director.awaitTasks([chaosTaskId]);
|
|
5362
|
-
const pass1 = collectOutcomes(chaosResult, plan);
|
|
5363
|
-
const survivors = pass1.filter((m) => m.status === "survived");
|
|
5364
|
-
const maxAttempts = clamp(
|
|
5365
|
-
i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
|
|
5366
|
-
0,
|
|
5367
|
-
5
|
|
5423
|
+
const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
|
|
5424
|
+
const maxRepairAttempts = clampRepairAttempts(
|
|
5425
|
+
i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
|
|
5368
5426
|
);
|
|
5427
|
+
const repairResults = [];
|
|
5369
5428
|
const attempts = [];
|
|
5370
|
-
let
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
suspectedEquivalent: []
|
|
5429
|
+
for (let attempt = 1; ; attempt++) {
|
|
5430
|
+
const gateTaskIds = [];
|
|
5431
|
+
const taskRoleById = /* @__PURE__ */ new Map();
|
|
5432
|
+
if (runVerifier) {
|
|
5433
|
+
const subagentId = await director.spawn(
|
|
5434
|
+
makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
|
|
5435
|
+
);
|
|
5436
|
+
const taskId = await director.assign({
|
|
5437
|
+
id: randomUUID10(),
|
|
5438
|
+
subagentId,
|
|
5439
|
+
description: buildVerifierTask(i, {
|
|
5440
|
+
attempt,
|
|
5441
|
+
implementerResults,
|
|
5442
|
+
repairResults,
|
|
5443
|
+
priorAttempts: attempts
|
|
5444
|
+
}),
|
|
5445
|
+
timeoutMs: i.timeoutMs
|
|
5388
5446
|
});
|
|
5389
|
-
|
|
5447
|
+
gateTaskIds.push(taskId);
|
|
5448
|
+
taskRoleById.set(taskId, "verifier");
|
|
5390
5449
|
}
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5450
|
+
if (runReviewer) {
|
|
5451
|
+
const subagentId = await director.spawn(
|
|
5452
|
+
makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
|
|
5453
|
+
);
|
|
5454
|
+
const taskId = await director.assign({
|
|
5455
|
+
id: randomUUID10(),
|
|
5456
|
+
subagentId,
|
|
5457
|
+
description: buildReviewerTask(i, {
|
|
5458
|
+
attempt,
|
|
5459
|
+
implementerResults,
|
|
5460
|
+
repairResults,
|
|
5461
|
+
priorAttempts: attempts
|
|
5462
|
+
}),
|
|
5463
|
+
timeoutMs: i.timeoutMs
|
|
5464
|
+
});
|
|
5465
|
+
gateTaskIds.push(taskId);
|
|
5466
|
+
taskRoleById.set(taskId, "reviewer");
|
|
5467
|
+
}
|
|
5468
|
+
const gateResults = await director.awaitTasks(gateTaskIds);
|
|
5469
|
+
const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
|
|
5470
|
+
const assessment = assessQualityGate(reports);
|
|
5471
|
+
attempts.push({ attempt, reports, ...assessment });
|
|
5472
|
+
if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
|
|
5473
|
+
return {
|
|
5474
|
+
verdict: assessment.verdict,
|
|
5475
|
+
passed: assessment.passed,
|
|
5476
|
+
attempts,
|
|
5477
|
+
repairAttemptsUsed: repairResults.length,
|
|
5478
|
+
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
5479
|
+
nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
|
|
5480
|
+
};
|
|
5481
|
+
}
|
|
5482
|
+
const repairTaskId = await director.assign({
|
|
5396
5483
|
id: randomUUID10(),
|
|
5397
|
-
subagentId:
|
|
5398
|
-
description:
|
|
5484
|
+
subagentId: i.repairSubagentId,
|
|
5485
|
+
description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
|
|
5399
5486
|
timeoutMs: i.timeoutMs
|
|
5400
5487
|
});
|
|
5401
|
-
const [
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5488
|
+
const [repairResult] = await director.awaitTasks([repairTaskId]);
|
|
5489
|
+
if (repairResult) repairResults.push(repairResult);
|
|
5490
|
+
if (repairResult?.status !== "success") {
|
|
5491
|
+
return {
|
|
5492
|
+
verdict: "fail",
|
|
5493
|
+
passed: false,
|
|
5494
|
+
attempts,
|
|
5495
|
+
repairAttemptsUsed: repairResults.length,
|
|
5496
|
+
repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
|
|
5497
|
+
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
5498
|
+
nextAction: "repair_failed"
|
|
5499
|
+
};
|
|
5500
|
+
}
|
|
5414
5501
|
}
|
|
5415
|
-
const finalSurvivors = current.filter((m) => m.status === "survived");
|
|
5416
|
-
const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
|
|
5417
|
-
const skippedCount = pass1.filter((m) => m.status === "skipped").length;
|
|
5418
|
-
const rerunUnknownCount = rerunUnknowns.length;
|
|
5419
|
-
const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
|
|
5420
|
-
const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
|
|
5421
|
-
return {
|
|
5422
|
-
verdict,
|
|
5423
|
-
passed: verdict === "pass",
|
|
5424
|
-
mutationScore: Number.parseFloat(score.toFixed(3)),
|
|
5425
|
-
planned: plan.length,
|
|
5426
|
-
killed: pass1.filter((m) => isKill(m.status)).length,
|
|
5427
|
-
// Breakout of `killed`: how many kills were detected by the test
|
|
5428
|
-
// command hanging rather than by a failing assertion. A subset of
|
|
5429
|
-
// `killed`, surfaced so a director can distinguish a hang-heavy
|
|
5430
|
-
// suite (mutants breaking termination, not assertions) from an
|
|
5431
|
-
// assertion-strong one. hangHeavy = killedByHang === killed.
|
|
5432
|
-
killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
|
|
5433
|
-
survived: pass1.filter((m) => m.status === "survived").length,
|
|
5434
|
-
skipped: pass1.filter((m) => m.status === "skipped").length,
|
|
5435
|
-
finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
5436
|
-
suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
|
|
5437
|
-
strengthenAttempts: attempts.length,
|
|
5438
|
-
attempts,
|
|
5439
|
-
chaosTaskId,
|
|
5440
|
-
// Unverified leftovers from the strengthen loop: surfaced so the
|
|
5441
|
-
// caller can see WHICH mutants lack kill evidence, and counted by
|
|
5442
|
-
// the verdict gate above.
|
|
5443
|
-
unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
5444
|
-
nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
|
|
5445
|
-
};
|
|
5446
5502
|
}
|
|
5447
5503
|
};
|
|
5448
5504
|
}
|
|
5449
|
-
function
|
|
5505
|
+
function normalizeQualityGateInput(input) {
|
|
5450
5506
|
const raw = input ?? {};
|
|
5451
|
-
const targets = stringArray(raw["targets"]) ?? [];
|
|
5452
|
-
const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
|
|
5453
5507
|
return {
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5508
|
+
task: typeof raw.task === "string" ? raw.task : void 0,
|
|
5509
|
+
implementerTaskIds: stringArray(raw.implementerTaskIds),
|
|
5510
|
+
repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
|
|
5511
|
+
maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
|
|
5512
|
+
targets: stringArray(raw.targets),
|
|
5513
|
+
commands: stringArray(raw.commands),
|
|
5514
|
+
expected: typeof raw.expected === "string" ? raw.expected : void 0,
|
|
5515
|
+
evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
|
|
5516
|
+
reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
|
|
5517
|
+
verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
|
|
5518
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
|
|
5519
|
+
reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
|
|
5520
|
+
verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
|
|
5463
5521
|
};
|
|
5464
5522
|
}
|
|
5465
|
-
function
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
function buildPlan(i, projectRoot) {
|
|
5469
|
-
const plan = [];
|
|
5470
|
-
for (const target of i.targets) {
|
|
5471
|
-
const abs = isAbsolute2(target) ? target : join3(projectRoot ?? process.cwd(), target);
|
|
5472
|
-
let source;
|
|
5473
|
-
try {
|
|
5474
|
-
source = readFileSync(abs, "utf8");
|
|
5475
|
-
} catch {
|
|
5476
|
-
continue;
|
|
5477
|
-
}
|
|
5478
|
-
plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
|
|
5479
|
-
}
|
|
5480
|
-
return plan;
|
|
5523
|
+
function clampRepairAttempts(value) {
|
|
5524
|
+
if (!Number.isFinite(value)) return 0;
|
|
5525
|
+
return Math.max(0, Math.min(5, Math.floor(value)));
|
|
5481
5526
|
}
|
|
5482
|
-
function
|
|
5483
|
-
|
|
5527
|
+
function makeQualityGateSubagentConfig(role, roster, worktree) {
|
|
5528
|
+
const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
|
|
5529
|
+
return {
|
|
5530
|
+
...instantiateRosterConfig2(role, base),
|
|
5531
|
+
worktree
|
|
5532
|
+
};
|
|
5484
5533
|
}
|
|
5485
|
-
function
|
|
5486
|
-
const mutants = plan.map(
|
|
5487
|
-
(m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
|
|
5488
|
-
).join("\n");
|
|
5489
|
-
const prior = priorSurvivors.length > 0 ? `
|
|
5490
|
-
These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
|
|
5491
|
-
${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
|
|
5534
|
+
function buildVerifierTask(input, state) {
|
|
5492
5535
|
return [
|
|
5493
|
-
"
|
|
5494
|
-
"",
|
|
5495
|
-
"
|
|
5496
|
-
"1. Apply ONLY that mutation at its exact (file, line, column).",
|
|
5497
|
-
`2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
|
|
5498
|
-
"3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
|
|
5499
|
-
"4. Restore the file byte-for-byte before the next mutant.",
|
|
5500
|
-
"",
|
|
5501
|
-
"Mutants:",
|
|
5502
|
-
mutants,
|
|
5503
|
-
prior,
|
|
5536
|
+
"Run the independent verification gate for this implementation.",
|
|
5537
|
+
"Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
|
|
5538
|
+
"Do not edit code. Run the smallest meaningful command set and include exact failures.",
|
|
5504
5539
|
"",
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5540
|
+
`Gate attempt: ${state.attempt}`,
|
|
5541
|
+
input.task ? `Original task:
|
|
5542
|
+
${input.task}` : void 0,
|
|
5543
|
+
input.targets?.length ? `Targets:
|
|
5544
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
5545
|
+
input.commands?.length ? `Required or suggested commands:
|
|
5546
|
+
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
5547
|
+
input.expected ? `Expected behavior:
|
|
5548
|
+
${input.expected}` : void 0,
|
|
5549
|
+
input.evidence ? `Known evidence:
|
|
5550
|
+
${input.evidence}` : void 0,
|
|
5551
|
+
taskResultsBlock("Implementer results", state.implementerResults),
|
|
5552
|
+
taskResultsBlock("Repair results so far", state.repairResults),
|
|
5553
|
+
priorAttemptsBlock(state.priorAttempts)
|
|
5554
|
+
].filter((part) => !!part).join("\n\n");
|
|
5508
5555
|
}
|
|
5509
|
-
function
|
|
5510
|
-
const confirmed = survivors.filter((s) => s.status === "survived");
|
|
5511
|
-
const unverified = survivors.filter((s) => s.status === "skipped");
|
|
5512
|
-
const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
|
|
5556
|
+
function buildReviewerTask(input, state) {
|
|
5513
5557
|
return [
|
|
5514
|
-
|
|
5558
|
+
"Run independent code review for this implementation.",
|
|
5559
|
+
"Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
|
|
5560
|
+
"Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
|
|
5515
5561
|
"",
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5562
|
+
`Gate attempt: ${state.attempt}`,
|
|
5563
|
+
input.task ? `Original task:
|
|
5564
|
+
${input.task}` : void 0,
|
|
5565
|
+
input.targets?.length ? `Targets:
|
|
5566
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
5567
|
+
input.expected ? `Expected behavior:
|
|
5568
|
+
${input.expected}` : void 0,
|
|
5569
|
+
input.evidence ? `Known evidence:
|
|
5570
|
+
${input.evidence}` : void 0,
|
|
5571
|
+
taskResultsBlock("Implementer results", state.implementerResults),
|
|
5572
|
+
taskResultsBlock("Repair results so far", state.repairResults),
|
|
5573
|
+
priorAttemptsBlock(state.priorAttempts)
|
|
5574
|
+
].filter((part) => !!part).join("\n\n");
|
|
5575
|
+
}
|
|
5576
|
+
function buildRepairTask(input, attempt, attemptNumber) {
|
|
5577
|
+
return [
|
|
5578
|
+
`Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
|
|
5579
|
+
"Address every must-fix item. Run relevant checks before returning.",
|
|
5580
|
+
"Do not claim done unless verifier/reviewer feedback is resolved.",
|
|
5527
5581
|
"",
|
|
5528
|
-
|
|
5529
|
-
|
|
5582
|
+
input.task ? `Original task:
|
|
5583
|
+
${input.task}` : void 0,
|
|
5584
|
+
input.targets?.length ? `Targets:
|
|
5585
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
5586
|
+
input.commands?.length ? `Commands expected to pass:
|
|
5587
|
+
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
5588
|
+
input.expected ? `Expected behavior:
|
|
5589
|
+
${input.expected}` : void 0,
|
|
5590
|
+
attempt.mustFix.length ? `Must fix:
|
|
5591
|
+
${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
5592
|
+
attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
|
|
5593
|
+
${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
5594
|
+
`Reviewer/verifier reports:
|
|
5595
|
+
${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
|
|
5596
|
+
${r.summary}`).join("\n\n")}`
|
|
5597
|
+
].filter((part) => !!part).join("\n\n");
|
|
5530
5598
|
}
|
|
5531
|
-
function
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5599
|
+
function taskResultsBlock(title, results) {
|
|
5600
|
+
if (results.length === 0) return void 0;
|
|
5601
|
+
return `${title}:
|
|
5602
|
+
${results.map((r) => `### ${r.subagentId}/${r.taskId}
|
|
5603
|
+
${summarizeTaskResult(r).summary}`).join("\n\n")}`;
|
|
5604
|
+
}
|
|
5605
|
+
function priorAttemptsBlock(attempts) {
|
|
5606
|
+
if (attempts.length === 0) return void 0;
|
|
5607
|
+
return `Prior quality gate attempts:
|
|
5608
|
+
${attempts.map(
|
|
5609
|
+
(a) => `### Attempt ${a.attempt}
|
|
5610
|
+
${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
|
|
5611
|
+
).join("\n\n")}`;
|
|
5612
|
+
}
|
|
5613
|
+
function summarizeTaskResult(result) {
|
|
5614
|
+
const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
|
|
5615
|
+
const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
|
|
5616
|
+
return {
|
|
5617
|
+
taskId: result.taskId,
|
|
5618
|
+
subagentId: result.subagentId,
|
|
5619
|
+
status: result.status,
|
|
5620
|
+
summary: excerpt(text || error || "(no output)", 4e3),
|
|
5621
|
+
error
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5624
|
+
function assessRoleResult(role, result) {
|
|
5625
|
+
const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
|
|
5626
|
+
const summary = summarizeTaskResult(result);
|
|
5627
|
+
const text = summary.summary;
|
|
5628
|
+
const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
|
|
5629
|
+
if (result.status !== "success") {
|
|
5630
|
+
return {
|
|
5631
|
+
role: resolvedRole,
|
|
5632
|
+
subagentId: result.subagentId,
|
|
5633
|
+
taskId: result.taskId,
|
|
5634
|
+
status: result.status,
|
|
5635
|
+
verdict: "fail",
|
|
5636
|
+
summary: text,
|
|
5637
|
+
uncertaintyFlags,
|
|
5638
|
+
error: summary.error ?? result.status
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
return {
|
|
5642
|
+
role: resolvedRole,
|
|
5643
|
+
subagentId: result.subagentId,
|
|
5644
|
+
taskId: result.taskId,
|
|
5645
|
+
status: result.status,
|
|
5646
|
+
verdict: parseQualityVerdict(resolvedRole, text),
|
|
5647
|
+
summary: text,
|
|
5648
|
+
uncertaintyFlags
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
function assessQualityGate(reports) {
|
|
5652
|
+
const mustFix = [];
|
|
5653
|
+
const uncertaintyFlags = [];
|
|
5654
|
+
let hasFail = false;
|
|
5655
|
+
let hasInconclusive = false;
|
|
5656
|
+
for (const report of reports) {
|
|
5657
|
+
if (report.verdict === "fail") hasFail = true;
|
|
5658
|
+
if (report.verdict === "inconclusive") hasInconclusive = true;
|
|
5659
|
+
const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
|
|
5660
|
+
if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
|
|
5661
|
+
if (report.uncertaintyFlags) {
|
|
5662
|
+
uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
|
|
5541
5663
|
}
|
|
5542
|
-
if (
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5664
|
+
if (report.error) mustFix.push(`${report.role}: ${report.error}`);
|
|
5665
|
+
}
|
|
5666
|
+
if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
|
|
5667
|
+
if (hasInconclusive || reports.length === 0) {
|
|
5668
|
+
return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
|
|
5669
|
+
}
|
|
5670
|
+
return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
|
|
5671
|
+
}
|
|
5672
|
+
function parseQualityVerdict(role, text) {
|
|
5673
|
+
const normalized = text.toLowerCase();
|
|
5674
|
+
const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
|
|
5675
|
+
if (role === "reviewer") {
|
|
5676
|
+
if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
|
|
5677
|
+
verdictBlock
|
|
5678
|
+
)) {
|
|
5679
|
+
return "fail";
|
|
5680
|
+
}
|
|
5681
|
+
if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
|
|
5682
|
+
if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
|
|
5683
|
+
return "fail";
|
|
5552
5684
|
}
|
|
5685
|
+
return "inconclusive";
|
|
5553
5686
|
}
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
kind: p.kind,
|
|
5559
|
-
status: "skipped",
|
|
5560
|
-
evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
|
|
5561
|
-
}));
|
|
5687
|
+
if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
|
|
5688
|
+
if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
|
|
5689
|
+
if (sectionHasBlockingContent(text, "Failures")) return "fail";
|
|
5690
|
+
return "inconclusive";
|
|
5562
5691
|
}
|
|
5563
|
-
function
|
|
5564
|
-
|
|
5692
|
+
function sectionHasBlockingContent(text, heading) {
|
|
5693
|
+
const section = extractSection(text, heading);
|
|
5694
|
+
if (!section) return false;
|
|
5695
|
+
return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
|
|
5696
|
+
}
|
|
5697
|
+
function extractSection(text, heading) {
|
|
5698
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5699
|
+
const pattern = new RegExp(
|
|
5700
|
+
`(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
|
|
5701
|
+
"i"
|
|
5702
|
+
);
|
|
5703
|
+
const match = text.match(pattern);
|
|
5704
|
+
const body = match?.[1]?.trim();
|
|
5705
|
+
return body ? body : void 0;
|
|
5565
5706
|
}
|
|
5566
|
-
function
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
if (!parsed) return [];
|
|
5571
|
-
return parsed.mutants.map((m) => ({
|
|
5572
|
-
id: m.id,
|
|
5573
|
-
file: m.file,
|
|
5574
|
-
line: m.line,
|
|
5575
|
-
kind: m.kind,
|
|
5576
|
-
status: m.status,
|
|
5577
|
-
evidence: m.evidence
|
|
5578
|
-
}));
|
|
5707
|
+
function excerpt(text, max) {
|
|
5708
|
+
if (text.length <= max) return text;
|
|
5709
|
+
return `${text.slice(0, max - 20).trimEnd()}
|
|
5710
|
+
...(truncated)`;
|
|
5579
5711
|
}
|
|
5580
5712
|
|
|
5581
5713
|
// src/coordination/director-tools.ts
|
|
@@ -5630,6 +5762,10 @@ function makeSpawnTool(director, roster) {
|
|
|
5630
5762
|
type: "string",
|
|
5631
5763
|
description: "Model id within the provider. Defaults to the leader model when omitted."
|
|
5632
5764
|
},
|
|
5765
|
+
tier: {
|
|
5766
|
+
type: "string",
|
|
5767
|
+
description: "Cost/capability level for this worker: 'budget' (cheap + fast, for mechanical or well-specified work), 'standard' (the default), or 'premium' (expensive + most capable, for work where being wrong is costly). Resolved deterministically into a model, a failover chain, and a spend budget from `modelTiers` config, so you do NOT need to know any model id. Omit to let the routing table decide by role. An explicit `model` always wins over the tier.\n\nThis is the RIGHT place to spend a cheap tier. Because this spawn is non-blocking, a budget-tier worker being slower costs you nothing \u2014 you keep working while it runs. The same tier on `delegate` would just make you wait longer."
|
|
5768
|
+
},
|
|
5633
5769
|
systemPromptOverride: {
|
|
5634
5770
|
type: "string",
|
|
5635
5771
|
description: "Extra prompt text appended after the role-base prompt."
|
|
@@ -5667,7 +5803,7 @@ function makeSpawnTool(director, roster) {
|
|
|
5667
5803
|
mutating: false,
|
|
5668
5804
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
5669
5805
|
inputSchema,
|
|
5670
|
-
async execute(input) {
|
|
5806
|
+
async execute(input, ctx) {
|
|
5671
5807
|
const i = input ?? {};
|
|
5672
5808
|
const role = typeof i.role === "string" ? i.role : void 0;
|
|
5673
5809
|
const description = typeof i.description === "string" ? i.description : void 0;
|
|
@@ -5707,11 +5843,20 @@ function makeSpawnTool(director, roster) {
|
|
|
5707
5843
|
if (typeof i.timeoutMs === "number") cfg.timeoutMs = i.timeoutMs;
|
|
5708
5844
|
if (typeof i.idleTimeoutMs === "number") cfg.idleTimeoutMs = i.idleTimeoutMs;
|
|
5709
5845
|
if (typeof i.maxTokens === "number") cfg.maxTokens = i.maxTokens;
|
|
5846
|
+
if (typeof i.tier === "string" && i.tier) cfg.tier = i.tier;
|
|
5847
|
+
const budgetPins = [];
|
|
5848
|
+
if (typeof i.maxIterations === "number") budgetPins.push("maxIterations");
|
|
5849
|
+
if (typeof i.maxToolCalls === "number") budgetPins.push("maxToolCalls");
|
|
5850
|
+
if (typeof i.maxCostUsd === "number") budgetPins.push("maxCostUsd");
|
|
5851
|
+
if (typeof i.maxTokens === "number") budgetPins.push("maxTokens");
|
|
5852
|
+
if (typeof i.timeoutMs === "number") budgetPins.push("timeoutMs");
|
|
5853
|
+
if (budgetPins.length) cfg.budgetPins = budgetPins;
|
|
5710
5854
|
if (typeof i.worktree === "boolean" || i.worktree === "auto" || i.worktree === "required" || i.worktree === "off") {
|
|
5711
5855
|
cfg.worktree = i.worktree;
|
|
5712
5856
|
}
|
|
5713
5857
|
try {
|
|
5714
|
-
const
|
|
5858
|
+
const origin = callerSessionId(ctx);
|
|
5859
|
+
const subagentId = await director.spawn(origin ? { ...cfg, originSessionId: origin } : cfg);
|
|
5715
5860
|
return {
|
|
5716
5861
|
subagentId,
|
|
5717
5862
|
provider: cfg.provider,
|
|
@@ -5784,6 +5929,10 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5784
5929
|
role: { type: "string" },
|
|
5785
5930
|
provider: { type: "string" },
|
|
5786
5931
|
model: { type: "string" },
|
|
5932
|
+
tier: {
|
|
5933
|
+
type: "string",
|
|
5934
|
+
description: "Cost level for the dispatched workers ('budget' | 'standard' | 'premium'). Resolved from `modelTiers`; an explicit provider/model still wins."
|
|
5935
|
+
},
|
|
5787
5936
|
fallbackModels: { type: "array", items: { type: "string" } },
|
|
5788
5937
|
tools: { type: "array", items: { type: "string" } },
|
|
5789
5938
|
allowedCapabilities: { type: "array", items: { type: "string" } },
|
|
@@ -5800,6 +5949,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5800
5949
|
}
|
|
5801
5950
|
const projectRoot = ctx.projectRoot;
|
|
5802
5951
|
if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
|
|
5952
|
+
const sessionId = ctx.eventSessionId();
|
|
5803
5953
|
const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
|
|
5804
5954
|
const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
|
|
5805
5955
|
const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
|
|
@@ -5820,6 +5970,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5820
5970
|
if (candidateTaskIds && !candidateTaskId) break;
|
|
5821
5971
|
if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
|
|
5822
5972
|
const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
|
|
5973
|
+
sessionId,
|
|
5823
5974
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {},
|
|
5824
5975
|
...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
|
|
5825
5976
|
routing: {
|
|
@@ -5828,6 +5979,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5828
5979
|
...i.role !== void 0 ? { role: i.role } : {},
|
|
5829
5980
|
...i.provider !== void 0 ? { provider: i.provider } : {},
|
|
5830
5981
|
...i.model !== void 0 ? { model: i.model } : {},
|
|
5982
|
+
...i.tier !== void 0 ? { tier: i.tier } : {},
|
|
5831
5983
|
...i.fallbackModels !== void 0 ? { fallbackModels: i.fallbackModels } : {},
|
|
5832
5984
|
...i.tools !== void 0 ? { tools: i.tools } : {},
|
|
5833
5985
|
...i.allowedCapabilities !== void 0 ? { allowedCapabilities: i.allowedCapabilities } : {}
|
|
@@ -5858,7 +6010,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5858
6010
|
status: "failed",
|
|
5859
6011
|
error: budgetError
|
|
5860
6012
|
},
|
|
5861
|
-
{ expectedLeaseId: ourLeaseId }
|
|
6013
|
+
{ sessionId, expectedLeaseId: ourLeaseId }
|
|
5862
6014
|
);
|
|
5863
6015
|
errors.push({
|
|
5864
6016
|
taskId: claim.task.id,
|
|
@@ -5901,6 +6053,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5901
6053
|
}
|
|
5902
6054
|
};
|
|
5903
6055
|
const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
|
|
6056
|
+
sessionId,
|
|
5904
6057
|
boardId: claim.board.id,
|
|
5905
6058
|
taskId: claim.task.id,
|
|
5906
6059
|
leaseId: ourLeaseId,
|
|
@@ -5946,6 +6099,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5946
6099
|
}
|
|
5947
6100
|
}
|
|
5948
6101
|
await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
6102
|
+
sessionId,
|
|
5949
6103
|
boardId: claim.board.id,
|
|
5950
6104
|
taskId: claim.task.id,
|
|
5951
6105
|
leaseId: ourLeaseId,
|
|
@@ -5973,6 +6127,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
5973
6127
|
for (const dispatch of dispatches) {
|
|
5974
6128
|
if (!disposers.has(dispatch.runTaskId)) continue;
|
|
5975
6129
|
void renewAndRevokeLease({
|
|
6130
|
+
sessionId,
|
|
5976
6131
|
projectRoot,
|
|
5977
6132
|
boardId: dispatch.boardId,
|
|
5978
6133
|
taskId: dispatch.taskId,
|
|
@@ -6027,7 +6182,8 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
6027
6182
|
{
|
|
6028
6183
|
leaseExpiresAt: refreshedExpiry,
|
|
6029
6184
|
expectedLeaseId: dispatch.leaseId
|
|
6030
|
-
}
|
|
6185
|
+
},
|
|
6186
|
+
{ sessionId, expectedLeaseId: dispatch.leaseId }
|
|
6031
6187
|
);
|
|
6032
6188
|
} catch {
|
|
6033
6189
|
}
|
|
@@ -6052,6 +6208,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
6052
6208
|
runTaskIds.delete(dispatch.runTaskId);
|
|
6053
6209
|
if (result.status === "success") {
|
|
6054
6210
|
const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
|
|
6211
|
+
sessionId,
|
|
6055
6212
|
boardId: dispatch.boardId,
|
|
6056
6213
|
taskId: dispatch.taskId,
|
|
6057
6214
|
leaseId: dispatch.leaseId,
|
|
@@ -6068,6 +6225,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
6068
6225
|
}
|
|
6069
6226
|
} else {
|
|
6070
6227
|
const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
6228
|
+
sessionId,
|
|
6071
6229
|
boardId: dispatch.boardId,
|
|
6072
6230
|
taskId: dispatch.taskId,
|
|
6073
6231
|
leaseId: dispatch.leaseId,
|
|
@@ -6114,6 +6272,7 @@ async function renewAndRevokeLease(opts) {
|
|
|
6114
6272
|
runTaskId,
|
|
6115
6273
|
ourLeaseId,
|
|
6116
6274
|
refreshedExpiry,
|
|
6275
|
+
sessionId,
|
|
6117
6276
|
director,
|
|
6118
6277
|
disposers
|
|
6119
6278
|
} = opts;
|
|
@@ -6132,10 +6291,13 @@ async function renewAndRevokeLease(opts) {
|
|
|
6132
6291
|
} catch {
|
|
6133
6292
|
}
|
|
6134
6293
|
if (!revoked) {
|
|
6135
|
-
await kanbanDispatch().heartbeatTaskAssignment(
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6294
|
+
await kanbanDispatch().heartbeatTaskAssignment(
|
|
6295
|
+
projectRoot,
|
|
6296
|
+
boardId,
|
|
6297
|
+
taskId,
|
|
6298
|
+
{ leaseExpiresAt: refreshedExpiry, expectedLeaseId: ourLeaseId },
|
|
6299
|
+
{ sessionId, expectedLeaseId: ourLeaseId }
|
|
6300
|
+
).catch(() => {
|
|
6139
6301
|
});
|
|
6140
6302
|
}
|
|
6141
6303
|
}
|
|
@@ -6320,6 +6482,28 @@ function resolveDirectorSpawnModel(config, opts) {
|
|
|
6320
6482
|
if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
|
|
6321
6483
|
}
|
|
6322
6484
|
}
|
|
6485
|
+
if (opts.config) {
|
|
6486
|
+
if (opts.tier) {
|
|
6487
|
+
const decision = classifyTier(opts.config, { role: config.role, tier: opts.tier });
|
|
6488
|
+
if (decision && !decision.configured) {
|
|
6489
|
+
const available = listTierIds(opts.config);
|
|
6490
|
+
opts.logger?.warn(
|
|
6491
|
+
`spawn: tier "${opts.tier}" is not a configured level${available.length ? ` (available: ${available.join(", ")})` : " (modelTiers.levels is empty)"} \u2014 falling through to matrix/session resolution.`
|
|
6492
|
+
);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
const resolvedTier = resolveTier(opts.config, {
|
|
6496
|
+
role: config.role,
|
|
6497
|
+
tier: opts.tier
|
|
6498
|
+
});
|
|
6499
|
+
if (resolvedTier) {
|
|
6500
|
+
applyTierToSubagentConfig(config, resolvedTier);
|
|
6501
|
+
opts.onTierResolved?.(resolvedTier);
|
|
6502
|
+
opts.logger?.info(
|
|
6503
|
+
`spawn: tier="${resolvedTier.tier}" (via ${resolvedTier.source}) applied for role "${config.role ?? "?"}"`
|
|
6504
|
+
);
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6323
6507
|
if (!config.provider && opts.sessionProvider) {
|
|
6324
6508
|
config.provider = opts.sessionProvider;
|
|
6325
6509
|
opts.logger?.info(
|
|
@@ -6447,22 +6631,35 @@ var FleetUsageAggregator = class {
|
|
|
6447
6631
|
metaLookup;
|
|
6448
6632
|
perSubagent = /* @__PURE__ */ new Map();
|
|
6449
6633
|
total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
6634
|
+
retired = {
|
|
6635
|
+
subagents: 0,
|
|
6636
|
+
input: 0,
|
|
6637
|
+
output: 0,
|
|
6638
|
+
cacheRead: 0,
|
|
6639
|
+
cacheWrite: 0,
|
|
6640
|
+
cost: 0
|
|
6641
|
+
};
|
|
6450
6642
|
unsub = [];
|
|
6451
6643
|
/**
|
|
6452
|
-
*
|
|
6453
|
-
*
|
|
6454
|
-
*
|
|
6455
|
-
*
|
|
6644
|
+
* Drop a terminated subagent's per-agent entry, folding its spend into
|
|
6645
|
+
* {@link FleetUsage.retired} so the run total stays whole.
|
|
6646
|
+
*
|
|
6647
|
+
* The per-agent map is what needed bounding — one entry per spawn, for
|
|
6648
|
+
* entities that will never emit again. Deducting from `total` as well (the
|
|
6649
|
+
* original behavior) also un-spent real tokens: a run that retires each
|
|
6650
|
+
* worker on completion reported a total that shrank as it made progress, and
|
|
6651
|
+
* the director budgets against that number.
|
|
6456
6652
|
*/
|
|
6457
6653
|
removeSubagent(subagentId) {
|
|
6458
6654
|
const snap = this.perSubagent.get(subagentId);
|
|
6459
6655
|
if (!snap) return;
|
|
6460
6656
|
this.perSubagent.delete(subagentId);
|
|
6461
|
-
this.
|
|
6462
|
-
this.
|
|
6463
|
-
this.
|
|
6464
|
-
this.
|
|
6465
|
-
this.
|
|
6657
|
+
this.retired.subagents += 1;
|
|
6658
|
+
this.retired.input += snap.input;
|
|
6659
|
+
this.retired.output += snap.output;
|
|
6660
|
+
this.retired.cacheRead += snap.cacheRead;
|
|
6661
|
+
this.retired.cacheWrite += snap.cacheWrite;
|
|
6662
|
+
this.retired.cost += snap.cost;
|
|
6466
6663
|
}
|
|
6467
6664
|
/** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
|
|
6468
6665
|
dispose() {
|
|
@@ -6475,7 +6672,8 @@ var FleetUsageAggregator = class {
|
|
|
6475
6672
|
total: { ...this.total },
|
|
6476
6673
|
perSubagent: Object.fromEntries(
|
|
6477
6674
|
Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
|
|
6478
|
-
)
|
|
6675
|
+
),
|
|
6676
|
+
retired: { ...this.retired }
|
|
6479
6677
|
};
|
|
6480
6678
|
}
|
|
6481
6679
|
ensure(subagentId) {
|
|
@@ -6664,11 +6862,15 @@ async function spawn2(host, config, priceLookup) {
|
|
|
6664
6862
|
);
|
|
6665
6863
|
host.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
|
|
6666
6864
|
host.subagentBridges.set(result.subagentId, subagentBridge);
|
|
6865
|
+
const currentSessionId = typeof host.coordinator.sessionOf === "function" ? host.coordinator.sessionOf(
|
|
6866
|
+
result.subagentId
|
|
6867
|
+
) : void 0;
|
|
6667
6868
|
host.fleet.emit({
|
|
6668
6869
|
subagentId: result.subagentId,
|
|
6669
6870
|
ts: Date.now(),
|
|
6670
6871
|
type: "subagent.spawned",
|
|
6671
6872
|
payload: {
|
|
6873
|
+
...currentSessionId ? { sessionId: currentSessionId } : {},
|
|
6672
6874
|
subagentId: result.subagentId,
|
|
6673
6875
|
taskId: "",
|
|
6674
6876
|
// taskId will be set when assign() is called
|
|
@@ -6740,7 +6942,12 @@ var LargeAnswerStore = class _LargeAnswerStore {
|
|
|
6740
6942
|
if (value === void 0 || value === null) {
|
|
6741
6943
|
return { summary: String(value), inline: true };
|
|
6742
6944
|
}
|
|
6743
|
-
|
|
6945
|
+
let serialized;
|
|
6946
|
+
try {
|
|
6947
|
+
serialized = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
|
|
6948
|
+
} catch {
|
|
6949
|
+
serialized = String(value);
|
|
6950
|
+
}
|
|
6744
6951
|
const size = serialized.length;
|
|
6745
6952
|
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
6746
6953
|
if (size <= this.sizeThreshold) {
|
|
@@ -7117,6 +7324,7 @@ var Director = class _Director {
|
|
|
7117
7324
|
leaderContextPressure = 0;
|
|
7118
7325
|
maxLeaderContextLoad;
|
|
7119
7326
|
maxContext;
|
|
7327
|
+
appConfig;
|
|
7120
7328
|
modelMatrix;
|
|
7121
7329
|
workCompleteFlag = false;
|
|
7122
7330
|
btwNotes = new DirectorBtwNotes();
|
|
@@ -7146,6 +7354,7 @@ var Director = class _Director {
|
|
|
7146
7354
|
this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
|
|
7147
7355
|
this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
|
|
7148
7356
|
this.maxContext = opts.maxContext ?? 128e3;
|
|
7357
|
+
this.appConfig = opts.appConfig;
|
|
7149
7358
|
this.modelMatrix = opts.modelMatrix;
|
|
7150
7359
|
this.sessionsRoot = opts.sessionsRoot;
|
|
7151
7360
|
this.directorRunId = opts.directorRunId ?? this.id;
|
|
@@ -7412,8 +7621,14 @@ var Director = class _Director {
|
|
|
7412
7621
|
return subagentId;
|
|
7413
7622
|
}
|
|
7414
7623
|
resolveSpawnModel(config) {
|
|
7624
|
+
const appConfig = typeof this.appConfig === "function" ? this.appConfig() : this.appConfig;
|
|
7415
7625
|
resolveDirectorSpawnModel(config, {
|
|
7416
7626
|
modelMatrix: this.modelMatrix,
|
|
7627
|
+
...appConfig ? { config: appConfig } : {},
|
|
7628
|
+
...config.tier ? { tier: config.tier } : {},
|
|
7629
|
+
onTierResolved: (resolved) => {
|
|
7630
|
+
config.tier = resolved.tier;
|
|
7631
|
+
},
|
|
7417
7632
|
sessionProvider: this.sessionProvider,
|
|
7418
7633
|
sessionModel: this.sessionModel,
|
|
7419
7634
|
statusTracker: this.statusTracker,
|
|
@@ -7518,6 +7733,25 @@ var Director = class _Director {
|
|
|
7518
7733
|
void this.remove(id).catch((err) => this.logShutdownError("terminate_all_remove", err));
|
|
7519
7734
|
}
|
|
7520
7735
|
}
|
|
7736
|
+
/**
|
|
7737
|
+
* Terminate every subagent spawned by ONE session.
|
|
7738
|
+
*
|
|
7739
|
+
* What a tab's Stop button needs. `terminateAll()` is the wrong tool once
|
|
7740
|
+
* several sessions share a director: it would kill three other tabs' fleets
|
|
7741
|
+
* along with this one's. Aborting the leader's run only unwinds workers the
|
|
7742
|
+
* leader is BLOCKED on (the delegate tool terminates those itself); anything
|
|
7743
|
+
* started with `spawn_subagent` + `assign_task` keeps running because nobody
|
|
7744
|
+
* asked it to stop. This is that ask, scoped to the session that owns them.
|
|
7745
|
+
*/
|
|
7746
|
+
async terminateSession(sessionId) {
|
|
7747
|
+
if (!sessionId) return;
|
|
7748
|
+
const ids = this.coordinator.subagentIdsForSession(sessionId);
|
|
7749
|
+
if (ids.length === 0) return;
|
|
7750
|
+
await this.coordinator.stopSession(sessionId);
|
|
7751
|
+
for (const id of ids) {
|
|
7752
|
+
void this.remove(id).catch((err) => this.logShutdownError("terminate_session_remove", err));
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7521
7755
|
async remove(subagentId) {
|
|
7522
7756
|
this.clearSubagentIdleRetirement(subagentId);
|
|
7523
7757
|
this.subagentIdleDelayMs.delete(subagentId);
|
|
@@ -8195,7 +8429,7 @@ function makeFleetStatusTool(opts = {}) {
|
|
|
8195
8429
|
const resolveMailbox = opts.resolveMailbox ?? ((ctx) => getSharedProjectMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
8196
8430
|
return {
|
|
8197
8431
|
name: "fleet_status",
|
|
8198
|
-
description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only.
|
|
8432
|
+
description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. Same-session talk: session_note with the returned id. Durable cross-session mail: mail_send.",
|
|
8199
8433
|
usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
|
|
8200
8434
|
category: "Coordination",
|
|
8201
8435
|
permission: "auto",
|
|
@@ -8626,7 +8860,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
8626
8860
|
});
|
|
8627
8861
|
await this.opts.actions.notifyLeader(
|
|
8628
8862
|
"Fleet rebalanced",
|
|
8629
|
-
`Supervisor moved ${moved.length} queued task(s) off busy worker ${fromWorkerId}: ${moved.join(", ")}
|
|
8863
|
+
`Supervisor moved ${moved.length} queued task(s) off busy worker ${fromWorkerId}: ${moved.join(", ")}.`,
|
|
8864
|
+
fromWorkerId
|
|
8630
8865
|
).catch(() => {
|
|
8631
8866
|
});
|
|
8632
8867
|
}
|
|
@@ -8683,7 +8918,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
8683
8918
|
});
|
|
8684
8919
|
await this.opts.actions.notifyLeader(
|
|
8685
8920
|
"Fleet helper spawned",
|
|
8686
|
-
`Supervisor spawned helper ${res.subagentId} to drain a ${pendingCount}-task backlog
|
|
8921
|
+
`Supervisor spawned helper ${res.subagentId} to drain a ${pendingCount}-task backlog.`,
|
|
8922
|
+
res.subagentId
|
|
8687
8923
|
).catch(() => {
|
|
8688
8924
|
});
|
|
8689
8925
|
} else {
|
|
@@ -8748,7 +8984,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
8748
8984
|
this.emitAction("steer", true, "stuck nudge", s.id);
|
|
8749
8985
|
await this.opts.actions.notifyLeader(
|
|
8750
8986
|
`Worker ${s.name} may be stuck`,
|
|
8751
|
-
`No observable activity for ${Math.round(this.cfg.stuckMs / 1e3)}s. Supervisor nudged it; consider terminate_subagent + reassigning if it stays silent
|
|
8987
|
+
`No observable activity for ${Math.round(this.cfg.stuckMs / 1e3)}s. Supervisor nudged it; consider terminate_subagent + reassigning if it stays silent.`,
|
|
8988
|
+
s.id
|
|
8752
8989
|
).catch(() => {
|
|
8753
8990
|
});
|
|
8754
8991
|
this.record({
|
|
@@ -8799,7 +9036,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
8799
9036
|
this.emitAction("terminate", true, `${streak} consecutive failures`, subagentId);
|
|
8800
9037
|
await this.opts.actions.notifyLeader(
|
|
8801
9038
|
`Worker ${subagentId} terminated`,
|
|
8802
|
-
`Supervisor terminated ${subagentId} after ${streak} consecutive failures. Reassign its pending work
|
|
9039
|
+
`Supervisor terminated ${subagentId} after ${streak} consecutive failures. Reassign its pending work.`,
|
|
9040
|
+
subagentId
|
|
8803
9041
|
).catch(() => {
|
|
8804
9042
|
});
|
|
8805
9043
|
this.record({
|
|
@@ -8821,7 +9059,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
8821
9059
|
this.emitAction("steer", true, "failure streak", subagentId);
|
|
8822
9060
|
await this.opts.actions.notifyLeader(
|
|
8823
9061
|
`Worker ${subagentId} failing repeatedly`,
|
|
8824
|
-
`${streak} consecutive failures. Supervisor steered it; consider reassigning its work if the next task also fails
|
|
9062
|
+
`${streak} consecutive failures. Supervisor steered it; consider reassigning its work if the next task also fails.`,
|
|
9063
|
+
subagentId
|
|
8825
9064
|
).catch(() => {
|
|
8826
9065
|
});
|
|
8827
9066
|
this.record({
|
|
@@ -9101,6 +9340,12 @@ function assertCapability(actor, cap, op) {
|
|
|
9101
9340
|
}
|
|
9102
9341
|
|
|
9103
9342
|
// src/coordination/mail-tools.ts
|
|
9343
|
+
function scopeAgentMailToOwningSession(ctx, to, sessionId) {
|
|
9344
|
+
const owning = ctx.meta["sessionId"];
|
|
9345
|
+
if (typeof owning !== "string" || owning.length === 0) return {};
|
|
9346
|
+
if (to.trim().toLowerCase() !== "leader") return {};
|
|
9347
|
+
return { sessionAffinity: { sessionId } };
|
|
9348
|
+
}
|
|
9104
9349
|
function makeResolver(opts) {
|
|
9105
9350
|
return opts.resolveMailbox ?? ((ctx) => getSharedProjectMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
9106
9351
|
}
|
|
@@ -9204,7 +9449,8 @@ function makeMailSendTool(opts = {}) {
|
|
|
9204
9449
|
body: parsed.body,
|
|
9205
9450
|
priority: parsed.priority,
|
|
9206
9451
|
replyTo: parsed.replyTo,
|
|
9207
|
-
senderSessionId: identity.sessionId
|
|
9452
|
+
senderSessionId: identity.sessionId,
|
|
9453
|
+
...scopeAgentMailToOwningSession(ctx, delivery.to, identity.sessionId)
|
|
9208
9454
|
});
|
|
9209
9455
|
return {
|
|
9210
9456
|
ok: true,
|
|
@@ -9301,6 +9547,61 @@ function makeMailInboxTool(opts = {}) {
|
|
|
9301
9547
|
};
|
|
9302
9548
|
}
|
|
9303
9549
|
|
|
9550
|
+
// src/coordination/session-note-tool.ts
|
|
9551
|
+
var KINDS = ["note", "result", "ask", "steer"];
|
|
9552
|
+
function isKind(value) {
|
|
9553
|
+
return KINDS.includes(value);
|
|
9554
|
+
}
|
|
9555
|
+
function makeSessionNoteTool() {
|
|
9556
|
+
return {
|
|
9557
|
+
name: "session_note",
|
|
9558
|
+
description: 'Send an ephemeral note to the leader or another agent in THIS session. Delivered at their next iteration. Use it for same-session talk (findings, a short ask, a steer). Durable mail remains the durable cross-session channel. to="leader" reaches the session leader; to="@session" fans out to every other live agent in the session; an exact agent id reaches one peer. You never receive your own note.',
|
|
9559
|
+
usageHint: 'session_note to="leader" kind="result" body="file:line \u2014 what it is"',
|
|
9560
|
+
category: "Coordination",
|
|
9561
|
+
permission: "auto",
|
|
9562
|
+
mutating: false,
|
|
9563
|
+
capabilities: [ToolCapabilities.SESSION_NOTE],
|
|
9564
|
+
inputSchema: {
|
|
9565
|
+
type: "object",
|
|
9566
|
+
properties: {
|
|
9567
|
+
to: {
|
|
9568
|
+
type: "string",
|
|
9569
|
+
description: 'Recipient: "leader", "@session", or a live agent id.'
|
|
9570
|
+
},
|
|
9571
|
+
body: { type: "string", description: "The note. Keep it compact." },
|
|
9572
|
+
kind: {
|
|
9573
|
+
type: "string",
|
|
9574
|
+
enum: [...KINDS],
|
|
9575
|
+
description: "note (default), result, ask, or steer."
|
|
9576
|
+
},
|
|
9577
|
+
subject: { type: "string", description: "Optional short subject (e.g. [explore])." }
|
|
9578
|
+
},
|
|
9579
|
+
required: ["to", "body"]
|
|
9580
|
+
},
|
|
9581
|
+
async execute(input, ctx) {
|
|
9582
|
+
const rec = input && typeof input === "object" ? input : {};
|
|
9583
|
+
const to = typeof rec["to"] === "string" ? rec["to"].trim() : "";
|
|
9584
|
+
const body = typeof rec["body"] === "string" ? rec["body"] : "";
|
|
9585
|
+
const subject = typeof rec["subject"] === "string" ? rec["subject"] : void 0;
|
|
9586
|
+
const kindRaw = typeof rec["kind"] === "string" ? rec["kind"].trim().toLowerCase() : "note";
|
|
9587
|
+
const kind = isKind(kindRaw) ? kindRaw : "note";
|
|
9588
|
+
if (!to || !body.trim()) {
|
|
9589
|
+
return { ok: false, delivered: 0, error: "to and body are required" };
|
|
9590
|
+
}
|
|
9591
|
+
const from = typeof ctx.meta["globalAgentId"] === "string" && ctx.meta["globalAgentId"] || ctx.agentId || mailboxIdentityBase(ctx.agentId);
|
|
9592
|
+
const { delivered } = postSessionNote({
|
|
9593
|
+
sessionId: resolveOwningSessionId(ctx),
|
|
9594
|
+
from,
|
|
9595
|
+
to,
|
|
9596
|
+
kind,
|
|
9597
|
+
body,
|
|
9598
|
+
subject
|
|
9599
|
+
});
|
|
9600
|
+
return { ok: delivered > 0, delivered, to, kind };
|
|
9601
|
+
}
|
|
9602
|
+
};
|
|
9603
|
+
}
|
|
9604
|
+
|
|
9304
9605
|
// src/coordination/mailbox-actions.ts
|
|
9305
9606
|
function actionToAckInput(action, input) {
|
|
9306
9607
|
switch (action) {
|
|
@@ -9587,60 +9888,6 @@ function createMailboxHooks(opts) {
|
|
|
9587
9888
|
};
|
|
9588
9889
|
}
|
|
9589
9890
|
|
|
9590
|
-
// src/coordination/mailbox-http-auth.ts
|
|
9591
|
-
import { timingSafeEqual } from "node:crypto";
|
|
9592
|
-
function authorizeMailboxBearerToken(request, expectedToken) {
|
|
9593
|
-
const header = request.headers.authorization;
|
|
9594
|
-
if (typeof header !== "string") return { allowed: false };
|
|
9595
|
-
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
9596
|
-
if (match === null) return { allowed: false };
|
|
9597
|
-
const presented = Buffer.from(match[1] ?? "", "utf8");
|
|
9598
|
-
const expected = Buffer.from(expectedToken, "utf8");
|
|
9599
|
-
if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
|
|
9600
|
-
return { allowed: false };
|
|
9601
|
-
}
|
|
9602
|
-
return { allowed: true, rateLimitKey: expectedToken };
|
|
9603
|
-
}
|
|
9604
|
-
async function authorizePersistedMailboxCredential(request, store) {
|
|
9605
|
-
const parsed = parseCredentialAuthorization(request);
|
|
9606
|
-
if (parsed === void 0) return { allowed: false };
|
|
9607
|
-
return credentialDecision(
|
|
9608
|
-
parsed.credentialId,
|
|
9609
|
-
await store.verifyPersisted(parsed.credentialId, parsed.secret)
|
|
9610
|
-
);
|
|
9611
|
-
}
|
|
9612
|
-
function credentialDecision(credentialId, result) {
|
|
9613
|
-
const credential = result.credential;
|
|
9614
|
-
const projectId = credential?.projectId;
|
|
9615
|
-
if (!result.valid || credential === void 0 || projectId === void 0) {
|
|
9616
|
-
return { allowed: false };
|
|
9617
|
-
}
|
|
9618
|
-
const identitySeparator = credential.principalId.indexOf("@");
|
|
9619
|
-
const role = credential.kind === "agent" ? credential.principalId.slice(0, identitySeparator === -1 ? void 0 : identitySeparator) : void 0;
|
|
9620
|
-
const sessionId = identitySeparator === -1 ? void 0 : credential.principalId.slice(identitySeparator + 1) || void 0;
|
|
9621
|
-
return {
|
|
9622
|
-
allowed: true,
|
|
9623
|
-
rateLimitKey: `cred:${credentialId}`,
|
|
9624
|
-
actor: {
|
|
9625
|
-
actorId: credential.principalId,
|
|
9626
|
-
projectId,
|
|
9627
|
-
kind: credential.kind,
|
|
9628
|
-
capabilities: new Set(credential.capabilities),
|
|
9629
|
-
authMode: "identity-token",
|
|
9630
|
-
recipientAliases: role === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([role]),
|
|
9631
|
-
...role !== void 0 ? { role } : {},
|
|
9632
|
-
...sessionId !== void 0 ? { sessionId } : {}
|
|
9633
|
-
}
|
|
9634
|
-
};
|
|
9635
|
-
}
|
|
9636
|
-
function parseCredentialAuthorization(request) {
|
|
9637
|
-
const header = request.headers.authorization;
|
|
9638
|
-
if (typeof header !== "string") return void 0;
|
|
9639
|
-
const match = /^Credential\s+([^:]+):(.+)$/i.exec(header);
|
|
9640
|
-
if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
|
|
9641
|
-
return { credentialId: match[1], secret: match[2] };
|
|
9642
|
-
}
|
|
9643
|
-
|
|
9644
9891
|
// src/coordination/mailbox-http-validation.ts
|
|
9645
9892
|
var MAILBOX_HTTP_MAX_AGE_CEILING_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
9646
9893
|
var MailboxHttpValidationError = class extends Error {
|
|
@@ -9986,13 +10233,19 @@ function validateAgentRegistration(body, actor) {
|
|
|
9986
10233
|
}
|
|
9987
10234
|
return result;
|
|
9988
10235
|
}
|
|
9989
|
-
function validateAgentHeartbeat(body,
|
|
10236
|
+
function validateAgentHeartbeat(body, actor) {
|
|
9990
10237
|
if (typeof body !== "object" || body === null) {
|
|
9991
10238
|
throw validationError("expected JSON object body");
|
|
9992
10239
|
}
|
|
9993
10240
|
const object = body;
|
|
9994
|
-
if (
|
|
9995
|
-
|
|
10241
|
+
if (actor !== void 0) {
|
|
10242
|
+
rejectConflictingIdentity(object, "agentId", actor.actorId);
|
|
10243
|
+
rejectConflictingOptionalIdentity(object, "sessionId", actor.sessionId);
|
|
10244
|
+
rejectConflictingOptionalIdentity(object, "role", actor.role);
|
|
10245
|
+
}
|
|
10246
|
+
const agentId = actor?.actorId ?? requireString2(object, "agentId");
|
|
10247
|
+
if (actor === void 0) validateReaderId(agentId);
|
|
10248
|
+
const result = { agentId };
|
|
9996
10249
|
const status = optionalString2(object, "status");
|
|
9997
10250
|
const currentTool = optionalString2(object, "currentTool");
|
|
9998
10251
|
const currentTask = optionalString2(object, "currentTask");
|
|
@@ -10013,6 +10266,25 @@ function validateAgentHeartbeat(body, actorId) {
|
|
|
10013
10266
|
}
|
|
10014
10267
|
result.toolCalls = toolCalls;
|
|
10015
10268
|
}
|
|
10269
|
+
const name = optionalString2(object, "name");
|
|
10270
|
+
const pid = optionalNumber(object, "pid");
|
|
10271
|
+
if (name !== void 0) result.name = name;
|
|
10272
|
+
if (pid !== void 0) {
|
|
10273
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
10274
|
+
throw validationError('field "pid" must be a positive integer when present');
|
|
10275
|
+
}
|
|
10276
|
+
result.pid = pid;
|
|
10277
|
+
}
|
|
10278
|
+
if (actor === void 0) {
|
|
10279
|
+
const sessionId = optionalString2(object, "sessionId");
|
|
10280
|
+
const role = optionalString2(object, "role");
|
|
10281
|
+
if (sessionId !== void 0) result.sessionId = sessionId;
|
|
10282
|
+
if (role !== void 0) result.role = role;
|
|
10283
|
+
} else {
|
|
10284
|
+
if (actor.sessionId !== void 0) result.sessionId = actor.sessionId;
|
|
10285
|
+
if (actor.role !== void 0) result.role = actor.role;
|
|
10286
|
+
}
|
|
10287
|
+
result.source = "http";
|
|
10016
10288
|
return result;
|
|
10017
10289
|
}
|
|
10018
10290
|
function validateClientRegistration(body) {
|
|
@@ -10192,6 +10464,60 @@ async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState
|
|
|
10192
10464
|
return { data, count: data.length };
|
|
10193
10465
|
}
|
|
10194
10466
|
|
|
10467
|
+
// src/coordination/mailbox-http-auth.ts
|
|
10468
|
+
import { timingSafeEqual } from "node:crypto";
|
|
10469
|
+
function authorizeMailboxBearerToken(request, expectedToken) {
|
|
10470
|
+
const header = request.headers.authorization;
|
|
10471
|
+
if (typeof header !== "string") return { allowed: false };
|
|
10472
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
10473
|
+
if (match === null) return { allowed: false };
|
|
10474
|
+
const presented = Buffer.from(match[1] ?? "", "utf8");
|
|
10475
|
+
const expected = Buffer.from(expectedToken, "utf8");
|
|
10476
|
+
if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
|
|
10477
|
+
return { allowed: false };
|
|
10478
|
+
}
|
|
10479
|
+
return { allowed: true, rateLimitKey: expectedToken };
|
|
10480
|
+
}
|
|
10481
|
+
async function authorizePersistedMailboxCredential(request, store) {
|
|
10482
|
+
const parsed = parseCredentialAuthorization(request);
|
|
10483
|
+
if (parsed === void 0) return { allowed: false };
|
|
10484
|
+
return credentialDecision(
|
|
10485
|
+
parsed.credentialId,
|
|
10486
|
+
await store.verifyPersisted(parsed.credentialId, parsed.secret)
|
|
10487
|
+
);
|
|
10488
|
+
}
|
|
10489
|
+
function credentialDecision(credentialId, result) {
|
|
10490
|
+
const credential = result.credential;
|
|
10491
|
+
const projectId = credential?.projectId;
|
|
10492
|
+
if (!result.valid || credential === void 0 || projectId === void 0) {
|
|
10493
|
+
return { allowed: false };
|
|
10494
|
+
}
|
|
10495
|
+
const identitySeparator = credential.principalId.indexOf("@");
|
|
10496
|
+
const role = credential.kind === "agent" ? credential.principalId.slice(0, identitySeparator === -1 ? void 0 : identitySeparator) : void 0;
|
|
10497
|
+
const sessionId = identitySeparator === -1 ? void 0 : credential.principalId.slice(identitySeparator + 1) || void 0;
|
|
10498
|
+
return {
|
|
10499
|
+
allowed: true,
|
|
10500
|
+
rateLimitKey: `cred:${credentialId}`,
|
|
10501
|
+
actor: {
|
|
10502
|
+
actorId: credential.principalId,
|
|
10503
|
+
projectId,
|
|
10504
|
+
kind: credential.kind,
|
|
10505
|
+
capabilities: new Set(credential.capabilities),
|
|
10506
|
+
authMode: "identity-token",
|
|
10507
|
+
recipientAliases: role === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([role]),
|
|
10508
|
+
...role !== void 0 ? { role } : {},
|
|
10509
|
+
...sessionId !== void 0 ? { sessionId } : {}
|
|
10510
|
+
}
|
|
10511
|
+
};
|
|
10512
|
+
}
|
|
10513
|
+
function parseCredentialAuthorization(request) {
|
|
10514
|
+
const header = request.headers.authorization;
|
|
10515
|
+
if (typeof header !== "string") return void 0;
|
|
10516
|
+
const match = /^Credential\s+([^:]+):(.+)$/i.exec(header);
|
|
10517
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
|
|
10518
|
+
return { credentialId: match[1], secret: match[2] };
|
|
10519
|
+
}
|
|
10520
|
+
|
|
10195
10521
|
// src/coordination/mailbox-http-sse.ts
|
|
10196
10522
|
var MAX_SSE_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
10197
10523
|
function mailboxEventRecords(event) {
|
|
@@ -10422,7 +10748,10 @@ function createMailboxHttpRouter(options) {
|
|
|
10422
10748
|
response,
|
|
10423
10749
|
access3.status ?? 401,
|
|
10424
10750
|
access3.body ?? {
|
|
10425
|
-
error: {
|
|
10751
|
+
error: {
|
|
10752
|
+
code: "UNAUTHORIZED",
|
|
10753
|
+
message: "invalid or missing authorization credential"
|
|
10754
|
+
}
|
|
10426
10755
|
}
|
|
10427
10756
|
);
|
|
10428
10757
|
return;
|
|
@@ -10480,7 +10809,10 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
10480
10809
|
const requiredCapability = requiredCredentialCapability(method, path12);
|
|
10481
10810
|
if (requiredCapability === void 0) {
|
|
10482
10811
|
writeJson(response, 403, {
|
|
10483
|
-
error: {
|
|
10812
|
+
error: {
|
|
10813
|
+
code: "FORBIDDEN",
|
|
10814
|
+
message: `credential access is not permitted for ${method} ${path12}`
|
|
10815
|
+
}
|
|
10484
10816
|
});
|
|
10485
10817
|
return;
|
|
10486
10818
|
}
|
|
@@ -10613,7 +10945,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
10613
10945
|
return;
|
|
10614
10946
|
}
|
|
10615
10947
|
if (method === "POST" && path12 === "/mailbox/agents/heartbeat") {
|
|
10616
|
-
const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor
|
|
10948
|
+
const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor);
|
|
10617
10949
|
if (actor !== void 0) input.agentId = actor.actorId;
|
|
10618
10950
|
await mailbox.heartbeat(input);
|
|
10619
10951
|
writeJson(response, 200, { ok: true });
|
|
@@ -10786,7 +11118,8 @@ async function getPackagesByAgent(opts, agentId) {
|
|
|
10786
11118
|
const map = /* @__PURE__ */ new Map();
|
|
10787
11119
|
for (const e of log.entries) {
|
|
10788
11120
|
if (e.agentId === agentId) {
|
|
10789
|
-
const
|
|
11121
|
+
const normPath2 = e.manifestPath.replace(/\\/g, "/");
|
|
11122
|
+
const key = `${normPath2}|${e.packageName}`;
|
|
10790
11123
|
map.set(key, e);
|
|
10791
11124
|
}
|
|
10792
11125
|
}
|
|
@@ -11001,12 +11334,14 @@ var DEFAULTS2 = {
|
|
|
11001
11334
|
degradedDurationMs: 3e4,
|
|
11002
11335
|
blockAfterRateLimitHits: 1,
|
|
11003
11336
|
blockAfterFailures: 5,
|
|
11004
|
-
blockDurationMs:
|
|
11337
|
+
blockDurationMs: 12e4,
|
|
11005
11338
|
quotaBlockDurationMs: 9e5,
|
|
11339
|
+
quotaBlockEscalationMs: [18e5, 36e5],
|
|
11006
11340
|
recoverAfterSuccesses: 3,
|
|
11007
11341
|
maxErrorHistory: 50,
|
|
11008
11342
|
quarantineSiblingsOnQuotaExhausted: true
|
|
11009
11343
|
};
|
|
11344
|
+
var NON_QUOTA_HINT_CAP_FACTOR = 3;
|
|
11010
11345
|
var ProviderModelStatusTracker = class {
|
|
11011
11346
|
cfg;
|
|
11012
11347
|
map = /* @__PURE__ */ new Map();
|
|
@@ -11015,6 +11350,7 @@ var ProviderModelStatusTracker = class {
|
|
|
11015
11350
|
events;
|
|
11016
11351
|
constructor(opts) {
|
|
11017
11352
|
this.cfg = { ...DEFAULTS2, ...opts?.config };
|
|
11353
|
+
this.cfg.quotaBlockEscalationMs = [...this.cfg.quotaBlockEscalationMs];
|
|
11018
11354
|
this.events = opts?.events;
|
|
11019
11355
|
}
|
|
11020
11356
|
// ── Public API ──────────────────────────────────────────────────────────
|
|
@@ -11032,6 +11368,7 @@ var ProviderModelStatusTracker = class {
|
|
|
11032
11368
|
const key = pairKey(providerId, model);
|
|
11033
11369
|
const s = this.getOrCreate(key, providerId, model);
|
|
11034
11370
|
s.consecutiveFailures = 0;
|
|
11371
|
+
s.quotaBlockStreak = 0;
|
|
11035
11372
|
s.consecutiveSuccesses += 1;
|
|
11036
11373
|
s.totalSuccesses += 1;
|
|
11037
11374
|
s.lastSuccessAt = Date.now();
|
|
@@ -11085,7 +11422,13 @@ var ProviderModelStatusTracker = class {
|
|
|
11085
11422
|
const quotaExhausted = kind === "quota_exhausted" || isQuotaExhausted(kind, status, message);
|
|
11086
11423
|
const providerWideQuota = quotaExhausted && !ROUTE_SCOPED_QUOTA_RE.test(message);
|
|
11087
11424
|
const proseHintMs = quotaExhausted || kind === "rate_limit" ? parseResetHintMs(message, now) : void 0;
|
|
11088
|
-
const
|
|
11425
|
+
const rawHintMs = meta?.retryAfterMs && meta.retryAfterMs > 0 ? meta.retryAfterMs : proseHintMs;
|
|
11426
|
+
const effectiveRetryAfterMs = rawHintMs && rawHintMs > 0 ? quotaExhausted ? (
|
|
11427
|
+
// Quota keeps the provider-published reset, but a corrupt or
|
|
11428
|
+
// absurd structured Retry-After still cannot park a model
|
|
11429
|
+
// beyond the prose-hint maximum.
|
|
11430
|
+
Math.min(rawHintMs, MAX_RESET_HINT_MS)
|
|
11431
|
+
) : Math.min(rawHintMs, this.cfg.blockDurationMs * NON_QUOTA_HINT_CAP_FACTOR) : void 0;
|
|
11089
11432
|
const entry = Object.freeze({
|
|
11090
11433
|
timestamp: now,
|
|
11091
11434
|
kind,
|
|
@@ -11105,7 +11448,8 @@ var ProviderModelStatusTracker = class {
|
|
|
11105
11448
|
if (quotaExhausted) {
|
|
11106
11449
|
newState = "blocked";
|
|
11107
11450
|
reason = "quota_exhausted";
|
|
11108
|
-
s.
|
|
11451
|
+
s.quotaBlockStreak += 1;
|
|
11452
|
+
s.stateExpiresAt = now + this.quotaBlockDurationForStreak(s.quotaBlockStreak);
|
|
11109
11453
|
} else if (endpointUnreachable) {
|
|
11110
11454
|
newState = "blocked";
|
|
11111
11455
|
reason = "endpoint_unreachable";
|
|
@@ -11131,13 +11475,18 @@ var ProviderModelStatusTracker = class {
|
|
|
11131
11475
|
}
|
|
11132
11476
|
if (newState !== "healthy" && effectiveRetryAfterMs && effectiveRetryAfterMs > 0) {
|
|
11133
11477
|
const hintExpiry = now + effectiveRetryAfterMs;
|
|
11134
|
-
if (
|
|
11478
|
+
if (quotaExhausted) {
|
|
11479
|
+
s.stateExpiresAt = hintExpiry;
|
|
11480
|
+
} else if (s.stateExpiresAt === null || hintExpiry > s.stateExpiresAt) {
|
|
11135
11481
|
s.stateExpiresAt = hintExpiry;
|
|
11136
11482
|
}
|
|
11137
11483
|
}
|
|
11138
11484
|
if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && s.stateExpiresAt !== null) {
|
|
11139
11485
|
const previous = this.providerQuotaBlocks.get(providerId) ?? 0;
|
|
11140
|
-
this.providerQuotaBlocks.set(
|
|
11486
|
+
this.providerQuotaBlocks.set(
|
|
11487
|
+
providerId,
|
|
11488
|
+
Math.max(previous, this.fanOutExpiryFor(now, s.stateExpiresAt))
|
|
11489
|
+
);
|
|
11141
11490
|
}
|
|
11142
11491
|
if (s.state !== newState) {
|
|
11143
11492
|
const oldState = s.state;
|
|
@@ -11147,10 +11496,26 @@ var ProviderModelStatusTracker = class {
|
|
|
11147
11496
|
this.emitStatusChanged(providerId, model, s.state, s.state, "cooldown_extended");
|
|
11148
11497
|
}
|
|
11149
11498
|
if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && newState === "blocked") {
|
|
11150
|
-
this.quarantineSiblings(
|
|
11499
|
+
this.quarantineSiblings(
|
|
11500
|
+
providerId,
|
|
11501
|
+
model,
|
|
11502
|
+
status,
|
|
11503
|
+
now,
|
|
11504
|
+
this.fanOutExpiryFor(now, s.stateExpiresAt ?? now)
|
|
11505
|
+
);
|
|
11151
11506
|
}
|
|
11152
11507
|
return newState;
|
|
11153
11508
|
}
|
|
11509
|
+
/**
|
|
11510
|
+
* Quarantine fan-out expiry: the fixed quota cooldown, never the
|
|
11511
|
+
* hint-extended trigger expiry. A weekly-cap reset time applies to the
|
|
11512
|
+
* model that published it; sibling models (and unseen pairs behind the
|
|
11513
|
+
* provider-wide gate) re-open after the base cooldown so one misclassified
|
|
11514
|
+
* or hint-stretched failure cannot silence a whole provider for hours.
|
|
11515
|
+
*/
|
|
11516
|
+
fanOutExpiryFor(now, triggerExpiry) {
|
|
11517
|
+
return Math.min(triggerExpiry, now + this.cfg.quotaBlockDurationMs);
|
|
11518
|
+
}
|
|
11154
11519
|
/**
|
|
11155
11520
|
* Mark every other tracked pair on the same provider as blocked with the
|
|
11156
11521
|
* same expiry as the triggering pair. Only pairs already known to the
|
|
@@ -11247,6 +11612,7 @@ var ProviderModelStatusTracker = class {
|
|
|
11247
11612
|
s.state = "healthy";
|
|
11248
11613
|
s.stateExpiresAt = null;
|
|
11249
11614
|
s.consecutiveFailures = 0;
|
|
11615
|
+
s.quotaBlockStreak = 0;
|
|
11250
11616
|
s.rateLimitHits = 0;
|
|
11251
11617
|
s.overloadedHits = 0;
|
|
11252
11618
|
s.serverErrors = 0;
|
|
@@ -11328,6 +11694,8 @@ var ProviderModelStatusTracker = class {
|
|
|
11328
11694
|
/**
|
|
11329
11695
|
* Release one entry for an immediate half-open probe on its next real use.
|
|
11330
11696
|
* History and totals are retained so operators do not lose diagnostics.
|
|
11697
|
+
* The quota escalation streak is also retained on purpose: a probe that
|
|
11698
|
+
* fails with quota exhaustion again keeps climbing the ladder.
|
|
11331
11699
|
*/
|
|
11332
11700
|
retryNow(providerId, model) {
|
|
11333
11701
|
({ providerId, model } = statusIdentity(providerId, model));
|
|
@@ -11470,13 +11838,29 @@ var ProviderModelStatusTracker = class {
|
|
|
11470
11838
|
);
|
|
11471
11839
|
if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && !ROUTE_SCOPED_QUOTA_RE.test(s.lastErrorMessage ?? "") && s.state === "blocked" && s.stateExpiresAt !== null) {
|
|
11472
11840
|
const previous = this.providerQuotaBlocks.get(providerId) ?? 0;
|
|
11473
|
-
this.providerQuotaBlocks.set(
|
|
11841
|
+
this.providerQuotaBlocks.set(
|
|
11842
|
+
providerId,
|
|
11843
|
+
Math.max(previous, this.fanOutExpiryFor(now, s.stateExpiresAt))
|
|
11844
|
+
);
|
|
11474
11845
|
}
|
|
11475
11846
|
restored += 1;
|
|
11476
11847
|
}
|
|
11477
11848
|
return restored;
|
|
11478
11849
|
}
|
|
11479
11850
|
// ── Private helpers ─────────────────────────────────────────────────────
|
|
11851
|
+
/**
|
|
11852
|
+
* Cooldown for the Nth consecutive quota block: the base duration for the
|
|
11853
|
+
* first block, then the escalation ladder for repeats, capped at the last
|
|
11854
|
+
* ladder entry. A provider-published reset hint bypasses this entirely
|
|
11855
|
+
* (the pair is closed until that exact time instead).
|
|
11856
|
+
*/
|
|
11857
|
+
quotaBlockDurationForStreak(streak) {
|
|
11858
|
+
if (streak <= 1) return this.cfg.quotaBlockDurationMs;
|
|
11859
|
+
const ladder = this.cfg.quotaBlockEscalationMs;
|
|
11860
|
+
if (ladder.length === 0) return this.cfg.quotaBlockDurationMs;
|
|
11861
|
+
const tier = Math.min(streak - 2, ladder.length - 1);
|
|
11862
|
+
return ladder[tier] ?? this.cfg.quotaBlockDurationMs;
|
|
11863
|
+
}
|
|
11480
11864
|
getOrCreate(key, _providerId, _model) {
|
|
11481
11865
|
let s = this.map.get(key);
|
|
11482
11866
|
if (!s) {
|
|
@@ -11494,6 +11878,7 @@ var ProviderModelStatusTracker = class {
|
|
|
11494
11878
|
firstFailureAt: null,
|
|
11495
11879
|
lastFailureAt: null,
|
|
11496
11880
|
stateExpiresAt: null,
|
|
11881
|
+
quotaBlockStreak: 0,
|
|
11497
11882
|
lastErrorKind: null,
|
|
11498
11883
|
lastErrorMessage: null,
|
|
11499
11884
|
lastErrorStatus: null,
|
|
@@ -11534,6 +11919,7 @@ var ProviderModelStatusTracker = class {
|
|
|
11534
11919
|
emitStatusChanged(providerId, model, oldState, newState, reason) {
|
|
11535
11920
|
if (!this.events) return;
|
|
11536
11921
|
try {
|
|
11922
|
+
const entry = this.map.get(pairKey(providerId, model));
|
|
11537
11923
|
this.events.emit("provider.status_changed", {
|
|
11538
11924
|
providerId,
|
|
11539
11925
|
model,
|
|
@@ -11541,7 +11927,15 @@ var ProviderModelStatusTracker = class {
|
|
|
11541
11927
|
newState,
|
|
11542
11928
|
reason,
|
|
11543
11929
|
timestamp: Date.now(),
|
|
11544
|
-
stateExpiresAt:
|
|
11930
|
+
stateExpiresAt: entry?.stateExpiresAt ?? void 0,
|
|
11931
|
+
// Error context for durable audit logs (who/what/why): the failure
|
|
11932
|
+
// that led to this state, with the session/agent that hit it. Nulls
|
|
11933
|
+
// normalize to undefined so JSON serialization drops them.
|
|
11934
|
+
lastErrorKind: entry?.lastErrorKind ?? void 0,
|
|
11935
|
+
lastErrorStatus: entry?.lastErrorStatus ?? void 0,
|
|
11936
|
+
lastErrorMessage: entry?.lastErrorMessage ?? void 0,
|
|
11937
|
+
lastSessionId: entry?.lastSessionId ?? void 0,
|
|
11938
|
+
lastAgentId: entry?.lastAgentId ?? void 0
|
|
11545
11939
|
});
|
|
11546
11940
|
} catch {
|
|
11547
11941
|
}
|
|
@@ -11686,7 +12080,7 @@ async function finalize(projectDir, tentative, boundPort) {
|
|
|
11686
12080
|
url: `http://${tentative.host}:${boundPort}`
|
|
11687
12081
|
};
|
|
11688
12082
|
await atomicWriteJson(lockPath, finalized);
|
|
11689
|
-
await
|
|
12083
|
+
await atomicWriteString(tokenPath, finalized.token);
|
|
11690
12084
|
return finalized;
|
|
11691
12085
|
}
|
|
11692
12086
|
async function release(projectDir, generation) {
|
|
@@ -11735,12 +12129,11 @@ async function readLiveLock(projectDir) {
|
|
|
11735
12129
|
}
|
|
11736
12130
|
return { kind: "absent" };
|
|
11737
12131
|
}
|
|
11738
|
-
async function
|
|
12132
|
+
async function atomicWriteString(targetPath, content) {
|
|
11739
12133
|
const dir = path8.dirname(targetPath);
|
|
11740
12134
|
await fsp7.mkdir(dir, { recursive: true });
|
|
11741
12135
|
const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
11742
|
-
|
|
11743
|
-
await fsp7.writeFile(tmp, body, { mode: 384 });
|
|
12136
|
+
await fsp7.writeFile(tmp, content, { mode: 384 });
|
|
11744
12137
|
try {
|
|
11745
12138
|
await fsp7.rename(tmp, targetPath);
|
|
11746
12139
|
} catch (err) {
|
|
@@ -11748,6 +12141,9 @@ async function atomicWriteJson(targetPath, value) {
|
|
|
11748
12141
|
throw err;
|
|
11749
12142
|
}
|
|
11750
12143
|
}
|
|
12144
|
+
async function atomicWriteJson(targetPath, value) {
|
|
12145
|
+
await atomicWriteString(targetPath, JSON.stringify(value, null, 2) + "\n");
|
|
12146
|
+
}
|
|
11751
12147
|
async function isProcessAlive(pid) {
|
|
11752
12148
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
11753
12149
|
if (pid === process.pid) return true;
|
|
@@ -11780,6 +12176,7 @@ async function probeHealthz(url) {
|
|
|
11780
12176
|
try {
|
|
11781
12177
|
const ctrl = new AbortController();
|
|
11782
12178
|
const t = setTimeout(() => ctrl.abort(), 500);
|
|
12179
|
+
if (typeof t.unref === "function") t.unref();
|
|
11783
12180
|
const res = await fetch(`${url}/healthz`, {
|
|
11784
12181
|
signal: ctrl.signal,
|
|
11785
12182
|
redirect: "manual"
|
|
@@ -12212,8 +12609,11 @@ var AdaptiveConcurrencyController = class {
|
|
|
12212
12609
|
}
|
|
12213
12610
|
notifyStateChange() {
|
|
12214
12611
|
const state = this.getState();
|
|
12215
|
-
for (const handler of this.stateChangeHandlers) {
|
|
12216
|
-
|
|
12612
|
+
for (const handler of [...this.stateChangeHandlers]) {
|
|
12613
|
+
try {
|
|
12614
|
+
handler(state);
|
|
12615
|
+
} catch {
|
|
12616
|
+
}
|
|
12217
12617
|
}
|
|
12218
12618
|
}
|
|
12219
12619
|
/**
|
|
@@ -12309,14 +12709,28 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
12309
12709
|
*
|
|
12310
12710
|
* Entries already in the ring win: a live subagent's in-memory segment is
|
|
12311
12711
|
* newer than its JSONL, which is only written when a segment closes.
|
|
12712
|
+
*
|
|
12713
|
+
* `only` restricts BOTH the disk scan and the returned set to a known list of
|
|
12714
|
+
* subagent ids. This is how a session gets ITS OWN subagents back: the
|
|
12715
|
+
* transcripts directory is shared by every session of the project, so the
|
|
12716
|
+
* unfiltered form hands each of four open tabs the union of all four tabs'
|
|
12717
|
+
* workers. The caller derives the list from the session's own journal
|
|
12718
|
+
* (`deriveSessionAgents`), which is the only record that says which agents
|
|
12719
|
+
* belong to which session.
|
|
12312
12720
|
*/
|
|
12313
|
-
async loadSessionsFromDisk() {
|
|
12721
|
+
async loadSessionsFromDisk(only) {
|
|
12722
|
+
const wanted = only === void 0 ? void 0 : new Set(only);
|
|
12723
|
+
if (wanted?.size === 0) return [];
|
|
12314
12724
|
let subagentIds;
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12725
|
+
if (wanted) {
|
|
12726
|
+
subagentIds = [...wanted];
|
|
12727
|
+
} else {
|
|
12728
|
+
try {
|
|
12729
|
+
const dirents = await fs3.readdir(this._transcriptsDir, { withFileTypes: true });
|
|
12730
|
+
subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
12731
|
+
} catch {
|
|
12732
|
+
return this.getAllSessions();
|
|
12733
|
+
}
|
|
12320
12734
|
}
|
|
12321
12735
|
for (const subagentId of subagentIds) {
|
|
12322
12736
|
if (this._sessions.has(subagentId)) continue;
|
|
@@ -12334,7 +12748,8 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
12334
12748
|
transcript: transcript.slice(-this._maxEntries)
|
|
12335
12749
|
});
|
|
12336
12750
|
}
|
|
12337
|
-
|
|
12751
|
+
const all = this.getAllSessions();
|
|
12752
|
+
return wanted ? all.filter((session) => wanted.has(session.subagentId)) : all;
|
|
12338
12753
|
}
|
|
12339
12754
|
async _readTranscriptFile(subagentId) {
|
|
12340
12755
|
const file = path9.join(this._transcriptsDir, subagentId, "transcript.jsonl");
|
|
@@ -12678,7 +13093,8 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
12678
13093
|
this._writeDrain = drain;
|
|
12679
13094
|
}
|
|
12680
13095
|
async _appendToFile(subagentId, line) {
|
|
12681
|
-
const
|
|
13096
|
+
const cleanId = subagentId.replace(/\\/g, "/");
|
|
13097
|
+
const dir = path9.join(this._transcriptsDir, cleanId);
|
|
12682
13098
|
if (!this._ensuredDirs.has(dir)) {
|
|
12683
13099
|
await fs3.mkdir(dir, { recursive: true });
|
|
12684
13100
|
this._ensuredDirs.add(dir);
|
|
@@ -13416,6 +13832,10 @@ var KnowledgeGraph = class _KnowledgeGraph {
|
|
|
13416
13832
|
this._trackSeq(parsed.node.id);
|
|
13417
13833
|
this._addToIndex(parsed.node, this._indexKeys(parsed.node));
|
|
13418
13834
|
} else {
|
|
13835
|
+
const oldNode = this.nodes.get(parsed.id);
|
|
13836
|
+
if (oldNode) {
|
|
13837
|
+
this._removeFromIndex(oldNode, this._indexKeys(oldNode));
|
|
13838
|
+
}
|
|
13419
13839
|
this.nodes.set(parsed.id, parsed);
|
|
13420
13840
|
this._trackSeq(parsed.id);
|
|
13421
13841
|
this._addToIndex(parsed, this._indexKeys(parsed));
|
|
@@ -14570,7 +14990,6 @@ var ChangeManager = class {
|
|
|
14570
14990
|
});
|
|
14571
14991
|
this.appliedChanges.set(appliedChangeId, rollback.id);
|
|
14572
14992
|
await this.graph.update(appliedChangeId, {
|
|
14573
|
-
rolledBackAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14574
14993
|
rollbackReason: reason
|
|
14575
14994
|
});
|
|
14576
14995
|
this._emit("change:rollback_proposed", {
|
|
@@ -16332,6 +16751,8 @@ export {
|
|
|
16332
16751
|
BrainTraceRecorder,
|
|
16333
16752
|
readBrainTrace,
|
|
16334
16753
|
CollaborationBus,
|
|
16754
|
+
collabPauseMiddleware,
|
|
16755
|
+
collabInjectMiddleware,
|
|
16335
16756
|
recordFileAction,
|
|
16336
16757
|
getLastAuthor,
|
|
16337
16758
|
getFileHistory,
|
|
@@ -16350,10 +16771,10 @@ export {
|
|
|
16350
16771
|
FleetSpawnBudgetError,
|
|
16351
16772
|
FleetCostCapError,
|
|
16352
16773
|
FleetTokenCapError,
|
|
16353
|
-
setKanbanDispatch,
|
|
16354
|
-
kanbanDispatch,
|
|
16355
16774
|
setKanbanBoundaryOps,
|
|
16356
16775
|
kanbanBoundaryOps,
|
|
16776
|
+
setKanbanDispatch,
|
|
16777
|
+
kanbanDispatch,
|
|
16357
16778
|
parseTaskBoundary,
|
|
16358
16779
|
renderTaskBoundaryBlock,
|
|
16359
16780
|
composeBoundedTaskDescription,
|
|
@@ -16369,8 +16790,8 @@ export {
|
|
|
16369
16790
|
makeCollabDebugTool,
|
|
16370
16791
|
makeFleetEmitTool,
|
|
16371
16792
|
makeWorkCompleteTool,
|
|
16372
|
-
makeQualityGateTool,
|
|
16373
16793
|
makeMutationTestTool,
|
|
16794
|
+
makeQualityGateTool,
|
|
16374
16795
|
makeSpawnTool,
|
|
16375
16796
|
makeKanbanQueueTool,
|
|
16376
16797
|
DEFAULT_DIRECTOR_PREAMBLE,
|
|
@@ -16408,6 +16829,7 @@ export {
|
|
|
16408
16829
|
parseMailboxAckInput,
|
|
16409
16830
|
makeMailSendTool,
|
|
16410
16831
|
makeMailInboxTool,
|
|
16832
|
+
makeSessionNoteTool,
|
|
16411
16833
|
actionToAckInput,
|
|
16412
16834
|
MAILBOX_HEALTH_DEFAULT_INTERVAL_MS,
|
|
16413
16835
|
MAILBOX_HEALTH_DEFAULT_TIMEOUT_MS,
|
|
@@ -16418,9 +16840,9 @@ export {
|
|
|
16418
16840
|
buildRecoveryAlert,
|
|
16419
16841
|
validateWatchdogOptions,
|
|
16420
16842
|
createMailboxHooks,
|
|
16843
|
+
MAILBOX_HTTP_MAX_AGE_CEILING_MS,
|
|
16421
16844
|
authorizeMailboxBearerToken,
|
|
16422
16845
|
authorizePersistedMailboxCredential,
|
|
16423
|
-
MAILBOX_HTTP_MAX_AGE_CEILING_MS,
|
|
16424
16846
|
MAILBOX_HTTP_RATE_LIMIT_PER_MINUTE,
|
|
16425
16847
|
MAILBOX_HTTP_RATE_LIMIT_WINDOW_MS,
|
|
16426
16848
|
MailboxHttpRateLimiter,
|
|
@@ -16445,8 +16867,6 @@ export {
|
|
|
16445
16867
|
release,
|
|
16446
16868
|
readLiveLock,
|
|
16447
16869
|
startTechStackConsumer,
|
|
16448
|
-
collabPauseMiddleware,
|
|
16449
|
-
collabInjectMiddleware,
|
|
16450
16870
|
AdaptiveConcurrencyController,
|
|
16451
16871
|
AgentMonitorService,
|
|
16452
16872
|
createAgentMonitorService,
|
|
@@ -16461,4 +16881,4 @@ export {
|
|
|
16461
16881
|
AgentStatusTracker,
|
|
16462
16882
|
FleetNotifier
|
|
16463
16883
|
};
|
|
16464
|
-
//# sourceMappingURL=chunk-
|
|
16884
|
+
//# sourceMappingURL=chunk-45WI72ZB.js.map
|