@drisp/cli 0.5.23 → 0.5.26

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.
@@ -12,29 +12,33 @@ import {
12
12
  isToolEvent,
13
13
  isValidHookEventEnvelope,
14
14
  resolveHookSocketPath
15
- } from "./chunk-BTKQ67RE.js";
15
+ } from "./chunk-QYB6N2OT.js";
16
16
  import {
17
17
  TransportUnreachableError,
18
18
  createUdsClientTransport,
19
+ createWsClientTransport,
19
20
  generateChannelRequestId,
21
+ openVersionedDb,
20
22
  readDashboardClientConfig,
21
23
  refreshDashboardAccessToken,
22
24
  resolveGatewayPaths,
23
- traceGatewayFrame,
24
25
  trackGatewayTransportReconnect,
25
- writeGatewayTrace
26
- } from "./chunk-BTY7MYYT.js";
26
+ writeGatewayTrace,
27
+ wsClientOptionsForEndpoint
28
+ } from "./chunk-3U37HT4T.js";
27
29
  import {
28
30
  compileWorkflowPlan,
29
31
  createWorkflowRunner,
30
32
  installWorkflowFromSource,
33
+ isMarketplaceRef,
31
34
  readConfig,
32
35
  readGlobalConfig,
33
36
  resolveActiveWorkflow,
37
+ resolveMarketplacePlugin,
34
38
  resolveWorkflow,
35
39
  resolveWorkflowInstall,
36
40
  resolveWorkflowPlugins
37
- } from "./chunk-JHSADKDJ.js";
41
+ } from "./chunk-73V7GXV6.js";
38
42
 
39
43
  // src/infra/daemon/stateDir.ts
40
44
  import fs from "fs";
@@ -237,6 +241,16 @@ var RULES = {
237
241
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
238
242
  canBlock: false
239
243
  },
244
+ // Observation-only: the batch fires after every call in it has already
245
+ // resolved, so nothing downstream produces a decision or block for it. The
246
+ // claim is not cosmetic — hook dispatch keeps every decision-capable event
247
+ // on Claude's critical path, so a false `canBlock` would make every
248
+ // parallel tool batch wait on the forwarder for a reply never sent.
249
+ "tool.batch": {
250
+ expectsDecision: false,
251
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
252
+ canBlock: false
253
+ },
240
254
  "tool.delta": {
241
255
  expectsDecision: false,
242
256
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
@@ -287,6 +301,16 @@ var RULES = {
287
301
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
288
302
  canBlock: true
289
303
  },
304
+ // Observation-only: nothing in the pipeline produces a decision or a block
305
+ // for an expansion, so it must not claim `canBlock`. The claim is not
306
+ // cosmetic — hook dispatch keeps every decision-capable event on Claude's
307
+ // critical path, so a false `canBlock` would make every slash command wait
308
+ // on the forwarder for a reply that is never sent.
309
+ "prompt.expansion": {
310
+ expectsDecision: false,
311
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
312
+ canBlock: false
313
+ },
290
314
  "turn.start": {
291
315
  expectsDecision: false,
292
316
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
@@ -419,6 +443,15 @@ function asRecord(value) {
419
443
  }
420
444
  return {};
421
445
  }
446
+ function readToolBatchCalls(value) {
447
+ if (!Array.isArray(value)) return [];
448
+ return value.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => asRecord(entry)).map((call) => ({
449
+ tool_name: call["tool_name"],
450
+ tool_input: call["tool_input"],
451
+ tool_use_id: call["tool_use_id"],
452
+ tool_response: call["tool_response"]
453
+ }));
454
+ }
422
455
  function translateClaudeEnvelope(envelope) {
423
456
  const payload = asRecord(envelope.payload);
424
457
  let toolName;
@@ -443,7 +476,9 @@ function translateClaudeEnvelope(envelope) {
443
476
  kind: "session.start",
444
477
  data: {
445
478
  source: payload["source"],
446
- agent_type: payload["agent_type"]
479
+ model: payload["model"],
480
+ agent_type: payload["agent_type"],
481
+ session_title: payload["session_title"]
447
482
  }
448
483
  };
449
484
  case "SessionEnd":
@@ -457,6 +492,22 @@ function translateClaudeEnvelope(envelope) {
457
492
  return {
458
493
  kind: "user.prompt",
459
494
  data: {
495
+ prompt: payload["prompt"],
496
+ cwd: payload["cwd"],
497
+ permission_mode: payload["permission_mode"]
498
+ }
499
+ };
500
+ // Fires immediately before UserPromptSubmit, sharing its prompt_id,
501
+ // when a typed command expands into a prompt. Field names are pinned
502
+ // to the payload captured in the #116 spike.
503
+ case "UserPromptExpansion":
504
+ return {
505
+ kind: "prompt.expansion",
506
+ data: {
507
+ expansion_type: payload["expansion_type"],
508
+ command_name: payload["command_name"],
509
+ command_args: payload["command_args"],
510
+ command_source: payload["command_source"],
460
511
  prompt: payload["prompt"],
461
512
  permission_mode: payload["permission_mode"]
462
513
  }
@@ -484,6 +535,18 @@ function translateClaudeEnvelope(envelope) {
484
535
  tool_response: payload["tool_response"]
485
536
  }
486
537
  };
538
+ // Fires once per assistant tool batch, after every PostToolUse in it.
539
+ // Field names are pinned to the payload captured in the #116 spike;
540
+ // note that `tool_calls[].tool_response` is a STRING there, unlike
541
+ // PostToolUse's structured `tool_response`.
542
+ case "PostToolBatch":
543
+ return {
544
+ kind: "tool.batch",
545
+ data: {
546
+ tool_calls: readToolBatchCalls(payload["tool_calls"]),
547
+ permission_mode: payload["permission_mode"]
548
+ }
549
+ };
487
550
  case "PostToolUseFailure":
488
551
  return {
489
552
  kind: "tool.failure",
@@ -494,7 +557,10 @@ function translateClaudeEnvelope(envelope) {
494
557
  tool_input: payload["tool_input"] ?? {},
495
558
  tool_use_id: toolUseId,
496
559
  error: payload["error"],
497
- is_interrupt: payload["is_interrupt"]
560
+ is_interrupt: payload["is_interrupt"],
561
+ exit_code: payload["exit_code"],
562
+ output: payload["output"],
563
+ error_code: payload["error_code"]
498
564
  }
499
565
  };
500
566
  case "PermissionRequest":
@@ -720,6 +786,12 @@ function buildClaudeDisplay(kind, data) {
720
786
  }
721
787
 
722
788
  // src/harnesses/claude/runtime/mapper.ts
789
+ function readEffortLevel(payload) {
790
+ const effort = payload["effort"];
791
+ if (typeof effort !== "object" || effort === null) return void 0;
792
+ const level = effort["level"];
793
+ return typeof level === "string" ? level : void 0;
794
+ }
723
795
  function mapEnvelopeToRuntimeEvent(envelope) {
724
796
  const payload = envelope.payload;
725
797
  const safePayload = typeof payload === "object" && payload !== null ? payload : { value: payload };
@@ -737,6 +809,13 @@ function mapEnvelopeToRuntimeEvent(envelope) {
737
809
  data: translated.data,
738
810
  hookName: envelope.hook_event_name,
739
811
  sessionId: envelope.session_id,
812
+ // prompt_id is a Claude common input field (v2.1.196+) on every hook
813
+ // payload; carried harness-neutrally as the Prompt identity (ADR 0009).
814
+ promptId: safePayloadRecord["prompt_id"],
815
+ // `effort` is likewise a common input field ({level}), observed on
816
+ // PostToolUse-class payloads and never on SessionStart, so it is read here
817
+ // rather than in any one hook's translation.
818
+ effortLevel: readEffortLevel(safePayloadRecord),
740
819
  toolName: translated.toolName,
741
820
  toolUseId: translated.toolUseId,
742
821
  agentId: translated.agentId,
@@ -854,9 +933,23 @@ function extractResultText(content) {
854
933
  }
855
934
  return parts.join("\n");
856
935
  }
857
- function createStreamJsonToolParser(onToolResult, onToolUse) {
936
+ function extractAssistantText(content) {
937
+ if (!Array.isArray(content)) return "";
938
+ const parts = [];
939
+ for (const block of content) {
940
+ if (typeof block === "object" && block !== null) {
941
+ const rec = block;
942
+ if (rec["type"] === "text" && typeof rec["text"] === "string") {
943
+ parts.push(rec["text"]);
944
+ }
945
+ }
946
+ }
947
+ return parts.join("");
948
+ }
949
+ function createStreamJsonToolParser(onToolResult, onToolUse, onMessageDelta, onMessageComplete) {
858
950
  let buffer = "";
859
951
  const toolNameById = /* @__PURE__ */ new Map();
952
+ let currentMessageId;
860
953
  function processLine(line) {
861
954
  const trimmed = line.trim();
862
955
  if (!trimmed) return;
@@ -867,6 +960,19 @@ function createStreamJsonToolParser(onToolResult, onToolUse) {
867
960
  return;
868
961
  }
869
962
  const record = parsed.type === "stream_event" && parsed.event ? parsed.event : parsed;
963
+ if (record.type === "message_start") {
964
+ const id = record.message?.["id"];
965
+ if (typeof id === "string") currentMessageId = id;
966
+ }
967
+ if (record.type === "content_block_delta") {
968
+ const delta = record["delta"];
969
+ if (typeof delta === "object" && delta !== null) {
970
+ const d = delta;
971
+ if (d["type"] === "text_delta" && typeof d["text"] === "string") {
972
+ onMessageDelta?.({ item_id: currentMessageId, delta: d["text"] });
973
+ }
974
+ }
975
+ }
870
976
  const contentBlocks = (record.type === "assistant" ? record.message?.content : record.type === "message" && record.role === "assistant" ? record.content : void 0) ?? [];
871
977
  for (const block of contentBlocks) {
872
978
  if (typeof block !== "object" || block === null) continue;
@@ -876,6 +982,14 @@ function createStreamJsonToolParser(onToolResult, onToolUse) {
876
982
  onToolUse?.({ tool_use_id: rec["id"], tool_name: rec["name"] });
877
983
  }
878
984
  }
985
+ if (record.type === "assistant" || record.type === "message" && record.role === "assistant") {
986
+ const messageText = extractAssistantText(contentBlocks);
987
+ if (messageText) {
988
+ const idValue = record.type === "assistant" ? record.message?.["id"] : record["id"];
989
+ const itemId = typeof idValue === "string" ? idValue : currentMessageId;
990
+ onMessageComplete?.({ item_id: itemId, message: messageText });
991
+ }
992
+ }
879
993
  if (record.type === "tool_result") {
880
994
  const toolUseId = typeof record.tool_use_id === "string" ? record.tool_use_id : void 0;
881
995
  const text = extractResultText(record.content);
@@ -974,6 +1088,38 @@ function createServer2(opts) {
974
1088
  },
975
1089
  () => {
976
1090
  transportStats.streamToolUses++;
1091
+ },
1092
+ (messageDelta) => {
1093
+ emit({
1094
+ id: `stream-msg-delta-${messageDelta.item_id ?? "unknown"}-${Date.now()}`,
1095
+ timestamp: Date.now(),
1096
+ kind: "message.delta",
1097
+ data: {
1098
+ item_id: messageDelta.item_id,
1099
+ delta: messageDelta.delta
1100
+ },
1101
+ hookName: "stream-json",
1102
+ sessionId,
1103
+ context: { cwd: projectDir, transcriptPath: "" },
1104
+ interaction: getInteractionHints("message.delta"),
1105
+ payload: null
1106
+ });
1107
+ },
1108
+ (messageComplete) => {
1109
+ emit({
1110
+ id: `stream-msg-complete-${messageComplete.item_id ?? "unknown"}-${Date.now()}`,
1111
+ timestamp: Date.now(),
1112
+ kind: "message.complete",
1113
+ data: {
1114
+ item_id: messageComplete.item_id,
1115
+ message: messageComplete.message
1116
+ },
1117
+ hookName: "stream-json",
1118
+ sessionId,
1119
+ context: { cwd: projectDir, transcriptPath: "" },
1120
+ interaction: getInteractionHints("message.complete"),
1121
+ payload: null
1122
+ });
977
1123
  }
978
1124
  );
979
1125
  function emit(event) {
@@ -1374,6 +1520,10 @@ var TOOL_HOOK_EVENTS = [
1374
1520
  "PreToolUse",
1375
1521
  "PostToolUse",
1376
1522
  "PostToolUseFailure",
1523
+ // Batch-level, not per-tool, but Claude accepts (and requires) a matcher
1524
+ // here: the #116 capture registered it with `matcher: '*'` and it fired in
1525
+ // every run. Matcher-less registration is unverified.
1526
+ "PostToolBatch",
1377
1527
  "PermissionRequest",
1378
1528
  "PermissionDenied",
1379
1529
  "Elicitation",
@@ -1388,6 +1538,7 @@ var NON_TOOL_HOOK_EVENTS = [
1388
1538
  "SubagentStart",
1389
1539
  "SubagentStop",
1390
1540
  "UserPromptSubmit",
1541
+ "UserPromptExpansion",
1391
1542
  "PreCompact",
1392
1543
  "PostCompact",
1393
1544
  "Setup",
@@ -1400,6 +1551,27 @@ var NON_TOOL_HOOK_EVENTS = [
1400
1551
  "WorktreeCreate",
1401
1552
  "WorktreeRemove"
1402
1553
  ];
1554
+ var SYNC_HOOK_EVENTS = [
1555
+ // Decision/blocking — kept in step with `runtime/interactionRules.ts`.
1556
+ "PreToolUse",
1557
+ "PermissionRequest",
1558
+ "UserPromptSubmit",
1559
+ "Stop",
1560
+ "StopFailure",
1561
+ "SubagentStop",
1562
+ "Elicitation",
1563
+ "ElicitationResult",
1564
+ "TeammateIdle",
1565
+ "TaskCreated",
1566
+ "TaskCompleted",
1567
+ "ConfigChange",
1568
+ // Teardown — see (2) above.
1569
+ "SessionEnd"
1570
+ ];
1571
+ var SYNC_HOOK_EVENT_SET = new Set(SYNC_HOOK_EVENTS);
1572
+ function isAsyncHookEvent(event) {
1573
+ return !SYNC_HOOK_EVENT_SET.has(event);
1574
+ }
1403
1575
  function quoteShellArg(value) {
1404
1576
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1405
1577
  }
@@ -1447,23 +1619,20 @@ function generateHookSettings(tempDir, authOverlay) {
1447
1619
  if (process.env["ATHENA_DEBUG"]) {
1448
1620
  console.error("[athena-debug] Hook forwarder path:", hookForwarder.command);
1449
1621
  }
1450
- const hookCommand = {
1451
- type: "command",
1452
- command: hookForwarder.command
1453
- };
1622
+ const hookCommandFor = (event) => isAsyncHookEvent(event) ? { type: "command", command: hookForwarder.command, async: true } : { type: "command", command: hookForwarder.command };
1454
1623
  const hooks = {};
1455
1624
  for (const event of TOOL_HOOK_EVENTS) {
1456
1625
  hooks[event] = [
1457
1626
  {
1458
1627
  matcher: "*",
1459
- hooks: [hookCommand]
1628
+ hooks: [hookCommandFor(event)]
1460
1629
  }
1461
1630
  ];
1462
1631
  }
1463
1632
  for (const event of NON_TOOL_HOOK_EVENTS) {
1464
1633
  hooks[event] = [
1465
1634
  {
1466
- hooks: [hookCommand]
1635
+ hooks: [hookCommandFor(event)]
1467
1636
  }
1468
1637
  ];
1469
1638
  }
@@ -2823,6 +2992,17 @@ import os8 from "os";
2823
2992
  import path10 from "path";
2824
2993
 
2825
2994
  // src/harnesses/claude/config/isolation.ts
2995
+ var CLAUDE_EFFORT_LEVELS = [
2996
+ "low",
2997
+ "medium",
2998
+ "high",
2999
+ "xhigh",
3000
+ "max"
3001
+ ];
3002
+ function normalizeEffort(value) {
3003
+ if (value === void 0) return void 0;
3004
+ return CLAUDE_EFFORT_LEVELS.includes(value) ? value : void 0;
3005
+ }
2826
3006
  var DISALLOWED_FIRST_PARTY_MCPS = [
2827
3007
  "mcp__claude_ai_Gmail__*",
2828
3008
  "mcp__claude_ai_Google_Calendar__*",
@@ -2933,6 +3113,7 @@ var FLAG_REGISTRY = [
2933
3113
  { field: "additionalDirectories", flag: "--add-dir", kind: "array" },
2934
3114
  // === Model & Agent ===
2935
3115
  { field: "model", flag: "--model", kind: "value" },
3116
+ { field: "effort", flag: "--effort", kind: "value" },
2936
3117
  { field: "fallbackModel", flag: "--fallback-model", kind: "value" },
2937
3118
  { field: "agent", flag: "--agent", kind: "value" },
2938
3119
  { field: "agents", flag: "--agents", kind: "json" },
@@ -3374,7 +3555,7 @@ function createAssistantMessageAccumulator() {
3374
3555
  };
3375
3556
  }
3376
3557
 
3377
- // src/harnesses/claude/process/useProcess.ts
3558
+ // src/harnesses/claude/session/turnConfig.ts
3378
3559
  function mergeIsolation(base, pluginMcpConfig, perCommand) {
3379
3560
  if (!pluginMcpConfig && !perCommand) return base;
3380
3561
  return {
@@ -3383,6 +3564,17 @@ function mergeIsolation(base, pluginMcpConfig, perCommand) {
3383
3564
  ...pluginMcpConfig ? { mcpConfig: pluginMcpConfig } : {}
3384
3565
  };
3385
3566
  }
3567
+ function resolveClaudeSessionId(continuation) {
3568
+ if (!continuation || continuation.mode === "fresh") {
3569
+ return void 0;
3570
+ }
3571
+ if (continuation.mode === "resume") {
3572
+ return continuation.handle;
3573
+ }
3574
+ throw new Error("Claude harness does not support reuse-current continuation");
3575
+ }
3576
+
3577
+ // src/harnesses/claude/process/useProcess.ts
3386
3578
  var MAX_OUTPUT = 1e3;
3387
3579
  var KILL_TIMEOUT_MS = 3e3;
3388
3580
  var NULL_TOKENS = {
@@ -3433,17 +3625,6 @@ function tokenUsageEquals(a, b) {
3433
3625
  return a.input === b.input && a.output === b.output && a.cacheRead === b.cacheRead && a.cacheWrite === b.cacheWrite && a.total === b.total && a.contextSize === b.contextSize && a.contextWindowSize === b.contextWindowSize;
3434
3626
  }
3435
3627
  var JQ_ASSISTANT_TEXT_FILTER = 'select(.type == "message" and .role == "assistant") | .content[] | select(.type == "text") | .text';
3436
- function resolveClaudeSessionId(continuation) {
3437
- if (!continuation || continuation.mode === "fresh") {
3438
- return void 0;
3439
- }
3440
- if (continuation.mode === "resume") {
3441
- return continuation.handle;
3442
- }
3443
- throw new Error(
3444
- "Claude process hook does not support reuse-current continuation"
3445
- );
3446
- }
3447
3628
  function useClaudeProcess(projectDir, instanceId, isolation, pluginMcpConfig, verbose, workflow, options) {
3448
3629
  const processRef = useRef(null);
3449
3630
  const abortRef = useRef(new AbortController());
@@ -3728,25 +3909,6 @@ function useClaudeProcess(projectDir, instanceId, isolation, pluginMcpConfig, ve
3728
3909
  }
3729
3910
 
3730
3911
  // src/harnesses/claude/session/controller.ts
3731
- function mergeIsolation2(base, pluginMcpConfig, overrides) {
3732
- if (!pluginMcpConfig && !overrides) return base;
3733
- return {
3734
- ...resolveIsolationConfig(base),
3735
- ...overrides ?? {},
3736
- ...pluginMcpConfig ? { mcpConfig: pluginMcpConfig } : {}
3737
- };
3738
- }
3739
- function resolveClaudeSessionId2(continuation) {
3740
- if (!continuation || continuation.mode === "fresh") {
3741
- return void 0;
3742
- }
3743
- if (continuation.mode === "resume") {
3744
- return continuation.handle;
3745
- }
3746
- throw new Error(
3747
- "Claude session controller does not support reuse-current continuation"
3748
- );
3749
- }
3750
3912
  function createClaudeSessionController(input) {
3751
3913
  const spawnProcess = input.spawnProcess ?? spawnClaude;
3752
3914
  const processConfig = input.processConfig;
@@ -3791,8 +3953,8 @@ function createClaudeSessionController(input) {
3791
3953
  prompt,
3792
3954
  projectDir: input.projectDir,
3793
3955
  instanceId: input.instanceId,
3794
- sessionId: resolveClaudeSessionId2(continuation),
3795
- isolation: mergeIsolation2(
3956
+ sessionId: resolveClaudeSessionId(continuation),
3957
+ isolation: mergeIsolation(
3796
3958
  processConfig,
3797
3959
  input.pluginMcpConfig,
3798
3960
  configOverride
@@ -3849,20 +4011,49 @@ function buildClaudeCompatibleIsolationConfig({
3849
4011
  additionalDirectories,
3850
4012
  pluginDirs,
3851
4013
  verbose,
3852
- configuredModel
4014
+ configuredModel,
4015
+ configuredEffort
3853
4016
  }) {
3854
4017
  return {
3855
4018
  preset: isolationPreset,
3856
4019
  additionalDirectories,
3857
4020
  pluginDirs: pluginDirs.length > 0 ? pluginDirs : void 0,
3858
4021
  debug: verbose,
3859
- model: configuredModel
4022
+ model: configuredModel,
4023
+ effort: normalizeEffort(configuredEffort)
3860
4024
  };
3861
4025
  }
4026
+ var CLAUDE_MODEL_OPTIONS = [
4027
+ {
4028
+ value: "sonnet",
4029
+ label: "Sonnet",
4030
+ description: "Balanced default for day-to-day coding work."
4031
+ },
4032
+ {
4033
+ value: "opus",
4034
+ label: "Opus",
4035
+ description: "Stronger reasoning for complex architecture and debugging."
4036
+ },
4037
+ {
4038
+ value: "haiku",
4039
+ label: "Haiku",
4040
+ description: "Fastest option for lighter tasks."
4041
+ },
4042
+ {
4043
+ value: "opusplan",
4044
+ label: "OpusPlan",
4045
+ description: "Uses Opus for planning and Sonnet for execution."
4046
+ }
4047
+ ];
3862
4048
  var CLAUDE_CONFIG_PROFILE = {
3863
4049
  harness: "claude-code",
3864
4050
  buildIsolationConfig: buildClaudeCompatibleIsolationConfig,
3865
- resolveModelName: ({ projectDir, configuredModel }) => resolveClaudeModel({ projectDir, configuredModel })
4051
+ resolveModelName: ({ projectDir, configuredModel }) => resolveClaudeModel({ projectDir, configuredModel }),
4052
+ pluginDelivery: {
4053
+ mergeWorkflowPluginDirs: true,
4054
+ registrationBuildsMcpConfig: true,
4055
+ workflowPluginsVia: "registration"
4056
+ }
3866
4057
  };
3867
4058
  var claudeHarnessAdapter = {
3868
4059
  id: "claude-code",
@@ -3872,7 +4063,9 @@ var claudeHarnessAdapter = {
3872
4063
  conversationModel: "fresh_per_turn",
3873
4064
  killWaitsForTurnSettlement: true,
3874
4065
  supportsEphemeralSessions: false,
3875
- supportsConfigurableIsolation: true
4066
+ supportsConfigurableIsolation: true,
4067
+ emitsStartupDiagnostics: true,
4068
+ extraAllowedTools: []
3876
4069
  },
3877
4070
  verify: () => verifyClaudeHarness(),
3878
4071
  createRuntime: (input) => createClaudeHookRuntime({
@@ -3908,7 +4101,8 @@ var claudeHarnessAdapter = {
3908
4101
  };
3909
4102
  return controller;
3910
4103
  },
3911
- resolveConfigProfile: () => CLAUDE_CONFIG_PROFILE
4104
+ resolveConfigProfile: () => CLAUDE_CONFIG_PROFILE,
4105
+ listModels: async () => CLAUDE_MODEL_OPTIONS
3912
4106
  };
3913
4107
 
3914
4108
  // src/harnesses/codex/runtime/appServerManager.ts
@@ -6751,6 +6945,74 @@ function buildCodexPromptOptions(input) {
6751
6945
  };
6752
6946
  }
6753
6947
 
6948
+ // src/harnesses/codex/session/turnRunner.ts
6949
+ function createCodexTurnEventCollector() {
6950
+ let message = "";
6951
+ let tokenDelta = { ...NULL_TOKENS2 };
6952
+ let turnStatus;
6953
+ let turnErrorMessage;
6954
+ return {
6955
+ handle(event) {
6956
+ const data = typeof event.data === "object" ? event.data : {};
6957
+ if (event.kind === "message.delta") {
6958
+ const delta = typeof data["delta"] === "string" ? data["delta"] : "";
6959
+ message += delta;
6960
+ }
6961
+ if (event.kind === "usage.update") {
6962
+ tokenDelta = readTokenUsage(data["delta"]);
6963
+ }
6964
+ if (event.kind === "turn.complete") {
6965
+ turnStatus = typeof data["status"] === "string" ? data["status"] : turnStatus;
6966
+ }
6967
+ if (event.kind === "unknown" && event.hookName === "error") {
6968
+ const payload = typeof data["payload"] === "object" && data["payload"] !== null ? data["payload"] : null;
6969
+ const errorValue = typeof payload?.["error"] === "object" && payload["error"] !== null ? payload["error"] : null;
6970
+ if (typeof errorValue?.["message"] === "string") {
6971
+ turnErrorMessage = errorValue["message"];
6972
+ }
6973
+ }
6974
+ },
6975
+ result() {
6976
+ if (turnStatus === "failed") {
6977
+ return {
6978
+ exitCode: 1,
6979
+ error: new Error(turnErrorMessage ?? "Codex turn failed"),
6980
+ tokens: tokenDelta,
6981
+ streamMessage: message || null
6982
+ };
6983
+ }
6984
+ return {
6985
+ exitCode: 0,
6986
+ error: null,
6987
+ tokens: tokenDelta,
6988
+ streamMessage: message || null
6989
+ };
6990
+ },
6991
+ errorResult(error) {
6992
+ return {
6993
+ exitCode: null,
6994
+ error,
6995
+ tokens: tokenDelta,
6996
+ streamMessage: message || null
6997
+ };
6998
+ }
6999
+ };
7000
+ }
7001
+ async function runCodexTurn(runtime, prompt, optionsInput, hooks) {
7002
+ const collector = createCodexTurnEventCollector();
7003
+ const unsubscribe = runtime.onEvent(collector.handle);
7004
+ try {
7005
+ await runtime.sendPrompt(prompt, buildCodexPromptOptions(optionsInput));
7006
+ return collector.result();
7007
+ } catch (error) {
7008
+ const normalized = error instanceof Error ? error : new Error(String(error));
7009
+ hooks?.onError?.(normalized);
7010
+ return collector.errorResult(normalized);
7011
+ } finally {
7012
+ unsubscribe();
7013
+ }
7014
+ }
7015
+
6754
7016
  // src/harnesses/codex/session/controller.ts
6755
7017
  function createCodexSessionController(input) {
6756
7018
  const runtime = input.runtime;
@@ -6770,73 +7032,19 @@ function createCodexSessionController(input) {
6770
7032
  streamMessage: null
6771
7033
  };
6772
7034
  }
6773
- let message = "";
6774
- let tokenDelta = { ...NULL_TOKENS2 };
6775
- let turnStatus;
6776
- let turnErrorMessage;
6777
- const unsubscribe = runtime.onEvent((event) => {
6778
- const data = typeof event.data === "object" ? event.data : {};
6779
- if (event.kind === "message.delta") {
6780
- const delta = typeof data["delta"] === "string" ? data["delta"] : "";
6781
- message += delta;
6782
- }
6783
- if (event.kind === "usage.update") {
6784
- tokenDelta = readTokenUsage(data["delta"]);
6785
- }
6786
- if (event.kind === "turn.complete") {
6787
- turnStatus = typeof data["status"] === "string" ? data["status"] : turnStatus;
6788
- }
6789
- if (event.kind === "unknown" && event.hookName === "error") {
6790
- const payload = typeof data["payload"] === "object" && data["payload"] !== null ? data["payload"] : null;
6791
- const errorValue = typeof payload?.["error"] === "object" && payload["error"] !== null ? payload["error"] : null;
6792
- if (typeof errorValue?.["message"] === "string") {
6793
- turnErrorMessage = errorValue["message"];
6794
- }
6795
- }
7035
+ const turnPromise = runCodexTurn(runtime, prompt, {
7036
+ processConfig,
7037
+ continuation,
7038
+ configOverride,
7039
+ workflowPlan: input.workflowPlan,
7040
+ pluginMcpConfig: input.pluginMcpConfig,
7041
+ ephemeral: input.ephemeral
6796
7042
  });
6797
- const turnPromise = (async () => {
6798
- try {
6799
- await runtime.sendPrompt(
6800
- prompt,
6801
- buildCodexPromptOptions({
6802
- processConfig,
6803
- continuation,
6804
- configOverride,
6805
- workflowPlan: input.workflowPlan,
6806
- pluginMcpConfig: input.pluginMcpConfig,
6807
- ephemeral: input.ephemeral
6808
- })
6809
- );
6810
- if (turnStatus === "failed") {
6811
- return {
6812
- exitCode: 1,
6813
- error: new Error(turnErrorMessage ?? "Codex turn failed"),
6814
- tokens: tokenDelta,
6815
- streamMessage: message || null
6816
- };
6817
- }
6818
- return {
6819
- exitCode: 0,
6820
- error: null,
6821
- tokens: tokenDelta,
6822
- streamMessage: message || null
6823
- };
6824
- } catch (error) {
6825
- return {
6826
- exitCode: null,
6827
- error: error instanceof Error ? error : new Error(String(error)),
6828
- tokens: tokenDelta,
6829
- streamMessage: message || null
6830
- };
6831
- } finally {
6832
- activeTurnPromise = null;
6833
- }
6834
- })();
6835
7043
  activeTurnPromise = turnPromise;
6836
7044
  try {
6837
7045
  return await turnPromise;
6838
7046
  } finally {
6839
- unsubscribe();
7047
+ activeTurnPromise = null;
6840
7048
  }
6841
7049
  },
6842
7050
  interrupt() {
@@ -6893,81 +7101,38 @@ function useCodexSessionController(runtime, processConfig, workflowPlan, ephemer
6893
7101
  };
6894
7102
  }
6895
7103
  setIsRunning(true);
6896
- let streamedMessage = "";
6897
- let turnTokens = { ...NULL_TOKENS2 };
6898
- let turnStatus;
6899
- let turnErrorMessage;
6900
- const unsubscribe = codexRuntime.onEvent((event) => {
6901
- const data = typeof event.data === "object" ? event.data : {};
6902
- if (event.kind === "message.delta") {
6903
- const delta = typeof data["delta"] === "string" ? data["delta"] : "";
6904
- streamedMessage += delta;
6905
- }
6906
- if (event.kind === "usage.update") {
6907
- turnTokens = readTokenUsage(data["delta"]);
6908
- }
6909
- if (event.kind === "turn.complete") {
6910
- turnStatus = typeof data["status"] === "string" ? data["status"] : turnStatus;
6911
- }
6912
- if (event.kind === "unknown" && event.hookName === "error") {
6913
- const payload = typeof data["payload"] === "object" && data["payload"] !== null ? data["payload"] : null;
6914
- const errorValue = typeof payload?.["error"] === "object" && payload["error"] !== null ? payload["error"] : null;
6915
- if (typeof errorValue?.["message"] === "string") {
6916
- turnErrorMessage = errorValue["message"];
6917
- }
6918
- }
6919
- });
6920
- const turnPromise = (async () => {
6921
- try {
6922
- await codexRuntime.sendPrompt(
6923
- prompt,
6924
- buildCodexPromptOptions({
6925
- processConfig,
6926
- continuation,
6927
- configOverride: _configOverride,
6928
- workflowPlan,
6929
- pluginMcpConfig,
6930
- ephemeral
6931
- })
6932
- );
6933
- if (turnStatus === "failed") {
6934
- return {
6935
- exitCode: 1,
6936
- error: new Error(turnErrorMessage ?? "Codex turn failed"),
6937
- tokens: turnTokens,
6938
- streamMessage: streamedMessage || null
6939
- };
6940
- }
6941
- return {
6942
- exitCode: 0,
6943
- error: null,
6944
- tokens: turnTokens,
6945
- streamMessage: streamedMessage || null
6946
- };
6947
- } catch (error) {
6948
- if (!abortRef.current.signal.aborted) {
6949
- onLifecycleEventRef.current?.({
6950
- type: "spawn_error",
6951
- message: error instanceof Error ? error.message : "Unknown Codex error",
6952
- failureCode: "spawn_error"
6953
- });
6954
- }
6955
- return {
6956
- exitCode: null,
6957
- error: error instanceof Error ? error : new Error("Unknown Codex error"),
6958
- tokens: turnTokens,
6959
- streamMessage: streamedMessage || null
6960
- };
6961
- } finally {
6962
- activeTurnPromiseRef.current = null;
6963
- unsubscribe();
6964
- if (!abortRef.current.signal.aborted) {
6965
- setIsRunning(false);
7104
+ const turnPromise = runCodexTurn(
7105
+ codexRuntime,
7106
+ prompt,
7107
+ {
7108
+ processConfig,
7109
+ continuation,
7110
+ configOverride: _configOverride,
7111
+ workflowPlan,
7112
+ pluginMcpConfig,
7113
+ ephemeral
7114
+ },
7115
+ {
7116
+ onError: (error) => {
7117
+ if (!abortRef.current.signal.aborted) {
7118
+ onLifecycleEventRef.current?.({
7119
+ type: "spawn_error",
7120
+ message: error.message,
7121
+ failureCode: "spawn_error"
7122
+ });
7123
+ }
6966
7124
  }
6967
7125
  }
6968
- })();
7126
+ );
6969
7127
  activeTurnPromiseRef.current = turnPromise;
6970
- return await turnPromise;
7128
+ try {
7129
+ return await turnPromise;
7130
+ } finally {
7131
+ activeTurnPromiseRef.current = null;
7132
+ if (!abortRef.current.signal.aborted) {
7133
+ setIsRunning(false);
7134
+ }
7135
+ }
6971
7136
  },
6972
7137
  [codexRuntime, processConfig, workflowPlan, pluginMcpConfig, ephemeral]
6973
7138
  );
@@ -7076,7 +7241,12 @@ var CODEX_CONFIG_PROFILE = {
7076
7241
  preset: isolationPreset,
7077
7242
  model: configuredModel
7078
7243
  }),
7079
- resolveModelName: ({ configuredModel }) => configuredModel ?? null
7244
+ resolveModelName: ({ configuredModel }) => configuredModel ?? null,
7245
+ pluginDelivery: {
7246
+ mergeWorkflowPluginDirs: false,
7247
+ registrationBuildsMcpConfig: false,
7248
+ workflowPluginsVia: "generated-mcp"
7249
+ }
7080
7250
  };
7081
7251
  var codexHarnessAdapter = {
7082
7252
  id: "openai-codex",
@@ -7086,7 +7256,12 @@ var codexHarnessAdapter = {
7086
7256
  conversationModel: "persistent_thread",
7087
7257
  killWaitsForTurnSettlement: true,
7088
7258
  supportsEphemeralSessions: true,
7089
- supportsConfigurableIsolation: true
7259
+ supportsConfigurableIsolation: true,
7260
+ emitsStartupDiagnostics: false,
7261
+ // Codex auto-approves Bash/Edit (outside-sandbox commandExecution /
7262
+ // fileChange) and the scoped 'Permissions' capability, on top of the
7263
+ // shared 'mcp__*' baseline seeded via allowedTools (see buildInitialRules).
7264
+ extraAllowedTools: ["Permissions", "Bash", "Edit"]
7090
7265
  },
7091
7266
  verify: () => verifyCodexHarness(),
7092
7267
  createRuntime: (input) => createCodexRuntime({
@@ -7113,8 +7288,23 @@ var codexHarnessAdapter = {
7113
7288
  };
7114
7289
  return controller;
7115
7290
  },
7116
- resolveConfigProfile: () => CODEX_CONFIG_PROFILE
7291
+ resolveConfigProfile: () => CODEX_CONFIG_PROFILE,
7292
+ listModels: async (runtime) => {
7293
+ if (!isCodexRuntimeWithModels(runtime)) {
7294
+ throw new Error("Codex runtime is not available");
7295
+ }
7296
+ const models = await runtime.listModels();
7297
+ return models.map((model) => ({
7298
+ value: model.model,
7299
+ label: model.displayName,
7300
+ description: model.description,
7301
+ isDefault: model.isDefault
7302
+ }));
7303
+ }
7117
7304
  };
7305
+ function isCodexRuntimeWithModels(runtime) {
7306
+ return Boolean(runtime && "listModels" in runtime);
7307
+ }
7118
7308
 
7119
7309
  // src/harnesses/registry.ts
7120
7310
  function buildClaudeCompatibleIsolationConfig2({
@@ -7140,7 +7330,9 @@ var opencodeHarnessAdapter = {
7140
7330
  conversationModel: "fresh_per_turn",
7141
7331
  killWaitsForTurnSettlement: true,
7142
7332
  supportsEphemeralSessions: false,
7143
- supportsConfigurableIsolation: false
7333
+ supportsConfigurableIsolation: false,
7334
+ emitsStartupDiagnostics: false,
7335
+ extraAllowedTools: []
7144
7336
  },
7145
7337
  createRuntime: (input) => claudeHarnessAdapter.createRuntime(input),
7146
7338
  createSessionController: (input) => claudeHarnessAdapter.createSessionController(input),
@@ -7148,8 +7340,14 @@ var opencodeHarnessAdapter = {
7148
7340
  resolveConfigProfile: () => ({
7149
7341
  harness: "opencode",
7150
7342
  buildIsolationConfig: (input) => buildClaudeCompatibleIsolationConfig2(input),
7151
- resolveModelName: ({ configuredModel }) => configuredModel ?? null
7152
- })
7343
+ resolveModelName: ({ configuredModel }) => configuredModel ?? null,
7344
+ pluginDelivery: {
7345
+ mergeWorkflowPluginDirs: true,
7346
+ registrationBuildsMcpConfig: true,
7347
+ workflowPluginsVia: "registration"
7348
+ }
7349
+ }),
7350
+ listModels: async () => []
7153
7351
  };
7154
7352
  var HARNESS_ADAPTERS = [
7155
7353
  claudeHarnessAdapter,
@@ -7265,6 +7463,28 @@ function loadPlugin(pluginDir) {
7265
7463
  }
7266
7464
  return commands2;
7267
7465
  }
7466
+ function loadPersonalSkills(skills) {
7467
+ const commands2 = [];
7468
+ for (const skill of skills) {
7469
+ const skillPath = path12.join(skill.path, "SKILL.md");
7470
+ if (!fs14.existsSync(skillPath)) {
7471
+ console.warn(
7472
+ `Skipping personal skill '${skill.name}' [${skill.sourceLayer}]: SKILL.md not found at ${skill.path}`
7473
+ );
7474
+ continue;
7475
+ }
7476
+ try {
7477
+ const parsed = parseFrontmatter(fs14.readFileSync(skillPath, "utf-8"));
7478
+ commands2.push(skillToCommand(parsed.frontmatter, parsed.body));
7479
+ } catch (error) {
7480
+ const message = error instanceof Error ? error.message : String(error);
7481
+ console.warn(
7482
+ `Skipping personal skill '${skill.name}' [${skill.sourceLayer}]: ${message}`
7483
+ );
7484
+ }
7485
+ }
7486
+ return commands2;
7487
+ }
7268
7488
  function skillToCommand(frontmatter, body, mcpConfigPath) {
7269
7489
  const args = frontmatter["argument-hint"] ? [
7270
7490
  {
@@ -7288,8 +7508,9 @@ function skillToCommand(frontmatter, body, mcpConfigPath) {
7288
7508
  }
7289
7509
 
7290
7510
  // src/infra/plugins/register.ts
7291
- function buildPluginMcpConfig(pluginDirs, mcpServerOptions) {
7511
+ function buildPluginMcpConfig(pluginDirs, mcpServerOptions, personalMcpServers = []) {
7292
7512
  const mergedServers = {};
7513
+ const conflicts = [];
7293
7514
  for (const dir of pluginDirs) {
7294
7515
  const mcpPath = path13.join(dir, ".mcp.json");
7295
7516
  if (!fs15.existsSync(mcpPath)) {
@@ -7315,22 +7536,99 @@ function buildPluginMcpConfig(pluginDirs, mcpServerOptions) {
7315
7536
  mergedServers[serverName] = rest;
7316
7537
  }
7317
7538
  }
7539
+ for (const personal of personalMcpServers) {
7540
+ const { name, sourceLayer: _sourceLayer, ...server } = personal;
7541
+ if (name in mergedServers) {
7542
+ conflicts.push(personal);
7543
+ continue;
7544
+ }
7545
+ mergedServers[name] = server;
7546
+ }
7318
7547
  if (Object.keys(mergedServers).length === 0) {
7319
- return void 0;
7548
+ return { mcpConfig: void 0, conflicts };
7320
7549
  }
7321
7550
  const mcpConfig = path13.join(os10.tmpdir(), `athena-mcp-${process.pid}.json`);
7322
7551
  fs15.writeFileSync(mcpConfig, JSON.stringify({ mcpServers: mergedServers }));
7323
- return mcpConfig;
7552
+ return { mcpConfig, conflicts };
7324
7553
  }
7325
- function registerPlugins(pluginDirs, mcpServerOptions, includeMcpConfig = true) {
7554
+ function registerPlugins(pluginDirs, mcpServerOptions, includeMcpConfig = true, personalMcpServers = [], personalSkills = []) {
7326
7555
  for (const dir of pluginDirs) {
7327
7556
  const commands2 = loadPlugin(dir);
7328
7557
  for (const command of commands2) {
7329
7558
  register(command);
7330
7559
  }
7331
7560
  }
7332
- const mcpConfig = includeMcpConfig ? buildPluginMcpConfig(pluginDirs, mcpServerOptions) : void 0;
7333
- return { mcpConfig };
7561
+ const skillByName = new Map(personalSkills.map((skill) => [skill.name, skill]));
7562
+ const skillConflicts = [];
7563
+ for (const command of loadPersonalSkills(personalSkills)) {
7564
+ if (get(command.name)) {
7565
+ const shadowed = skillByName.get(command.name);
7566
+ if (shadowed) {
7567
+ skillConflicts.push(shadowed);
7568
+ }
7569
+ continue;
7570
+ }
7571
+ register(command);
7572
+ }
7573
+ const mcpResult = includeMcpConfig ? buildPluginMcpConfig(pluginDirs, mcpServerOptions, personalMcpServers) : { mcpConfig: void 0, conflicts: [] };
7574
+ return {
7575
+ mcpConfig: mcpResult.mcpConfig,
7576
+ conflicts: { mcpServers: mcpResult.conflicts, skills: skillConflicts }
7577
+ };
7578
+ }
7579
+
7580
+ // src/infra/plugins/pluginRefResolution.ts
7581
+ function resolvePluginDirs(entries) {
7582
+ const dirs = [];
7583
+ const warnings = [];
7584
+ for (const entry of entries) {
7585
+ if (!isMarketplaceRef(entry)) {
7586
+ dirs.push(entry);
7587
+ continue;
7588
+ }
7589
+ try {
7590
+ dirs.push(resolveMarketplacePlugin(entry));
7591
+ } catch (error) {
7592
+ warnings.push(`Skipping plugin "${entry}": ${error.message}`);
7593
+ }
7594
+ }
7595
+ return { dirs, warnings };
7596
+ }
7597
+
7598
+ // src/infra/capabilities/effective.ts
7599
+ function resolveEffectiveCapabilities(input) {
7600
+ const { globalConfig, projectConfig } = input;
7601
+ return {
7602
+ mcpServers: mergeMcpServers(
7603
+ globalConfig.mcpServers,
7604
+ projectConfig.mcpServers
7605
+ ),
7606
+ skills: mergeSkills(globalConfig.skills, projectConfig.skills)
7607
+ };
7608
+ }
7609
+ function mergeMcpServers(global, project) {
7610
+ const projectNames = new Set(Object.keys(project ?? {}));
7611
+ const result = [];
7612
+ for (const [name, server] of Object.entries(global ?? {})) {
7613
+ if (projectNames.has(name)) continue;
7614
+ result.push({ name, ...server, sourceLayer: "global" });
7615
+ }
7616
+ for (const [name, server] of Object.entries(project ?? {})) {
7617
+ result.push({ name, ...server, sourceLayer: "project" });
7618
+ }
7619
+ return result;
7620
+ }
7621
+ function mergeSkills(global, project) {
7622
+ const projectNames = new Set((project ?? []).map((s) => s.name));
7623
+ const result = [];
7624
+ for (const skill of global ?? []) {
7625
+ if (projectNames.has(skill.name)) continue;
7626
+ result.push({ ...skill, sourceLayer: "global" });
7627
+ }
7628
+ for (const skill of project ?? []) {
7629
+ result.push({ ...skill, sourceLayer: "project" });
7630
+ }
7631
+ return result;
7334
7632
  }
7335
7633
 
7336
7634
  // src/setup/shouldResolveWorkflow.ts
@@ -7386,6 +7684,9 @@ function bootstrapRuntimeConfig({
7386
7684
  const projectConfig = providedProjectConfig ?? readConfig(projectDir);
7387
7685
  const warnings = [];
7388
7686
  const harness = harnessOverride ?? projectConfig.harness ?? globalConfig.harness ?? DEFAULT_HARNESS;
7687
+ const harnessConfigProfile = resolveHarnessConfigProfile(harness);
7688
+ const pluginDelivery = harnessConfigProfile.pluginDelivery;
7689
+ const workflowPluginsAsGeneratedMcp = pluginDelivery.workflowPluginsVia === "generated-mcp";
7389
7690
  const activeWorkflowSelection = resolveActiveWorkflow({
7390
7691
  globalConfig,
7391
7692
  projectConfig,
@@ -7408,34 +7709,45 @@ function bootstrapRuntimeConfig({
7408
7709
  (plugin) => plugin.claudeArtifactDir
7409
7710
  );
7410
7711
  }
7712
+ const globalPluginResolution = resolvePluginDirs(globalConfig.plugins);
7713
+ const projectPluginResolution = resolvePluginDirs(projectConfig.plugins);
7714
+ warnings.push(
7715
+ ...globalPluginResolution.warnings,
7716
+ ...projectPluginResolution.warnings
7717
+ );
7411
7718
  const pluginDirs = mergePluginDirs({
7412
- workflowPluginDirs: harness === "openai-codex" ? [] : workflowPluginDirs,
7413
- globalPlugins: globalConfig.plugins,
7414
- projectPlugins: projectConfig.plugins,
7719
+ workflowPluginDirs: pluginDelivery.mergeWorkflowPluginDirs ? workflowPluginDirs : [],
7720
+ globalPlugins: globalPluginResolution.dirs,
7721
+ projectPlugins: projectPluginResolution.dirs,
7415
7722
  pluginFlags
7416
7723
  });
7417
- const pluginResult = pluginDirs.length > 0 ? registerPlugins(
7724
+ const effectiveCapabilities = harness === "openai-codex" ? { mcpServers: [], skills: [] } : resolveEffectiveCapabilities({ globalConfig, projectConfig });
7725
+ const personalMcpServers = effectiveCapabilities.mcpServers;
7726
+ const personalSkills = effectiveCapabilities.skills;
7727
+ const pluginResult = pluginDirs.length > 0 || personalMcpServers.length > 0 || personalSkills.length > 0 ? registerPlugins(
7418
7728
  pluginDirs,
7419
7729
  workflowToResolve ? activeWorkflowConfig.workflowSelections?.[workflowToResolve]?.mcpServerOptions : void 0,
7420
- harness !== "openai-codex"
7421
- ) : { mcpConfig: void 0 };
7422
- const workflowPluginMcpConfig = harness === "openai-codex" ? buildPluginMcpConfig(
7730
+ pluginDelivery.registrationBuildsMcpConfig,
7731
+ personalMcpServers,
7732
+ personalSkills
7733
+ ) : { mcpConfig: void 0, conflicts: { mcpServers: [], skills: [] } };
7734
+ const workflowPluginMcpConfig = workflowPluginsAsGeneratedMcp ? buildPluginMcpConfig(
7423
7735
  workflowPluginDirs,
7424
7736
  workflowToResolve ? activeWorkflowConfig.workflowSelections?.[workflowToResolve]?.mcpServerOptions : void 0
7425
- ) : void 0;
7426
- const pluginMcpConfig = harness === "openai-codex" ? void 0 : pluginResult.mcpConfig;
7737
+ ).mcpConfig : void 0;
7738
+ const pluginMcpConfig = workflowPluginsAsGeneratedMcp ? void 0 : pluginResult.mcpConfig;
7427
7739
  const activeWorkflow = resolvedWorkflow;
7428
7740
  const additionalDirectories = [
7429
7741
  ...globalConfig.additionalDirectories,
7430
7742
  ...projectConfig.additionalDirectories
7431
7743
  ];
7432
- const harnessConfigProfile = resolveHarnessConfigProfile(harness);
7433
7744
  const workflowPlan = compileWorkflowPlan({
7434
7745
  workflow: activeWorkflow,
7435
7746
  resolvedPlugins: activeWorkflow && resolvedWorkflow?.name === activeWorkflow.name ? workflowResolvedPlugins : void 0,
7436
- pluginMcpConfig: harness === "openai-codex" && activeWorkflow && resolvedWorkflow?.name === activeWorkflow.name ? workflowPluginMcpConfig : pluginResult.mcpConfig
7747
+ pluginMcpConfig: workflowPluginsAsGeneratedMcp && activeWorkflow && resolvedWorkflow?.name === activeWorkflow.name ? workflowPluginMcpConfig : pluginResult.mcpConfig
7437
7748
  });
7438
7749
  const configModel = projectConfig.model || globalConfig.model || activeWorkflow?.model;
7750
+ const configEffort = activeWorkflow?.effort;
7439
7751
  let isolationPreset = initialIsolationPreset;
7440
7752
  if (activeWorkflow?.isolation) {
7441
7753
  const presetOrder = ["strict", "minimal", "permissive"];
@@ -7454,7 +7766,8 @@ function bootstrapRuntimeConfig({
7454
7766
  additionalDirectories,
7455
7767
  pluginDirs,
7456
7768
  verbose,
7457
- configuredModel: configModel
7769
+ configuredModel: configModel,
7770
+ configuredEffort: configEffort
7458
7771
  });
7459
7772
  const modelName = harnessConfigProfile.resolveModelName({
7460
7773
  projectDir,
@@ -7470,6 +7783,9 @@ function bootstrapRuntimeConfig({
7470
7783
  workflow: activeWorkflow,
7471
7784
  workflowPlan,
7472
7785
  modelName,
7786
+ personalMcpServers,
7787
+ personalSkills,
7788
+ capabilityConflicts: pluginResult.conflicts,
7473
7789
  warnings
7474
7790
  };
7475
7791
  }
@@ -7491,7 +7807,7 @@ var EXEC_EXIT_CODE = {
7491
7807
 
7492
7808
  // src/app/exec/runner.ts
7493
7809
  import crypto2 from "crypto";
7494
- import path18 from "path";
7810
+ import path17 from "path";
7495
7811
 
7496
7812
  // src/core/feed/entities.ts
7497
7813
  var ActorRegistry = class {
@@ -7682,8 +7998,10 @@ function generateTitle(event, ascii = false) {
7682
7998
  }
7683
7999
  function generateNeutralTitle(event, g) {
7684
8000
  switch (event.kind) {
7685
- case "session.start":
7686
- return `Session started (${event.data.source})`;
8001
+ case "session.start": {
8002
+ const sessionTitle = event.data.session_title?.trim();
8003
+ return sessionTitle ? truncate(sessionTitle) : `Session started (${event.data.source})`;
8004
+ }
7687
8005
  case "session.end":
7688
8006
  return `Session ended (${event.data.reason})`;
7689
8007
  case "run.start":
@@ -7692,6 +8010,8 @@ function generateNeutralTitle(event, g) {
7692
8010
  return `Run ${event.data.status}`;
7693
8011
  case "user.prompt":
7694
8012
  return truncate(event.data.prompt);
8013
+ case "prompt.expansion":
8014
+ return event.data.command_name ? truncate(`Expanded /${event.data.command_name}`) : "Prompt expanded";
7695
8015
  case "plan.update":
7696
8016
  return truncate(
7697
8017
  event.data.explanation || event.data.delta || "Plan updated"
@@ -7706,6 +8026,10 @@ function generateNeutralTitle(event, g) {
7706
8026
  return `${g["tool.bullet"]} ${event.data.tool_name}`;
7707
8027
  case "tool.post":
7708
8028
  return `${g["tool.gutter"]} ${event.data.tool_name} result`;
8029
+ case "tool.batch": {
8030
+ const n = event.data.tool_calls.length;
8031
+ return `Batch of ${n} tool call${n === 1 ? "" : "s"}`;
8032
+ }
7709
8033
  case "tool.failure":
7710
8034
  return truncate(
7711
8035
  `${g["status.blocked"]} ${event.data.tool_name} failed: ${event.data.error}`
@@ -7936,6 +8260,7 @@ function createRunLifecycle(boundary) {
7936
8260
  let currentRun = null;
7937
8261
  let seq = 0;
7938
8262
  let runSeq = 0;
8263
+ let currentPromptId;
7939
8264
  function getRunId() {
7940
8265
  const sessId = currentSession?.session_id ?? "unknown";
7941
8266
  return `${sessId}:R${runSeq}`;
@@ -7984,7 +8309,13 @@ function createRunLifecycle(boundary) {
7984
8309
  );
7985
8310
  }
7986
8311
  function beginRun(runtimeEvent, triggerType = "other", promptPreview) {
7987
- if (currentRun && triggerType === "other") return [];
8312
+ const promptId = runtimeEvent.promptId;
8313
+ const isExplicitContextTrigger = triggerType === "resume" || triggerType === "clear" || triggerType === "compact";
8314
+ if (promptId !== void 0 && !isExplicitContextTrigger) {
8315
+ if (currentRun && promptId === currentPromptId) return [];
8316
+ } else if (promptId === void 0 && currentRun && triggerType === "other") {
8317
+ return [];
8318
+ }
7988
8319
  const results = [];
7989
8320
  const closeEvt = closeRunIntoEvent(runtimeEvent, "completed");
7990
8321
  if (closeEvt) results.push(closeEvt);
@@ -7995,6 +8326,7 @@ function createRunLifecycle(boundary) {
7995
8326
  triggerType,
7996
8327
  promptPreview
7997
8328
  );
8329
+ currentPromptId = promptId;
7998
8330
  results.push(
7999
8331
  makeEvent(
8000
8332
  "run.start",
@@ -8074,6 +8406,7 @@ function createRunLifecycle(boundary) {
8074
8406
  if (e.kind === "tool.failure") currentRun.counters.tool_failures++;
8075
8407
  if (e.kind === "permission.request")
8076
8408
  currentRun.counters.permission_requests++;
8409
+ if (e.prompt_id !== void 0) currentPromptId = e.prompt_id;
8077
8410
  }
8078
8411
  }
8079
8412
  }
@@ -8336,28 +8669,11 @@ function createAgentMessageStream(eventBuilder, transcriptReader) {
8336
8669
  };
8337
8670
  }
8338
8671
 
8339
- // src/core/feed/internals/rootPlanTracker.ts
8340
- function createRootPlanTracker() {
8341
- let items = [];
8342
- return {
8343
- set(next) {
8344
- items = next;
8345
- },
8346
- current() {
8347
- return items;
8348
- },
8349
- differs(next) {
8350
- if (next.length !== items.length) return true;
8351
- for (let i = 0; i < next.length; i++) {
8352
- if (next[i]?.content !== items[i]?.content) return true;
8353
- if (next[i]?.status !== items[i]?.status) return true;
8354
- }
8355
- return false;
8356
- }
8357
- };
8672
+ // src/core/feed/internals/taskStateTracker.ts
8673
+ function extractTodoItems(toolInput) {
8674
+ const input = toolInput;
8675
+ return Array.isArray(input?.todos) ? input.todos : [];
8358
8676
  }
8359
-
8360
- // src/core/feed/internals/taskLifecycleTracker.ts
8361
8677
  function coerceTaskStatus(status) {
8362
8678
  switch (status) {
8363
8679
  case "pending":
@@ -8369,57 +8685,6 @@ function coerceTaskStatus(status) {
8369
8685
  return null;
8370
8686
  }
8371
8687
  }
8372
- function createTaskLifecycleTracker() {
8373
- let items = [];
8374
- return {
8375
- current() {
8376
- return items;
8377
- },
8378
- upsertCreated({ taskId, subject, description, activeForm }) {
8379
- const task = {
8380
- taskId,
8381
- content: subject,
8382
- status: "pending",
8383
- activeForm: activeForm ?? description
8384
- };
8385
- const existingIndex = items.findIndex((item) => item.taskId === taskId);
8386
- if (existingIndex === -1) {
8387
- items = [...items, task];
8388
- return;
8389
- }
8390
- items = items.map(
8391
- (item, index) => index === existingIndex ? {
8392
- ...item,
8393
- taskId,
8394
- content: subject,
8395
- activeForm: task.activeForm ?? item.activeForm
8396
- } : item
8397
- );
8398
- },
8399
- markCompleted({ taskId, subject }) {
8400
- const existingIndex = items.findIndex((item) => item.taskId === taskId);
8401
- if (existingIndex === -1) {
8402
- if (!subject) return;
8403
- items = [...items, { taskId, content: subject, status: "completed" }];
8404
- return;
8405
- }
8406
- items = items.map(
8407
- (item, index) => index === existingIndex ? { ...item, status: "completed" } : item
8408
- );
8409
- },
8410
- updateStatus({ taskId, status }) {
8411
- items = items.map(
8412
- (item) => item.taskId === taskId ? { ...item, status } : item
8413
- );
8414
- }
8415
- };
8416
- }
8417
-
8418
- // src/core/feed/internals/taskStateTracker.ts
8419
- function extractTodoItems(toolInput) {
8420
- const input = toolInput;
8421
- return Array.isArray(input?.todos) ? input.todos : [];
8422
- }
8423
8688
  function mapPlanStepStatus(status) {
8424
8689
  switch (status) {
8425
8690
  case "inProgress":
@@ -8432,18 +8697,69 @@ function mapPlanStepStatus(status) {
8432
8697
  }
8433
8698
  }
8434
8699
  function createTaskStateTracker() {
8435
- const rootPlan = createRootPlanTracker();
8436
- const taskLifecycle = createTaskLifecycleTracker();
8700
+ let rootPlanItems = [];
8701
+ let taskItems = [];
8702
+ function rootPlanDiffers(next) {
8703
+ if (next.length !== rootPlanItems.length) return true;
8704
+ for (let i = 0; i < next.length; i++) {
8705
+ if (next[i]?.content !== rootPlanItems[i]?.content) return true;
8706
+ if (next[i]?.status !== rootPlanItems[i]?.status) return true;
8707
+ }
8708
+ return false;
8709
+ }
8710
+ function upsertCreatedTask(input) {
8711
+ const { taskId, subject, description, activeForm } = input;
8712
+ const task = {
8713
+ taskId,
8714
+ content: subject,
8715
+ status: "pending",
8716
+ activeForm: activeForm ?? description
8717
+ };
8718
+ const existingIndex = taskItems.findIndex((item) => item.taskId === taskId);
8719
+ if (existingIndex === -1) {
8720
+ taskItems = [...taskItems, task];
8721
+ return;
8722
+ }
8723
+ taskItems = taskItems.map(
8724
+ (item, index) => index === existingIndex ? {
8725
+ ...item,
8726
+ taskId,
8727
+ content: subject,
8728
+ activeForm: task.activeForm ?? item.activeForm
8729
+ } : item
8730
+ );
8731
+ }
8732
+ function markTaskCompleted(input) {
8733
+ const { taskId, subject } = input;
8734
+ const existingIndex = taskItems.findIndex((item) => item.taskId === taskId);
8735
+ if (existingIndex === -1) {
8736
+ if (!subject) return;
8737
+ taskItems = [
8738
+ ...taskItems,
8739
+ { taskId, content: subject, status: "completed" }
8740
+ ];
8741
+ return;
8742
+ }
8743
+ taskItems = taskItems.map(
8744
+ (item, index) => index === existingIndex ? { ...item, status: "completed" } : item
8745
+ );
8746
+ }
8747
+ function updateTaskStatus(input) {
8748
+ const { taskId, status } = input;
8749
+ taskItems = taskItems.map(
8750
+ (item) => item.taskId === taskId ? { ...item, status } : item
8751
+ );
8752
+ }
8437
8753
  function applyToolPre(input) {
8438
8754
  const { toolName, toolInput, actorId } = input;
8439
8755
  if (toolName === "TodoWrite" && actorId === "agent:root") {
8440
- rootPlan.set(extractTodoItems(toolInput));
8756
+ rootPlanItems = extractTodoItems(toolInput);
8441
8757
  }
8442
8758
  if (toolName === "TaskUpdate") {
8443
8759
  const taskId = readString(toolInput["taskId"], toolInput["task_id"]);
8444
8760
  const status = coerceTaskStatus(toolInput["status"]);
8445
8761
  if (taskId && status) {
8446
- taskLifecycle.updateStatus({ taskId, status });
8762
+ updateTaskStatus({ taskId, status });
8447
8763
  }
8448
8764
  }
8449
8765
  }
@@ -8455,7 +8771,7 @@ function createTaskStateTracker() {
8455
8771
  const taskId = readString(task["id"], task["task_id"]);
8456
8772
  const subject = readString(task["subject"], toolInput["subject"]);
8457
8773
  if (taskId && subject) {
8458
- taskLifecycle.upsertCreated({
8774
+ upsertCreatedTask({
8459
8775
  taskId,
8460
8776
  subject,
8461
8777
  description: readString(toolInput["description"]),
@@ -8475,7 +8791,7 @@ function createTaskStateTracker() {
8475
8791
  readObject(response["statusChange"])["to"] ?? toolInput["status"]
8476
8792
  );
8477
8793
  if (taskId && status) {
8478
- taskLifecycle.updateStatus({ taskId, status });
8794
+ updateTaskStatus({ taskId, status });
8479
8795
  }
8480
8796
  }
8481
8797
  }
@@ -8484,19 +8800,19 @@ function createTaskStateTracker() {
8484
8800
  const subject = readString(data["task_subject"]);
8485
8801
  const description = readString(data["task_description"]);
8486
8802
  if (taskId && subject) {
8487
- taskLifecycle.upsertCreated({ taskId, subject, description });
8803
+ upsertCreatedTask({ taskId, subject, description });
8488
8804
  }
8489
8805
  }
8490
8806
  function applyTaskCompletedEvent(data) {
8491
8807
  const taskId = readString(data["task_id"]);
8492
8808
  const subject = readString(data["task_subject"]);
8493
8809
  if (taskId) {
8494
- taskLifecycle.markCompleted({ taskId, subject });
8810
+ markTaskCompleted({ taskId, subject });
8495
8811
  }
8496
8812
  }
8497
8813
  return {
8498
8814
  current() {
8499
- return [...rootPlan.current(), ...taskLifecycle.current()];
8815
+ return [...rootPlanItems, ...taskItems];
8500
8816
  },
8501
8817
  applyToolPre,
8502
8818
  applyToolPost,
@@ -8506,8 +8822,8 @@ function createTaskStateTracker() {
8506
8822
  content: typeof step.step === "string" ? step.step : "",
8507
8823
  status: mapPlanStepStatus(step.status)
8508
8824
  }));
8509
- if (!rootPlan.differs(next)) return false;
8510
- rootPlan.set(next);
8825
+ if (!rootPlanDiffers(next)) return false;
8826
+ rootPlanItems = next;
8511
8827
  return true;
8512
8828
  },
8513
8829
  applyTaskCreatedEvent,
@@ -8538,48 +8854,6 @@ function createTaskStateTracker() {
8538
8854
  };
8539
8855
  }
8540
8856
 
8541
- // src/core/feed/internals/subagentTracker.ts
8542
- function createSubagentTracker() {
8543
- const stack = [];
8544
- const descriptions = /* @__PURE__ */ new Map();
8545
- let pendingDescription;
8546
- return {
8547
- pushActor(actorId) {
8548
- stack.push(actorId);
8549
- },
8550
- popActor(actorId) {
8551
- const idx = stack.lastIndexOf(actorId);
8552
- if (idx !== -1) stack.splice(idx, 1);
8553
- },
8554
- peek() {
8555
- return stack.length > 0 ? stack[stack.length - 1] : void 0;
8556
- },
8557
- clear() {
8558
- stack.length = 0;
8559
- },
8560
- currentScope() {
8561
- return stack.length > 0 ? "subagent" : "root";
8562
- },
8563
- recordPendingDescription(description) {
8564
- pendingDescription = description;
8565
- },
8566
- clearPendingDescription() {
8567
- pendingDescription = void 0;
8568
- },
8569
- consumePendingDescription() {
8570
- const value = pendingDescription;
8571
- pendingDescription = void 0;
8572
- return value;
8573
- },
8574
- setDescription(agentId, description) {
8575
- descriptions.set(agentId, description);
8576
- },
8577
- description(agentId) {
8578
- return descriptions.get(agentId);
8579
- }
8580
- };
8581
- }
8582
-
8583
8857
  // src/core/feed/todo.ts
8584
8858
  function isSubagentTool(toolName) {
8585
8859
  return toolName === "Task" || toolName === "Agent";
@@ -8588,44 +8862,48 @@ function isSubagentTool(toolName) {
8588
8862
  // src/core/feed/internals/subagentLifecycle.ts
8589
8863
  function createSubagentLifecycle(args) {
8590
8864
  const { actors, runLifecycle } = args;
8591
- const tracker = createSubagentTracker();
8865
+ const stack = [];
8866
+ const descriptions = /* @__PURE__ */ new Map();
8867
+ let pendingDescription;
8592
8868
  const actorIdFor = (agentId) => `subagent:${agentId}`;
8593
8869
  return {
8594
8870
  observeToolInput(toolName, toolInput) {
8595
8871
  if (!isSubagentTool(toolName)) return;
8596
- if (typeof toolInput["description"] === "string") {
8597
- tracker.recordPendingDescription(toolInput["description"]);
8598
- } else {
8599
- tracker.clearPendingDescription();
8600
- }
8872
+ pendingDescription = typeof toolInput["description"] === "string" ? toolInput["description"] : void 0;
8601
8873
  },
8602
8874
  startSubagent({ agentId, agentType, fallbackDescription }) {
8603
- const description = tracker.consumePendingDescription() ?? fallbackDescription;
8875
+ const consumed = pendingDescription;
8876
+ pendingDescription = void 0;
8877
+ const description = consumed ?? fallbackDescription;
8604
8878
  if (agentId) {
8605
8879
  actors.ensureSubagent(agentId, agentType ?? "unknown");
8606
8880
  const currentRun = runLifecycle.getCurrentRun();
8607
8881
  if (currentRun) currentRun.actors.subagent_ids.push(agentId);
8608
- tracker.pushActor(actorIdFor(agentId));
8609
- if (description) tracker.setDescription(agentId, description);
8882
+ stack.push(actorIdFor(agentId));
8883
+ if (description) descriptions.set(agentId, description);
8610
8884
  }
8611
8885
  return { actorId: "agent:root", description: description ?? void 0 };
8612
8886
  },
8613
8887
  stopSubagent(agentId) {
8614
- if (agentId) tracker.popActor(actorIdFor(agentId));
8888
+ if (agentId) {
8889
+ const actorId = actorIdFor(agentId);
8890
+ const idx = stack.lastIndexOf(actorId);
8891
+ if (idx !== -1) stack.splice(idx, 1);
8892
+ }
8615
8893
  return {
8616
8894
  actorId: actorIdFor(agentId ?? "unknown"),
8617
- description: tracker.description(agentId ?? "")
8895
+ description: descriptions.get(agentId ?? "")
8618
8896
  };
8619
8897
  },
8620
8898
  currentActor() {
8621
- return tracker.peek() ?? "agent:root";
8899
+ return stack.at(-1) ?? "agent:root";
8622
8900
  },
8623
8901
  currentScope() {
8624
- return tracker.currentScope();
8902
+ return stack.length > 0 ? "subagent" : "root";
8625
8903
  },
8626
8904
  actorIdFor,
8627
8905
  clear() {
8628
- tracker.clear();
8906
+ stack.length = 0;
8629
8907
  }
8630
8908
  };
8631
8909
  }
@@ -8640,6 +8918,15 @@ function toolUseCause(toolUseId, parentId) {
8640
8918
  ...parentId ? { parent_event_id: parentId } : {}
8641
8919
  };
8642
8920
  }
8921
+ function readToolBatchCalls2(value) {
8922
+ if (!Array.isArray(value)) return [];
8923
+ return value.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => readObject(entry)).map((call) => ({
8924
+ tool_name: readString(call["tool_name"]),
8925
+ tool_input: readObject(call["tool_input"]),
8926
+ tool_use_id: readString(call["tool_use_id"]),
8927
+ tool_response: readString(call["tool_response"])
8928
+ }));
8929
+ }
8643
8930
  function createToolProjection(args) {
8644
8931
  const {
8645
8932
  ensureRunArray,
@@ -8696,6 +8983,21 @@ function createToolProjection(args) {
8696
8983
  return {
8697
8984
  mapToolEvent(event, data) {
8698
8985
  const results = ensureRunArray(event);
8986
+ if (event.kind === "tool.batch") {
8987
+ results.push(
8988
+ makeEvent(
8989
+ "tool.batch",
8990
+ "info",
8991
+ resolveToolActor(),
8992
+ {
8993
+ tool_calls: readToolBatchCalls2(data["tool_calls"]),
8994
+ permission_mode: readString(data["permission_mode"])
8995
+ },
8996
+ event
8997
+ )
8998
+ );
8999
+ return results;
9000
+ }
8699
9001
  const toolUseId = resolveToolUseId(event, data);
8700
9002
  const toolName = event.toolName ?? readString(data["tool_name"]) ?? "Unknown";
8701
9003
  const toolInput = readObject(data["tool_input"]);
@@ -9474,7 +9776,8 @@ function createRunSessionProjection(args) {
9474
9776
  {
9475
9777
  source,
9476
9778
  agent_type: readString(data["agent_type"]),
9477
- model: readString(data["model"])
9779
+ model: readString(data["model"]),
9780
+ session_title: readString(data["session_title"])
9478
9781
  },
9479
9782
  event
9480
9783
  )
@@ -9519,6 +9822,34 @@ function createRunSessionProjection(args) {
9519
9822
  );
9520
9823
  return results;
9521
9824
  }
9825
+ if (event.kind === "prompt.expansion") {
9826
+ if (event.promptId !== void 0) {
9827
+ results.push(
9828
+ ...ensureRunArray(
9829
+ event,
9830
+ "user_prompt_submit",
9831
+ readString(data["prompt"])?.slice(0, 80)
9832
+ )
9833
+ );
9834
+ }
9835
+ results.push(
9836
+ makeEvent(
9837
+ "prompt.expansion",
9838
+ "info",
9839
+ "user",
9840
+ {
9841
+ expansion_type: readString(data["expansion_type"]),
9842
+ command_name: readString(data["command_name"]),
9843
+ command_args: readString(data["command_args"]),
9844
+ command_source: readString(data["command_source"]),
9845
+ prompt: readString(data["prompt"]),
9846
+ permission_mode: event.context.permissionMode ?? readString(data["permission_mode"])
9847
+ },
9848
+ event
9849
+ )
9850
+ );
9851
+ return results;
9852
+ }
9522
9853
  if (event.kind === "turn.start") {
9523
9854
  agentMessageStream.clearPending();
9524
9855
  const prompt = readString(data["prompt"]);
@@ -9751,6 +10082,7 @@ var RUN_SESSION_EVENT_KINDS = /* @__PURE__ */ new Set([
9751
10082
  "session.start",
9752
10083
  "session.end",
9753
10084
  "user.prompt",
10085
+ "prompt.expansion",
9754
10086
  "turn.start",
9755
10087
  "message.delta",
9756
10088
  "message.complete",
@@ -9763,6 +10095,7 @@ var TOOL_EVENT_KINDS = /* @__PURE__ */ new Set([
9763
10095
  "tool.delta",
9764
10096
  "tool.pre",
9765
10097
  "tool.post",
10098
+ "tool.batch",
9766
10099
  "tool.failure"
9767
10100
  ]);
9768
10101
  var DECISION_EVENT_KINDS = /* @__PURE__ */ new Set([
@@ -9824,6 +10157,8 @@ function createFeedMapper(bootstrap) {
9824
10157
  ts: runtimeEvent.timestamp,
9825
10158
  session_id: runtimeEvent.sessionId,
9826
10159
  run_id: runId,
10160
+ prompt_id: runtimeEvent.promptId,
10161
+ effort_level: runtimeEvent.effortLevel,
9827
10162
  kind,
9828
10163
  level,
9829
10164
  actor_id: actorId,
@@ -9978,185 +10313,10 @@ function createFeedMapper(bootstrap) {
9978
10313
  };
9979
10314
  }
9980
10315
 
9981
- // src/core/controller/rules.ts
9982
- function ruleMatches(ruleToolName, toolName) {
9983
- if (ruleToolName === "*") return true;
9984
- if (ruleToolName === toolName) return true;
9985
- if (ruleToolName.endsWith("__*")) {
9986
- const prefix = ruleToolName.slice(0, -1);
9987
- return toolName.startsWith(prefix);
9988
- }
9989
- return false;
9990
- }
9991
- function matchRule(rules, toolName) {
9992
- const denyMatch = rules.find(
9993
- (r) => r.action === "deny" && ruleMatches(r.toolName, toolName)
9994
- );
9995
- if (denyMatch) return denyMatch;
9996
- return rules.find(
9997
- (r) => r.action === "approve" && ruleMatches(r.toolName, toolName)
9998
- );
9999
- }
10000
-
10001
- // src/core/controller/permission.ts
10002
- var SESSION_APPROVAL_REQUEST_HOOKS = /* @__PURE__ */ new Set([
10003
- "item/commandExecution/requestApproval",
10004
- "item/fileChange/requestApproval",
10005
- "item/permissions/requestApproval"
10006
- ]);
10007
- function isScopedPermissionsRequest(hookName) {
10008
- return hookName === "item/permissions/requestApproval";
10009
- }
10010
- function supportsSessionApproval(hookName) {
10011
- return hookName !== void 0 && SESSION_APPROVAL_REQUEST_HOOKS.has(hookName);
10012
- }
10013
- function extractPermissionSnapshot(event) {
10014
- const data = event.data;
10015
- const toolNameFromData = typeof data["tool_name"] === "string" ? data["tool_name"] : void 0;
10016
- const toolInputFromData = typeof data["tool_input"] === "object" && data["tool_input"] !== null ? data["tool_input"] : void 0;
10017
- const toolUseIdFromData = typeof data["tool_use_id"] === "string" ? data["tool_use_id"] : void 0;
10018
- return {
10019
- request_id: event.id,
10020
- ts: event.timestamp,
10021
- kind: event.kind,
10022
- hookName: event.hookName,
10023
- tool_name: event.toolName ?? toolNameFromData ?? "Unknown",
10024
- tool_input: toolInputFromData ?? {},
10025
- tool_use_id: event.toolUseId ?? toolUseIdFromData,
10026
- suggestions: data.permission_suggestions
10027
- };
10028
- }
10029
-
10030
- // src/core/controller/runtimeController.ts
10031
- function handleEvent(event, cb) {
10032
- const eventKind2 = event.kind;
10033
- const isScoped = isScopedPermissionsRequest(event.hookName);
10034
- const eventData = event.data;
10035
- const toolName = event.toolName ?? (typeof eventData["tool_name"] === "string" ? eventData["tool_name"] : void 0);
10036
- if (eventKind2 === "permission.request" && toolName === "user_input") {
10037
- cb.enqueueQuestion(event.id);
10038
- cb.relayQuestion?.(event);
10039
- return { handled: true };
10040
- }
10041
- if (eventKind2 === "permission.request" && toolName) {
10042
- const rule = matchRule(cb.getRules(), toolName);
10043
- if (rule?.action === "deny") {
10044
- return {
10045
- handled: true,
10046
- decision: {
10047
- type: "json",
10048
- source: "rule",
10049
- intent: {
10050
- kind: "permission_deny",
10051
- reason: `Blocked by rule: ${rule.addedBy}`
10052
- }
10053
- }
10054
- };
10055
- }
10056
- if (rule?.action === "approve") {
10057
- return {
10058
- handled: true,
10059
- decision: {
10060
- type: "json",
10061
- source: "rule",
10062
- intent: { kind: "permission_allow" },
10063
- // Scoped Codex permission grants are session-scoped under
10064
- // auto rules so the same capability isn't re-prompted.
10065
- ...isScoped ? { data: { scope: "session" } } : {}
10066
- }
10067
- };
10068
- }
10069
- cb.enqueuePermission(event);
10070
- cb.relayPermission?.(event);
10071
- return { handled: true };
10072
- }
10073
- if (eventKind2 === "tool.pre" && toolName === "AskUserQuestion") {
10074
- cb.enqueueQuestion(event.id);
10075
- cb.relayQuestion?.(event);
10076
- return { handled: true };
10077
- }
10078
- if (eventKind2 === "tool.pre" && toolName) {
10079
- const rule = matchRule(cb.getRules(), toolName);
10080
- if (rule?.action === "deny") {
10081
- return {
10082
- handled: true,
10083
- decision: {
10084
- type: "json",
10085
- source: "rule",
10086
- intent: {
10087
- kind: "pre_tool_deny",
10088
- reason: `Blocked by rule: ${rule.addedBy}`
10089
- }
10090
- }
10091
- };
10092
- }
10093
- return {
10094
- handled: true,
10095
- decision: {
10096
- type: "json",
10097
- source: "rule",
10098
- intent: { kind: "pre_tool_allow" }
10099
- }
10100
- };
10101
- }
10102
- return { handled: false };
10103
- }
10104
-
10105
- // src/core/feed/ingest.ts
10106
- function ingestRuntimeEvent(event, ctx) {
10107
- const controllerResult = handleEvent(event, ctx.controllerCallbacks);
10108
- const feedEvents = ctx.mapper.mapEvent(event);
10109
- if (ctx.store) {
10110
- persistOrDegrade(
10111
- ctx.store,
10112
- () => ctx.store.recordEvent(event, feedEvents),
10113
- "recordEvent failed",
10114
- ctx.onPersistFailure
10115
- );
10116
- }
10117
- return {
10118
- feedEvents,
10119
- decision: controllerResult.handled && controllerResult.decision ? controllerResult.decision : null
10120
- };
10121
- }
10122
- function ingestRuntimeDecision(eventId, decision, ctx) {
10123
- const feedEvent = ctx.mapper.mapDecision(eventId, decision);
10124
- if (feedEvent && ctx.store) {
10125
- persistOrDegrade(
10126
- ctx.store,
10127
- () => ctx.store.recordFeedEvents([feedEvent]),
10128
- "recordFeedEvents failed",
10129
- ctx.onPersistFailure
10130
- );
10131
- }
10132
- return feedEvent;
10133
- }
10134
- function persistOrDegrade(store, action, label, onFailure) {
10135
- try {
10136
- action();
10137
- } catch (err) {
10138
- const reason = err instanceof Error ? err.message : String(err);
10139
- const message = `${label}: ${reason}`;
10140
- store.markDegraded(message);
10141
- onFailure?.(message);
10142
- }
10143
- }
10144
-
10145
- // src/infra/sessions/store.ts
10146
- import fs17 from "fs";
10147
- import path14 from "path";
10148
- import Database from "better-sqlite3";
10149
-
10150
10316
  // src/infra/sessions/schema.ts
10151
- var SCHEMA_VERSION = 6;
10152
- function initSchema(db) {
10153
- db.exec("PRAGMA journal_mode = WAL");
10154
- db.exec("PRAGMA foreign_keys = ON");
10317
+ var SCHEMA_VERSION = 7;
10318
+ var applySessionSchema = (db, fromVersion) => {
10155
10319
  db.exec(`
10156
- CREATE TABLE IF NOT EXISTS schema_version (
10157
- version INTEGER NOT NULL
10158
- );
10159
-
10160
10320
  CREATE TABLE IF NOT EXISTS session (
10161
10321
  id TEXT PRIMARY KEY,
10162
10322
  project_dir TEXT NOT NULL,
@@ -10184,6 +10344,10 @@ function initSchema(db) {
10184
10344
  actor_id TEXT NOT NULL,
10185
10345
  timestamp INTEGER NOT NULL,
10186
10346
  data JSON NOT NULL,
10347
+ -- Harness-native Prompt identity (Claude prompt_id): a nullable
10348
+ -- correlation key, NOT the Run identity (that stays run_id). Unset for
10349
+ -- pre-prompt bootstrap + harnesses/versions without it (ADR 0009).
10350
+ prompt_id TEXT,
10187
10351
  FOREIGN KEY (runtime_event_id) REFERENCES runtime_events(id)
10188
10352
  );
10189
10353
 
@@ -10216,45 +10380,6 @@ function initSchema(db) {
10216
10380
  FOREIGN KEY (session_id) REFERENCES session(id)
10217
10381
  );
10218
10382
 
10219
- -- Channel I/O ledger: every inbound chat normalized by an adapter and every
10220
- -- outbound chat dispatched back to a provider gets one row. Idempotency
10221
- -- key prevents double-delivery on retry/restart; session_id ties chat to a
10222
- -- particular Athena interactive runtime when known.
10223
- CREATE TABLE IF NOT EXISTS channel_messages (
10224
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10225
- channel_id TEXT NOT NULL,
10226
- account_id TEXT NOT NULL,
10227
- peer_id TEXT,
10228
- room_id TEXT,
10229
- thread_id TEXT,
10230
- provider_message_id TEXT NOT NULL,
10231
- direction TEXT NOT NULL CHECK(direction IN ('in','out')),
10232
- session_id TEXT REFERENCES adapter_sessions(session_id),
10233
- agent_id TEXT,
10234
- idempotency_key TEXT,
10235
- feed_event_id TEXT,
10236
- created_at INTEGER NOT NULL
10237
- );
10238
-
10239
- -- Audit log for cloud function invocations brokered by the gateway. One
10240
- -- row per invocation across all callers (agent tool, /run channel cmd,
10241
- -- hook helper). Idempotency cache is in-memory + write-through here.
10242
- CREATE TABLE IF NOT EXISTS gateway_function_invocations (
10243
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10244
- name TEXT NOT NULL,
10245
- caller_kind TEXT NOT NULL CHECK(caller_kind IN ('agent','channel','hook')),
10246
- session_id TEXT REFERENCES adapter_sessions(session_id),
10247
- agent_id TEXT,
10248
- idempotency_key TEXT,
10249
- request_hash TEXT NOT NULL,
10250
- status TEXT NOT NULL CHECK(status IN ('pending','ok','error','timeout')),
10251
- http_status INTEGER,
10252
- duration_ms INTEGER,
10253
- error TEXT,
10254
- started_at INTEGER NOT NULL,
10255
- completed_at INTEGER
10256
- );
10257
-
10258
10383
  -- Durable retry queue for outbound channel sends. Drained by the gateway
10259
10384
  -- daemon on startup and after transient send failures.
10260
10385
  CREATE TABLE IF NOT EXISTS channel_outbox (
@@ -10274,127 +10399,91 @@ function initSchema(db) {
10274
10399
  CREATE UNIQUE INDEX IF NOT EXISTS idx_feed_seq ON feed_events(seq);
10275
10400
  CREATE INDEX IF NOT EXISTS idx_runtime_seq ON runtime_events(seq);
10276
10401
  CREATE INDEX IF NOT EXISTS idx_workflow_runs_session ON workflow_runs(session_id);
10277
- CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_messages_idem
10278
- ON channel_messages(channel_id, account_id, idempotency_key)
10279
- WHERE idempotency_key IS NOT NULL;
10280
- CREATE INDEX IF NOT EXISTS idx_channel_messages_session_key
10281
- ON channel_messages(channel_id, account_id, peer_id, room_id, thread_id, created_at);
10282
- CREATE UNIQUE INDEX IF NOT EXISTS idx_fn_idem
10283
- ON gateway_function_invocations(name, idempotency_key)
10284
- WHERE idempotency_key IS NOT NULL;
10285
10402
  CREATE INDEX IF NOT EXISTS idx_outbox_due ON channel_outbox(next_attempt_at);
10286
10403
  `);
10287
- const existing = db.prepare("SELECT version FROM schema_version").get();
10288
- if (existing && existing.version > SCHEMA_VERSION) {
10289
- throw new Error(
10290
- `Database has newer schema version ${existing.version} (expected <= ${SCHEMA_VERSION}). Update athena-cli to open this session.`
10404
+ if (fromVersion === void 0 || fromVersion >= 7) {
10405
+ db.exec(
10406
+ "CREATE INDEX IF NOT EXISTS idx_feed_prompt ON feed_events(prompt_id);"
10291
10407
  );
10292
10408
  }
10293
- if (!existing) {
10294
- db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(
10295
- SCHEMA_VERSION
10409
+ if (fromVersion === void 0) return;
10410
+ if (fromVersion >= SCHEMA_VERSION) return;
10411
+ if (fromVersion < 2) {
10412
+ throw new Error(
10413
+ `Session database is at schema version ${fromVersion} which predates the first release. Delete the session database and start fresh.`
10296
10414
  );
10297
- } else if (existing.version < SCHEMA_VERSION) {
10298
- if (existing.version < 2) {
10299
- throw new Error(
10300
- `Session database is at schema version ${existing.version} which predates the first release. Delete the session database and start fresh.`
10301
- );
10302
- }
10303
- if (existing.version === 2) {
10304
- db.exec(`
10305
- ALTER TABLE adapter_sessions ADD COLUMN tokens_input INTEGER;
10306
- ALTER TABLE adapter_sessions ADD COLUMN tokens_output INTEGER;
10307
- ALTER TABLE adapter_sessions ADD COLUMN tokens_cache_read INTEGER;
10308
- ALTER TABLE adapter_sessions ADD COLUMN tokens_cache_write INTEGER;
10309
- ALTER TABLE adapter_sessions ADD COLUMN tokens_context_size INTEGER;
10310
- ALTER TABLE adapter_sessions ADD COLUMN tokens_context_window_size INTEGER;
10311
- UPDATE schema_version SET version = 4;
10312
- `);
10313
- }
10314
- if (existing.version === 3) {
10315
- db.exec(`
10316
- ALTER TABLE adapter_sessions ADD COLUMN tokens_context_window_size INTEGER;
10317
- UPDATE schema_version SET version = 4;
10318
- `);
10319
- }
10320
- const currentVersion = db.prepare("SELECT version FROM schema_version").get().version;
10321
- if (currentVersion === 4) {
10322
- db.exec(`
10323
- CREATE TABLE IF NOT EXISTS workflow_runs (
10324
- id TEXT PRIMARY KEY,
10325
- session_id TEXT NOT NULL,
10326
- workflow_name TEXT,
10327
- started_at INTEGER NOT NULL,
10328
- ended_at INTEGER,
10329
- iteration INTEGER NOT NULL DEFAULT 0,
10330
- max_iterations INTEGER NOT NULL DEFAULT 1,
10331
- status TEXT NOT NULL DEFAULT 'running',
10332
- stop_reason TEXT,
10333
- tracker_path TEXT,
10334
- FOREIGN KEY (session_id) REFERENCES session(id)
10335
- );
10336
- CREATE INDEX IF NOT EXISTS idx_workflow_runs_session ON workflow_runs(session_id);
10337
- ALTER TABLE adapter_sessions ADD COLUMN run_id TEXT REFERENCES workflow_runs(id);
10338
- UPDATE schema_version SET version = 5;
10339
- `);
10340
- }
10341
- const versionAfterV5 = db.prepare("SELECT version FROM schema_version").get().version;
10342
- if (versionAfterV5 === 5) {
10343
- db.exec(`
10344
- CREATE TABLE IF NOT EXISTS channel_messages (
10345
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10346
- channel_id TEXT NOT NULL,
10347
- account_id TEXT NOT NULL,
10348
- peer_id TEXT,
10349
- room_id TEXT,
10350
- thread_id TEXT,
10351
- provider_message_id TEXT NOT NULL,
10352
- direction TEXT NOT NULL CHECK(direction IN ('in','out')),
10353
- session_id TEXT REFERENCES adapter_sessions(session_id),
10354
- agent_id TEXT,
10355
- idempotency_key TEXT,
10356
- feed_event_id TEXT,
10357
- created_at INTEGER NOT NULL
10358
- );
10359
- CREATE TABLE IF NOT EXISTS gateway_function_invocations (
10360
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10361
- name TEXT NOT NULL,
10362
- caller_kind TEXT NOT NULL CHECK(caller_kind IN ('agent','channel','hook')),
10363
- session_id TEXT REFERENCES adapter_sessions(session_id),
10364
- agent_id TEXT,
10365
- idempotency_key TEXT,
10366
- request_hash TEXT NOT NULL,
10367
- status TEXT NOT NULL CHECK(status IN ('pending','ok','error','timeout')),
10368
- http_status INTEGER,
10369
- duration_ms INTEGER,
10370
- error TEXT,
10371
- started_at INTEGER NOT NULL,
10372
- completed_at INTEGER
10373
- );
10374
- CREATE TABLE IF NOT EXISTS channel_outbox (
10375
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10376
- channel_id TEXT NOT NULL,
10377
- account_id TEXT NOT NULL,
10378
- payload_json TEXT NOT NULL,
10379
- attempt INTEGER NOT NULL DEFAULT 0,
10380
- next_attempt_at INTEGER NOT NULL,
10381
- last_error TEXT,
10382
- created_at INTEGER NOT NULL
10383
- );
10384
- CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_messages_idem
10385
- ON channel_messages(channel_id, account_id, idempotency_key)
10386
- WHERE idempotency_key IS NOT NULL;
10387
- CREATE INDEX IF NOT EXISTS idx_channel_messages_session_key
10388
- ON channel_messages(channel_id, account_id, peer_id, room_id, thread_id, created_at);
10389
- CREATE UNIQUE INDEX IF NOT EXISTS idx_fn_idem
10390
- ON gateway_function_invocations(name, idempotency_key)
10391
- WHERE idempotency_key IS NOT NULL;
10392
- CREATE INDEX IF NOT EXISTS idx_outbox_due ON channel_outbox(next_attempt_at);
10393
- UPDATE schema_version SET version = 6;
10394
- `);
10395
- }
10396
10415
  }
10397
- }
10416
+ if (fromVersion === 2) {
10417
+ db.exec(`
10418
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_input INTEGER;
10419
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_output INTEGER;
10420
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_cache_read INTEGER;
10421
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_cache_write INTEGER;
10422
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_context_size INTEGER;
10423
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_context_window_size INTEGER;
10424
+ UPDATE schema_version SET version = 4;
10425
+ `);
10426
+ }
10427
+ if (fromVersion === 3) {
10428
+ db.exec(`
10429
+ ALTER TABLE adapter_sessions ADD COLUMN tokens_context_window_size INTEGER;
10430
+ UPDATE schema_version SET version = 4;
10431
+ `);
10432
+ }
10433
+ const currentVersion = db.prepare("SELECT version FROM schema_version").get().version;
10434
+ if (currentVersion === 4) {
10435
+ db.exec(`
10436
+ CREATE TABLE IF NOT EXISTS workflow_runs (
10437
+ id TEXT PRIMARY KEY,
10438
+ session_id TEXT NOT NULL,
10439
+ workflow_name TEXT,
10440
+ started_at INTEGER NOT NULL,
10441
+ ended_at INTEGER,
10442
+ iteration INTEGER NOT NULL DEFAULT 0,
10443
+ max_iterations INTEGER NOT NULL DEFAULT 1,
10444
+ status TEXT NOT NULL DEFAULT 'running',
10445
+ stop_reason TEXT,
10446
+ tracker_path TEXT,
10447
+ FOREIGN KEY (session_id) REFERENCES session(id)
10448
+ );
10449
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_session ON workflow_runs(session_id);
10450
+ ALTER TABLE adapter_sessions ADD COLUMN run_id TEXT REFERENCES workflow_runs(id);
10451
+ UPDATE schema_version SET version = 5;
10452
+ `);
10453
+ }
10454
+ const versionAfterV5 = db.prepare("SELECT version FROM schema_version").get().version;
10455
+ if (versionAfterV5 === 5) {
10456
+ db.exec(`
10457
+ CREATE TABLE IF NOT EXISTS channel_outbox (
10458
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10459
+ channel_id TEXT NOT NULL,
10460
+ account_id TEXT NOT NULL,
10461
+ payload_json TEXT NOT NULL,
10462
+ attempt INTEGER NOT NULL DEFAULT 0,
10463
+ next_attempt_at INTEGER NOT NULL,
10464
+ last_error TEXT,
10465
+ created_at INTEGER NOT NULL
10466
+ );
10467
+ CREATE INDEX IF NOT EXISTS idx_outbox_due ON channel_outbox(next_attempt_at);
10468
+ UPDATE schema_version SET version = 6;
10469
+ `);
10470
+ }
10471
+ const versionAfterV6 = db.prepare("SELECT version FROM schema_version").get().version;
10472
+ if (versionAfterV6 === 6) {
10473
+ db.exec(`
10474
+ ALTER TABLE feed_events ADD COLUMN prompt_id TEXT;
10475
+ CREATE INDEX IF NOT EXISTS idx_feed_prompt ON feed_events(prompt_id);
10476
+ UPDATE schema_version SET version = 7;
10477
+ `);
10478
+ }
10479
+ };
10480
+ var SESSION_SCHEMA = {
10481
+ version: SCHEMA_VERSION,
10482
+ migrate: applySessionSchema,
10483
+ onNewerVersion: (found, expected) => new Error(
10484
+ `Database has newer schema version ${found} (expected <= ${expected}). Update athena-cli to open this session.`
10485
+ )
10486
+ };
10398
10487
 
10399
10488
  // src/infra/sessions/types.ts
10400
10489
  function rowToAthenaSession(row, adapterSessionIds, firstPrompt) {
@@ -10412,11 +10501,11 @@ function rowToAthenaSession(row, adapterSessionIds, firstPrompt) {
10412
10501
 
10413
10502
  // src/infra/sessions/store.ts
10414
10503
  function createSessionStore(opts) {
10415
- if (opts.dbPath !== ":memory:") {
10416
- fs17.mkdirSync(path14.dirname(opts.dbPath), { recursive: true });
10417
- }
10418
- const db = new Database(opts.dbPath);
10419
- initSchema(db);
10504
+ const db = openVersionedDb(opts.dbPath, {
10505
+ ...SESSION_SCHEMA,
10506
+ foreignKeys: true,
10507
+ ensureDir: true
10508
+ });
10420
10509
  if (opts.dbPath !== ":memory:") {
10421
10510
  db.pragma("locking_mode = EXCLUSIVE");
10422
10511
  db.exec("BEGIN IMMEDIATE; COMMIT");
@@ -10443,8 +10532,8 @@ function createSessionStore(opts) {
10443
10532
  VALUES (?, ?, ?, ?, ?, ?)`
10444
10533
  );
10445
10534
  const insertFeedEvent = db.prepare(
10446
- `INSERT OR IGNORE INTO feed_events (event_id, runtime_event_id, seq, kind, run_id, actor_id, timestamp, data)
10447
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
10535
+ `INSERT OR IGNORE INTO feed_events (event_id, runtime_event_id, seq, kind, run_id, actor_id, timestamp, data, prompt_id)
10536
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
10448
10537
  );
10449
10538
  const insertAdapterSession = db.prepare(
10450
10539
  `INSERT OR IGNORE INTO adapter_sessions (session_id, started_at)
@@ -10499,7 +10588,8 @@ function createSessionStore(opts) {
10499
10588
  fe.run_id,
10500
10589
  fe.actor_id,
10501
10590
  fe.ts,
10502
- JSON.stringify(fe)
10591
+ JSON.stringify(fe),
10592
+ fe.prompt_id ?? null
10503
10593
  );
10504
10594
  }
10505
10595
  updateEventCount.run(feedEvents.length, opts.sessionId);
@@ -10515,7 +10605,8 @@ function createSessionStore(opts) {
10515
10605
  fe.run_id,
10516
10606
  fe.actor_id,
10517
10607
  fe.ts,
10518
- JSON.stringify(fe)
10608
+ JSON.stringify(fe),
10609
+ fe.prompt_id ?? null
10519
10610
  );
10520
10611
  }
10521
10612
  updateEventCount.run(feedEvents.length, opts.sessionId);
@@ -10687,9 +10778,6 @@ function createSessionStore(opts) {
10687
10778
  };
10688
10779
  }
10689
10780
 
10690
- // src/infra/sessions/hookAudit.ts
10691
- import Database2 from "better-sqlite3";
10692
-
10693
10781
  // src/core/feed/filter.ts
10694
10782
  var TASK_TOOL_NAMES = /* @__PURE__ */ new Set([
10695
10783
  "TodoWrite",
@@ -11365,8 +11453,8 @@ var PRIMARY_INPUT_EXTRACTORS = {
11365
11453
  return compactText(colonIdx >= 0 ? name.slice(colonIdx + 1) : name, 80);
11366
11454
  },
11367
11455
  NotebookEdit: (input) => {
11368
- const path22 = String(input.notebook_path ?? "");
11369
- return path22 ? shortenPath(path22) : "";
11456
+ const path21 = String(input.notebook_path ?? "");
11457
+ return path21 ? shortenPath(path21) : "";
11370
11458
  },
11371
11459
  AskUserQuestion: (input) => {
11372
11460
  const questions = input.questions;
@@ -12352,6 +12440,13 @@ var stopFailure = defaultRenderer(
12352
12440
  var permissionDenied = defaultRenderer(
12353
12441
  (event) => `${event.data.tool_name}${event.data.reason ? `: ${event.data.reason}` : ""}`
12354
12442
  );
12443
+ var promptExpansion = defaultRenderer(
12444
+ (event) => event.data.command_name ? `/${event.data.command_name}${event.data.command_args ? ` ${event.data.command_args}` : ""}` : event.data.expansion_type ?? ""
12445
+ );
12446
+ var toolBatch = defaultRenderer((event) => {
12447
+ const names = event.data.tool_calls.map((call) => call.tool_name).filter((name) => Boolean(name));
12448
+ return names.length > 0 ? names.join(", ") : "";
12449
+ });
12355
12450
  var elicitationRequest = defaultRenderer((event) => `elicitation from ${event.data.mcp_server}`);
12356
12451
  var elicitationResult = defaultRenderer(
12357
12452
  (event) => `${event.data.mcp_server} \u2192 ${event.data.action}`
@@ -12400,6 +12495,7 @@ var RENDERERS = {
12400
12495
  "tool.delta": toolDelta,
12401
12496
  "tool.pre": toolPre,
12402
12497
  "tool.post": toolPost,
12498
+ "tool.batch": toolBatch,
12403
12499
  "tool.failure": toolFailure,
12404
12500
  "permission.request": permissionRequest,
12405
12501
  "permission.decision": permissionDecision,
@@ -12439,6 +12535,7 @@ var RENDERERS = {
12439
12535
  "worktree.remove": worktreeRemove,
12440
12536
  "stop.failure": stopFailure,
12441
12537
  "permission.denied": permissionDenied,
12538
+ "prompt.expansion": promptExpansion,
12442
12539
  "elicitation.request": elicitationRequest,
12443
12540
  "elicitation.result": elicitationResult,
12444
12541
  "channel.permission.relayed": channelPermissionRelayed,
@@ -12637,16 +12734,16 @@ function extractDomain(url) {
12637
12734
  }
12638
12735
  }
12639
12736
  function filePathSegments(input) {
12640
- const path22 = prop2(input, "file_path") ?? prop2(input, "notebook_path") ?? "";
12641
- if (typeof path22 !== "string" || !path22) return [];
12642
- const { prefix, filename } = shortenPathStructured(path22);
12737
+ const path21 = prop2(input, "file_path") ?? prop2(input, "notebook_path") ?? "";
12738
+ if (typeof path21 !== "string" || !path21) return [];
12739
+ const { prefix, filename } = shortenPathStructured(path21);
12643
12740
  if (prefix && filename) {
12644
12741
  return [
12645
12742
  { text: prefix, role: "target" },
12646
12743
  { text: filename, role: "filename" }
12647
12744
  ];
12648
12745
  }
12649
- return [{ text: filename || path22, role: "filename" }];
12746
+ return [{ text: filename || path21, role: "filename" }];
12650
12747
  }
12651
12748
  function grepSegments(input) {
12652
12749
  const pattern = String(prop2(input, "pattern") ?? "");
@@ -13280,6 +13377,7 @@ function resolveEventToolColumn(event) {
13280
13377
  case "tool.delta":
13281
13378
  case "tool.pre":
13282
13379
  case "tool.post":
13380
+ case "tool.batch":
13283
13381
  case "tool.failure":
13284
13382
  case "permission.decision":
13285
13383
  case "permission.denied":
@@ -13307,6 +13405,7 @@ function resolveEventToolColumn(event) {
13307
13405
  case "todo.done":
13308
13406
  case "agent.message":
13309
13407
  case "teammate.idle":
13408
+ case "prompt.expansion":
13310
13409
  case "task.created":
13311
13410
  case "task.completed":
13312
13411
  case "config.change":
@@ -13779,57 +13878,93 @@ ${details}` : entry.summary;
13779
13878
  }
13780
13879
  };
13781
13880
 
13881
+ // src/infra/sessions/sessionDbReader.ts
13882
+ import Database from "better-sqlite3";
13883
+ function openSessionDbReadonly(dbPath, options = {}) {
13884
+ const db = new Database(dbPath, {
13885
+ readonly: true,
13886
+ ...options.fileMustExist ? { fileMustExist: true } : {}
13887
+ });
13888
+ return {
13889
+ schemaVersion() {
13890
+ const row = db.prepare("SELECT version FROM schema_version").get();
13891
+ return row?.version;
13892
+ },
13893
+ sessionRow() {
13894
+ return db.prepare("SELECT * FROM session LIMIT 1").get();
13895
+ },
13896
+ adapterSessionIds() {
13897
+ const rows = db.prepare("SELECT session_id FROM adapter_sessions ORDER BY started_at").all();
13898
+ return rows.map((r) => r.session_id);
13899
+ },
13900
+ firstUserPrompt() {
13901
+ const row = db.prepare(
13902
+ `SELECT json_extract(payload, '$.data.prompt') as prompt FROM runtime_events WHERE hook_name = 'UserPromptSubmit' ORDER BY seq ASC LIMIT 1`
13903
+ ).get();
13904
+ return row?.prompt ?? void 0;
13905
+ },
13906
+ runtimeHookCounts() {
13907
+ const rows = db.prepare(
13908
+ "SELECT hook_name, COUNT(*) AS count FROM runtime_events GROUP BY hook_name"
13909
+ ).all();
13910
+ const counts = {};
13911
+ for (const row of rows) counts[row.hook_name] = row.count;
13912
+ return counts;
13913
+ },
13914
+ feedEvents() {
13915
+ const rows = db.prepare("SELECT data FROM feed_events ORDER BY seq").all();
13916
+ return rows.map((r) => JSON.parse(r.data));
13917
+ },
13918
+ close() {
13919
+ db.close();
13920
+ }
13921
+ };
13922
+ }
13923
+
13782
13924
  // src/infra/sessions/registry.ts
13783
- import fs18 from "fs";
13925
+ import fs17 from "fs";
13784
13926
  import os11 from "os";
13785
- import path15 from "path";
13786
- import Database3 from "better-sqlite3";
13927
+ import path14 from "path";
13787
13928
  function sessionsDir() {
13788
- return path15.join(os11.homedir(), ".config", "athena", "sessions");
13929
+ return path14.join(os11.homedir(), ".config", "athena", "sessions");
13789
13930
  }
13790
13931
  function sessionDbPath(sessionId) {
13791
- return path15.join(sessionsDir(), sessionId, "session.db");
13932
+ return path14.join(sessionsDir(), sessionId, "session.db");
13792
13933
  }
13793
13934
  function readSessionFromDb(dbPath) {
13794
- if (!fs18.existsSync(dbPath)) return null;
13795
- let db;
13935
+ if (!fs17.existsSync(dbPath)) return null;
13936
+ let reader;
13796
13937
  try {
13797
- db = new Database3(dbPath, { readonly: true });
13798
- const versionRow = db.prepare("SELECT version FROM schema_version").get();
13799
- if (versionRow && versionRow.version > SCHEMA_VERSION) {
13938
+ reader = openSessionDbReadonly(dbPath);
13939
+ const version = reader.schemaVersion();
13940
+ if (version !== void 0 && version > SCHEMA_VERSION) {
13800
13941
  return null;
13801
13942
  }
13802
- const row = db.prepare("SELECT * FROM session LIMIT 1").get();
13943
+ const row = reader.sessionRow();
13803
13944
  if (!row) return null;
13804
- const adapters = db.prepare("SELECT session_id FROM adapter_sessions ORDER BY started_at").all();
13945
+ const adapterSessionIds = reader.adapterSessionIds();
13805
13946
  let firstPrompt;
13806
13947
  if (!row.label && (row.event_count ?? 0) > 0) {
13807
- const promptRow = db.prepare(
13808
- `SELECT json_extract(payload, '$.data.prompt') as prompt FROM runtime_events WHERE hook_name = 'UserPromptSubmit' ORDER BY seq ASC LIMIT 1`
13809
- ).get();
13810
- if (promptRow?.prompt) {
13811
- firstPrompt = promptRow.prompt;
13948
+ const prompt = reader.firstUserPrompt();
13949
+ if (prompt) {
13950
+ firstPrompt = prompt;
13812
13951
  }
13813
13952
  }
13814
- return rowToAthenaSession(
13815
- row,
13816
- adapters.map((a) => a.session_id),
13817
- firstPrompt
13818
- );
13953
+ return rowToAthenaSession(row, adapterSessionIds, firstPrompt);
13819
13954
  } catch {
13820
13955
  return null;
13821
13956
  } finally {
13822
- db?.close();
13957
+ reader?.close();
13823
13958
  }
13824
13959
  }
13825
13960
  function listSessions(projectDir) {
13826
13961
  const dir = sessionsDir();
13827
- if (!fs18.existsSync(dir)) return [];
13828
- const entries = fs18.readdirSync(dir, { withFileTypes: true });
13962
+ if (!fs17.existsSync(dir)) return [];
13963
+ const entries = fs17.readdirSync(dir, { withFileTypes: true });
13829
13964
  const sessions = [];
13830
13965
  for (const entry of entries) {
13831
13966
  if (!entry.isDirectory()) continue;
13832
- const dbPath = path15.join(dir, entry.name, "session.db");
13967
+ const dbPath = path14.join(dir, entry.name, "session.db");
13833
13968
  const session = readSessionFromDb(dbPath);
13834
13969
  if (session && (session.eventCount ?? 0) > 0) {
13835
13970
  if (!projectDir || session.projectDir === projectDir) {
@@ -13974,7 +14109,7 @@ function extractRelayQuestions(event) {
13974
14109
  }
13975
14110
 
13976
14111
  // src/app/channels/gatewayControlClient.ts
13977
- import { readFileSync as readFileSync3 } from "fs";
14112
+ import { readFileSync as readFileSync2 } from "fs";
13978
14113
 
13979
14114
  // src/gateway/control/client.ts
13980
14115
  import crypto from "crypto";
@@ -14082,6 +14217,7 @@ async function connect(opts) {
14082
14217
  preHelloResolve = (frame) => {
14083
14218
  clearTimeout(timer);
14084
14219
  preHelloAbort = null;
14220
+ helloAcked = true;
14085
14221
  resolve(frame);
14086
14222
  };
14087
14223
  preHelloAbort = (err) => {
@@ -14104,7 +14240,6 @@ async function connect(opts) {
14104
14240
  }
14105
14241
  throw new GatewayProtocolError(String(msg));
14106
14242
  }
14107
- helloAcked = true;
14108
14243
  const request = async (kind, payload, reqOpts) => {
14109
14244
  const requestId = crypto.randomUUID();
14110
14245
  const envelope = {
@@ -14125,9 +14260,6 @@ async function connect(opts) {
14125
14260
  );
14126
14261
  connection.send(envelope);
14127
14262
  const res = await responsePromise;
14128
- if (res.request_id !== requestId) {
14129
- throw new GatewayProtocolError("response request_id mismatch");
14130
- }
14131
14263
  if (!res.ok) {
14132
14264
  throw new GatewayProtocolError(
14133
14265
  `${res.error.code}: ${res.error.message}`,
@@ -14164,93 +14296,6 @@ function isStringRecord(v) {
14164
14296
  return typeof v === "object" && v !== null && !Array.isArray(v);
14165
14297
  }
14166
14298
 
14167
- // src/gateway/transport/wsClient.ts
14168
- import { WebSocket } from "ws";
14169
- import { readFileSync as readFileSync2 } from "fs";
14170
- function createWsClientTransport(opts) {
14171
- return {
14172
- kind: "ws",
14173
- connect: () => connectWs(opts)
14174
- };
14175
- }
14176
- function wsClientOptionsForEndpoint(input) {
14177
- return {
14178
- url: input.url,
14179
- ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
14180
- ...input.tlsCaPath !== void 0 ? { tlsCaPath: input.tlsCaPath } : {}
14181
- };
14182
- }
14183
- async function connectWs(opts) {
14184
- const timeoutMs = opts.timeoutMs ?? 5e3;
14185
- const wsOpts = opts.tlsCaPath ? { ca: readFileSync2(opts.tlsCaPath) } : {};
14186
- const ws = new WebSocket(opts.url, wsOpts);
14187
- await new Promise((resolve, reject) => {
14188
- const timer = setTimeout(() => {
14189
- ws.close();
14190
- reject(
14191
- new TransportUnreachableError(`connect timed out after ${timeoutMs}ms`)
14192
- );
14193
- }, timeoutMs);
14194
- ws.once("open", () => {
14195
- clearTimeout(timer);
14196
- resolve();
14197
- });
14198
- ws.once("error", (err) => {
14199
- clearTimeout(timer);
14200
- reject(
14201
- new TransportUnreachableError(
14202
- `gateway not reachable at ${opts.url}: ${err.message}`
14203
- )
14204
- );
14205
- });
14206
- });
14207
- return createWsConnection(ws, opts.url);
14208
- }
14209
- function createWsConnection(ws, peer) {
14210
- const frameHandlers = /* @__PURE__ */ new Set();
14211
- const closeHandlers = /* @__PURE__ */ new Set();
14212
- const errorHandlers = /* @__PURE__ */ new Set();
14213
- ws.on("message", (data) => {
14214
- let parsed;
14215
- try {
14216
- parsed = JSON.parse(data.toString());
14217
- } catch {
14218
- ws.close();
14219
- return;
14220
- }
14221
- traceGatewayFrame("ws-client", peer, "in", parsed);
14222
- for (const handler of frameHandlers) handler(parsed);
14223
- });
14224
- ws.on("error", (err) => {
14225
- for (const handler of errorHandlers) handler(err);
14226
- });
14227
- ws.on("close", () => {
14228
- for (const handler of closeHandlers) handler();
14229
- });
14230
- return {
14231
- kind: "ws",
14232
- peer,
14233
- send: (frame) => {
14234
- if (ws.readyState !== ws.OPEN) return;
14235
- traceGatewayFrame("ws-client", peer, "out", frame);
14236
- ws.send(JSON.stringify(frame));
14237
- },
14238
- close: () => ws.close(),
14239
- onFrame: (cb) => {
14240
- frameHandlers.add(cb);
14241
- return () => frameHandlers.delete(cb);
14242
- },
14243
- onClose: (cb) => {
14244
- closeHandlers.add(cb);
14245
- return () => closeHandlers.delete(cb);
14246
- },
14247
- onError: (cb) => {
14248
- errorHandlers.add(cb);
14249
- return () => errorHandlers.delete(cb);
14250
- }
14251
- };
14252
- }
14253
-
14254
14299
  // src/app/channels/gatewayControlClient.ts
14255
14300
  async function connectGatewayControlClient(opts) {
14256
14301
  const loadToken = opts.loadToken ?? defaultLoadToken;
@@ -14272,13 +14317,13 @@ async function connectGatewayControlClient(opts) {
14272
14317
  });
14273
14318
  }
14274
14319
  function defaultLoadToken(tokenPath) {
14275
- return readFileSync3(tokenPath, "utf8").trim();
14320
+ return readFileSync2(tokenPath, "utf8").trim();
14276
14321
  }
14277
14322
 
14278
14323
  // src/infra/config/gatewayClient.ts
14279
- import fs19 from "fs";
14324
+ import fs18 from "fs";
14280
14325
  import os12 from "os";
14281
- import path16 from "path";
14326
+ import path15 from "path";
14282
14327
 
14283
14328
  // src/shared/gateway-protocol/endpoint.ts
14284
14329
  function parseRuntimeEndpoint(value) {
@@ -14334,16 +14379,16 @@ function isRecord2(value) {
14334
14379
  // src/infra/config/gatewayClient.ts
14335
14380
  function resolveGatewayClientConfigPath(env = process.env) {
14336
14381
  const home = env["HOME"] ?? os12.homedir();
14337
- return path16.join(home, ".config", "athena", "gateway.json");
14382
+ return path15.join(home, ".config", "athena", "gateway.json");
14338
14383
  }
14339
14384
  function readGatewayClientConfig(env = process.env) {
14340
14385
  const configPath = resolveGatewayClientConfigPath(env);
14341
- if (!fs19.existsSync(configPath)) {
14386
+ if (!fs18.existsSync(configPath)) {
14342
14387
  return { mode: "local" };
14343
14388
  }
14344
14389
  let parsed;
14345
14390
  try {
14346
- parsed = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
14391
+ parsed = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
14347
14392
  } catch (err) {
14348
14393
  throw new Error(
14349
14394
  `gateway client config ${configPath} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -14360,16 +14405,16 @@ function readGatewayClientConfig(env = process.env) {
14360
14405
  function writeGatewayClientConfig(config, env = process.env) {
14361
14406
  const parsed = parseRuntimeEndpoint(config);
14362
14407
  const configPath = resolveGatewayClientConfigPath(env);
14363
- const dir = path16.dirname(configPath);
14364
- fs19.mkdirSync(dir, { recursive: true, mode: 448 });
14365
- fs19.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
14408
+ const dir = path15.dirname(configPath);
14409
+ fs18.mkdirSync(dir, { recursive: true, mode: 448 });
14410
+ fs18.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
14366
14411
  encoding: "utf-8",
14367
14412
  mode: 384
14368
14413
  });
14369
14414
  if (process.platform !== "win32") {
14370
14415
  try {
14371
- fs19.chmodSync(dir, 448);
14372
- fs19.chmodSync(configPath, 384);
14416
+ fs18.chmodSync(dir, 448);
14417
+ fs18.chmodSync(configPath, 384);
14373
14418
  } catch {
14374
14419
  }
14375
14420
  }
@@ -14797,14 +14842,11 @@ function sleepOrAbort(ms, signal) {
14797
14842
  }
14798
14843
 
14799
14844
  // src/app/dashboard/dashboardFeedPublisher.ts
14800
- import Database4 from "better-sqlite3";
14801
14845
  function dashboardFeedOutboxPath() {
14802
14846
  return `${ensureDaemonStateDir().dir}/dashboard-feed-outbox.db`;
14803
14847
  }
14804
14848
  function initOutboxSchema(db) {
14805
14849
  db.exec(`
14806
- PRAGMA journal_mode = WAL;
14807
-
14808
14850
  CREATE TABLE IF NOT EXISTS dashboard_feed_outbox (
14809
14851
  delivery_seq INTEGER PRIMARY KEY AUTOINCREMENT,
14810
14852
  instance_id TEXT NOT NULL,
@@ -14838,8 +14880,9 @@ function makeEnvelope(input) {
14838
14880
  };
14839
14881
  }
14840
14882
  function createDashboardFeedOutbox(options = {}) {
14841
- const db = new Database4(options.dbPath ?? dashboardFeedOutboxPath());
14842
- initOutboxSchema(db);
14883
+ const db = openVersionedDb(options.dbPath ?? dashboardFeedOutboxPath(), {
14884
+ migrate: initOutboxSchema
14885
+ });
14843
14886
  const insert = db.prepare(`
14844
14887
  INSERT OR IGNORE INTO dashboard_feed_outbox (
14845
14888
  instance_id,
@@ -14949,91 +14992,342 @@ function createDashboardFeedOutbox(options = {}) {
14949
14992
  close() {
14950
14993
  db.close();
14951
14994
  }
14952
- };
14953
- }
14954
-
14955
- // src/app/dashboard/pairedFeedPublisher.ts
14956
- var DEFAULT_DRAIN_INTERVAL_MS = 1e3;
14957
- function createPairedFeedPublisher(options = {}) {
14958
- const readConfig2 = options.readConfig ?? (() => readDashboardClientConfig());
14959
- const now = options.now ?? (() => Date.now());
14960
- const onError = options.onError ?? (() => {
14961
- });
14962
- const drainIntervalMs = options.drainIntervalMs ?? DEFAULT_DRAIN_INTERVAL_MS;
14963
- let ownedOutbox = null;
14964
- let transport = null;
14965
- let drainTimer = null;
14966
- function getOutbox() {
14967
- if (options.outbox) return options.outbox;
14968
- ownedOutbox ??= createDashboardFeedOutbox();
14969
- return ownedOutbox;
14995
+ };
14996
+ }
14997
+
14998
+ // src/app/dashboard/pairedFeedPublisher.ts
14999
+ var DEFAULT_DRAIN_INTERVAL_MS = 1e3;
15000
+ function createPairedFeedPublisher(options = {}) {
15001
+ const readConfig2 = options.readConfig ?? (() => readDashboardClientConfig());
15002
+ const now = options.now ?? (() => Date.now());
15003
+ const onError = options.onError ?? (() => {
15004
+ });
15005
+ const drainIntervalMs = options.drainIntervalMs ?? DEFAULT_DRAIN_INTERVAL_MS;
15006
+ let ownedOutbox = null;
15007
+ let transport = null;
15008
+ let drainTimer = null;
15009
+ function getOutbox() {
15010
+ if (options.outbox) return options.outbox;
15011
+ ownedOutbox ??= createDashboardFeedOutbox();
15012
+ return ownedOutbox;
15013
+ }
15014
+ function clearDrainTimer() {
15015
+ if (!drainTimer) return;
15016
+ clearInterval(drainTimer);
15017
+ drainTimer = null;
15018
+ }
15019
+ function drain(force = false) {
15020
+ if (!transport) return;
15021
+ const rows = getOutbox().pendingBatch({
15022
+ limit: 100,
15023
+ now: force ? Number.POSITIVE_INFINITY : now()
15024
+ });
15025
+ for (const row of rows) {
15026
+ transport.sendFeedEvent({
15027
+ deliverySeq: row.deliverySeq,
15028
+ envelope: row.envelope
15029
+ });
15030
+ getOutbox().markAttempted({
15031
+ deliverySeq: row.deliverySeq,
15032
+ nextAttemptAt: now() + Math.min(3e4, (row.attempt + 1) * 1e3)
15033
+ });
15034
+ }
15035
+ }
15036
+ function startDrainTimer() {
15037
+ clearDrainTimer();
15038
+ const timer = setInterval(drain, drainIntervalMs);
15039
+ timer.unref();
15040
+ drainTimer = timer;
15041
+ drain(true);
15042
+ }
15043
+ return {
15044
+ publish(input) {
15045
+ if (input.feedEvents.length === 0) return;
15046
+ try {
15047
+ const config = readConfig2();
15048
+ if (!config) return;
15049
+ getOutbox().enqueue({
15050
+ instanceId: config.instanceId,
15051
+ athenaSessionId: input.athenaSessionId,
15052
+ origin: input.origin,
15053
+ feedEvents: input.feedEvents,
15054
+ emittedAt: now()
15055
+ });
15056
+ drain();
15057
+ } catch (err) {
15058
+ onError(
15059
+ `paired feed publish failed: ${err instanceof Error ? err.message : String(err)}`
15060
+ );
15061
+ }
15062
+ },
15063
+ attachTransport(nextTransport) {
15064
+ transport = nextTransport;
15065
+ startDrainTimer();
15066
+ },
15067
+ detachTransport() {
15068
+ transport = null;
15069
+ clearDrainTimer();
15070
+ },
15071
+ handleAck(frame) {
15072
+ getOutbox().markAcked({
15073
+ ...typeof frame.deliverySeq === "number" ? { deliverySeq: frame.deliverySeq } : {},
15074
+ ...typeof frame.eventId === "string" ? { eventId: frame.eventId } : {}
15075
+ });
15076
+ },
15077
+ close() {
15078
+ this.detachTransport();
15079
+ ownedOutbox?.close();
15080
+ }
15081
+ };
15082
+ }
15083
+
15084
+ // src/core/controller/rules.ts
15085
+ function ruleMatches(ruleToolName, toolName) {
15086
+ if (ruleToolName === "*") return true;
15087
+ if (ruleToolName === toolName) return true;
15088
+ if (ruleToolName.endsWith("__*")) {
15089
+ const prefix = ruleToolName.slice(0, -1);
15090
+ return toolName.startsWith(prefix);
15091
+ }
15092
+ return false;
15093
+ }
15094
+ function matchRule(rules, toolName) {
15095
+ const denyMatch = rules.find(
15096
+ (r) => r.action === "deny" && ruleMatches(r.toolName, toolName)
15097
+ );
15098
+ if (denyMatch) return denyMatch;
15099
+ return rules.find(
15100
+ (r) => r.action === "approve" && ruleMatches(r.toolName, toolName)
15101
+ );
15102
+ }
15103
+
15104
+ // src/core/controller/permission.ts
15105
+ var SESSION_APPROVAL_REQUEST_HOOKS = /* @__PURE__ */ new Set([
15106
+ "item/commandExecution/requestApproval",
15107
+ "item/fileChange/requestApproval",
15108
+ "item/permissions/requestApproval"
15109
+ ]);
15110
+ function isScopedPermissionsRequest(hookName) {
15111
+ return hookName === "item/permissions/requestApproval";
15112
+ }
15113
+ function supportsSessionApproval(hookName) {
15114
+ return hookName !== void 0 && SESSION_APPROVAL_REQUEST_HOOKS.has(hookName);
15115
+ }
15116
+ function extractPermissionSnapshot(event) {
15117
+ const data = event.data;
15118
+ const toolNameFromData = typeof data["tool_name"] === "string" ? data["tool_name"] : void 0;
15119
+ const toolInputFromData = typeof data["tool_input"] === "object" && data["tool_input"] !== null ? data["tool_input"] : void 0;
15120
+ const toolUseIdFromData = typeof data["tool_use_id"] === "string" ? data["tool_use_id"] : void 0;
15121
+ return {
15122
+ request_id: event.id,
15123
+ ts: event.timestamp,
15124
+ kind: event.kind,
15125
+ hookName: event.hookName,
15126
+ tool_name: event.toolName ?? toolNameFromData ?? "Unknown",
15127
+ tool_input: toolInputFromData ?? {},
15128
+ tool_use_id: event.toolUseId ?? toolUseIdFromData,
15129
+ suggestions: data.permission_suggestions
15130
+ };
15131
+ }
15132
+
15133
+ // src/core/controller/runtimeController.ts
15134
+ function handleEvent(event, cb) {
15135
+ const eventKind2 = event.kind;
15136
+ const isScoped = isScopedPermissionsRequest(event.hookName);
15137
+ const eventData = event.data;
15138
+ const toolName = event.toolName ?? (typeof eventData["tool_name"] === "string" ? eventData["tool_name"] : void 0);
15139
+ if (eventKind2 === "permission.request" && toolName === "user_input") {
15140
+ cb.enqueueQuestion(event.id);
15141
+ cb.relayQuestion?.(event);
15142
+ return { handled: true };
15143
+ }
15144
+ if (eventKind2 === "permission.request" && toolName) {
15145
+ const rule = matchRule(cb.getRules(), toolName);
15146
+ if (rule?.action === "deny") {
15147
+ return {
15148
+ handled: true,
15149
+ decision: {
15150
+ type: "json",
15151
+ source: "rule",
15152
+ intent: {
15153
+ kind: "permission_deny",
15154
+ reason: `Blocked by rule: ${rule.addedBy}`
15155
+ }
15156
+ }
15157
+ };
15158
+ }
15159
+ if (rule?.action === "approve") {
15160
+ return {
15161
+ handled: true,
15162
+ decision: {
15163
+ type: "json",
15164
+ source: "rule",
15165
+ intent: { kind: "permission_allow" },
15166
+ // Scoped Codex permission grants are session-scoped under
15167
+ // auto rules so the same capability isn't re-prompted.
15168
+ ...isScoped ? { data: { scope: "session" } } : {}
15169
+ }
15170
+ };
15171
+ }
15172
+ cb.enqueuePermission(event);
15173
+ cb.relayPermission?.(event);
15174
+ return { handled: true };
14970
15175
  }
14971
- function clearDrainTimer() {
14972
- if (!drainTimer) return;
14973
- clearInterval(drainTimer);
14974
- drainTimer = null;
15176
+ if (eventKind2 === "tool.pre" && toolName === "AskUserQuestion") {
15177
+ cb.enqueueQuestion(event.id);
15178
+ cb.relayQuestion?.(event);
15179
+ return { handled: true };
14975
15180
  }
14976
- function drain(force = false) {
14977
- if (!transport) return;
14978
- const rows = getOutbox().pendingBatch({
14979
- limit: 100,
14980
- now: force ? Number.POSITIVE_INFINITY : now()
14981
- });
14982
- for (const row of rows) {
14983
- transport.sendFeedEvent({
14984
- deliverySeq: row.deliverySeq,
14985
- envelope: row.envelope
14986
- });
14987
- getOutbox().markAttempted({
14988
- deliverySeq: row.deliverySeq,
14989
- nextAttemptAt: now() + Math.min(3e4, (row.attempt + 1) * 1e3)
14990
- });
15181
+ if (eventKind2 === "tool.pre" && toolName) {
15182
+ const rule = matchRule(cb.getRules(), toolName);
15183
+ if (rule?.action === "deny") {
15184
+ return {
15185
+ handled: true,
15186
+ decision: {
15187
+ type: "json",
15188
+ source: "rule",
15189
+ intent: {
15190
+ kind: "pre_tool_deny",
15191
+ reason: `Blocked by rule: ${rule.addedBy}`
15192
+ }
15193
+ }
15194
+ };
14991
15195
  }
15196
+ return {
15197
+ handled: true,
15198
+ decision: {
15199
+ type: "json",
15200
+ source: "rule",
15201
+ intent: { kind: "pre_tool_allow" }
15202
+ }
15203
+ };
14992
15204
  }
14993
- function startDrainTimer() {
14994
- clearDrainTimer();
14995
- const timer = setInterval(drain, drainIntervalMs);
14996
- timer.unref();
14997
- drainTimer = timer;
14998
- drain(true);
15205
+ return { handled: false };
15206
+ }
15207
+
15208
+ // src/core/feed/ingest.ts
15209
+ function ingestRuntimeEvent(event, ctx) {
15210
+ const controllerResult = handleEvent(event, ctx.controllerCallbacks);
15211
+ const feedEvents = ctx.mapper.mapEvent(event);
15212
+ if (ctx.store) {
15213
+ persistOrDegrade(
15214
+ ctx.store,
15215
+ () => ctx.store.recordEvent(event, feedEvents),
15216
+ "recordEvent failed",
15217
+ ctx.onPersistFailure
15218
+ );
14999
15219
  }
15000
15220
  return {
15001
- publish(input) {
15002
- if (input.feedEvents.length === 0) return;
15221
+ feedEvents,
15222
+ decision: controllerResult.handled && controllerResult.decision ? controllerResult.decision : null
15223
+ };
15224
+ }
15225
+ function ingestRuntimeDecision(eventId, decision, ctx) {
15226
+ const feedEvent = ctx.mapper.mapDecision(eventId, decision);
15227
+ if (feedEvent && ctx.store) {
15228
+ persistOrDegrade(
15229
+ ctx.store,
15230
+ () => ctx.store.recordFeedEvents([feedEvent]),
15231
+ "recordFeedEvents failed",
15232
+ ctx.onPersistFailure
15233
+ );
15234
+ }
15235
+ return feedEvent;
15236
+ }
15237
+ function persistOrDegrade(store, action, label, onFailure) {
15238
+ try {
15239
+ action();
15240
+ } catch (err) {
15241
+ const reason = err instanceof Error ? err.message : String(err);
15242
+ const message = `${label}: ${reason}`;
15243
+ store.markDegraded(message);
15244
+ onFailure?.(message);
15245
+ }
15246
+ }
15247
+
15248
+ // src/app/runtime/runtimeEventLoop.ts
15249
+ var DASHBOARD_DECISION_POLL_LIMIT = 25;
15250
+ var DASHBOARD_DECISION_POLL_INTERVAL_MS = 1e3;
15251
+ function resolveIngest(source) {
15252
+ return typeof source === "function" ? source() : source;
15253
+ }
15254
+ function attachRuntimeEventLoop(options) {
15255
+ const { runtime } = options;
15256
+ const runEvent = (event) => {
15257
+ options.onEventReceived?.(event);
15258
+ if (options.skipEvent?.(event)) return;
15259
+ const { feedEvents, decision } = ingestRuntimeEvent(
15260
+ event,
15261
+ resolveIngest(options.ingest)
15262
+ );
15263
+ if (decision) {
15264
+ runtime.sendDecision(event.id, decision);
15265
+ }
15266
+ options.emitEventFeed(feedEvents, event);
15267
+ };
15268
+ const runDecision = (eventId, decision) => {
15269
+ options.onDecisionReceived?.(eventId, decision);
15270
+ if (options.skipDecision?.(eventId, decision)) return;
15271
+ options.beforeDecisionIngest?.(eventId, decision);
15272
+ const feedEvent = ingestRuntimeDecision(
15273
+ eventId,
15274
+ decision,
15275
+ resolveIngest(options.ingest)
15276
+ );
15277
+ options.emitDecisionFeed(feedEvent, eventId, decision);
15278
+ };
15279
+ const unsubscribeEvent = runtime.onEvent((event) => {
15280
+ if (options.wrapEvent) {
15281
+ options.wrapEvent(event, () => runEvent(event));
15282
+ } else {
15283
+ runEvent(event);
15284
+ }
15285
+ });
15286
+ const unsubscribeDecision = runtime.onDecision((eventId, decision) => {
15287
+ if (options.wrapDecision) {
15288
+ options.wrapDecision(
15289
+ eventId,
15290
+ decision,
15291
+ () => runDecision(eventId, decision)
15292
+ );
15293
+ } else {
15294
+ runDecision(eventId, decision);
15295
+ }
15296
+ });
15297
+ return {
15298
+ stop() {
15299
+ unsubscribeEvent();
15300
+ unsubscribeDecision();
15301
+ }
15302
+ };
15303
+ }
15304
+ function startDashboardDecisionDrain(options) {
15305
+ const { runtime, inbox, athenaSessionId } = options;
15306
+ const drainOnce = () => {
15307
+ const rows = inbox.pendingForSession({
15308
+ athenaSessionId,
15309
+ limit: DASHBOARD_DECISION_POLL_LIMIT
15310
+ });
15311
+ for (const row of rows) {
15003
15312
  try {
15004
- const config = readConfig2();
15005
- if (!config) return;
15006
- getOutbox().enqueue({
15007
- instanceId: config.instanceId,
15008
- athenaSessionId: input.athenaSessionId,
15009
- origin: input.origin,
15010
- feedEvents: input.feedEvents,
15011
- emittedAt: now()
15012
- });
15013
- drain();
15014
- } catch (err) {
15015
- onError(
15016
- `paired feed publish failed: ${err instanceof Error ? err.message : String(err)}`
15017
- );
15313
+ runtime.sendDecision(row.requestId, row.decision);
15314
+ inbox.markConsumed({ id: row.id });
15315
+ } catch (error) {
15316
+ if (!options.onError) throw error;
15317
+ options.onError(error);
15018
15318
  }
15019
- },
15020
- attachTransport(nextTransport) {
15021
- transport = nextTransport;
15022
- startDrainTimer();
15023
- },
15024
- detachTransport() {
15025
- transport = null;
15026
- clearDrainTimer();
15027
- },
15028
- handleAck(frame) {
15029
- getOutbox().markAcked({
15030
- ...typeof frame.deliverySeq === "number" ? { deliverySeq: frame.deliverySeq } : {},
15031
- ...typeof frame.eventId === "string" ? { eventId: frame.eventId } : {}
15032
- });
15033
- },
15034
- close() {
15035
- this.detachTransport();
15036
- ownedOutbox?.close();
15319
+ }
15320
+ };
15321
+ drainOnce();
15322
+ const timer = setInterval(
15323
+ drainOnce,
15324
+ options.pollIntervalMs ?? DASHBOARD_DECISION_POLL_INTERVAL_MS
15325
+ );
15326
+ options.configureTimer?.(timer);
15327
+ return {
15328
+ drainOnce,
15329
+ stop() {
15330
+ clearInterval(timer);
15037
15331
  }
15038
15332
  };
15039
15333
  }
@@ -15088,8 +15382,8 @@ function exitCodeFromFailure(failure) {
15088
15382
  }
15089
15383
 
15090
15384
  // src/app/exec/output.ts
15091
- import fs20 from "fs/promises";
15092
- import path17 from "path";
15385
+ import fs19 from "fs/promises";
15386
+ import path16 from "path";
15093
15387
 
15094
15388
  // src/app/exec/jsonl.ts
15095
15389
  function createExecJsonlEvent(type, data, ts = Date.now()) {
@@ -15117,6 +15411,10 @@ function createExecOutputWriter(options) {
15117
15411
  if (!options.verbose) return;
15118
15412
  writeLine(options.stderr, `[athena exec] ${message}`);
15119
15413
  },
15414
+ notice(message) {
15415
+ if (options.json) return;
15416
+ writeLine(options.stderr, `[athena exec] ${message}`);
15417
+ },
15120
15418
  warn(message) {
15121
15419
  writeLine(options.stderr, `[athena exec] warning: ${message}`);
15122
15420
  },
@@ -15128,9 +15426,9 @@ function createExecOutputWriter(options) {
15128
15426
  writeLine(options.stdout, message);
15129
15427
  },
15130
15428
  async writeLastMessage(filePath, message) {
15131
- const absPath = path17.resolve(filePath);
15132
- await fs20.mkdir(path17.dirname(absPath), { recursive: true });
15133
- await fs20.writeFile(absPath, message, "utf-8");
15429
+ const absPath = path16.resolve(filePath);
15430
+ await fs19.mkdir(path16.dirname(absPath), { recursive: true });
15431
+ await fs19.writeFile(absPath, message, "utf-8");
15134
15432
  }
15135
15433
  };
15136
15434
  }
@@ -15145,6 +15443,34 @@ var NULL_TOKENS3 = {
15145
15443
  contextSize: null,
15146
15444
  contextWindowSize: null
15147
15445
  };
15446
+ function formatPersonalCapabilityNotice(summary) {
15447
+ const parts = [];
15448
+ if (summary.mcpServers.length > 0) {
15449
+ const list = summary.mcpServers.map((server) => `${server.name} [${server.sourceLayer}]`).join(", ");
15450
+ parts.push(`mcp servers: ${list}`);
15451
+ }
15452
+ if (summary.skills.length > 0) {
15453
+ const list = summary.skills.map((skill) => `${skill.name} [${skill.sourceLayer}]`).join(", ");
15454
+ parts.push(`skills: ${list}`);
15455
+ }
15456
+ if (parts.length === 0) return null;
15457
+ return `personal capabilities active \u2014 ${parts.join("; ")}`;
15458
+ }
15459
+ function formatCapabilityConflictNotice(summary) {
15460
+ const parts = [];
15461
+ if (summary.mcpServers.length > 0) {
15462
+ const list = summary.mcpServers.map((server) => `${server.name} [${server.sourceLayer}]`).join(", ");
15463
+ parts.push(`mcp servers: ${list}`);
15464
+ }
15465
+ if (summary.skills.length > 0) {
15466
+ const list = summary.skills.map((skill) => `${skill.name} [${skill.sourceLayer}]`).join(", ");
15467
+ parts.push(`skills: ${list}`);
15468
+ }
15469
+ if (parts.length === 0) return null;
15470
+ return `personal capability conflicts \u2014 workflow plugin wins; shadowed: ${parts.join(
15471
+ "; "
15472
+ )}`;
15473
+ }
15148
15474
  function workflowFailure(state, message) {
15149
15475
  return {
15150
15476
  kind: "workflow",
@@ -15209,7 +15535,7 @@ async function runExec(options) {
15209
15535
  store = sessionStoreFactory({
15210
15536
  sessionId: athenaSessionId,
15211
15537
  projectDir: options.projectDir,
15212
- dbPath: options.ephemeral ? ":memory:" : path18.join(sessionsDir(), athenaSessionId, "session.db")
15538
+ dbPath: options.ephemeral ? ":memory:" : path17.join(sessionsDir(), athenaSessionId, "session.db")
15213
15539
  });
15214
15540
  } catch (error) {
15215
15541
  const message = `Failed to initialize session store: ${error instanceof Error ? error.message : String(error)}`;
@@ -15300,23 +15626,6 @@ async function runExec(options) {
15300
15626
  ...options.signal ? { signal: options.signal } : {}
15301
15627
  };
15302
15628
  const dashboardDecisionInbox = options.dashboardDecisionInbox;
15303
- const applyPendingDashboardDecisions = () => {
15304
- if (!dashboardDecisionInbox) return;
15305
- const rows = dashboardDecisionInbox.pendingForSession({
15306
- athenaSessionId,
15307
- limit: 25
15308
- });
15309
- for (const row of rows) {
15310
- try {
15311
- runtime.sendDecision(row.requestId, row.decision);
15312
- dashboardDecisionInbox.markConsumed({ id: row.id });
15313
- } catch (error) {
15314
- output.warn(
15315
- `dashboard decision failed: ${error instanceof Error ? error.message : String(error)}`
15316
- );
15317
- }
15318
- }
15319
- };
15320
15629
  const linkedAdapterSessions = /* @__PURE__ */ new Set();
15321
15630
  function publishFeedEvents(feedEvents) {
15322
15631
  if (feedEvents.length === 0) return;
@@ -15377,60 +15686,57 @@ async function runExec(options) {
15377
15686
  });
15378
15687
  }
15379
15688
  };
15380
- const unsubscribeEvent = runtime.onEvent((runtimeEvent) => {
15381
- adapterSessionId = runtimeEvent.sessionId;
15382
- if (runtimeEvent.sessionId && activeRunId && !linkedAdapterSessions.has(runtimeEvent.sessionId)) {
15383
- linkedAdapterSessions.add(runtimeEvent.sessionId);
15384
- safePersist(
15385
- store,
15386
- () => store.linkAdapterSession(runtimeEvent.sessionId, activeRunId),
15387
- (message) => output.warn(message),
15388
- "linkAdapterSession failed"
15389
- );
15390
- }
15391
- output.emitJsonEvent("runtime.event", {
15392
- id: runtimeEvent.id,
15393
- kind: runtimeEvent.kind,
15394
- hookName: runtimeEvent.hookName,
15395
- sessionId: runtimeEvent.sessionId,
15396
- toolName: runtimeEvent.toolName ?? null,
15397
- data: runtimeEvent.data
15398
- });
15399
- if (latch.hasFailure()) return;
15400
- const { feedEvents, decision } = ingestRuntimeEvent(runtimeEvent, {
15689
+ const runtimeEventLoop = attachRuntimeEventLoop({
15690
+ runtime,
15691
+ ingest: {
15401
15692
  mapper,
15402
15693
  store,
15403
15694
  controllerCallbacks,
15404
15695
  onPersistFailure: (message) => output.warn(message)
15405
- });
15406
- if (decision) {
15407
- runtime.sendDecision(runtimeEvent.id, decision);
15408
- }
15409
- for (const event of feedEvents) {
15410
- if (event.kind === "agent.message") {
15411
- mappedFinalMessage = event.data.message;
15696
+ },
15697
+ onEventReceived: (runtimeEvent) => {
15698
+ adapterSessionId = runtimeEvent.sessionId;
15699
+ if (runtimeEvent.sessionId && activeRunId && !linkedAdapterSessions.has(runtimeEvent.sessionId)) {
15700
+ linkedAdapterSessions.add(runtimeEvent.sessionId);
15701
+ safePersist(
15702
+ store,
15703
+ () => store.linkAdapterSession(runtimeEvent.sessionId, activeRunId),
15704
+ (message) => output.warn(message),
15705
+ "linkAdapterSession failed"
15706
+ );
15412
15707
  }
15413
- }
15414
- publishFeedEvents(feedEvents);
15415
- });
15416
- const unsubscribeDecision = runtime.onDecision(
15417
- (eventId, decision) => {
15708
+ output.emitJsonEvent("runtime.event", {
15709
+ id: runtimeEvent.id,
15710
+ kind: runtimeEvent.kind,
15711
+ hookName: runtimeEvent.hookName,
15712
+ sessionId: runtimeEvent.sessionId,
15713
+ toolName: runtimeEvent.toolName ?? null,
15714
+ data: runtimeEvent.data
15715
+ });
15716
+ },
15717
+ skipEvent: () => latch.hasFailure(),
15718
+ emitEventFeed: (feedEvents) => {
15719
+ for (const event of feedEvents) {
15720
+ if (event.kind === "agent.message") {
15721
+ mappedFinalMessage = event.data.message;
15722
+ }
15723
+ }
15724
+ publishFeedEvents(feedEvents);
15725
+ },
15726
+ onDecisionReceived: (eventId, decision) => {
15418
15727
  output.emitJsonEvent("runtime.decision", {
15419
15728
  eventId,
15420
15729
  decision
15421
15730
  });
15422
- const feedEvent = ingestRuntimeDecision(eventId, decision, {
15423
- mapper,
15424
- store,
15425
- onPersistFailure: (message) => output.warn(message)
15426
- });
15731
+ },
15732
+ emitDecisionFeed: (feedEvent) => {
15427
15733
  if (feedEvent) {
15428
15734
  publishFeedEvents([feedEvent]);
15429
15735
  }
15430
15736
  }
15431
- );
15737
+ });
15432
15738
  let timeoutTimer;
15433
- let dashboardDecisionTimer;
15739
+ let dashboardDecisionDrain;
15434
15740
  if (typeof options.timeoutMs === "number" && options.timeoutMs > 0) {
15435
15741
  timeoutTimer = setTimeout(() => {
15436
15742
  latch.register({
@@ -15439,11 +15745,29 @@ async function runExec(options) {
15439
15745
  });
15440
15746
  }, options.timeoutMs);
15441
15747
  }
15748
+ const personalCapabilities = options.personalCapabilities ?? {
15749
+ mcpServers: [],
15750
+ skills: []
15751
+ };
15752
+ const capabilityConflicts = options.capabilityConflicts ?? {
15753
+ mcpServers: [],
15754
+ skills: []
15755
+ };
15442
15756
  output.emitJsonEvent("exec.started", {
15443
15757
  projectDir: options.projectDir,
15444
15758
  harness: options.harness,
15445
- athenaSessionId: options.ephemeral ? null : athenaSessionId
15759
+ athenaSessionId: options.ephemeral ? null : athenaSessionId,
15760
+ personalCapabilities,
15761
+ capabilityConflicts
15446
15762
  });
15763
+ const personalCapabilityNotice = formatPersonalCapabilityNotice(personalCapabilities);
15764
+ if (personalCapabilityNotice) {
15765
+ output.notice(personalCapabilityNotice);
15766
+ }
15767
+ const capabilityConflictNotice = formatCapabilityConflictNotice(capabilityConflicts);
15768
+ if (capabilityConflictNotice) {
15769
+ output.notice(capabilityConflictNotice);
15770
+ }
15447
15771
  try {
15448
15772
  await runtime.start();
15449
15773
  runtimeStarted = true;
@@ -15451,12 +15775,16 @@ async function runExec(options) {
15451
15775
  status: runtime.getStatus()
15452
15776
  });
15453
15777
  if (dashboardDecisionInbox) {
15454
- applyPendingDashboardDecisions();
15455
- dashboardDecisionTimer = setInterval(
15456
- applyPendingDashboardDecisions,
15457
- options.dashboardDecisionPollIntervalMs ?? 1e3
15458
- );
15459
- dashboardDecisionTimer.unref();
15778
+ dashboardDecisionDrain = startDashboardDecisionDrain({
15779
+ runtime,
15780
+ inbox: dashboardDecisionInbox,
15781
+ athenaSessionId,
15782
+ ...options.dashboardDecisionPollIntervalMs !== void 0 ? { pollIntervalMs: options.dashboardDecisionPollIntervalMs } : {},
15783
+ onError: (error) => output.warn(
15784
+ `dashboard decision failed: ${error instanceof Error ? error.message : String(error)}`
15785
+ ),
15786
+ configureTimer: (timer) => timer.unref()
15787
+ });
15460
15788
  }
15461
15789
  const workflow = options.workflow;
15462
15790
  output.emitJsonEvent("run.started", {
@@ -15543,14 +15871,11 @@ async function runExec(options) {
15543
15871
  if (timeoutTimer) {
15544
15872
  clearTimeout(timeoutTimer);
15545
15873
  }
15546
- if (dashboardDecisionTimer) {
15547
- clearInterval(dashboardDecisionTimer);
15548
- }
15874
+ dashboardDecisionDrain?.stop();
15549
15875
  await writeLastMessageBeforeTerminalCompletion();
15550
15876
  await runBeforeTerminalCompletion();
15551
15877
  await sessionController.kill();
15552
- unsubscribeEvent();
15553
- unsubscribeDecision();
15878
+ runtimeEventLoop.stop();
15554
15879
  if (runtimeStarted) {
15555
15880
  runtime.stop();
15556
15881
  }
@@ -15599,7 +15924,7 @@ async function runExec(options) {
15599
15924
  }
15600
15925
 
15601
15926
  // src/app/dashboard/instanceSocketClient.ts
15602
- import { WebSocket as WebSocket2 } from "ws";
15927
+ import { WebSocket } from "ws";
15603
15928
  var DEFAULT_HEARTBEAT_MS = 3e4;
15604
15929
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
15605
15930
  function instanceSocketUrl(dashboardUrl, instanceId) {
@@ -15616,7 +15941,7 @@ function createInstanceSocketClient(opts) {
15616
15941
  const log = opts.log ?? (() => {
15617
15942
  });
15618
15943
  const now = opts.now ?? (() => Date.now());
15619
- const makeWebSocket = opts.makeWebSocket ?? ((url, accessToken) => new WebSocket2(url, [accessToken]));
15944
+ const makeWebSocket = opts.makeWebSocket ?? ((url, accessToken) => new WebSocket(url, [accessToken]));
15620
15945
  const frameHandlers = /* @__PURE__ */ new Set();
15621
15946
  const closeHandlers = /* @__PURE__ */ new Set();
15622
15947
  let ws = null;
@@ -15816,7 +16141,7 @@ function normalizeHarnessOverride(value) {
15816
16141
  }
15817
16142
 
15818
16143
  // src/app/dashboard/runStreamClient.ts
15819
- import { WebSocket as WebSocket3 } from "ws";
16144
+ import { WebSocket as WebSocket2 } from "ws";
15820
16145
  var DEFAULT_RECONNECT_DELAYS_MS = [250, 1e3, 2e3, 5e3, 15e3];
15821
16146
  var DEFAULT_HEARTBEAT_MS2 = 3e4;
15822
16147
  var DEFAULT_WATCHDOG_MS = 9e4;
@@ -15829,7 +16154,7 @@ function createRunStreamClient(opts) {
15829
16154
  const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS2;
15830
16155
  const watchdogMs = opts.watchdogTimeoutMs ?? DEFAULT_WATCHDOG_MS;
15831
16156
  const maxQueueSize = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
15832
- const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket3(url));
16157
+ const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket2(url));
15833
16158
  const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
15834
16159
  const clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
15835
16160
  let nextSeq = 1;
@@ -16169,8 +16494,8 @@ async function createRemoteRunEventPublisher({
16169
16494
  // src/app/dashboard/artifactCapture.ts
16170
16495
  import { execFile } from "child_process";
16171
16496
  import crypto3 from "crypto";
16172
- import fs21 from "fs/promises";
16173
- import path19 from "path";
16497
+ import fs20 from "fs/promises";
16498
+ import path18 from "path";
16174
16499
  import { promisify } from "util";
16175
16500
  var execFileAsync = promisify(execFile);
16176
16501
  function parseArtifactUploadSpec(value) {
@@ -16353,21 +16678,21 @@ async function collectArtifactPayloads(input) {
16353
16678
  return payloads;
16354
16679
  }
16355
16680
  async function readWorkspaceFile(projectDir, rel) {
16356
- const absolute = path19.resolve(projectDir, rel);
16357
- const workspaceRoot = await fs21.realpath(projectDir);
16681
+ const absolute = path18.resolve(projectDir, rel);
16682
+ const workspaceRoot = await fs20.realpath(projectDir);
16358
16683
  let stat;
16359
16684
  try {
16360
- stat = await fs21.lstat(absolute);
16685
+ stat = await fs20.lstat(absolute);
16361
16686
  } catch {
16362
16687
  return null;
16363
16688
  }
16364
16689
  if (!stat.isFile() || stat.isSymbolicLink()) return null;
16365
- const real = await fs21.realpath(absolute);
16366
- const relativeToRoot = path19.relative(workspaceRoot, real);
16367
- if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path19.sep}`) || path19.isAbsolute(relativeToRoot)) {
16690
+ const real = await fs20.realpath(absolute);
16691
+ const relativeToRoot = path18.relative(workspaceRoot, real);
16692
+ if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path18.sep}`) || path18.isAbsolute(relativeToRoot)) {
16368
16693
  return null;
16369
16694
  }
16370
- return fs21.readFile(absolute);
16695
+ return fs20.readFile(absolute);
16371
16696
  }
16372
16697
  async function gitDiffForAllowedPaths(input) {
16373
16698
  const paths = splitNul(await git(input.projectDir, input.nameArgs)).filter(
@@ -16458,13 +16783,13 @@ var DEFAULT_HARD_DENY = [
16458
16783
  ".ssh/**"
16459
16784
  ];
16460
16785
  function isAllowedRelativePath(rel, hardDeny) {
16461
- if (path19.isAbsolute(rel) || rel.includes("\0")) return false;
16462
- const normalized = path19.posix.normalize(rel.replaceAll(path19.sep, "/"));
16786
+ if (path18.isAbsolute(rel) || rel.includes("\0")) return false;
16787
+ const normalized = path18.posix.normalize(rel.replaceAll(path18.sep, "/"));
16463
16788
  if (normalized === ".." || normalized.startsWith("../")) return false;
16464
16789
  return !hardDeny.some((pattern) => matchesDenyPattern(normalized, pattern));
16465
16790
  }
16466
16791
  function matchesDenyPattern(rel, pattern) {
16467
- const normalized = pattern.replaceAll(path19.sep, "/");
16792
+ const normalized = pattern.replaceAll(path18.sep, "/");
16468
16793
  if (normalized.includes("*") || normalized.includes("?")) {
16469
16794
  return globToRegExp(normalized).test(rel);
16470
16795
  }
@@ -16861,18 +17186,18 @@ async function executeRemoteAssignment({
16861
17186
 
16862
17187
  // src/infra/config/attachmentMirror.ts
16863
17188
  import crypto4 from "crypto";
16864
- import fs22 from "fs";
17189
+ import fs21 from "fs";
16865
17190
  import os13 from "os";
16866
- import path20 from "path";
17191
+ import path19 from "path";
16867
17192
  function attachmentMirrorPath(env = process.env) {
16868
17193
  const home = env["HOME"] ?? os13.homedir();
16869
- return path20.join(home, ".config", "athena", "attachments.json");
17194
+ return path19.join(home, ".config", "athena", "attachments.json");
16870
17195
  }
16871
17196
  function readAttachmentMirror(env = process.env) {
16872
17197
  const file = attachmentMirrorPath(env);
16873
17198
  let raw;
16874
17199
  try {
16875
- raw = fs22.readFileSync(file, "utf-8");
17200
+ raw = fs21.readFileSync(file, "utf-8");
16876
17201
  } catch (err) {
16877
17202
  if (err.code === "ENOENT") return null;
16878
17203
  throw err;
@@ -16896,29 +17221,29 @@ function readAttachmentMirror(env = process.env) {
16896
17221
  function writeAttachmentMirror(mirror, env = process.env) {
16897
17222
  const validated = parseAttachmentMirror(mirror);
16898
17223
  const file = attachmentMirrorPath(env);
16899
- const dir = path20.dirname(file);
16900
- fs22.mkdirSync(dir, { recursive: true, mode: 448 });
17224
+ const dir = path19.dirname(file);
17225
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
16901
17226
  const tmp = `${file}.${process.pid}.${crypto4.randomBytes(4).toString("hex")}.tmp`;
16902
- const fd = fs22.openSync(tmp, "w", 384);
17227
+ const fd = fs21.openSync(tmp, "w", 384);
16903
17228
  try {
16904
- fs22.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
16905
- fs22.fsyncSync(fd);
17229
+ fs21.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
17230
+ fs21.fsyncSync(fd);
16906
17231
  } finally {
16907
- fs22.closeSync(fd);
17232
+ fs21.closeSync(fd);
16908
17233
  }
16909
17234
  try {
16910
- fs22.renameSync(tmp, file);
17235
+ fs21.renameSync(tmp, file);
16911
17236
  } catch (err) {
16912
17237
  try {
16913
- fs22.unlinkSync(tmp);
17238
+ fs21.unlinkSync(tmp);
16914
17239
  } catch {
16915
17240
  }
16916
17241
  throw err;
16917
17242
  }
16918
17243
  if (process.platform !== "win32") {
16919
17244
  try {
16920
- fs22.chmodSync(dir, 448);
16921
- fs22.chmodSync(file, 384);
17245
+ fs21.chmodSync(dir, 448);
17246
+ fs21.chmodSync(file, 384);
16922
17247
  } catch {
16923
17248
  }
16924
17249
  }
@@ -16926,7 +17251,7 @@ function writeAttachmentMirror(mirror, env = process.env) {
16926
17251
  function removeAttachmentMirror(env = process.env) {
16927
17252
  const file = attachmentMirrorPath(env);
16928
17253
  try {
16929
- fs22.unlinkSync(file);
17254
+ fs21.unlinkSync(file);
16930
17255
  } catch (err) {
16931
17256
  if (err.code !== "ENOENT") throw err;
16932
17257
  }
@@ -17054,7 +17379,6 @@ function createAttachmentReconciler(options) {
17054
17379
  }
17055
17380
 
17056
17381
  // src/app/dashboard/dashboardDecisionInbox.ts
17057
- import Database5 from "better-sqlite3";
17058
17382
  function dashboardDecisionInboxPath() {
17059
17383
  return `${ensureDaemonStateDir().dir}/dashboard-decision-inbox.db`;
17060
17384
  }
@@ -17099,8 +17423,6 @@ function migrateLegacyUniqueConstraint(db) {
17099
17423
  }
17100
17424
  function initInboxSchema(db) {
17101
17425
  db.exec(`
17102
- PRAGMA journal_mode = WAL;
17103
-
17104
17426
  CREATE TABLE IF NOT EXISTS dashboard_decision_inbox (
17105
17427
  id INTEGER PRIMARY KEY AUTOINCREMENT,
17106
17428
  athena_session_id TEXT NOT NULL,
@@ -17122,8 +17444,9 @@ function initInboxSchema(db) {
17122
17444
  `);
17123
17445
  }
17124
17446
  function createDashboardDecisionInbox(options = {}) {
17125
- const db = new Database5(options.dbPath ?? dashboardDecisionInboxPath());
17126
- initInboxSchema(db);
17447
+ const db = openVersionedDb(options.dbPath ?? dashboardDecisionInboxPath(), {
17448
+ migrate: initInboxSchema
17449
+ });
17127
17450
  const upsertUnconsumed = db.prepare(`
17128
17451
  INSERT INTO dashboard_decision_inbox (
17129
17452
  athena_session_id,
@@ -17332,9 +17655,9 @@ function createDashboardPairedExecution(options) {
17332
17655
  }
17333
17656
 
17334
17657
  // src/app/dashboard/remoteWorkspaceResolver.ts
17335
- import fs23 from "fs";
17658
+ import fs22 from "fs";
17336
17659
  import os14 from "os";
17337
- import path21 from "path";
17660
+ import path20 from "path";
17338
17661
  function resolveRemoteWorkspace(assignment, options = {}) {
17339
17662
  const { spec, runId, runnerId } = assignment;
17340
17663
  if (spec.projectDir) {
@@ -17343,14 +17666,14 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17343
17666
  const sessionId = spec.athenaSessionId ?? spec.sessionId;
17344
17667
  const deploymentSlug = deploymentSlugFromUrl(options.dashboardUrl);
17345
17668
  const stateDir = daemonStatePaths(options.env).dir;
17346
- const projectDir = sessionId ? path21.join(
17669
+ const projectDir = sessionId ? path20.join(
17347
17670
  stateDir,
17348
17671
  "remote-workspaces",
17349
17672
  deploymentSlug,
17350
17673
  sanitizePathSegment(runnerId),
17351
17674
  "sessions",
17352
17675
  sanitizePathSegment(sessionId)
17353
- ) : path21.join(
17676
+ ) : path20.join(
17354
17677
  stateDir,
17355
17678
  "remote-workspaces",
17356
17679
  deploymentSlug,
@@ -17359,10 +17682,10 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17359
17682
  sanitizePathSegment(runId)
17360
17683
  );
17361
17684
  try {
17362
- fs23.mkdirSync(projectDir, { recursive: true, mode: 448 });
17685
+ fs22.mkdirSync(projectDir, { recursive: true, mode: 448 });
17363
17686
  if (process.platform !== "win32") {
17364
17687
  try {
17365
- fs23.chmodSync(projectDir, 448);
17688
+ fs22.chmodSync(projectDir, 448);
17366
17689
  } catch {
17367
17690
  }
17368
17691
  }
@@ -17378,8 +17701,8 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17378
17701
  return validateProjectDir(projectDir, options.env);
17379
17702
  }
17380
17703
  function validateProjectDir(projectDir, env = process.env) {
17381
- const resolved = path21.resolve(projectDir);
17382
- if (!path21.isAbsolute(projectDir)) {
17704
+ const resolved = path20.resolve(projectDir);
17705
+ if (!path20.isAbsolute(projectDir)) {
17383
17706
  return {
17384
17707
  kind: "rejected",
17385
17708
  rejection: {
@@ -17388,7 +17711,7 @@ function validateProjectDir(projectDir, env = process.env) {
17388
17711
  }
17389
17712
  };
17390
17713
  }
17391
- const home = path21.resolve(env["HOME"] ?? os14.homedir());
17714
+ const home = path20.resolve(env["HOME"] ?? os14.homedir());
17392
17715
  if (resolved === home) {
17393
17716
  return {
17394
17717
  kind: "rejected",
@@ -17400,7 +17723,7 @@ function validateProjectDir(projectDir, env = process.env) {
17400
17723
  }
17401
17724
  let stat;
17402
17725
  try {
17403
- stat = fs23.statSync(resolved);
17726
+ stat = fs22.statSync(resolved);
17404
17727
  } catch {
17405
17728
  return {
17406
17729
  kind: "rejected",
@@ -17845,18 +18168,18 @@ async function runDashboardRuntimeDaemon(options = {}) {
17845
18168
  }
17846
18169
 
17847
18170
  // src/infra/daemon/pidLock.ts
17848
- import fs24 from "fs";
18171
+ import fs23 from "fs";
17849
18172
  function acquirePidLock(pidPath) {
17850
18173
  const ownPid = process.pid;
17851
18174
  for (let attempt = 0; attempt < 2; attempt += 1) {
17852
18175
  try {
17853
- const fd = fs24.openSync(pidPath, "wx", 384);
18176
+ const fd = fs23.openSync(pidPath, "wx", 384);
17854
18177
  try {
17855
- fs24.writeSync(fd, `${ownPid}
18178
+ fs23.writeSync(fd, `${ownPid}
17856
18179
  `);
17857
- fs24.fsyncSync(fd);
18180
+ fs23.fsyncSync(fd);
17858
18181
  } finally {
17859
- fs24.closeSync(fd);
18182
+ fs23.closeSync(fd);
17860
18183
  }
17861
18184
  return makeHandle(pidPath, ownPid);
17862
18185
  } catch (err) {
@@ -17870,7 +18193,7 @@ function acquirePidLock(pidPath) {
17870
18193
  }
17871
18194
  if (existing.state === "stale") {
17872
18195
  try {
17873
- fs24.unlinkSync(pidPath);
18196
+ fs23.unlinkSync(pidPath);
17874
18197
  } catch (err) {
17875
18198
  if (err.code !== "ENOENT") throw err;
17876
18199
  }
@@ -17884,7 +18207,7 @@ function acquirePidLock(pidPath) {
17884
18207
  function readPidLock(pidPath) {
17885
18208
  let raw;
17886
18209
  try {
17887
- raw = fs24.readFileSync(pidPath, "utf-8");
18210
+ raw = fs23.readFileSync(pidPath, "utf-8");
17888
18211
  } catch (err) {
17889
18212
  if (err.code === "ENOENT") {
17890
18213
  return { state: "absent" };
@@ -17908,9 +18231,9 @@ function makeHandle(pidPath, pid) {
17908
18231
  if (released) return;
17909
18232
  released = true;
17910
18233
  try {
17911
- const raw = fs24.readFileSync(pidPath, "utf-8").trim();
18234
+ const raw = fs23.readFileSync(pidPath, "utf-8").trim();
17912
18235
  if (raw === String(pid)) {
17913
- fs24.unlinkSync(pidPath);
18236
+ fs23.unlinkSync(pidPath);
17914
18237
  }
17915
18238
  } catch (err) {
17916
18239
  if (err.code !== "ENOENT") {
@@ -17936,7 +18259,7 @@ function isProcessAlive(pid) {
17936
18259
  }
17937
18260
 
17938
18261
  // src/infra/daemon/udsIpc.ts
17939
- import fs25 from "fs";
18262
+ import fs24 from "fs";
17940
18263
  import net2 from "net";
17941
18264
 
17942
18265
  // src/infra/daemon/udsFrameCodec.ts
@@ -17981,7 +18304,7 @@ async function startUdsServer(socketPath, handler, log) {
17981
18304
  });
17982
18305
  if (process.platform !== "win32") {
17983
18306
  try {
17984
- fs25.chmodSync(socketPath, 384);
18307
+ fs24.chmodSync(socketPath, 384);
17985
18308
  } catch {
17986
18309
  }
17987
18310
  }
@@ -17991,7 +18314,7 @@ async function startUdsServer(socketPath, handler, log) {
17991
18314
  server.close(() => resolve());
17992
18315
  });
17993
18316
  try {
17994
- fs25.unlinkSync(socketPath);
18317
+ fs24.unlinkSync(socketPath);
17995
18318
  } catch (err) {
17996
18319
  if (err.code !== "ENOENT") {
17997
18320
  }
@@ -18109,7 +18432,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
18109
18432
  async function unlinkStaleSocket(socketPath) {
18110
18433
  let stat;
18111
18434
  try {
18112
- stat = fs25.statSync(socketPath);
18435
+ stat = fs24.statSync(socketPath);
18113
18436
  } catch (err) {
18114
18437
  if (err.code === "ENOENT") return;
18115
18438
  throw err;
@@ -18136,7 +18459,7 @@ async function unlinkStaleSocket(socketPath) {
18136
18459
  `uds path ${socketPath} is in use by another process; aborting`
18137
18460
  );
18138
18461
  }
18139
- fs25.unlinkSync(socketPath);
18462
+ fs24.unlinkSync(socketPath);
18140
18463
  }
18141
18464
 
18142
18465
  export {
@@ -18154,8 +18477,8 @@ export {
18154
18477
  mergeFeedItems,
18155
18478
  buildPostByToolUseId,
18156
18479
  createFeedMapper,
18157
- ingestRuntimeEvent,
18158
- ingestRuntimeDecision,
18480
+ attachRuntimeEventLoop,
18481
+ startDashboardDecisionDrain,
18159
18482
  generateId,
18160
18483
  daemonStatePaths,
18161
18484
  ensureDaemonStateDir,
@@ -18196,8 +18519,6 @@ export {
18196
18519
  GatewayUnreachableError,
18197
18520
  GatewayUnauthorizedError,
18198
18521
  connect,
18199
- createWsClientTransport,
18200
- wsClientOptionsForEndpoint,
18201
18522
  isSupportedGatewayUrl,
18202
18523
  readGatewayClientConfig,
18203
18524
  writeGatewayClientConfig,
@@ -18225,6 +18546,8 @@ export {
18225
18546
  isBashToolResponse,
18226
18547
  resolveToolColumn,
18227
18548
  IndexedTimeline,
18549
+ parseFrontmatter,
18550
+ resolveEffectiveCapabilities,
18228
18551
  bootstrapRuntimeConfig,
18229
18552
  EXEC_EXIT_CODE,
18230
18553
  runExec,
@@ -18237,4 +18560,4 @@ export {
18237
18560
  startUdsServer,
18238
18561
  sendUdsRequest
18239
18562
  };
18240
- //# sourceMappingURL=chunk-EC67PEFT.js.map
18563
+ //# sourceMappingURL=chunk-YZ7RCBKO.js.map