@drisp/cli 0.5.26 → 0.5.28

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ATHENA_HOOK_SOCKET_ENV,
3
3
  createAskUserQuestionResult,
4
+ createCompactBlockResult,
4
5
  createPermissionRequestAllowResult,
5
6
  createPermissionRequestDenyResult,
6
7
  createPreToolUseAllowResult,
@@ -12,7 +13,7 @@ import {
12
13
  isToolEvent,
13
14
  isValidHookEventEnvelope,
14
15
  resolveHookSocketPath
15
- } from "./chunk-QYB6N2OT.js";
16
+ } from "./chunk-USK5VSQ7.js";
16
17
  import {
17
18
  TransportUnreachableError,
18
19
  createUdsClientTransport,
@@ -25,10 +26,12 @@ import {
25
26
  trackGatewayTransportReconnect,
26
27
  writeGatewayTrace,
27
28
  wsClientOptionsForEndpoint
28
- } from "./chunk-3U37HT4T.js";
29
+ } from "./chunk-VAFS446Y.js";
29
30
  import {
31
+ DEFAULT_MAX_TURN_TOKEN_COUNT,
30
32
  compileWorkflowPlan,
31
33
  createWorkflowRunner,
34
+ deserializeRunMemory,
32
35
  installWorkflowFromSource,
33
36
  isMarketplaceRef,
34
37
  readConfig,
@@ -38,7 +41,7 @@ import {
38
41
  resolveWorkflow,
39
42
  resolveWorkflowInstall,
40
43
  resolveWorkflowPlugins
41
- } from "./chunk-73V7GXV6.js";
44
+ } from "./chunk-2QV7YSX6.js";
42
45
 
43
46
  // src/infra/daemon/stateDir.ts
44
47
  import fs from "fs";
@@ -291,10 +294,17 @@ var RULES = {
291
294
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
292
295
  canBlock: false
293
296
  },
297
+ // Decision-bearing for Handover (ADR 0014): a Workflow Run's orchestrator
298
+ // blocks the compaction and forks the conversation instead. Both claims are
299
+ // required — `canBlock` alone leaves the event off the decision-waiting
300
+ // path and the block reply is never sent. The timeout is deliberately a
301
+ // number, not null: with no decision (non-workflow session, orchestration
302
+ // failure) the adapter fires a passthrough and Claude proceeds with normal
303
+ // vendor compaction — Handover degrades, it never hangs.
294
304
  "compact.pre": {
295
- expectsDecision: false,
305
+ expectsDecision: true,
296
306
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
297
- canBlock: false
307
+ canBlock: true
298
308
  },
299
309
  "user.prompt": {
300
310
  expectsDecision: false,
@@ -858,6 +868,8 @@ function mapDecisionToResult(_event, decision) {
858
868
  return createPreToolUseDenyResult(intent.reason);
859
869
  case "stop_block":
860
870
  return createStopBlockResult(intent.reason);
871
+ case "compact_block":
872
+ return createCompactBlockResult(intent.reason);
861
873
  default:
862
874
  return { action: "passthrough" };
863
875
  }
@@ -1565,6 +1577,10 @@ var SYNC_HOOK_EVENTS = [
1565
1577
  "TaskCreated",
1566
1578
  "TaskCompleted",
1567
1579
  "ConfigChange",
1580
+ // Handover interception (ADR 0014): the forwarder may reply with a
1581
+ // compaction block. Dispatched async, Claude would ignore the stdout and
1582
+ // silently drop the block.
1583
+ "PreCompact",
1568
1584
  // Teardown — see (2) above.
1569
1585
  "SessionEnd"
1570
1586
  ];
@@ -3417,8 +3433,14 @@ function spawnClaude(options) {
3417
3433
  cwd: projectDir,
3418
3434
  stdio: ["ignore", "pipe", "pipe"],
3419
3435
  env: {
3420
- // Claude compacts around 95% of this window; 185k keeps the trigger above 175k tokens used.
3421
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: "185000",
3436
+ // Claude compacts around 95% of this window. The default sits well
3437
+ // under the model window (ADR 0014 §5): Handover needs headroom to
3438
+ // hold the conversation AND emit a Handoff file in the fork — the old
3439
+ // 185k default sat at the window and defeated that. Precedence:
3440
+ // default < process.env < extraEnv, so a workflow's explicit
3441
+ // maxTurnTokenCount (delivered via extraEnv) beats a user env var,
3442
+ // while the bare default stays env-overridable.
3443
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(DEFAULT_MAX_TURN_TOKEN_COUNT),
3422
3444
  ...process.env,
3423
3445
  ...extraEnv ?? {},
3424
3446
  ATHENA_INSTANCE_ID: String(instanceId),
@@ -3564,6 +3586,16 @@ function mergeIsolation(base, pluginMcpConfig, perCommand) {
3564
3586
  ...pluginMcpConfig ? { mcpConfig: pluginMcpConfig } : {}
3565
3587
  };
3566
3588
  }
3589
+ function resolveWorkflowSpawnEnv(workflow) {
3590
+ const maxTurnTokenCount = workflow?.loop?.maxTurnTokenCount;
3591
+ if (maxTurnTokenCount === void 0) {
3592
+ return workflow?.env;
3593
+ }
3594
+ return {
3595
+ ...workflow?.env,
3596
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(maxTurnTokenCount)
3597
+ };
3598
+ }
3567
3599
  function resolveClaudeSessionId(continuation) {
3568
3600
  if (!continuation || continuation.mode === "fresh") {
3569
3601
  return void 0;
@@ -3788,7 +3820,7 @@ function useClaudeProcess(projectDir, instanceId, isolation, pluginMcpConfig, ve
3788
3820
  pluginMcpConfig,
3789
3821
  perCallIsolation
3790
3822
  ),
3791
- env: workflow?.env,
3823
+ env: resolveWorkflowSpawnEnv(workflow),
3792
3824
  ...verbose ? {
3793
3825
  jqFilter: JQ_ASSISTANT_TEXT_FILTER,
3794
3826
  onFilteredStdout: (data) => {
@@ -3927,6 +3959,7 @@ function createClaudeSessionController(input) {
3927
3959
  const tokenAccumulator = createTokenAccumulator();
3928
3960
  const messageAccumulator = createAssistantMessageAccumulator();
3929
3961
  let lastStderr = "";
3962
+ let stderrTail = "";
3930
3963
  if (supportsTransportDiagnostics) {
3931
3964
  runtime.beginTurn();
3932
3965
  }
@@ -3945,6 +3978,7 @@ function createClaudeSessionController(input) {
3945
3978
  tokens: tokenAccumulator.getUsage(),
3946
3979
  streamMessage: messageAccumulator.getLastMessage(),
3947
3980
  lastStderr,
3981
+ stderrTail,
3948
3982
  diagnostics: supportsTransportDiagnostics ? { transport: runtime.getTransportStats() } : void 0
3949
3983
  });
3950
3984
  };
@@ -3959,7 +3993,7 @@ function createClaudeSessionController(input) {
3959
3993
  input.pluginMcpConfig,
3960
3994
  configOverride
3961
3995
  ),
3962
- env: input.workflow?.env,
3996
+ env: resolveWorkflowSpawnEnv(input.workflow),
3963
3997
  onStdout: (data) => {
3964
3998
  tokenAccumulator.feed(data);
3965
3999
  messageAccumulator.feed(data);
@@ -3972,6 +4006,7 @@ function createClaudeSessionController(input) {
3972
4006
  if (!lastStderr) {
3973
4007
  lastStderr = trimmed;
3974
4008
  }
4009
+ stderrTail = (stderrTail + "\n" + trimmed).slice(-2048).trim();
3975
4010
  if (!input.verbose) return;
3976
4011
  onStderrLine?.(trimmed);
3977
4012
  },
@@ -5796,6 +5831,10 @@ function mapToV2Decision(event, decision) {
5796
5831
  return { decision: wantsSessionApproval ? "acceptForSession" : "accept" };
5797
5832
  case "stop_block":
5798
5833
  return { decision: "cancel" };
5834
+ // Claude-specific Handover interception; Codex has no PreCompact
5835
+ // approval, so an (unexpected) compact_block on an approval request
5836
+ // falls through to accept like any other unmappable intent.
5837
+ case "compact_block":
5799
5838
  default:
5800
5839
  return { decision: wantsSessionApproval ? "acceptForSession" : "accept" };
5801
5840
  }
@@ -6914,7 +6953,7 @@ function resolveIsolation(preset) {
6914
6953
  case "strict":
6915
6954
  return { approvalPolicy: "on-request", sandbox: "read-only" };
6916
6955
  case "permissive":
6917
- return { approvalPolicy: "auto-edit", sandbox: "danger-full-access" };
6956
+ return { approvalPolicy: "never", sandbox: "danger-full-access" };
6918
6957
  case "minimal":
6919
6958
  case void 0:
6920
6959
  return { approvalPolicy: "on-request", sandbox: "workspace-write" };
@@ -6933,7 +6972,11 @@ function buildCodexPromptOptions(input) {
6933
6972
  agentRoots: input.workflowPlan?.agentRoots && input.workflowPlan.agentRoots.length > 0 ? input.workflowPlan.agentRoots : void 0,
6934
6973
  plugins: resolveCodexWorkflowPlugins(input.workflowPlan),
6935
6974
  config: {
6936
- model_auto_compact_token_limit: 175e3,
6975
+ // The harness-neutral maxTurnTokenCount (ADR 0014 §5): the bound sits
6976
+ // well under the model window so a Handover has headroom to hold the
6977
+ // conversation and emit a Handoff file — the old 175k default sat at
6978
+ // the window and defeated that.
6979
+ model_auto_compact_token_limit: input.workflowPlan?.workflow.loop?.maxTurnTokenCount ?? DEFAULT_MAX_TURN_TOKEN_COUNT,
6937
6980
  // Steer Codex's history compaction toward a handoff-style summary.
6938
6981
  // `compact_prompt` replaces the default summarization prompt.
6939
6982
  compact_prompt: HANDOFF_COMPACT_PROMPT,
@@ -7638,6 +7681,89 @@ function shouldResolveWorkflow({
7638
7681
  return Boolean(workflowName);
7639
7682
  }
7640
7683
 
7684
+ // src/core/workflows/builtins/handoffSkill.ts
7685
+ import fs16 from "fs";
7686
+ import os11 from "os";
7687
+ import path14 from "path";
7688
+ var DEFAULT_HANDOFF_FILE_PATH = ".athena/handoff.md";
7689
+ var PLUGIN_MANIFEST = `{
7690
+ "name": "athena-handoff",
7691
+ "description": "Athena's first-party handoff skill: distill the current conversation into a Handoff file",
7692
+ "version": "1.0.0"
7693
+ }
7694
+ `;
7695
+ var SKILL_CONTENT = `---
7696
+ name: handoff
7697
+ description: Distill the current conversation into a Handoff file on disk so a fresh agent session can continue the work without re-reading the history. Use when asked to hand off, or before context is reset.
7698
+ argument-hint: '[output-path] \u2014 where to write the Handoff file (default: ${DEFAULT_HANDOFF_FILE_PATH})'
7699
+ user-invocable: false
7700
+ ---
7701
+
7702
+ # Handoff
7703
+
7704
+ Write a **Handoff file**: a distillation of this conversation that lets a fresh
7705
+ agent session \u2014 with none of your context \u2014 continue the work seamlessly.
7706
+
7707
+ ## Output path
7708
+
7709
+ Write the file to: \`$ARGUMENTS\`
7710
+
7711
+ If no path was provided above (it reads "(none provided)"), write to
7712
+ \`${DEFAULT_HANDOFF_FILE_PATH}\` relative to the current working directory.
7713
+ Create parent directories as needed. Overwrite an existing file at that path.
7714
+
7715
+ ## What the file must contain
7716
+
7717
+ Structure the Handoff file with these sections, in this order:
7718
+
7719
+ 1. **Task and status** \u2014 what we are trying to accomplish, and exactly where it
7720
+ stands right now: what is done and verified, what is in flight, what has not
7721
+ been started. Include the concrete state of any in-flight step (e.g. "test X
7722
+ written but failing on Y").
7723
+ 2. **Decisions and rationale** \u2014 every decision already made, each with the
7724
+ *why*. A fresh session that doesn't know why a decision was made will
7725
+ re-litigate it and may silently reverse it.
7726
+ 3. **Open questions** \u2014 anything unresolved, ambiguous, or awaiting an answer,
7727
+ including from whom or where the answer should come.
7728
+ 4. **Files touched** \u2014 every file read, written, or relevant, referenced **by
7729
+ path** (repo-relative where possible). Never paste file contents \u2014 the next
7730
+ session can read the files itself.
7731
+ 5. **Suggested next steps / skills** \u2014 what the next session should do first,
7732
+ and which skills, commands, or workflows to invoke for it.
7733
+
7734
+ ## Rules
7735
+
7736
+ - **Reference, don't duplicate.** Point to existing artifacts \u2014 PRDs, plans,
7737
+ ADRs, issues, PRs, commits, diffs, trackers \u2014 by path or URL instead of
7738
+ copying their content into the Handoff file.
7739
+ - **Redact secrets.** Never write API keys, passwords, tokens, credentials, or
7740
+ personally identifiable information into the file. Replace any such value
7741
+ with \`[REDACTED]\` and, if needed, name where the real value lives (e.g. an
7742
+ env var name).
7743
+ - **Be selective, not exhaustive.** The file is a briefing, not a transcript.
7744
+ Everything the next session needs; nothing it can rediscover cheaply.
7745
+ - After writing the file, reply with only the absolute path of the file you
7746
+ wrote \u2014 no other prose.
7747
+ `;
7748
+ function writeIfChanged(filePath, content) {
7749
+ if (!fs16.existsSync(filePath) || fs16.readFileSync(filePath, "utf-8") !== content) {
7750
+ fs16.mkdirSync(path14.dirname(filePath), { recursive: true });
7751
+ fs16.writeFileSync(filePath, content, "utf-8");
7752
+ }
7753
+ }
7754
+ function ensureHandoffSkillPlugin(baseDir = path14.join(os11.homedir(), ".config", "athena", "builtins")) {
7755
+ const pluginDir = path14.join(baseDir, "handoff");
7756
+ writeIfChanged(
7757
+ path14.join(pluginDir, ".claude-plugin", "plugin.json"),
7758
+ PLUGIN_MANIFEST
7759
+ );
7760
+ writeIfChanged(
7761
+ path14.join(pluginDir, "skills", "handoff", "SKILL.md"),
7762
+ SKILL_CONTENT
7763
+ );
7764
+ return pluginDir;
7765
+ }
7766
+
7641
7767
  // src/app/runtime/createRuntime.ts
7642
7768
  var DEFAULT_HARNESS = "claude-code";
7643
7769
  function createRuntime(input) {
@@ -7708,6 +7834,15 @@ function bootstrapRuntimeConfig({
7708
7834
  workflowPluginDirs = workflowResolvedPlugins.map(
7709
7835
  (plugin) => plugin.claudeArtifactDir
7710
7836
  );
7837
+ if (pluginDelivery.mergeWorkflowPluginDirs) {
7838
+ try {
7839
+ workflowPluginDirs.push(ensureHandoffSkillPlugin());
7840
+ } catch (error) {
7841
+ warnings.push(
7842
+ `Failed to materialize the handoff skill plugin: ${error instanceof Error ? error.message : String(error)}`
7843
+ );
7844
+ }
7845
+ }
7711
7846
  }
7712
7847
  const globalPluginResolution = resolvePluginDirs(globalConfig.plugins);
7713
7848
  const projectPluginResolution = resolvePluginDirs(projectConfig.plugins);
@@ -7807,7 +7942,7 @@ var EXEC_EXIT_CODE = {
7807
7942
 
7808
7943
  // src/app/exec/runner.ts
7809
7944
  import crypto2 from "crypto";
7810
- import path17 from "path";
7945
+ import path18 from "path";
7811
7946
 
7812
7947
  // src/core/feed/entities.ts
7813
7948
  var ActorRegistry = class {
@@ -8180,27 +8315,27 @@ function generateNeutralTitle(event, g) {
8180
8315
  }
8181
8316
 
8182
8317
  // src/core/feed/transcript.ts
8183
- import * as fs16 from "fs";
8318
+ import * as fs17 from "fs";
8184
8319
  function createTranscriptReader() {
8185
8320
  const offsets = /* @__PURE__ */ new Map();
8186
8321
  function readNewAssistantMessages(transcriptPath) {
8187
8322
  const offset = offsets.get(transcriptPath) ?? 0;
8188
8323
  let fileSize;
8189
8324
  try {
8190
- fileSize = fs16.statSync(transcriptPath).size;
8325
+ fileSize = fs17.statSync(transcriptPath).size;
8191
8326
  if (fileSize <= offset) return [];
8192
8327
  } catch {
8193
8328
  return [];
8194
8329
  }
8195
8330
  let fd;
8196
8331
  try {
8197
- fd = fs16.openSync(transcriptPath, "r");
8332
+ fd = fs17.openSync(transcriptPath, "r");
8198
8333
  } catch {
8199
8334
  return [];
8200
8335
  }
8201
8336
  try {
8202
8337
  const buf = Buffer.alloc(fileSize - offset);
8203
- const bytesRead = fs16.readSync(fd, buf, 0, buf.length, offset);
8338
+ const bytesRead = fs17.readSync(fd, buf, 0, buf.length, offset);
8204
8339
  let lastNewline = -1;
8205
8340
  for (let i = bytesRead - 1; i >= 0; i--) {
8206
8341
  if (buf[i] === 10) {
@@ -8244,7 +8379,7 @@ function createTranscriptReader() {
8244
8379
  }
8245
8380
  return messages;
8246
8381
  } finally {
8247
- fs16.closeSync(fd);
8382
+ fs17.closeSync(fd);
8248
8383
  }
8249
8384
  }
8250
8385
  function getOffset(transcriptPath) {
@@ -10313,8 +10448,34 @@ function createFeedMapper(bootstrap) {
10313
10448
  };
10314
10449
  }
10315
10450
 
10451
+ // src/core/feed/syntheticEvents.ts
10452
+ var HOOK_NAME_BY_KIND = {
10453
+ "task.created": "TaskCreated",
10454
+ "task.completed": "TaskCompleted"
10455
+ };
10456
+ function buildSyntheticTaskEvent(kind, sessionId, data) {
10457
+ const hookName = HOOK_NAME_BY_KIND[kind];
10458
+ return {
10459
+ id: `${kind}-${generateId()}`,
10460
+ timestamp: Date.now(),
10461
+ kind,
10462
+ data,
10463
+ hookName,
10464
+ sessionId,
10465
+ context: { cwd: "", transcriptPath: "" },
10466
+ interaction: { expectsDecision: false },
10467
+ payload: {
10468
+ hook_event_name: hookName,
10469
+ session_id: sessionId,
10470
+ transcript_path: "",
10471
+ cwd: "",
10472
+ ...data
10473
+ }
10474
+ };
10475
+ }
10476
+
10316
10477
  // src/infra/sessions/schema.ts
10317
- var SCHEMA_VERSION = 7;
10478
+ var SCHEMA_VERSION = 9;
10318
10479
  var applySessionSchema = (db, fromVersion) => {
10319
10480
  db.exec(`
10320
10481
  CREATE TABLE IF NOT EXISTS session (
@@ -10377,6 +10538,18 @@ var applySessionSchema = (db, fromVersion) => {
10377
10538
  status TEXT NOT NULL DEFAULT 'running',
10378
10539
  stop_reason TEXT,
10379
10540
  tracker_path TEXT,
10541
+ -- Vendor session id (Claude Code session / Codex thread) of the Run's
10542
+ -- most recent Turn's Agent Session. Every resume- and fork-based
10543
+ -- transition (Nudge, Retry, human-resume, Handover) depends on it
10544
+ -- surviving a process restart (ADR 0014).
10545
+ adapter_session_id TEXT,
10546
+ -- Opaque JSON snapshot of the run-loop reducer's RunMemory (nudge/
10547
+ -- retry streaks, last tracker hash, in-flight stop prompt/
10548
+ -- continuation), so a resumed or woken process rehydrates the
10549
+ -- reducer instead of restarting its counters (ADR 0016). Written
10550
+ -- and read only by runMachine.ts's serializeRunMemory/
10551
+ -- deserializeRunMemory \u2014 this layer stores it opaquely.
10552
+ run_memory_json TEXT,
10380
10553
  FOREIGN KEY (session_id) REFERENCES session(id)
10381
10554
  );
10382
10555
 
@@ -10476,6 +10649,26 @@ var applySessionSchema = (db, fromVersion) => {
10476
10649
  UPDATE schema_version SET version = 7;
10477
10650
  `);
10478
10651
  }
10652
+ const versionAfterV7 = db.prepare("SELECT version FROM schema_version").get().version;
10653
+ if (versionAfterV7 === 7) {
10654
+ const hasColumn = db.prepare(
10655
+ `SELECT COUNT(*) AS n FROM pragma_table_info('workflow_runs') WHERE name = 'adapter_session_id'`
10656
+ ).get().n > 0;
10657
+ if (!hasColumn) {
10658
+ db.exec("ALTER TABLE workflow_runs ADD COLUMN adapter_session_id TEXT;");
10659
+ }
10660
+ db.exec("UPDATE schema_version SET version = 8;");
10661
+ }
10662
+ const versionAfterV8 = db.prepare("SELECT version FROM schema_version").get().version;
10663
+ if (versionAfterV8 === 8) {
10664
+ const hasRunMemoryColumn = db.prepare(
10665
+ `SELECT COUNT(*) AS n FROM pragma_table_info('workflow_runs') WHERE name = 'run_memory_json'`
10666
+ ).get().n > 0;
10667
+ if (!hasRunMemoryColumn) {
10668
+ db.exec("ALTER TABLE workflow_runs ADD COLUMN run_memory_json TEXT;");
10669
+ }
10670
+ db.exec("UPDATE schema_version SET version = 9;");
10671
+ }
10479
10672
  };
10480
10673
  var SESSION_SCHEMA = {
10481
10674
  version: SCHEMA_VERSION,
@@ -10546,13 +10739,15 @@ function createSessionStore(opts) {
10546
10739
  "UPDATE session SET event_count = event_count + ? WHERE id = ?"
10547
10740
  );
10548
10741
  const upsertRun = db.prepare(
10549
- `INSERT INTO workflow_runs (id, session_id, workflow_name, started_at, iteration, max_iterations, status, stop_reason, tracker_path)
10550
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
10742
+ `INSERT INTO workflow_runs (id, session_id, workflow_name, started_at, iteration, max_iterations, status, stop_reason, tracker_path, adapter_session_id, run_memory_json)
10743
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10551
10744
  ON CONFLICT(id) DO UPDATE SET
10552
10745
  iteration = excluded.iteration,
10553
10746
  status = excluded.status,
10554
10747
  stop_reason = excluded.stop_reason,
10555
- ended_at = CASE WHEN excluded.status != 'running' THEN ? ELSE ended_at END`
10748
+ adapter_session_id = COALESCE(excluded.adapter_session_id, adapter_session_id),
10749
+ run_memory_json = COALESCE(excluded.run_memory_json, run_memory_json),
10750
+ ended_at = CASE WHEN excluded.status NOT IN ('running', 'awaiting_attention') THEN ? ELSE ended_at END`
10556
10751
  );
10557
10752
  const selectLatestRun = db.prepare(
10558
10753
  `SELECT * FROM workflow_runs WHERE session_id = ? ORDER BY started_at DESC, rowid DESC LIMIT 1`
@@ -10718,7 +10913,7 @@ function createSessionStore(opts) {
10718
10913
  }
10719
10914
  function persistRun(snapshot) {
10720
10915
  const now2 = Date.now();
10721
- const endedAt = snapshot.status !== "running" ? now2 : null;
10916
+ const endedAt = snapshot.status !== "running" && snapshot.status !== "awaiting_attention" ? now2 : null;
10722
10917
  upsertRun.run(
10723
10918
  snapshot.runId,
10724
10919
  snapshot.sessionId,
@@ -10729,6 +10924,8 @@ function createSessionStore(opts) {
10729
10924
  snapshot.status,
10730
10925
  snapshot.stopReason ?? null,
10731
10926
  snapshot.trackerPath ?? null,
10927
+ snapshot.adapterSessionId ?? null,
10928
+ snapshot.runMemoryJson ?? null,
10732
10929
  endedAt
10733
10930
  );
10734
10931
  }
@@ -10745,7 +10942,9 @@ function createSessionStore(opts) {
10745
10942
  maxIterations: row.max_iterations,
10746
10943
  status: row.status,
10747
10944
  stopReason: row.stop_reason ?? void 0,
10748
- trackerPath: row.tracker_path ?? void 0
10945
+ trackerPath: row.tracker_path ?? void 0,
10946
+ adapterSessionId: row.adapter_session_id ?? void 0,
10947
+ runMemoryJson: row.run_memory_json ?? void 0
10749
10948
  };
10750
10949
  }
10751
10950
  function linkAdapterSession(adapterSessionId, runId) {
@@ -11453,8 +11652,8 @@ var PRIMARY_INPUT_EXTRACTORS = {
11453
11652
  return compactText(colonIdx >= 0 ? name.slice(colonIdx + 1) : name, 80);
11454
11653
  },
11455
11654
  NotebookEdit: (input) => {
11456
- const path21 = String(input.notebook_path ?? "");
11457
- return path21 ? shortenPath(path21) : "";
11655
+ const path22 = String(input.notebook_path ?? "");
11656
+ return path22 ? shortenPath(path22) : "";
11458
11657
  },
11459
11658
  AskUserQuestion: (input) => {
11460
11659
  const questions = input.questions;
@@ -12734,16 +12933,16 @@ function extractDomain(url) {
12734
12933
  }
12735
12934
  }
12736
12935
  function filePathSegments(input) {
12737
- const path21 = prop2(input, "file_path") ?? prop2(input, "notebook_path") ?? "";
12738
- if (typeof path21 !== "string" || !path21) return [];
12739
- const { prefix, filename } = shortenPathStructured(path21);
12936
+ const path22 = prop2(input, "file_path") ?? prop2(input, "notebook_path") ?? "";
12937
+ if (typeof path22 !== "string" || !path22) return [];
12938
+ const { prefix, filename } = shortenPathStructured(path22);
12740
12939
  if (prefix && filename) {
12741
12940
  return [
12742
12941
  { text: prefix, role: "target" },
12743
12942
  { text: filename, role: "filename" }
12744
12943
  ];
12745
12944
  }
12746
- return [{ text: filename || path21, role: "filename" }];
12945
+ return [{ text: filename || path22, role: "filename" }];
12747
12946
  }
12748
12947
  function grepSegments(input) {
12749
12948
  const pattern = String(prop2(input, "pattern") ?? "");
@@ -13911,6 +14110,30 @@ function openSessionDbReadonly(dbPath, options = {}) {
13911
14110
  for (const row of rows) counts[row.hook_name] = row.count;
13912
14111
  return counts;
13913
14112
  },
14113
+ latestWorkflowRun() {
14114
+ try {
14115
+ const row = db.prepare(
14116
+ "SELECT * FROM workflow_runs ORDER BY started_at DESC, rowid DESC LIMIT 1"
14117
+ ).get();
14118
+ if (!row) return void 0;
14119
+ return {
14120
+ id: row["id"],
14121
+ sessionId: row["session_id"],
14122
+ workflowName: row["workflow_name"] ?? void 0,
14123
+ startedAt: row["started_at"],
14124
+ endedAt: row["ended_at"] ?? void 0,
14125
+ iteration: row["iteration"],
14126
+ maxIterations: row["max_iterations"],
14127
+ status: row["status"],
14128
+ stopReason: row["stop_reason"] ?? void 0,
14129
+ trackerPath: row["tracker_path"] ?? void 0,
14130
+ adapterSessionId: row["adapter_session_id"] ?? void 0,
14131
+ runMemoryJson: row["run_memory_json"] ?? void 0
14132
+ };
14133
+ } catch {
14134
+ return void 0;
14135
+ }
14136
+ },
13914
14137
  feedEvents() {
13915
14138
  const rows = db.prepare("SELECT data FROM feed_events ORDER BY seq").all();
13916
14139
  return rows.map((r) => JSON.parse(r.data));
@@ -13922,17 +14145,17 @@ function openSessionDbReadonly(dbPath, options = {}) {
13922
14145
  }
13923
14146
 
13924
14147
  // src/infra/sessions/registry.ts
13925
- import fs17 from "fs";
13926
- import os11 from "os";
13927
- import path14 from "path";
14148
+ import fs18 from "fs";
14149
+ import os12 from "os";
14150
+ import path15 from "path";
13928
14151
  function sessionsDir() {
13929
- return path14.join(os11.homedir(), ".config", "athena", "sessions");
14152
+ return path15.join(os12.homedir(), ".config", "athena", "sessions");
13930
14153
  }
13931
14154
  function sessionDbPath(sessionId) {
13932
- return path14.join(sessionsDir(), sessionId, "session.db");
14155
+ return path15.join(sessionsDir(), sessionId, "session.db");
13933
14156
  }
13934
14157
  function readSessionFromDb(dbPath) {
13935
- if (!fs17.existsSync(dbPath)) return null;
14158
+ if (!fs18.existsSync(dbPath)) return null;
13936
14159
  let reader;
13937
14160
  try {
13938
14161
  reader = openSessionDbReadonly(dbPath);
@@ -13959,12 +14182,12 @@ function readSessionFromDb(dbPath) {
13959
14182
  }
13960
14183
  function listSessions(projectDir) {
13961
14184
  const dir = sessionsDir();
13962
- if (!fs17.existsSync(dir)) return [];
13963
- const entries = fs17.readdirSync(dir, { withFileTypes: true });
14185
+ if (!fs18.existsSync(dir)) return [];
14186
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
13964
14187
  const sessions = [];
13965
14188
  for (const entry of entries) {
13966
14189
  if (!entry.isDirectory()) continue;
13967
- const dbPath = path14.join(dir, entry.name, "session.db");
14190
+ const dbPath = path15.join(dir, entry.name, "session.db");
13968
14191
  const session = readSessionFromDb(dbPath);
13969
14192
  if (session && (session.eventCount ?? 0) > 0) {
13970
14193
  if (!projectDir || session.projectDir === projectDir) {
@@ -13981,6 +14204,44 @@ function getMostRecentAthenaSession(projectDir) {
13981
14204
  const sessions = listSessions(projectDir);
13982
14205
  return sessions[0] ?? null;
13983
14206
  }
14207
+ function getLatestRunForSession(sessionId, baseDir) {
14208
+ const dbPath = path15.join(baseDir ?? sessionsDir(), sessionId, "session.db");
14209
+ if (!fs18.existsSync(dbPath)) return null;
14210
+ let reader;
14211
+ try {
14212
+ reader = openSessionDbReadonly(dbPath);
14213
+ return reader.latestWorkflowRun() ?? null;
14214
+ } catch {
14215
+ return null;
14216
+ } finally {
14217
+ reader?.close();
14218
+ }
14219
+ }
14220
+ function listAwaitingAttentionRuns(projectDir, baseDir) {
14221
+ const dir = baseDir ?? sessionsDir();
14222
+ if (!fs18.existsSync(dir)) return [];
14223
+ const results = [];
14224
+ for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
14225
+ if (!entry.isDirectory()) continue;
14226
+ const dbPath = path15.join(dir, entry.name, "session.db");
14227
+ const session = readSessionFromDb(dbPath);
14228
+ if (!session) continue;
14229
+ if (projectDir && session.projectDir !== projectDir) continue;
14230
+ const run = getLatestRunForSession(entry.name, dir);
14231
+ if (!run || run.status !== "awaiting_attention") continue;
14232
+ results.push({
14233
+ athenaSessionId: session.id,
14234
+ projectDir: session.projectDir,
14235
+ runId: run.id,
14236
+ workflowName: run.workflowName,
14237
+ stopReason: run.stopReason,
14238
+ adapterSessionId: run.adapterSessionId,
14239
+ startedAt: run.startedAt,
14240
+ sessionUpdatedAt: session.updatedAt
14241
+ });
14242
+ }
14243
+ return results.sort((a, b) => b.sessionUpdatedAt - a.sessionUpdatedAt);
14244
+ }
13984
14245
 
13985
14246
  // src/app/channels/relayAdapter.ts
13986
14247
  function createRelayPermissionCallback(bridge, runtime) {
@@ -14321,9 +14582,9 @@ function defaultLoadToken(tokenPath) {
14321
14582
  }
14322
14583
 
14323
14584
  // src/infra/config/gatewayClient.ts
14324
- import fs18 from "fs";
14325
- import os12 from "os";
14326
- import path15 from "path";
14585
+ import fs19 from "fs";
14586
+ import os13 from "os";
14587
+ import path16 from "path";
14327
14588
 
14328
14589
  // src/shared/gateway-protocol/endpoint.ts
14329
14590
  function parseRuntimeEndpoint(value) {
@@ -14378,17 +14639,17 @@ function isRecord2(value) {
14378
14639
 
14379
14640
  // src/infra/config/gatewayClient.ts
14380
14641
  function resolveGatewayClientConfigPath(env = process.env) {
14381
- const home = env["HOME"] ?? os12.homedir();
14382
- return path15.join(home, ".config", "athena", "gateway.json");
14642
+ const home = env["HOME"] ?? os13.homedir();
14643
+ return path16.join(home, ".config", "athena", "gateway.json");
14383
14644
  }
14384
14645
  function readGatewayClientConfig(env = process.env) {
14385
14646
  const configPath = resolveGatewayClientConfigPath(env);
14386
- if (!fs18.existsSync(configPath)) {
14647
+ if (!fs19.existsSync(configPath)) {
14387
14648
  return { mode: "local" };
14388
14649
  }
14389
14650
  let parsed;
14390
14651
  try {
14391
- parsed = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
14652
+ parsed = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
14392
14653
  } catch (err) {
14393
14654
  throw new Error(
14394
14655
  `gateway client config ${configPath} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -14405,16 +14666,16 @@ function readGatewayClientConfig(env = process.env) {
14405
14666
  function writeGatewayClientConfig(config, env = process.env) {
14406
14667
  const parsed = parseRuntimeEndpoint(config);
14407
14668
  const configPath = resolveGatewayClientConfigPath(env);
14408
- const dir = path15.dirname(configPath);
14409
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
14410
- fs18.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
14669
+ const dir = path16.dirname(configPath);
14670
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
14671
+ fs19.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
14411
14672
  encoding: "utf-8",
14412
14673
  mode: 384
14413
14674
  });
14414
14675
  if (process.platform !== "win32") {
14415
14676
  try {
14416
- fs18.chmodSync(dir, 448);
14417
- fs18.chmodSync(configPath, 384);
14677
+ fs19.chmodSync(dir, 448);
14678
+ fs19.chmodSync(configPath, 384);
14418
14679
  } catch {
14419
14680
  }
14420
14681
  }
@@ -15202,6 +15463,20 @@ function handleEvent(event, cb) {
15202
15463
  }
15203
15464
  };
15204
15465
  }
15466
+ if (eventKind2 === "compact.pre" && cb.interceptCompaction) {
15467
+ const reason = cb.interceptCompaction(event);
15468
+ if (reason !== null) {
15469
+ return {
15470
+ handled: true,
15471
+ decision: {
15472
+ type: "json",
15473
+ source: "rule",
15474
+ intent: { kind: "compact_block", reason }
15475
+ }
15476
+ };
15477
+ }
15478
+ return { handled: false };
15479
+ }
15205
15480
  return { handled: false };
15206
15481
  }
15207
15482
 
@@ -15382,8 +15657,8 @@ function exitCodeFromFailure(failure) {
15382
15657
  }
15383
15658
 
15384
15659
  // src/app/exec/output.ts
15385
- import fs19 from "fs/promises";
15386
- import path16 from "path";
15660
+ import fs20 from "fs/promises";
15661
+ import path17 from "path";
15387
15662
 
15388
15663
  // src/app/exec/jsonl.ts
15389
15664
  function createExecJsonlEvent(type, data, ts = Date.now()) {
@@ -15426,9 +15701,9 @@ function createExecOutputWriter(options) {
15426
15701
  writeLine(options.stdout, message);
15427
15702
  },
15428
15703
  async writeLastMessage(filePath, message) {
15429
- const absPath = path16.resolve(filePath);
15430
- await fs19.mkdir(path16.dirname(absPath), { recursive: true });
15431
- await fs19.writeFile(absPath, message, "utf-8");
15704
+ const absPath = path17.resolve(filePath);
15705
+ await fs20.mkdir(path17.dirname(absPath), { recursive: true });
15706
+ await fs20.writeFile(absPath, message, "utf-8");
15432
15707
  }
15433
15708
  };
15434
15709
  }
@@ -15471,12 +15746,29 @@ function formatCapabilityConflictNotice(summary) {
15471
15746
  "; "
15472
15747
  )}`;
15473
15748
  }
15474
- function workflowFailure(state, message) {
15475
- return {
15476
- kind: "workflow",
15477
- state,
15478
- message
15479
- };
15749
+ function describeAttentionRequest(event) {
15750
+ const data = event.data;
15751
+ const toolInput = data["tool_input"];
15752
+ const questions = typeof toolInput === "object" && toolInput !== null ? toolInput["questions"] : void 0;
15753
+ if (Array.isArray(questions)) {
15754
+ const texts = questions.map(
15755
+ (q) => typeof q === "object" && q !== null ? q["question"] : void 0
15756
+ ).filter((q) => typeof q === "string");
15757
+ if (texts.length > 0) {
15758
+ return `agent asked a question with no human attached to answer: ${texts.join(" | ")}`;
15759
+ }
15760
+ }
15761
+ if (event.kind === "permission.request" && event.toolName !== "user_input") {
15762
+ return `agent requested sandbox approval (${event.toolName ?? "unknown tool"}) with no human attached to answer \u2014 rerun with a more permissive --isolation, or wake with guidance`;
15763
+ }
15764
+ return "agent asked a question with no human attached to answer";
15765
+ }
15766
+ function isQuestionEvent(event) {
15767
+ return event.kind === "tool.pre" && event.toolName === "AskUserQuestion" || // ANY permission request, not just Codex's 'user_input': a sandbox
15768
+ // approval (e.g. item/fileChange/requestApproval under a read-only
15769
+ // sandbox) is equally unanswerable unattended — observed live hanging a
15770
+ // headless codex workflow run forever on the null-timeout decision.
15771
+ event.kind === "permission.request" || event.kind === "elicitation.request";
15480
15772
  }
15481
15773
  function buildEarlyFailureResult(input) {
15482
15774
  return {
@@ -15530,12 +15822,13 @@ async function runExec(options) {
15530
15822
  let adapterSessionId = null;
15531
15823
  let activeRunId = null;
15532
15824
  let beforeTerminalCompletionRan = false;
15825
+ let attentionRequest = null;
15533
15826
  let store;
15534
15827
  try {
15535
15828
  store = sessionStoreFactory({
15536
15829
  sessionId: athenaSessionId,
15537
15830
  projectDir: options.projectDir,
15538
- dbPath: options.ephemeral ? ":memory:" : path17.join(sessionsDir(), athenaSessionId, "session.db")
15831
+ dbPath: options.ephemeral ? ":memory:" : path18.join(sessionsDir(), athenaSessionId, "session.db")
15539
15832
  });
15540
15833
  } catch (error) {
15541
15834
  const message = `Failed to initialize session store: ${error instanceof Error ? error.message : String(error)}`;
@@ -15611,6 +15904,26 @@ async function runExec(options) {
15611
15904
  defaultAgentId: "main",
15612
15905
  ...options.signal ? { signal: options.signal } : {}
15613
15906
  }) : null;
15907
+ let handoverRequest = null;
15908
+ let handoverForkInProgress = false;
15909
+ const handoverDegradedSessions = /* @__PURE__ */ new Set();
15910
+ const interceptCompaction = (event) => {
15911
+ const handle = event.sessionId;
15912
+ if (!handle) return null;
15913
+ if (handoverDegradedSessions.has(handle)) return null;
15914
+ if (handoverForkInProgress) {
15915
+ return "Handover fork in progress \u2014 compaction stays blocked while the Handoff file is written.";
15916
+ }
15917
+ if (handoverRequest === null) {
15918
+ handoverRequest = { handle };
15919
+ output.notice(
15920
+ `handover: context bound reached \u2014 forking session ${handle} to write a Handoff file`
15921
+ );
15922
+ output.emitJsonEvent("run.handover", { adapterSessionId: handle });
15923
+ void sessionController.kill();
15924
+ }
15925
+ return "Handover in progress \u2014 Athena forks the conversation instead of compacting.";
15926
+ };
15614
15927
  const controllerCallbacks = {
15615
15928
  getRules: () => rules,
15616
15929
  // No UI queue in exec; with no bridge attached, the runtime never
@@ -15623,6 +15936,10 @@ async function runExec(options) {
15623
15936
  relayPermission: createRelayPermissionCallback(bridge, runtime),
15624
15937
  relayQuestion: createRelayQuestionCallback(bridge, runtime)
15625
15938
  } : {},
15939
+ // Handover interception is Claude-only for now: the fork transition
15940
+ // rides --fork-session, which Codex has no equivalent for. Non-workflow
15941
+ // sessions never intercept — vendor compaction proceeds unchanged.
15942
+ ...options.harness === "claude-code" && options.workflow?.loop?.enabled ? { interceptCompaction } : {},
15626
15943
  ...options.signal ? { signal: options.signal } : {}
15627
15944
  };
15628
15945
  const dashboardDecisionInbox = options.dashboardDecisionInbox;
@@ -15696,6 +16013,10 @@ async function runExec(options) {
15696
16013
  },
15697
16014
  onEventReceived: (runtimeEvent) => {
15698
16015
  adapterSessionId = runtimeEvent.sessionId;
16016
+ if (options.workflow?.loop?.enabled && !bridge && attentionRequest === null && isQuestionEvent(runtimeEvent)) {
16017
+ attentionRequest = describeAttentionRequest(runtimeEvent);
16018
+ void sessionController.kill();
16019
+ }
15699
16020
  if (runtimeEvent.sessionId && activeRunId && !linkedAdapterSessions.has(runtimeEvent.sessionId)) {
15700
16021
  linkedAdapterSessions.add(runtimeEvent.sessionId);
15701
16022
  safePersist(
@@ -15792,6 +16113,15 @@ async function runExec(options) {
15792
16113
  loopEnabled: workflow?.loop?.enabled ?? false
15793
16114
  });
15794
16115
  const nextContinuation = options.adapterResumeSessionId ? { mode: "resume", handle: options.adapterResumeSessionId } : { mode: "fresh" };
16116
+ let resumedRunMemory;
16117
+ let resumedStopReason;
16118
+ if (options.resumeRunId) {
16119
+ const resumedRun = store.getLatestRun();
16120
+ if (resumedRun?.id === options.resumeRunId) {
16121
+ resumedRunMemory = deserializeRunMemory(resumedRun.runMemoryJson) ?? void 0;
16122
+ resumedStopReason = resumedRun.stopReason;
16123
+ }
16124
+ }
15795
16125
  const handle = createWorkflowRunner({
15796
16126
  sessionId: athenaSessionId,
15797
16127
  projectDir: options.projectDir,
@@ -15799,6 +16129,9 @@ async function runExec(options) {
15799
16129
  workflow,
15800
16130
  prompt: options.prompt,
15801
16131
  initialContinuation: nextContinuation,
16132
+ resumeRunId: options.resumeRunId,
16133
+ resumedRunMemory,
16134
+ resumedStopReason,
15802
16135
  startTurn: async (turnInput) => {
15803
16136
  const turnResult = await sessionController.startTurn({
15804
16137
  prompt: turnInput.prompt,
@@ -15828,32 +16161,88 @@ async function runExec(options) {
15828
16161
  "persistRun failed"
15829
16162
  );
15830
16163
  },
16164
+ checkSuspension: () => attentionRequest !== null ? { reason: attentionRequest } : null,
16165
+ currentAdapterSessionId,
16166
+ handover: {
16167
+ takeRequest: () => {
16168
+ const request = handoverRequest;
16169
+ handoverRequest = null;
16170
+ return request;
16171
+ },
16172
+ onForkStateChange: (forking) => {
16173
+ handoverForkInProgress = forking;
16174
+ },
16175
+ onDegraded: (handle2) => {
16176
+ handoverDegradedSessions.add(handle2);
16177
+ output.warn(
16178
+ `handover failed for session ${handle2} \u2014 falling back to normal vendor compaction`
16179
+ );
16180
+ output.emitJsonEvent("run.handover.degraded", {
16181
+ adapterSessionId: handle2
16182
+ });
16183
+ }
16184
+ },
15831
16185
  abortCurrentTurn: () => void sessionController.kill(),
15832
16186
  onIterationComplete: (runSnapshot) => {
15833
16187
  output.emitJsonEvent("iteration.complete", {
15834
16188
  iteration: runSnapshot.iteration,
15835
16189
  status: runSnapshot.status
15836
16190
  });
16191
+ },
16192
+ // Task-tool projection (ADR 0015 §7): the Tracker's `## Units` table +
16193
+ // unit-record frontmatter, diffed against what the Feed already knows
16194
+ // and reconciled through the same `task.created`/`task.completed`
16195
+ // path a live TodoWrite/TaskCreate/TaskUpdate call would take.
16196
+ projectTasks: (tasks) => {
16197
+ const known = new Map(
16198
+ mapper.getTasks().filter((task) => task.taskId).map((task) => [task.taskId, task])
16199
+ );
16200
+ const newEvents = [];
16201
+ for (const task of tasks) {
16202
+ const existing = known.get(task.taskId);
16203
+ if (!existing) {
16204
+ newEvents.push(
16205
+ ...mapper.mapEvent(
16206
+ buildSyntheticTaskEvent("task.created", athenaSessionId, {
16207
+ task_id: task.taskId,
16208
+ task_subject: task.content
16209
+ })
16210
+ )
16211
+ );
16212
+ }
16213
+ if (task.status === "completed" && existing?.status !== "completed") {
16214
+ newEvents.push(
16215
+ ...mapper.mapEvent(
16216
+ buildSyntheticTaskEvent("task.completed", athenaSessionId, {
16217
+ task_id: task.taskId,
16218
+ task_subject: task.content
16219
+ })
16220
+ )
16221
+ );
16222
+ }
16223
+ }
16224
+ if (newEvents.length > 0) {
16225
+ safePersist(
16226
+ store,
16227
+ () => store.recordFeedEvents(newEvents),
16228
+ (message) => output.warn(message),
16229
+ "recordFeedEvents failed"
16230
+ );
16231
+ }
15837
16232
  }
15838
16233
  });
15839
16234
  activeRunId = handle.runId;
15840
16235
  const runResult = await handle.result;
15841
16236
  cumulativeTokens = runResult.tokens;
15842
16237
  if (!latch.hasFailure()) {
15843
- if (runResult.status === "blocked") {
15844
- latch.register(
15845
- workflowFailure(
15846
- "blocked",
15847
- runResult.stopReason ? `Workflow blocked: ${runResult.stopReason}` : "Workflow blocked."
15848
- )
15849
- );
15850
- } else if (runResult.status === "exhausted") {
15851
- latch.register(
15852
- workflowFailure(
15853
- "exhausted",
15854
- `Workflow reached the maximum of ${workflow?.loop?.maxIterations ?? 0} iterations.`
15855
- )
15856
- );
16238
+ if (runResult.status === "awaiting_attention") {
16239
+ const reason = runResult.stopReason ?? "awaiting attention";
16240
+ output.notice(`workflow run suspended \u2014 ${reason}`);
16241
+ output.emitJsonEvent("run.suspended", {
16242
+ runId: runResult.runId,
16243
+ status: "awaiting_attention",
16244
+ stopReason: runResult.stopReason ?? null
16245
+ });
15857
16246
  } else if (runResult.status === "failed") {
15858
16247
  latch.register({
15859
16248
  kind: "process",
@@ -16494,8 +16883,8 @@ async function createRemoteRunEventPublisher({
16494
16883
  // src/app/dashboard/artifactCapture.ts
16495
16884
  import { execFile } from "child_process";
16496
16885
  import crypto3 from "crypto";
16497
- import fs20 from "fs/promises";
16498
- import path18 from "path";
16886
+ import fs21 from "fs/promises";
16887
+ import path19 from "path";
16499
16888
  import { promisify } from "util";
16500
16889
  var execFileAsync = promisify(execFile);
16501
16890
  function parseArtifactUploadSpec(value) {
@@ -16678,21 +17067,21 @@ async function collectArtifactPayloads(input) {
16678
17067
  return payloads;
16679
17068
  }
16680
17069
  async function readWorkspaceFile(projectDir, rel) {
16681
- const absolute = path18.resolve(projectDir, rel);
16682
- const workspaceRoot = await fs20.realpath(projectDir);
17070
+ const absolute = path19.resolve(projectDir, rel);
17071
+ const workspaceRoot = await fs21.realpath(projectDir);
16683
17072
  let stat;
16684
17073
  try {
16685
- stat = await fs20.lstat(absolute);
17074
+ stat = await fs21.lstat(absolute);
16686
17075
  } catch {
16687
17076
  return null;
16688
17077
  }
16689
17078
  if (!stat.isFile() || stat.isSymbolicLink()) return null;
16690
- const real = await fs20.realpath(absolute);
16691
- const relativeToRoot = path18.relative(workspaceRoot, real);
16692
- if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path18.sep}`) || path18.isAbsolute(relativeToRoot)) {
17079
+ const real = await fs21.realpath(absolute);
17080
+ const relativeToRoot = path19.relative(workspaceRoot, real);
17081
+ if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path19.sep}`) || path19.isAbsolute(relativeToRoot)) {
16693
17082
  return null;
16694
17083
  }
16695
- return fs20.readFile(absolute);
17084
+ return fs21.readFile(absolute);
16696
17085
  }
16697
17086
  async function gitDiffForAllowedPaths(input) {
16698
17087
  const paths = splitNul(await git(input.projectDir, input.nameArgs)).filter(
@@ -16783,13 +17172,13 @@ var DEFAULT_HARD_DENY = [
16783
17172
  ".ssh/**"
16784
17173
  ];
16785
17174
  function isAllowedRelativePath(rel, hardDeny) {
16786
- if (path18.isAbsolute(rel) || rel.includes("\0")) return false;
16787
- const normalized = path18.posix.normalize(rel.replaceAll(path18.sep, "/"));
17175
+ if (path19.isAbsolute(rel) || rel.includes("\0")) return false;
17176
+ const normalized = path19.posix.normalize(rel.replaceAll(path19.sep, "/"));
16788
17177
  if (normalized === ".." || normalized.startsWith("../")) return false;
16789
17178
  return !hardDeny.some((pattern) => matchesDenyPattern(normalized, pattern));
16790
17179
  }
16791
17180
  function matchesDenyPattern(rel, pattern) {
16792
- const normalized = pattern.replaceAll(path18.sep, "/");
17181
+ const normalized = pattern.replaceAll(path19.sep, "/");
16793
17182
  if (normalized.includes("*") || normalized.includes("?")) {
16794
17183
  return globToRegExp(normalized).test(rel);
16795
17184
  }
@@ -17186,18 +17575,18 @@ async function executeRemoteAssignment({
17186
17575
 
17187
17576
  // src/infra/config/attachmentMirror.ts
17188
17577
  import crypto4 from "crypto";
17189
- import fs21 from "fs";
17190
- import os13 from "os";
17191
- import path19 from "path";
17578
+ import fs22 from "fs";
17579
+ import os14 from "os";
17580
+ import path20 from "path";
17192
17581
  function attachmentMirrorPath(env = process.env) {
17193
- const home = env["HOME"] ?? os13.homedir();
17194
- return path19.join(home, ".config", "athena", "attachments.json");
17582
+ const home = env["HOME"] ?? os14.homedir();
17583
+ return path20.join(home, ".config", "athena", "attachments.json");
17195
17584
  }
17196
17585
  function readAttachmentMirror(env = process.env) {
17197
17586
  const file = attachmentMirrorPath(env);
17198
17587
  let raw;
17199
17588
  try {
17200
- raw = fs21.readFileSync(file, "utf-8");
17589
+ raw = fs22.readFileSync(file, "utf-8");
17201
17590
  } catch (err) {
17202
17591
  if (err.code === "ENOENT") return null;
17203
17592
  throw err;
@@ -17221,29 +17610,29 @@ function readAttachmentMirror(env = process.env) {
17221
17610
  function writeAttachmentMirror(mirror, env = process.env) {
17222
17611
  const validated = parseAttachmentMirror(mirror);
17223
17612
  const file = attachmentMirrorPath(env);
17224
- const dir = path19.dirname(file);
17225
- fs21.mkdirSync(dir, { recursive: true, mode: 448 });
17613
+ const dir = path20.dirname(file);
17614
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
17226
17615
  const tmp = `${file}.${process.pid}.${crypto4.randomBytes(4).toString("hex")}.tmp`;
17227
- const fd = fs21.openSync(tmp, "w", 384);
17616
+ const fd = fs22.openSync(tmp, "w", 384);
17228
17617
  try {
17229
- fs21.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
17230
- fs21.fsyncSync(fd);
17618
+ fs22.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
17619
+ fs22.fsyncSync(fd);
17231
17620
  } finally {
17232
- fs21.closeSync(fd);
17621
+ fs22.closeSync(fd);
17233
17622
  }
17234
17623
  try {
17235
- fs21.renameSync(tmp, file);
17624
+ fs22.renameSync(tmp, file);
17236
17625
  } catch (err) {
17237
17626
  try {
17238
- fs21.unlinkSync(tmp);
17627
+ fs22.unlinkSync(tmp);
17239
17628
  } catch {
17240
17629
  }
17241
17630
  throw err;
17242
17631
  }
17243
17632
  if (process.platform !== "win32") {
17244
17633
  try {
17245
- fs21.chmodSync(dir, 448);
17246
- fs21.chmodSync(file, 384);
17634
+ fs22.chmodSync(dir, 448);
17635
+ fs22.chmodSync(file, 384);
17247
17636
  } catch {
17248
17637
  }
17249
17638
  }
@@ -17251,7 +17640,7 @@ function writeAttachmentMirror(mirror, env = process.env) {
17251
17640
  function removeAttachmentMirror(env = process.env) {
17252
17641
  const file = attachmentMirrorPath(env);
17253
17642
  try {
17254
- fs21.unlinkSync(file);
17643
+ fs22.unlinkSync(file);
17255
17644
  } catch (err) {
17256
17645
  if (err.code !== "ENOENT") throw err;
17257
17646
  }
@@ -17655,9 +18044,9 @@ function createDashboardPairedExecution(options) {
17655
18044
  }
17656
18045
 
17657
18046
  // src/app/dashboard/remoteWorkspaceResolver.ts
17658
- import fs22 from "fs";
17659
- import os14 from "os";
17660
- import path20 from "path";
18047
+ import fs23 from "fs";
18048
+ import os15 from "os";
18049
+ import path21 from "path";
17661
18050
  function resolveRemoteWorkspace(assignment, options = {}) {
17662
18051
  const { spec, runId, runnerId } = assignment;
17663
18052
  if (spec.projectDir) {
@@ -17666,14 +18055,14 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17666
18055
  const sessionId = spec.athenaSessionId ?? spec.sessionId;
17667
18056
  const deploymentSlug = deploymentSlugFromUrl(options.dashboardUrl);
17668
18057
  const stateDir = daemonStatePaths(options.env).dir;
17669
- const projectDir = sessionId ? path20.join(
18058
+ const projectDir = sessionId ? path21.join(
17670
18059
  stateDir,
17671
18060
  "remote-workspaces",
17672
18061
  deploymentSlug,
17673
18062
  sanitizePathSegment(runnerId),
17674
18063
  "sessions",
17675
18064
  sanitizePathSegment(sessionId)
17676
- ) : path20.join(
18065
+ ) : path21.join(
17677
18066
  stateDir,
17678
18067
  "remote-workspaces",
17679
18068
  deploymentSlug,
@@ -17682,10 +18071,10 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17682
18071
  sanitizePathSegment(runId)
17683
18072
  );
17684
18073
  try {
17685
- fs22.mkdirSync(projectDir, { recursive: true, mode: 448 });
18074
+ fs23.mkdirSync(projectDir, { recursive: true, mode: 448 });
17686
18075
  if (process.platform !== "win32") {
17687
18076
  try {
17688
- fs22.chmodSync(projectDir, 448);
18077
+ fs23.chmodSync(projectDir, 448);
17689
18078
  } catch {
17690
18079
  }
17691
18080
  }
@@ -17701,8 +18090,8 @@ function resolveRemoteWorkspace(assignment, options = {}) {
17701
18090
  return validateProjectDir(projectDir, options.env);
17702
18091
  }
17703
18092
  function validateProjectDir(projectDir, env = process.env) {
17704
- const resolved = path20.resolve(projectDir);
17705
- if (!path20.isAbsolute(projectDir)) {
18093
+ const resolved = path21.resolve(projectDir);
18094
+ if (!path21.isAbsolute(projectDir)) {
17706
18095
  return {
17707
18096
  kind: "rejected",
17708
18097
  rejection: {
@@ -17711,7 +18100,7 @@ function validateProjectDir(projectDir, env = process.env) {
17711
18100
  }
17712
18101
  };
17713
18102
  }
17714
- const home = path20.resolve(env["HOME"] ?? os14.homedir());
18103
+ const home = path21.resolve(env["HOME"] ?? os15.homedir());
17715
18104
  if (resolved === home) {
17716
18105
  return {
17717
18106
  kind: "rejected",
@@ -17723,7 +18112,7 @@ function validateProjectDir(projectDir, env = process.env) {
17723
18112
  }
17724
18113
  let stat;
17725
18114
  try {
17726
- stat = fs22.statSync(resolved);
18115
+ stat = fs23.statSync(resolved);
17727
18116
  } catch {
17728
18117
  return {
17729
18118
  kind: "rejected",
@@ -18168,18 +18557,18 @@ async function runDashboardRuntimeDaemon(options = {}) {
18168
18557
  }
18169
18558
 
18170
18559
  // src/infra/daemon/pidLock.ts
18171
- import fs23 from "fs";
18560
+ import fs24 from "fs";
18172
18561
  function acquirePidLock(pidPath) {
18173
18562
  const ownPid = process.pid;
18174
18563
  for (let attempt = 0; attempt < 2; attempt += 1) {
18175
18564
  try {
18176
- const fd = fs23.openSync(pidPath, "wx", 384);
18565
+ const fd = fs24.openSync(pidPath, "wx", 384);
18177
18566
  try {
18178
- fs23.writeSync(fd, `${ownPid}
18567
+ fs24.writeSync(fd, `${ownPid}
18179
18568
  `);
18180
- fs23.fsyncSync(fd);
18569
+ fs24.fsyncSync(fd);
18181
18570
  } finally {
18182
- fs23.closeSync(fd);
18571
+ fs24.closeSync(fd);
18183
18572
  }
18184
18573
  return makeHandle(pidPath, ownPid);
18185
18574
  } catch (err) {
@@ -18193,7 +18582,7 @@ function acquirePidLock(pidPath) {
18193
18582
  }
18194
18583
  if (existing.state === "stale") {
18195
18584
  try {
18196
- fs23.unlinkSync(pidPath);
18585
+ fs24.unlinkSync(pidPath);
18197
18586
  } catch (err) {
18198
18587
  if (err.code !== "ENOENT") throw err;
18199
18588
  }
@@ -18207,7 +18596,7 @@ function acquirePidLock(pidPath) {
18207
18596
  function readPidLock(pidPath) {
18208
18597
  let raw;
18209
18598
  try {
18210
- raw = fs23.readFileSync(pidPath, "utf-8");
18599
+ raw = fs24.readFileSync(pidPath, "utf-8");
18211
18600
  } catch (err) {
18212
18601
  if (err.code === "ENOENT") {
18213
18602
  return { state: "absent" };
@@ -18231,9 +18620,9 @@ function makeHandle(pidPath, pid) {
18231
18620
  if (released) return;
18232
18621
  released = true;
18233
18622
  try {
18234
- const raw = fs23.readFileSync(pidPath, "utf-8").trim();
18623
+ const raw = fs24.readFileSync(pidPath, "utf-8").trim();
18235
18624
  if (raw === String(pid)) {
18236
- fs23.unlinkSync(pidPath);
18625
+ fs24.unlinkSync(pidPath);
18237
18626
  }
18238
18627
  } catch (err) {
18239
18628
  if (err.code !== "ENOENT") {
@@ -18259,7 +18648,7 @@ function isProcessAlive(pid) {
18259
18648
  }
18260
18649
 
18261
18650
  // src/infra/daemon/udsIpc.ts
18262
- import fs24 from "fs";
18651
+ import fs25 from "fs";
18263
18652
  import net2 from "net";
18264
18653
 
18265
18654
  // src/infra/daemon/udsFrameCodec.ts
@@ -18304,7 +18693,7 @@ async function startUdsServer(socketPath, handler, log) {
18304
18693
  });
18305
18694
  if (process.platform !== "win32") {
18306
18695
  try {
18307
- fs24.chmodSync(socketPath, 384);
18696
+ fs25.chmodSync(socketPath, 384);
18308
18697
  } catch {
18309
18698
  }
18310
18699
  }
@@ -18314,7 +18703,7 @@ async function startUdsServer(socketPath, handler, log) {
18314
18703
  server.close(() => resolve());
18315
18704
  });
18316
18705
  try {
18317
- fs24.unlinkSync(socketPath);
18706
+ fs25.unlinkSync(socketPath);
18318
18707
  } catch (err) {
18319
18708
  if (err.code !== "ENOENT") {
18320
18709
  }
@@ -18432,7 +18821,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
18432
18821
  async function unlinkStaleSocket(socketPath) {
18433
18822
  let stat;
18434
18823
  try {
18435
- stat = fs24.statSync(socketPath);
18824
+ stat = fs25.statSync(socketPath);
18436
18825
  } catch (err) {
18437
18826
  if (err.code === "ENOENT") return;
18438
18827
  throw err;
@@ -18459,7 +18848,7 @@ async function unlinkStaleSocket(socketPath) {
18459
18848
  `uds path ${socketPath} is in use by another process; aborting`
18460
18849
  );
18461
18850
  }
18462
- fs24.unlinkSync(socketPath);
18851
+ fs25.unlinkSync(socketPath);
18463
18852
  }
18464
18853
 
18465
18854
  export {
@@ -18489,6 +18878,8 @@ export {
18489
18878
  listSessions,
18490
18879
  getSessionMeta,
18491
18880
  getMostRecentAthenaSession,
18881
+ getLatestRunForSession,
18882
+ listAwaitingAttentionRuns,
18492
18883
  resolveClaudeBinary,
18493
18884
  detectClaudeVersion,
18494
18885
  resolveHookForwarderCommand,
@@ -18560,4 +18951,4 @@ export {
18560
18951
  startUdsServer,
18561
18952
  sendUdsRequest
18562
18953
  };
18563
- //# sourceMappingURL=chunk-YZ7RCBKO.js.map
18954
+ //# sourceMappingURL=chunk-4HWBXCVY.js.map