@gethmy/agent 1.10.9 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +857 -194
  2. package/dist/index.js +857 -194
  3. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -384,11 +384,12 @@ var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL
384
384
  var init_types = __esm(() => {
385
385
  init_plan_phase();
386
386
  DEFAULT_AGENT_CONFIG = {
387
- poolSize: 3,
387
+ poolSize: 6,
388
388
  maxTimeout: 1800000,
389
389
  pickupColumns: ["To Do"],
390
390
  priorityLabels: { urgent: 100, critical: 90, bug: 50 },
391
391
  columnBoost: true,
392
+ runner: "cli",
392
393
  completion: {
393
394
  createPR: false,
394
395
  moveToColumn: "Review",
@@ -430,7 +431,7 @@ var init_types = __esm(() => {
430
431
  },
431
432
  review: {
432
433
  enabled: true,
433
- poolSize: 2,
434
+ poolSize: 3,
434
435
  pickupColumns: ["Review"],
435
436
  moveToColumn: "Done",
436
437
  failColumn: "To Do",
@@ -562,6 +563,9 @@ function loadDaemonConfig() {
562
563
  ...agentOverrides.planning ?? {}
563
564
  }
564
565
  };
566
+ if (agent.runner !== "cli" && agent.runner !== "sdk") {
567
+ agent.runner = "cli";
568
+ }
565
569
  return {
566
570
  apiKey,
567
571
  apiUrl,
@@ -2580,6 +2584,13 @@ function formatTokenCount(tokens) {
2580
2584
  return `${(tokens / 1000).toFixed(1)}k`;
2581
2585
  return String(tokens);
2582
2586
  }
2587
+ function describeNoCommitFailure(numTurns, maxTurns) {
2588
+ const maxTurnsExhausted = maxTurns > 0 && numTurns >= maxTurns;
2589
+ return {
2590
+ maxTurnsExhausted,
2591
+ failureSummary: maxTurnsExhausted ? `Agent exhausted its ${maxTurns}-turn budget without committing any changes` : "Agent finished without making any changes to commit"
2592
+ };
2593
+ }
2583
2594
  function buildTokenPayload(stats) {
2584
2595
  if (!stats?.cost)
2585
2596
  return {};
@@ -2603,14 +2614,17 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2603
2614
  };
2604
2615
  const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
2605
2616
  if (!hasCommits) {
2606
- log.warn(TAG14, `No commits on branch ${branchName} skipping completion`);
2617
+ const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
2618
+ log.warn(TAG14, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
2619
+ await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
2607
2620
  await client.endAgentSession(card.id, {
2608
- status: "completed",
2609
- progressPercent: 100,
2621
+ status: "failed",
2622
+ failureReason: maxTurnsExhausted ? "timeout" : "other",
2623
+ failureSummary,
2610
2624
  ...buildTokenPayload(sessionStats)
2611
2625
  });
2612
2626
  cleanupWorktree(worktreePath, branchName);
2613
- return true;
2627
+ return false;
2614
2628
  }
2615
2629
  log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
2616
2630
  let lastPushedSha = null;
@@ -2877,6 +2891,20 @@ function signalGroup(proc, signal) {
2877
2891
  }
2878
2892
  }
2879
2893
  }
2894
+ function reapGroup(pgid) {
2895
+ if (!pgid || pgid <= 1 || pgid === process.pid)
2896
+ return;
2897
+ if (process.platform === "win32")
2898
+ return;
2899
+ try {
2900
+ process.kill(-pgid, "SIGKILL");
2901
+ } catch (err) {
2902
+ const code = err.code;
2903
+ if (code !== "ESRCH") {
2904
+ log.warn(TAG15, `reapGroup(${pgid}) failed: ${err instanceof Error ? err.message : err}`);
2905
+ }
2906
+ }
2907
+ }
2880
2908
  async function terminateGroup(proc, opts) {
2881
2909
  if (!proc.pid || proc.killed)
2882
2910
  return;
@@ -2928,8 +2956,8 @@ class ProgressTracker {
2928
2956
  filesEdited = new Set;
2929
2957
  filesRead = new Set;
2930
2958
  lastCost = null;
2931
- logBuffer = [];
2932
- sessionId = null;
2959
+ runEventSink = null;
2960
+ lastEmittedProgress = -1;
2933
2961
  lastAssistantText = "";
2934
2962
  assistantTextBlocks = [];
2935
2963
  constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
@@ -2942,32 +2970,15 @@ class ProgressTracker {
2942
2970
  this.phase = initialPhase;
2943
2971
  this.progress = PHASES[initialPhase].min;
2944
2972
  }
2945
- setSessionId(id) {
2946
- this.sessionId = id;
2973
+ setRunEventSink(sink) {
2974
+ this.runEventSink = sink;
2947
2975
  }
2948
2976
  attach(parser) {
2949
2977
  parser.on("tool_start", (name, input) => {
2950
2978
  this.onToolStart(name, input);
2951
- const desc = this.describeToolAction(name, input);
2952
- if (desc) {
2953
- this.pushLogEntry({
2954
- phase: this.phase,
2955
- eventType: "tool_start",
2956
- toolName: name,
2957
- description: desc,
2958
- metadata: this.extractToolMetadata(name, input)
2959
- });
2960
- }
2961
2979
  });
2962
2980
  parser.on("tool_end", (name, _id, content) => {
2963
2981
  this.onToolEnd(name, content);
2964
- this.pushLogEntry({
2965
- phase: this.phase,
2966
- eventType: "tool_end",
2967
- toolName: name,
2968
- description: `Completed: ${name}`,
2969
- metadata: {}
2970
- });
2971
2982
  });
2972
2983
  parser.on("text", (content) => {
2973
2984
  this.onText(content);
@@ -2977,6 +2988,37 @@ class ProgressTracker {
2977
2988
  });
2978
2989
  this.startHeartbeat();
2979
2990
  }
2991
+ ingest(draft) {
2992
+ switch (draft.kind) {
2993
+ case "run_started":
2994
+ this.startHeartbeat();
2995
+ break;
2996
+ case "tool_started":
2997
+ this.onToolStart(draft.payload.toolName, draft.payload.input);
2998
+ break;
2999
+ case "tool_ended":
3000
+ this.onToolEnd(draft.payload.toolName, draft.payload.output);
3001
+ break;
3002
+ case "assistant_text":
3003
+ this.onText(draft.payload.text);
3004
+ break;
3005
+ case "cost_updated": {
3006
+ const p = draft.payload;
3007
+ this.lastCost = {
3008
+ totalCostUsd: p.totalCostUsd,
3009
+ totalInputTokens: p.inputTokens,
3010
+ totalOutputTokens: p.outputTokens,
3011
+ totalCacheCreationInputTokens: p.cacheCreationInputTokens,
3012
+ totalCacheReadInputTokens: p.cacheReadInputTokens,
3013
+ durationMs: p.durationMs ?? 0,
3014
+ durationApiMs: 0,
3015
+ numTurns: p.numTurns,
3016
+ modelName: p.modelName
3017
+ };
3018
+ break;
3019
+ }
3020
+ }
3021
+ }
2980
3022
  stop() {
2981
3023
  this.stopped = true;
2982
3024
  if (this.pendingUpdate) {
@@ -3073,16 +3115,11 @@ class ProgressTracker {
3073
3115
  if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
3074
3116
  return;
3075
3117
  log.info(TAG16, `Phase: ${this.phase} → ${newPhase}`);
3118
+ const previousPhase = this.phase;
3119
+ this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
3076
3120
  this.phase = newPhase;
3077
3121
  this.progress = Math.max(this.progress, PHASES[newPhase].min);
3078
3122
  this.lastAction = "";
3079
- this.pushLogEntry({
3080
- phase: newPhase,
3081
- eventType: "phase_change",
3082
- toolName: null,
3083
- description: `Entering ${newPhase} phase`,
3084
- metadata: {}
3085
- });
3086
3123
  this.scheduleUpdate(PHASES[newPhase].label);
3087
3124
  }
3088
3125
  incrementProgress() {
@@ -3190,7 +3227,15 @@ class ProgressTracker {
3190
3227
  }).catch((err) => {
3191
3228
  log.warn(TAG16, `Failed to send progress update: ${err}`);
3192
3229
  });
3193
- this.flushActivityLog();
3230
+ if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
3231
+ this.lastEmittedProgress = this.progress;
3232
+ this.runEventSink.recordProgress({
3233
+ progressPercent: this.progress,
3234
+ currentTask: truncate(currentTask, MAX_TASK_LENGTH),
3235
+ phase: this.phase,
3236
+ filesChanged: this.filesEdited.size
3237
+ });
3238
+ }
3194
3239
  }
3195
3240
  startHeartbeat() {
3196
3241
  if (this.heartbeatTimer) {
@@ -3204,55 +3249,6 @@ class ProgressTracker {
3204
3249
  }
3205
3250
  }, HEARTBEAT_MS);
3206
3251
  }
3207
- flushFinal() {
3208
- this.flushActivityLog();
3209
- }
3210
- pushLogEntry(entry) {
3211
- this.logBuffer.push({
3212
- ...entry,
3213
- createdAt: new Date().toISOString()
3214
- });
3215
- if (this.logBuffer.length > MAX_LOG_BUFFER) {
3216
- this.logBuffer.shift();
3217
- }
3218
- }
3219
- flushActivityLog() {
3220
- if (!this.sessionId || this.logBuffer.length === 0)
3221
- return;
3222
- const raw = [...this.logBuffer];
3223
- this.logBuffer = [];
3224
- this.client.flushActivityLog(this.cardId, {
3225
- sessionId: this.sessionId,
3226
- entries: raw.map((e) => ({
3227
- ...e,
3228
- phase: e.phase ?? undefined,
3229
- toolName: e.toolName ?? undefined
3230
- }))
3231
- }).catch((err) => {
3232
- log.warn(TAG16, `Failed to flush activity log: ${err}`);
3233
- this.logBuffer.unshift(...raw);
3234
- if (this.logBuffer.length > MAX_LOG_BUFFER) {
3235
- this.logBuffer.length = MAX_LOG_BUFFER;
3236
- }
3237
- });
3238
- }
3239
- extractToolMetadata(_name, input) {
3240
- const meta = {};
3241
- const fp = this.extractString(input, "file_path");
3242
- if (fp)
3243
- meta.file_path = fp;
3244
- const cmd = this.extractString(input, "command");
3245
- if (cmd)
3246
- meta.command = cmd.split(`
3247
- `)[0].slice(0, 200);
3248
- const pattern = this.extractString(input, "pattern");
3249
- if (pattern)
3250
- meta.pattern = pattern;
3251
- const desc = this.extractString(input, "description");
3252
- if (desc)
3253
- meta.description = desc;
3254
- return meta;
3255
- }
3256
3252
  extractString(input, key) {
3257
3253
  if (typeof input === "object" && input !== null && key in input) {
3258
3254
  return String(input[key]);
@@ -3260,7 +3256,7 @@ class ProgressTracker {
3260
3256
  return null;
3261
3257
  }
3262
3258
  }
3263
- var TAG16 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_LOG_BUFFER = 500, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
3259
+ var TAG16 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
3264
3260
  var init_progress_tracker = __esm(() => {
3265
3261
  init_log();
3266
3262
  init_types();
@@ -3690,7 +3686,6 @@ var init_review_completion = __esm(() => {
3690
3686
  init_types();
3691
3687
  init_worktree();
3692
3688
  });
3693
-
3694
3689
  // ../harmony-shared/dist/cardLinks.js
3695
3690
  var init_cardLinks = () => {};
3696
3691
  // ../harmony-shared/dist/classification.js
@@ -4297,6 +4292,10 @@ var init_stream_parser = __esm(() => {
4297
4292
  toolNames = new Map;
4298
4293
  hasEmittedText = false;
4299
4294
  observedModel;
4295
+ capturedSessionId;
4296
+ get sessionId() {
4297
+ return this.capturedSessionId;
4298
+ }
4300
4299
  attach(stream) {
4301
4300
  if (this.attached) {
4302
4301
  throw new Error("StreamParser already attached to a stream");
@@ -4351,6 +4350,9 @@ var init_stream_parser = __esm(() => {
4351
4350
  this.observedModel = msg.message.model;
4352
4351
  }
4353
4352
  }
4353
+ if (!this.capturedSessionId && typeof msg.session_id === "string") {
4354
+ this.capturedSessionId = msg.session_id;
4355
+ }
4354
4356
  switch (msg.type) {
4355
4357
  case "assistant": {
4356
4358
  const blocks = msg.message?.content;
@@ -4361,10 +4363,11 @@ var init_stream_parser = __esm(() => {
4361
4363
  this.emit("text", block.text);
4362
4364
  this.hasEmittedText = true;
4363
4365
  } else if (block.type === "tool_use" && typeof block.name === "string") {
4364
- if (typeof block.id === "string") {
4365
- this.toolNames.set(block.id, block.name);
4366
+ const toolUseId = typeof block.id === "string" ? block.id : undefined;
4367
+ if (toolUseId) {
4368
+ this.toolNames.set(toolUseId, block.name);
4366
4369
  }
4367
- this.emit("tool_start", block.name, block.input);
4370
+ this.emit("tool_start", block.name, block.input, toolUseId);
4368
4371
  }
4369
4372
  }
4370
4373
  break;
@@ -5202,6 +5205,150 @@ var init_unblock = __esm(() => {
5202
5205
  init_log();
5203
5206
  });
5204
5207
 
5208
+ // src/cli-agent-runner.ts
5209
+ class CliAgentRunner {
5210
+ client;
5211
+ cardId;
5212
+ sessionId;
5213
+ buffer = [];
5214
+ flushTimer = null;
5215
+ flushing = false;
5216
+ stopped = false;
5217
+ constructor(client, cardId, sessionId) {
5218
+ this.client = client;
5219
+ this.cardId = cardId;
5220
+ this.sessionId = sessionId;
5221
+ }
5222
+ attach(parser) {
5223
+ if (this.stopped)
5224
+ return;
5225
+ parser.on("tool_start", (name, input, toolUseId) => {
5226
+ this.enqueue({
5227
+ kind: "tool_started",
5228
+ source: "agent",
5229
+ payload: { toolName: name, toolUseId, input }
5230
+ });
5231
+ });
5232
+ parser.on("tool_end", (name, toolUseId, content) => {
5233
+ this.enqueue({
5234
+ kind: "tool_ended",
5235
+ source: "agent",
5236
+ payload: {
5237
+ toolName: name,
5238
+ toolUseId,
5239
+ output: content === undefined ? undefined : content.slice(0, MAX_OUTPUT_LEN)
5240
+ }
5241
+ });
5242
+ });
5243
+ parser.on("text", (content) => {
5244
+ const text = content.trim();
5245
+ if (!text)
5246
+ return;
5247
+ this.enqueue({
5248
+ kind: "assistant_text",
5249
+ source: "agent",
5250
+ payload: { text: text.slice(0, MAX_TEXT_LEN) }
5251
+ });
5252
+ });
5253
+ parser.on("cost_update", (cost) => {
5254
+ this.enqueue({
5255
+ kind: "cost_updated",
5256
+ source: "agent",
5257
+ payload: mapCost(cost)
5258
+ });
5259
+ });
5260
+ this.startTimer();
5261
+ }
5262
+ recordRunStarted(payload) {
5263
+ this.enqueue({ kind: "run_started", source: "system", payload });
5264
+ this.startTimer();
5265
+ }
5266
+ recordPhaseChanged(phase, previousPhase) {
5267
+ this.enqueue({
5268
+ kind: "phase_changed",
5269
+ source: "system",
5270
+ payload: { phase, previousPhase }
5271
+ });
5272
+ }
5273
+ recordProgress(payload) {
5274
+ this.enqueue({ kind: "progress", source: "system", payload });
5275
+ }
5276
+ recordError(payload) {
5277
+ this.enqueue({ kind: "error", source: "system", payload });
5278
+ }
5279
+ recordFinished(payload) {
5280
+ this.enqueue({ kind: "run_finished", source: "system", payload });
5281
+ }
5282
+ record(body) {
5283
+ this.enqueue(body);
5284
+ this.startTimer();
5285
+ }
5286
+ enqueue(body) {
5287
+ if (this.stopped)
5288
+ return;
5289
+ this.buffer.push({ ...body, createdAt: new Date().toISOString() });
5290
+ if (this.buffer.length >= MAX_BUFFER)
5291
+ this.flush();
5292
+ }
5293
+ startTimer() {
5294
+ if (this.flushTimer || this.stopped)
5295
+ return;
5296
+ this.flushTimer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
5297
+ this.flushTimer.unref?.();
5298
+ }
5299
+ async flush() {
5300
+ if (this.flushing || this.buffer.length === 0)
5301
+ return;
5302
+ this.flushing = true;
5303
+ const batch = this.buffer.splice(0, MAX_BUFFER);
5304
+ try {
5305
+ await this.client.appendAgentRunEvents(this.cardId, {
5306
+ sessionId: this.sessionId,
5307
+ events: batch
5308
+ });
5309
+ } catch (err) {
5310
+ log.warn(TAG24, `Failed to flush run events: ${err}`);
5311
+ this.buffer.unshift(...batch);
5312
+ if (this.buffer.length > MAX_BUFFER) {
5313
+ this.buffer.length = MAX_BUFFER;
5314
+ }
5315
+ } finally {
5316
+ this.flushing = false;
5317
+ }
5318
+ }
5319
+ async flushFinal() {
5320
+ for (let i = 0;this.buffer.length > 0 && i < 5; i++) {
5321
+ const before = this.buffer.length;
5322
+ await this.flush();
5323
+ if (this.buffer.length >= before)
5324
+ break;
5325
+ }
5326
+ }
5327
+ stop() {
5328
+ this.stopped = true;
5329
+ if (this.flushTimer) {
5330
+ clearInterval(this.flushTimer);
5331
+ this.flushTimer = null;
5332
+ }
5333
+ }
5334
+ }
5335
+ function mapCost(cost) {
5336
+ return {
5337
+ totalCostUsd: cost.totalCostUsd,
5338
+ inputTokens: cost.totalInputTokens,
5339
+ outputTokens: cost.totalOutputTokens,
5340
+ cacheCreationInputTokens: cost.totalCacheCreationInputTokens,
5341
+ cacheReadInputTokens: cost.totalCacheReadInputTokens,
5342
+ numTurns: cost.numTurns,
5343
+ modelName: cost.modelName,
5344
+ durationMs: cost.durationMs
5345
+ };
5346
+ }
5347
+ var TAG24 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
5348
+ var init_cli_agent_runner = __esm(() => {
5349
+ init_log();
5350
+ });
5351
+
5205
5352
  // src/model-tier.ts
5206
5353
  function clampWithdrawn(model) {
5207
5354
  return WITHDRAWN_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
@@ -5252,11 +5399,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
5252
5399
  Do NOT push to main. All your work stays on \`${branchName}\`.
5253
5400
  When finished, call harmony_end_agent_session with status="completed".`
5254
5401
  });
5255
- log.info(TAG24, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
5402
+ log.info(TAG25, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
5256
5403
  return result.prompt + pastEpisodesSection;
5257
5404
  } catch (err) {
5258
5405
  const msg = err instanceof Error ? err.message : String(err);
5259
- log.warn(TAG24, `Failed to generate prompt via API, using fallback: ${msg}`);
5406
+ log.warn(TAG25, `Failed to generate prompt via API, using fallback: ${msg}`);
5260
5407
  const commentsSection = await renderCommentsSection(client, card.id);
5261
5408
  return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
5262
5409
  }
@@ -5274,7 +5421,7 @@ async function renderCommentsSection(client, cardId) {
5274
5421
 
5275
5422
  ${section}` : "";
5276
5423
  } catch (err) {
5277
- log.warn(TAG24, "comment-thread fetch failed", {
5424
+ log.warn(TAG25, "comment-thread fetch failed", {
5278
5425
  event: "comment_fetch_failed",
5279
5426
  error: err instanceof Error ? err.message : String(err)
5280
5427
  });
@@ -5324,7 +5471,7 @@ ${description}`.trim();
5324
5471
  ## Similar past tasks
5325
5472
  ${bullets}`;
5326
5473
  } catch (err) {
5327
- log.warn(TAG24, "past-episodes recall failed", {
5474
+ log.warn(TAG25, "past-episodes recall failed", {
5328
5475
  event: "episode_recall_failed",
5329
5476
  error: err instanceof Error ? err.message : String(err)
5330
5477
  });
@@ -5365,13 +5512,346 @@ ${subtaskStr}
5365
5512
  You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
5366
5513
  Do NOT push to main. All your work stays on \`${branchName}\`.`;
5367
5514
  }
5368
- var TAG24 = "prompt";
5515
+ var TAG25 = "prompt";
5369
5516
  var init_prompt = __esm(() => {
5370
5517
  init_dist();
5371
5518
  init_log();
5372
5519
  });
5373
5520
 
5521
+ // src/sdk-agent-runner.ts
5522
+ import {
5523
+ query
5524
+ } from "@anthropic-ai/claude-agent-sdk";
5525
+ function mapSdkErrorKind(e) {
5526
+ switch (e) {
5527
+ case "authentication_failed":
5528
+ case "oauth_org_not_allowed":
5529
+ return "auth";
5530
+ case "billing_error":
5531
+ return "out_of_credits";
5532
+ case "rate_limit":
5533
+ case "overloaded":
5534
+ return "rate_limit";
5535
+ default:
5536
+ return null;
5537
+ }
5538
+ }
5539
+
5540
+ class SdkAgentRunner {
5541
+ cfg;
5542
+ abort = null;
5543
+ capturedSessionId;
5544
+ child = null;
5545
+ leaderPid;
5546
+ capturedStderr = "";
5547
+ toolNames = new Map;
5548
+ observedModel;
5549
+ effectiveModel;
5550
+ constructor(cfg = {}) {
5551
+ this.cfg = cfg;
5552
+ }
5553
+ get sessionId() {
5554
+ return this.capturedSessionId;
5555
+ }
5556
+ get capturedStderrText() {
5557
+ return this.capturedStderr;
5558
+ }
5559
+ start(input) {
5560
+ return this.run(input);
5561
+ }
5562
+ resume(input) {
5563
+ return this.run(input, input.resumeSessionId);
5564
+ }
5565
+ async send(_message) {
5566
+ throw new Error("SdkAgentRunner.send() (streaming-input steering) is not wired; the worker steers via stop→resume");
5567
+ }
5568
+ async stop(_reason) {
5569
+ this.abort?.abort();
5570
+ if (this.child) {
5571
+ await terminateGroup(this.child, {
5572
+ sigintTimeoutMs: STOP_SIGINT_MS,
5573
+ sigtermTimeoutMs: STOP_SIGTERM_MS
5574
+ });
5575
+ }
5576
+ reapGroup(this.leaderPid);
5577
+ }
5578
+ async* run(input, resumeSessionId) {
5579
+ this.abort = new AbortController;
5580
+ this.capturedStderr = "";
5581
+ this.child = null;
5582
+ this.leaderPid = undefined;
5583
+ this.toolNames.clear();
5584
+ this.observedModel = undefined;
5585
+ this.effectiveModel = input.model ?? this.cfg.model;
5586
+ yield {
5587
+ kind: "run_started",
5588
+ source: "system",
5589
+ payload: { runner: "sdk", model: input.model }
5590
+ };
5591
+ const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
5592
+ const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
5593
+ const options = {
5594
+ cwd: input.cwd,
5595
+ model: input.model ?? this.cfg.model,
5596
+ allowedTools: allowed,
5597
+ tools: builtinTools,
5598
+ permissionMode: "dontAsk",
5599
+ maxTurns: this.cfg.maxTurns,
5600
+ abortController: this.abort,
5601
+ ...resumeSessionId ? { resume: resumeSessionId } : {},
5602
+ ...this.cfg.maxBudgetUsd ? { maxBudgetUsd: this.cfg.maxBudgetUsd } : {},
5603
+ ...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
5604
+ ...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
5605
+ ...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
5606
+ stderr: (data) => {
5607
+ this.capturedStderr += data;
5608
+ },
5609
+ spawnClaudeCodeProcess: (spawnOpts) => this.spawn(spawnOpts)
5610
+ };
5611
+ try {
5612
+ const q = query({ prompt: input.prompt, options });
5613
+ let failureReason = null;
5614
+ for await (const msg of q) {
5615
+ for (const ev of this.mapMessage(msg)) {
5616
+ if (ev.kind === "error") {
5617
+ failureReason = ev.payload.errorKind ?? "crash";
5618
+ }
5619
+ yield ev;
5620
+ }
5621
+ }
5622
+ yield {
5623
+ kind: "run_finished",
5624
+ source: "system",
5625
+ payload: failureReason ? { status: "failed", failureReason } : { status: "completed" }
5626
+ };
5627
+ } catch (err) {
5628
+ const message = err instanceof Error ? err.message : String(err);
5629
+ const cls = classifyRunError(`${message}
5630
+ ${this.capturedStderr}`);
5631
+ yield {
5632
+ kind: "error",
5633
+ source: "system",
5634
+ payload: {
5635
+ message,
5636
+ errorKind: cls.kind,
5637
+ retryable: cls.kind !== "auth" && cls.kind !== null
5638
+ }
5639
+ };
5640
+ yield {
5641
+ kind: "run_finished",
5642
+ source: "system",
5643
+ payload: { status: "failed", failureReason: cls.kind ?? "crash" }
5644
+ };
5645
+ } finally {
5646
+ reapGroup(this.leaderPid);
5647
+ }
5648
+ }
5649
+ spawn(spawnOpts) {
5650
+ const child = spawnInGroup(spawnOpts.command, spawnOpts.args, {
5651
+ cwd: spawnOpts.cwd,
5652
+ env: spawnOpts.env,
5653
+ stdio: ["pipe", "pipe", "pipe"]
5654
+ });
5655
+ this.child = child;
5656
+ this.leaderPid = child.pid;
5657
+ child.stderr?.on("data", (d) => {
5658
+ this.capturedStderr += d.toString();
5659
+ });
5660
+ this.cfg.onSpawn?.(child);
5661
+ return {
5662
+ stdin: child.stdin,
5663
+ stdout: child.stdout,
5664
+ get killed() {
5665
+ return child.killed;
5666
+ },
5667
+ get exitCode() {
5668
+ return child.exitCode;
5669
+ },
5670
+ kill: (signal) => {
5671
+ if (!child.pid)
5672
+ return false;
5673
+ try {
5674
+ process.kill(-child.pid, signal);
5675
+ return true;
5676
+ } catch {
5677
+ return child.kill(signal);
5678
+ }
5679
+ },
5680
+ on: (event, listener) => child.on(event, listener),
5681
+ once: (event, listener) => child.once(event, listener),
5682
+ off: (event, listener) => child.off(event, listener)
5683
+ };
5684
+ }
5685
+ *mapMessage(msg) {
5686
+ const sid = msg.session_id;
5687
+ if (sid && !this.capturedSessionId)
5688
+ this.capturedSessionId = sid;
5689
+ const model = msg.message?.model ?? msg.model;
5690
+ if (typeof model === "string" && !this.observedModel) {
5691
+ this.observedModel = model;
5692
+ }
5693
+ switch (msg.type) {
5694
+ case "assistant": {
5695
+ const am = msg;
5696
+ if (am.error) {
5697
+ yield {
5698
+ kind: "error",
5699
+ source: "system",
5700
+ payload: {
5701
+ message: `assistant error: ${am.error}`,
5702
+ errorKind: mapSdkErrorKind(am.error)
5703
+ }
5704
+ };
5705
+ }
5706
+ const blocks = am.message?.content;
5707
+ if (Array.isArray(blocks)) {
5708
+ for (const b of blocks) {
5709
+ if (b.type === "text" && typeof b.text === "string") {
5710
+ const text = b.text.trim();
5711
+ if (text) {
5712
+ yield {
5713
+ kind: "assistant_text",
5714
+ source: "agent",
5715
+ payload: { text: text.slice(0, MAX_TEXT_LEN2) }
5716
+ };
5717
+ }
5718
+ } else if (b.type === "tool_use" && typeof b.name === "string") {
5719
+ if (typeof b.id === "string")
5720
+ this.toolNames.set(b.id, b.name);
5721
+ yield {
5722
+ kind: "tool_started",
5723
+ source: "agent",
5724
+ payload: { toolName: b.name, toolUseId: b.id, input: b.input }
5725
+ };
5726
+ }
5727
+ }
5728
+ }
5729
+ break;
5730
+ }
5731
+ case "user": {
5732
+ const um = msg;
5733
+ const blocks = um.message?.content;
5734
+ if (Array.isArray(blocks)) {
5735
+ for (const b of blocks) {
5736
+ if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
5737
+ const toolName = this.toolNames.get(b.tool_use_id) ?? "";
5738
+ this.toolNames.delete(b.tool_use_id);
5739
+ yield {
5740
+ kind: "tool_ended",
5741
+ source: "agent",
5742
+ payload: {
5743
+ toolName,
5744
+ toolUseId: b.tool_use_id,
5745
+ output: normalize(b.content)?.slice(0, MAX_OUTPUT_LEN2),
5746
+ isError: b.is_error
5747
+ }
5748
+ };
5749
+ }
5750
+ }
5751
+ }
5752
+ break;
5753
+ }
5754
+ case "result": {
5755
+ const r = msg;
5756
+ if (typeof r.total_cost_usd === "number") {
5757
+ yield {
5758
+ kind: "cost_updated",
5759
+ source: "agent",
5760
+ payload: {
5761
+ totalCostUsd: r.total_cost_usd,
5762
+ inputTokens: r.usage?.input_tokens ?? 0,
5763
+ outputTokens: r.usage?.output_tokens ?? 0,
5764
+ cacheCreationInputTokens: r.usage?.cache_creation_input_tokens ?? 0,
5765
+ cacheReadInputTokens: r.usage?.cache_read_input_tokens ?? 0,
5766
+ numTurns: r.num_turns ?? 0,
5767
+ durationMs: r.duration_ms,
5768
+ modelName: this.observedModel ?? this.effectiveModel
5769
+ }
5770
+ };
5771
+ }
5772
+ if (r.subtype && r.subtype !== "success") {
5773
+ const joined = (r.errors ?? []).join(`
5774
+ `);
5775
+ const cls = classifyRunError(`${r.subtype}
5776
+ ${joined}
5777
+ ${this.capturedStderr}`);
5778
+ yield {
5779
+ kind: "error",
5780
+ source: "system",
5781
+ payload: {
5782
+ message: `result ${r.subtype}: ${joined || "(no detail)"}`,
5783
+ errorKind: cls.kind,
5784
+ retryable: cls.kind !== "auth" && cls.kind !== null
5785
+ }
5786
+ };
5787
+ }
5788
+ break;
5789
+ }
5790
+ }
5791
+ }
5792
+ }
5793
+ function normalize(raw) {
5794
+ if (raw == null)
5795
+ return;
5796
+ if (typeof raw === "string")
5797
+ return raw;
5798
+ if (Array.isArray(raw)) {
5799
+ const parts = [];
5800
+ for (const b of raw) {
5801
+ if (b && typeof b === "object" && "text" in b && typeof b.text === "string") {
5802
+ parts.push(b.text);
5803
+ }
5804
+ }
5805
+ return parts.length ? parts.join("") : JSON.stringify(raw);
5806
+ }
5807
+ try {
5808
+ return JSON.stringify(raw);
5809
+ } catch {
5810
+ return String(raw);
5811
+ }
5812
+ }
5813
+ var SDK_ALLOWED_TOOLS, MAX_TEXT_LEN2 = 8000, MAX_OUTPUT_LEN2 = 4000, STOP_SIGINT_MS = 2000, STOP_SIGTERM_MS = 2000;
5814
+ var init_sdk_agent_runner = __esm(() => {
5815
+ init_error_classifier();
5816
+ init_process_group();
5817
+ SDK_ALLOWED_TOOLS = [
5818
+ "Bash",
5819
+ "Read",
5820
+ "Write",
5821
+ "Edit",
5822
+ "Glob",
5823
+ "Grep",
5824
+ "Agent",
5825
+ "mcp__harmony__*"
5826
+ ];
5827
+ });
5828
+
5374
5829
  // src/worker.ts
5830
+ function sdkDraftLogLine(ev) {
5831
+ switch (ev.kind) {
5832
+ case "assistant_text":
5833
+ return ev.payload.text.slice(0, 200);
5834
+ case "tool_started":
5835
+ return ev.payload.toolName;
5836
+ case "tool_ended":
5837
+ return `${ev.payload.toolUseId ?? ""}${ev.payload.isError ? " (error)" : ""}`;
5838
+ case "cost_updated":
5839
+ return `$${ev.payload.totalCostUsd.toFixed(4)} turns=${ev.payload.numTurns}`;
5840
+ case "error":
5841
+ return ev.payload.message.slice(0, 200);
5842
+ case "run_finished":
5843
+ return ev.payload.status;
5844
+ default:
5845
+ return "";
5846
+ }
5847
+ }
5848
+ function buildSteeringPrompt(messages) {
5849
+ if (messages.length === 1)
5850
+ return messages[0];
5851
+ return messages.map((m, i) => `${i + 1}. ${m}`).join(`
5852
+ `);
5853
+ }
5854
+
5375
5855
  class Worker {
5376
5856
  config;
5377
5857
  client;
@@ -5392,12 +5872,16 @@ class Worker {
5392
5872
  timeoutTimer = null;
5393
5873
  heartbeatTimer = null;
5394
5874
  progressTracker = null;
5875
+ cliRunner = null;
5876
+ sdkRunner = null;
5395
5877
  lastSessionStats;
5396
5878
  aborted = false;
5397
5879
  timedOut = false;
5398
5880
  verificationFailed = false;
5399
5881
  sessionId = null;
5400
5882
  runId = null;
5883
+ cliSessionId = null;
5884
+ lastDrainedSeq = 0;
5401
5885
  runCostCents = 0;
5402
5886
  runTurns = 0;
5403
5887
  constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
@@ -5444,7 +5928,7 @@ class Worker {
5444
5928
  }
5445
5929
  }
5446
5930
  get tag() {
5447
- return `${TAG25}:${this.id}`;
5931
+ return `${TAG26}:${this.id}`;
5448
5932
  }
5449
5933
  get isIdle() {
5450
5934
  return this.state === "idle";
@@ -5466,6 +5950,8 @@ class Worker {
5466
5950
  this.verificationFailed = false;
5467
5951
  this.runCostCents = 0;
5468
5952
  this.runTurns = 0;
5953
+ this.cliSessionId = null;
5954
+ this.lastDrainedSeq = 0;
5469
5955
  this.cardId = card.id;
5470
5956
  this.startedAt = Date.now();
5471
5957
  this.runId = newRunId();
@@ -5503,9 +5989,16 @@ class Worker {
5503
5989
  });
5504
5990
  const sid = session && typeof session === "object" && "id" in session ? session.id : null;
5505
5991
  if (!sid) {
5506
- log.warn(TAG25, "startAgentSession returned no session id");
5992
+ log.warn(TAG26, "startAgentSession returned no session id");
5507
5993
  }
5508
5994
  this.sessionId = sid;
5995
+ if (this.sessionId) {
5996
+ this.cliRunner = new CliAgentRunner(this.client, card.id, this.sessionId);
5997
+ this.cliRunner.recordRunStarted({
5998
+ runner: this.config.runner,
5999
+ model: this.config.claude.model
6000
+ });
6001
+ }
5509
6002
  await this.recordPhase("preparing");
5510
6003
  const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
5511
6004
  if (!moved) {
@@ -5552,6 +6045,9 @@ class Worker {
5552
6045
  await this.spawnClaude(prompt, card, subtasks, {
5553
6046
  model: this.selectImplementModel(card)
5554
6047
  });
6048
+ if (this.aborted)
6049
+ return;
6050
+ await this.drainSteeringMessages(card, subtasks);
5555
6051
  if (this.aborted)
5556
6052
  return;
5557
6053
  if (this.timeoutTimer) {
@@ -5578,9 +6074,17 @@ class Worker {
5578
6074
  log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
5579
6075
  const rawStderr = err?.stderr;
5580
6076
  const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
6077
+ const sdkKind = err?.errorKind;
6078
+ if (errClass.kind === null && sdkKind != null)
6079
+ errClass.kind = sdkKind;
5581
6080
  const apiError = errClass.kind !== null;
5582
6081
  const baseError = err instanceof WorktreeBaseError;
5583
6082
  const noBudgetBurn = apiError || baseError;
6083
+ this.cliRunner?.recordError({
6084
+ message: msg.slice(0, 500),
6085
+ errorKind: errClass.kind,
6086
+ retryable: noBudgetBurn
6087
+ });
5584
6088
  if (apiError) {
5585
6089
  try {
5586
6090
  this.onApiError?.(errClass);
@@ -5674,6 +6178,26 @@ class Worker {
5674
6178
  } catch {}
5675
6179
  await this.recordOutcome(card.id, "failure");
5676
6180
  }
6181
+ if (this.cliRunner) {
6182
+ if (succeeded) {
6183
+ this.cliRunner.recordFinished({ status: "completed" });
6184
+ } else if (this.timedOut) {
6185
+ this.cliRunner.recordFinished({
6186
+ status: "failed",
6187
+ failureReason: "timeout"
6188
+ });
6189
+ } else if (this.aborted) {
6190
+ this.cliRunner.recordFinished({
6191
+ status: "stopped",
6192
+ stopReason: "user_requested"
6193
+ });
6194
+ } else {
6195
+ this.cliRunner.recordFinished({ status: "failed" });
6196
+ }
6197
+ try {
6198
+ await this.cliRunner.flushFinal();
6199
+ } catch {}
6200
+ }
5677
6201
  this.cleanup();
5678
6202
  this.state = "idle";
5679
6203
  this.onDone(this);
@@ -5763,7 +6287,9 @@ class Worker {
5763
6287
  this.aborted = true;
5764
6288
  this.state = "cancelling";
5765
6289
  log.info(this.tag, `Cancelling work on ${this.cardId}`);
5766
- if (this.process && !this.process.killed) {
6290
+ if (this.sdkRunner) {
6291
+ await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
6292
+ } else if (this.process && !this.process.killed) {
5767
6293
  await terminateGroup(this.process, {
5768
6294
  sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT2,
5769
6295
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
@@ -5798,7 +6324,9 @@ class Worker {
5798
6324
  const planTimeout = setTimeout(() => {
5799
6325
  planTimedOut = true;
5800
6326
  log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
5801
- if (this.process && !this.process.killed) {
6327
+ if (this.sdkRunner) {
6328
+ this.sdkRunner.stop("timeout").catch(() => {});
6329
+ } else if (this.process && !this.process.killed) {
5802
6330
  terminateGroup(this.process, {
5803
6331
  sigintTimeoutMs: 1e4,
5804
6332
  sigtermTimeoutMs: 5000
@@ -5892,7 +6420,40 @@ class Worker {
5892
6420
  }
5893
6421
  return false;
5894
6422
  }
5895
- async spawnClaude(prompt, card, subtasks, opts = {}) {
6423
+ async drainSteeringMessages(card, subtasks) {
6424
+ if (!this.cliSessionId || !this.sessionId || !this.cardId)
6425
+ return;
6426
+ for (let i = 0;i < MAX_STEERING_ITERATIONS && !this.aborted; i++) {
6427
+ let messages;
6428
+ try {
6429
+ const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
6430
+ messages = res.messages ?? [];
6431
+ } catch (err) {
6432
+ log.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
6433
+ return;
6434
+ }
6435
+ if (messages.length === 0)
6436
+ return;
6437
+ this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
6438
+ log.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
6439
+ this.state = "running";
6440
+ await this.recordPhase("running");
6441
+ try {
6442
+ await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
6443
+ model: this.selectImplementModel(card),
6444
+ maxTurns: STEERING_MAX_TURNS,
6445
+ resumeSessionId: this.cliSessionId
6446
+ });
6447
+ } catch (err) {
6448
+ log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
6449
+ return;
6450
+ }
6451
+ }
6452
+ }
6453
+ spawnClaude(prompt, card, subtasks, opts = {}) {
6454
+ return this.config.runner === "sdk" ? this.spawnClaudeSdk(prompt, card, subtasks, opts) : this.spawnClaudeCli(prompt, card, subtasks, opts);
6455
+ }
6456
+ async spawnClaudeCli(prompt, card, subtasks, opts = {}) {
5896
6457
  const model = opts.model ?? this.config.claude.model;
5897
6458
  const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
5898
6459
  const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
@@ -5909,6 +6470,7 @@ class Worker {
5909
6470
  String(maxTurns),
5910
6471
  "--allowedTools",
5911
6472
  allowedTools,
6473
+ ...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
5912
6474
  ...this.config.claude.additionalArgs,
5913
6475
  "--",
5914
6476
  prompt
@@ -5928,10 +6490,11 @@ class Worker {
5928
6490
  });
5929
6491
  const parser = new StreamParser;
5930
6492
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
5931
- if (this.sessionId) {
5932
- this.progressTracker.setSessionId(this.sessionId);
5933
- }
5934
6493
  this.progressTracker.attach(parser);
6494
+ this.cliRunner?.attach(parser);
6495
+ if (this.cliRunner) {
6496
+ this.progressTracker.setRunEventSink(this.cliRunner);
6497
+ }
5935
6498
  if (this.process.stdout) {
5936
6499
  parser.attach(this.process.stdout);
5937
6500
  if (runLog) {
@@ -5955,14 +6518,16 @@ class Worker {
5955
6518
  reject(new Error(`Failed to spawn claude: ${err.message}`));
5956
6519
  });
5957
6520
  this.process.on("close", (code) => {
6521
+ const leaderPid = this.process?.pid;
5958
6522
  this.process = null;
6523
+ if (parser.sessionId)
6524
+ this.cliSessionId = parser.sessionId;
5959
6525
  this.lastSessionStats = this.progressTracker?.stats;
5960
6526
  const spawnCost = this.lastSessionStats?.cost;
5961
6527
  if (spawnCost) {
5962
6528
  this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
5963
6529
  this.runTurns += spawnCost.numTurns;
5964
6530
  }
5965
- this.progressTracker?.flushFinal();
5966
6531
  this.progressTracker?.stop();
5967
6532
  this.progressTracker = null;
5968
6533
  if (runLog) {
@@ -5972,6 +6537,7 @@ class Worker {
5972
6537
  `);
5973
6538
  runLog.stream.end();
5974
6539
  }
6540
+ reapGroup(leaderPid);
5975
6541
  if (this.aborted) {
5976
6542
  resolve3();
5977
6543
  } else if (code === 0) {
@@ -5984,11 +6550,101 @@ class Worker {
5984
6550
  });
5985
6551
  });
5986
6552
  }
6553
+ async spawnClaudeSdk(prompt, card, subtasks, opts = {}) {
6554
+ const model = opts.model ?? this.config.claude.model;
6555
+ const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
6556
+ const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
6557
+ const initialPhase = opts.initialPhase ?? "exploring";
6558
+ const sdkCfg = this.config.sdk;
6559
+ log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
6560
+ const runLog = openRunLog(this.tag, this.runId, card.short_id);
6561
+ if (runLog) {
6562
+ log.info(this.tag, `Run log: ${runLog.path}`);
6563
+ runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
6564
+ ` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
6565
+
6566
+ `);
6567
+ }
6568
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
6569
+ if (this.cliRunner) {
6570
+ this.progressTracker.setRunEventSink(this.cliRunner);
6571
+ }
6572
+ const runner = new SdkAgentRunner({
6573
+ model,
6574
+ maxTurns,
6575
+ allowedTools,
6576
+ maxBudgetUsd: sdkCfg?.maxBudgetUsd,
6577
+ settingSources: sdkCfg?.settingSources,
6578
+ mcpServers: sdkCfg?.mcpServers,
6579
+ strictMcpConfig: sdkCfg?.strictMcpConfig,
6580
+ onSpawn: (child) => {
6581
+ this.process = child;
6582
+ }
6583
+ });
6584
+ this.sdkRunner = runner;
6585
+ const baseInput = {
6586
+ sessionId: this.sessionId ?? "",
6587
+ cardId: card.id,
6588
+ workspaceId: this.workspaceId,
6589
+ prompt,
6590
+ cwd: this.worktreePath,
6591
+ model
6592
+ };
6593
+ const stream = opts.resumeSessionId ? runner.resume({ ...baseInput, resumeSessionId: opts.resumeSessionId }) : runner.start(baseInput);
6594
+ let failure = null;
6595
+ let failureKind = null;
6596
+ try {
6597
+ for await (const ev of stream) {
6598
+ this.progressTracker?.ingest(ev);
6599
+ if (ev.source === "agent")
6600
+ this.cliRunner?.record(ev);
6601
+ if (ev.kind === "error") {
6602
+ failure = ev.payload.message;
6603
+ if (ev.payload.errorKind != null)
6604
+ failureKind = ev.payload.errorKind;
6605
+ }
6606
+ runLog?.stream.write(`[${ev.kind}] ${sdkDraftLogLine(ev)}
6607
+ `);
6608
+ }
6609
+ } finally {
6610
+ this.cliSessionId = runner.sessionId ?? this.cliSessionId;
6611
+ this.lastSessionStats = this.progressTracker?.stats;
6612
+ const spawnCost = this.lastSessionStats?.cost;
6613
+ if (spawnCost) {
6614
+ this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
6615
+ this.runTurns += spawnCost.numTurns;
6616
+ }
6617
+ if (runLog) {
6618
+ const stats = this.lastSessionStats;
6619
+ runLog.stream.write(`
6620
+ # runner=sdk aborted=${this.aborted} ` + `toolCalls=${stats?.toolCalls ?? 0} filesEdited=${stats?.filesEdited ?? 0} ` + `cost=$${stats?.cost?.totalCostUsd.toFixed(4) ?? "0"} ` + `ended=${new Date().toISOString()}
6621
+ `);
6622
+ runLog.stream.end();
6623
+ }
6624
+ this.progressTracker?.stop();
6625
+ this.progressTracker = null;
6626
+ this.process = null;
6627
+ this.sdkRunner = null;
6628
+ }
6629
+ if (this.aborted)
6630
+ return;
6631
+ if (failure) {
6632
+ const err = new Error(failure);
6633
+ err.stderr = runner.capturedStderrText;
6634
+ err.errorKind = failureKind;
6635
+ throw err;
6636
+ }
6637
+ }
5987
6638
  cleanup() {
5988
6639
  if (this.progressTracker) {
5989
6640
  this.progressTracker.stop();
5990
6641
  this.progressTracker = null;
5991
6642
  }
6643
+ if (this.cliRunner) {
6644
+ this.cliRunner.stop();
6645
+ this.cliRunner = null;
6646
+ }
6647
+ this.sdkRunner = null;
5992
6648
  this.stopHeartbeat();
5993
6649
  this.lastSessionStats = undefined;
5994
6650
  if (this.timeoutTimer) {
@@ -6013,9 +6669,10 @@ class Worker {
6013
6669
  this.runTurns = 0;
6014
6670
  }
6015
6671
  }
6016
- var TAG25 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
6672
+ var TAG26 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
6017
6673
  var init_worker = __esm(() => {
6018
6674
  init_board_helpers();
6675
+ init_cli_agent_runner();
6019
6676
  init_completion();
6020
6677
  init_error_classifier();
6021
6678
  init_log();
@@ -6025,6 +6682,7 @@ var init_worker = __esm(() => {
6025
6682
  init_progress_tracker();
6026
6683
  init_prompt();
6027
6684
  init_run_log();
6685
+ init_sdk_agent_runner();
6028
6686
  init_state_store();
6029
6687
  init_stream_parser();
6030
6688
  init_transitions();
@@ -6083,39 +6741,39 @@ class Pool {
6083
6741
  }
6084
6742
  async enqueue(card, column, labels, subtasks, mode = "implement") {
6085
6743
  if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
6086
- log.debug(TAG26, `Card ${card.id} already queued or active, skipping`);
6744
+ log.debug(TAG27, `Card ${card.id} already queued or active, skipping`);
6087
6745
  return;
6088
6746
  }
6089
6747
  if (mode === "implement") {
6090
6748
  if (this.authPaused) {
6091
- log.debug(TAG26, `#${card.short_id} held — agent paused (auth error)`);
6749
+ log.debug(TAG27, `#${card.short_id} held — agent paused (auth error)`);
6092
6750
  await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
6093
6751
  return;
6094
6752
  }
6095
6753
  const cooldownMs = this.apiCooldownRemainingMs();
6096
6754
  if (cooldownMs > 0) {
6097
- log.debug(TAG26, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
6755
+ log.debug(TAG27, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
6098
6756
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
6099
6757
  return;
6100
6758
  }
6101
6759
  const decision = this.budget.check(card.id);
6102
6760
  if (!decision.allow) {
6103
6761
  if (decision.reason === "daily_budget") {
6104
- log.warn(TAG26, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
6762
+ log.warn(TAG27, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
6105
6763
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
6106
6764
  } else {
6107
- log.debug(TAG26, `#${card.short_id} gave up: ${decision.detail}`);
6765
+ log.debug(TAG27, `#${card.short_id} gave up: ${decision.detail}`);
6108
6766
  }
6109
6767
  return;
6110
6768
  }
6111
6769
  const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
6112
6770
  if (blockers === null) {
6113
- log.warn(TAG26, `#${card.short_id} blocker check failed — deferring to next tick`);
6771
+ log.warn(TAG27, `#${card.short_id} blocker check failed — deferring to next tick`);
6114
6772
  return;
6115
6773
  }
6116
6774
  if (blockers.length > 0) {
6117
6775
  const list = blockers.map((b) => `#${b.shortId}`).join(", ");
6118
- log.info(TAG26, `#${card.short_id} blocked by ${list} — waiting`);
6776
+ log.info(TAG27, `#${card.short_id} blocked by ${list} — waiting`);
6119
6777
  await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
6120
6778
  return;
6121
6779
  }
@@ -6144,7 +6802,7 @@ class Pool {
6144
6802
  });
6145
6803
  this.lastWaitingEmit.set(cardId, currentTask);
6146
6804
  } catch (err) {
6147
- log.debug(TAG26, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6805
+ log.debug(TAG27, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6148
6806
  }
6149
6807
  }
6150
6808
  noteApiError(err) {
@@ -6152,7 +6810,7 @@ class Pool {
6152
6810
  return;
6153
6811
  if (err.kind === "auth") {
6154
6812
  if (!this.authPaused) {
6155
- log.error(TAG26, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6813
+ log.error(TAG27, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6156
6814
  }
6157
6815
  this.authPaused = true;
6158
6816
  return;
@@ -6161,7 +6819,7 @@ class Pool {
6161
6819
  const until = Date.now() + cooldownMs;
6162
6820
  if (until > this.apiCooldownUntil) {
6163
6821
  this.apiCooldownUntil = until;
6164
- log.warn(TAG26, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6822
+ log.warn(TAG27, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6165
6823
  }
6166
6824
  }
6167
6825
  apiCooldownRemainingMs() {
@@ -6174,13 +6832,13 @@ class Pool {
6174
6832
  const removed = queue.remove(cardId);
6175
6833
  if (removed) {
6176
6834
  this.cardDataCache.delete(cardId);
6177
- log.info(TAG26, `Removed #${removed.shortId} from ${removed.mode} queue`);
6835
+ log.info(TAG27, `Removed #${removed.shortId} from ${removed.mode} queue`);
6178
6836
  return;
6179
6837
  }
6180
6838
  }
6181
6839
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
6182
6840
  if (worker) {
6183
- log.info(TAG26, `Cancelling worker ${worker.id} for card ${cardId}`);
6841
+ log.info(TAG27, `Cancelling worker ${worker.id} for card ${cardId}`);
6184
6842
  await worker.cancel();
6185
6843
  }
6186
6844
  }
@@ -6213,10 +6871,10 @@ class Pool {
6213
6871
  async handleAgentCommand(cardId, command) {
6214
6872
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
6215
6873
  if (!worker) {
6216
- log.debug(TAG26, `No active worker for card ${cardId}, ignoring ${command}`);
6874
+ log.debug(TAG27, `No active worker for card ${cardId}, ignoring ${command}`);
6217
6875
  return;
6218
6876
  }
6219
- log.info(TAG26, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6877
+ log.info(TAG27, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6220
6878
  switch (command) {
6221
6879
  case "pause":
6222
6880
  await worker.pause();
@@ -6264,7 +6922,7 @@ class Pool {
6264
6922
  };
6265
6923
  }
6266
6924
  async shutdown() {
6267
- log.info(TAG26, "Shutting down pool...");
6925
+ log.info(TAG27, "Shutting down pool...");
6268
6926
  this.shuttingDown = true;
6269
6927
  const active = [
6270
6928
  ...this.implWorkers.filter((w) => w.isActive),
@@ -6272,7 +6930,7 @@ class Pool {
6272
6930
  ];
6273
6931
  await Promise.all(active.map((w) => w.cancel()));
6274
6932
  this.sleepGuard.stop();
6275
- log.info(TAG26, "Pool shutdown complete");
6933
+ log.info(TAG27, "Pool shutdown complete");
6276
6934
  }
6277
6935
  cardDataCache = new Map;
6278
6936
  tryDispatchFor(workers, queue, label) {
@@ -6280,7 +6938,7 @@ class Pool {
6280
6938
  return false;
6281
6939
  const idle = workers.find((w) => w.isIdle);
6282
6940
  if (!idle) {
6283
- log.debug(TAG26, `No idle ${label} workers (queue: ${queue.length})`);
6941
+ log.debug(TAG27, `No idle ${label} workers (queue: ${queue.length})`);
6284
6942
  return false;
6285
6943
  }
6286
6944
  const next = queue.dequeue();
@@ -6288,18 +6946,18 @@ class Pool {
6288
6946
  return false;
6289
6947
  const data = this.cardDataCache.get(next.cardId);
6290
6948
  if (!data) {
6291
- log.warn(TAG26, `No cached data for card ${next.cardId}, skipping`);
6949
+ log.warn(TAG27, `No cached data for card ${next.cardId}, skipping`);
6292
6950
  return false;
6293
6951
  }
6294
6952
  this.cardDataCache.delete(next.cardId);
6295
6953
  this.lastWaitingEmit.delete(next.cardId);
6296
- log.info(TAG26, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6954
+ log.info(TAG27, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6297
6955
  this.sleepGuard.acquire();
6298
6956
  idle.run(data.card, data.column, data.labels, data.subtasks);
6299
6957
  return true;
6300
6958
  }
6301
6959
  }
6302
- var TAG26 = "pool";
6960
+ var TAG27 = "pool";
6303
6961
  var init_pool = __esm(() => {
6304
6962
  init_error_classifier();
6305
6963
  init_log();
@@ -6341,7 +6999,7 @@ function load(path) {
6341
6999
  return parsed;
6342
7000
  return {};
6343
7001
  } catch (err) {
6344
- log.warn(TAG27, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
7002
+ log.warn(TAG28, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
6345
7003
  return {};
6346
7004
  }
6347
7005
  }
@@ -6359,7 +7017,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
6359
7017
  registry[projectId] = { ...entry, updatedAt: Date.now() };
6360
7018
  save(path, registry);
6361
7019
  } catch (err) {
6362
- log.warn(TAG27, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7020
+ log.warn(TAG28, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6363
7021
  }
6364
7022
  }
6365
7023
  function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
@@ -6375,10 +7033,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
6375
7033
  delete registry[projectId];
6376
7034
  save(path, registry);
6377
7035
  } catch (err) {
6378
- log.warn(TAG27, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7036
+ log.warn(TAG28, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6379
7037
  }
6380
7038
  }
6381
- var TAG27 = "port-registry";
7039
+ var TAG28 = "port-registry";
6382
7040
  var init_port_registry = __esm(() => {
6383
7041
  init_log();
6384
7042
  });
@@ -6399,7 +7057,7 @@ async function fetchCardSafely(client, cardId) {
6399
7057
  const { card } = await client.getCard(cardId);
6400
7058
  return card;
6401
7059
  } catch (err) {
6402
- log.warn(TAG28, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
7060
+ log.warn(TAG29, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
6403
7061
  return null;
6404
7062
  }
6405
7063
  }
@@ -6409,7 +7067,7 @@ async function recoverOrphans(store, client, config) {
6409
7067
  return [];
6410
7068
  }
6411
7069
  const outcomes = [];
6412
- log.info(TAG28, `recovering ${active.length} orphan run(s) from prior daemon`);
7070
+ log.info(TAG29, `recovering ${active.length} orphan run(s) from prior daemon`);
6413
7071
  for (const run of active) {
6414
7072
  const outcome = {
6415
7073
  runId: run.runId,
@@ -6421,11 +7079,11 @@ async function recoverOrphans(store, client, config) {
6421
7079
  };
6422
7080
  outcomes.push(outcome);
6423
7081
  if (isProcessAlive(run.daemonPid, process.pid)) {
6424
- log.warn(TAG28, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
7082
+ log.warn(TAG29, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
6425
7083
  outcome.actions.push("skipped: daemon pid still alive");
6426
7084
  continue;
6427
7085
  }
6428
- log.info(TAG28, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
7086
+ log.info(TAG29, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
6429
7087
  await recoverRun(run, store, client, config, outcome);
6430
7088
  }
6431
7089
  return outcomes;
@@ -6443,7 +7101,7 @@ async function recoverRun(run, store, client, config, outcome) {
6443
7101
  } catch (err) {
6444
7102
  const msg = err instanceof Error ? err.message : String(err);
6445
7103
  outcome.errors.push(`endAgentSession: ${msg}`);
6446
- log.warn(TAG28, `endAgentSession failed for ${run.cardId}: ${msg}`);
7104
+ log.warn(TAG29, `endAgentSession failed for ${run.cardId}: ${msg}`);
6447
7105
  }
6448
7106
  const card = await fetchCardSafely(client, run.cardId);
6449
7107
  if (card) {
@@ -6486,9 +7144,9 @@ async function recoverRun(run, store, client, config, outcome) {
6486
7144
  const msg = err instanceof Error ? err.message : String(err);
6487
7145
  outcome.errors.push(`endRun: ${msg}`);
6488
7146
  }
6489
- log.info(TAG28, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
7147
+ log.info(TAG29, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
6490
7148
  }
6491
- var TAG28 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
7149
+ var TAG29 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
6492
7150
  var init_recovery = __esm(() => {
6493
7151
  init_board_helpers();
6494
7152
  init_log();
@@ -6536,7 +7194,7 @@ class Reconciler {
6536
7194
  clearInterval(this.timer);
6537
7195
  this.timer = null;
6538
7196
  }
6539
- log.info(TAG29, "Heartbeat stopped");
7197
+ log.info(TAG30, "Heartbeat stopped");
6540
7198
  }
6541
7199
  async recoverStaleRuns() {
6542
7200
  if (!this.stateStore || !this.agentConfig)
@@ -6553,7 +7211,7 @@ class Reconciler {
6553
7211
  if (!daemonDead && !(heartbeatStale && ourZombie))
6554
7212
  continue;
6555
7213
  const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
6556
- log.warn(TAG29, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
7214
+ log.warn(TAG30, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
6557
7215
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
6558
7216
  runId: run.runId,
6559
7217
  cardId: run.cardId,
@@ -6580,11 +7238,11 @@ class Reconciler {
6580
7238
  const stalledAt = Date.parse(card.updated_at ?? "");
6581
7239
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
6582
7240
  continue;
6583
- log.warn(TAG29, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
7241
+ log.warn(TAG30, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
6584
7242
  try {
6585
7243
  await this.client.moveCard(card.id, pickupCol.id);
6586
7244
  } catch (err) {
6587
- log.error(TAG29, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7245
+ log.error(TAG30, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6588
7246
  }
6589
7247
  }
6590
7248
  }
@@ -6607,11 +7265,11 @@ class Reconciler {
6607
7265
  const parkedAt = Date.parse(card.updated_at ?? "");
6608
7266
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
6609
7267
  continue;
6610
- log.warn(TAG29, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
7268
+ log.warn(TAG30, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
6611
7269
  try {
6612
7270
  await this.client.moveCard(card.id, pickupCol.id);
6613
7271
  } catch (err) {
6614
- log.error(TAG29, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7272
+ log.error(TAG30, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6615
7273
  }
6616
7274
  }
6617
7275
  }
@@ -6640,18 +7298,18 @@ class Reconciler {
6640
7298
  const subtasks = card.subtasks ?? [];
6641
7299
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
6642
7300
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
6643
- log.debug(TAG29, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
7301
+ log.debug(TAG30, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
6644
7302
  continue;
6645
7303
  }
6646
7304
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
6647
- log.debug(TAG29, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
7305
+ log.debug(TAG30, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
6648
7306
  continue;
6649
7307
  }
6650
7308
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6651
- log.debug(TAG29, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
7309
+ log.debug(TAG30, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
6652
7310
  continue;
6653
7311
  }
6654
- log.info(TAG29, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
7312
+ log.info(TAG30, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
6655
7313
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
6656
7314
  }
6657
7315
  }
@@ -6661,18 +7319,18 @@ class Reconciler {
6661
7319
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
6662
7320
  for (const knownId of knownCardIds) {
6663
7321
  if (!allAgentCardIds.has(knownId)) {
6664
- log.info(TAG29, `Missed unassign: ${knownId} — removing`);
7322
+ log.info(TAG30, `Missed unassign: ${knownId} — removing`);
6665
7323
  await this.pool.removeCard(knownId);
6666
7324
  }
6667
7325
  }
6668
7326
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
6669
- log.debug(TAG29, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
7327
+ log.debug(TAG30, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
6670
7328
  } catch (err) {
6671
- log.error(TAG29, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
7329
+ log.error(TAG30, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
6672
7330
  }
6673
7331
  }
6674
7332
  }
6675
- var TAG29 = "reconcile";
7333
+ var TAG30 = "reconcile";
6676
7334
  var init_reconcile = __esm(() => {
6677
7335
  init_board_helpers();
6678
7336
  init_log();
@@ -6710,7 +7368,7 @@ function prettyBanner(config, version) {
6710
7368
  checks.push({ kind: "ok", message });
6711
7369
  },
6712
7370
  warn(message) {
6713
- log.warn(TAG30, message);
7371
+ log.warn(TAG31, message);
6714
7372
  checks.push({ kind: "warn", message: message.split(`
6715
7373
  `, 1)[0] });
6716
7374
  },
@@ -6735,25 +7393,25 @@ function prettyBanner(config, version) {
6735
7393
  };
6736
7394
  }
6737
7395
  function jsonBanner(config, version) {
6738
- log.info(TAG30, `Harmony Agent Daemon v${version} starting...`);
6739
- log.info(TAG30, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
7396
+ log.info(TAG31, `Harmony Agent Daemon v${version} starting...`);
7397
+ log.info(TAG31, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
6740
7398
  if (config.agent.review.enabled) {
6741
- log.info(TAG30, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
7399
+ log.info(TAG31, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
6742
7400
  }
6743
7401
  let failed = false;
6744
7402
  return {
6745
7403
  setProjectName(_name) {},
6746
7404
  setGitProvider(provider) {
6747
- log.info(TAG30, `Git provider: ${provider}`);
7405
+ log.info(TAG31, `Git provider: ${provider}`);
6748
7406
  },
6749
7407
  setHttpPort(port) {
6750
- log.info(TAG30, `HTTP server on port ${port}`);
7408
+ log.info(TAG31, `HTTP server on port ${port}`);
6751
7409
  },
6752
7410
  check(message) {
6753
- log.info(TAG30, message);
7411
+ log.info(TAG31, message);
6754
7412
  },
6755
7413
  warn(message) {
6756
- log.warn(TAG30, message);
7414
+ log.warn(TAG31, message);
6757
7415
  },
6758
7416
  fail() {
6759
7417
  failed = true;
@@ -6761,7 +7419,7 @@ function jsonBanner(config, version) {
6761
7419
  async ready(message) {
6762
7420
  if (failed)
6763
7421
  return;
6764
- log.info(TAG30, message);
7422
+ log.info(TAG31, message);
6765
7423
  }
6766
7424
  };
6767
7425
  }
@@ -6807,6 +7465,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
6807
7465
  label: "Model",
6808
7466
  value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
6809
7467
  });
7468
+ rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
6810
7469
  const tail = [];
6811
7470
  if (gitProvider)
6812
7471
  tail.push(gitProvider);
@@ -6824,6 +7483,9 @@ function titleRule(title) {
6824
7483
  const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
6825
7484
  return dim(`${prefix}${title}${surround}${suffix}`);
6826
7485
  }
7486
+ function runnerDesc(runner) {
7487
+ return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
7488
+ }
6827
7489
  function shortenId(id) {
6828
7490
  if (id.length <= 8)
6829
7491
  return id;
@@ -6838,7 +7500,7 @@ function cyan(s) {
6838
7500
  function yellow(s) {
6839
7501
  return `${ANSI.yellow}${s}${ANSI.reset}`;
6840
7502
  }
6841
- var TAG30 = "daemon", RULE_WIDTH = 70, ANSI;
7503
+ var TAG31 = "daemon", RULE_WIDTH = 70, ANSI;
6842
7504
  var init_startup_banner = __esm(() => {
6843
7505
  init_log();
6844
7506
  ANSI = {
@@ -6985,18 +7647,18 @@ class Watcher {
6985
7647
  }
6986
7648
  async start() {
6987
7649
  if (!isPretty()) {
6988
- log.info(TAG31, "Connecting to Supabase realtime (broadcast)...");
7650
+ log.info(TAG32, "Connecting to Supabase realtime (broadcast)...");
6989
7651
  }
6990
7652
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
6991
7653
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
6992
7654
  const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
6993
- log.debug(TAG31, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
7655
+ log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
6994
7656
  this.onCardBroadcast({
6995
7657
  event: "card_update",
6996
7658
  payload: msg.payload ?? {}
6997
7659
  });
6998
7660
  }).on("broadcast", { event: "card_created" }, (msg) => {
6999
- log.debug(TAG31, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
7661
+ log.debug(TAG32, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
7000
7662
  this.onCardBroadcast({
7001
7663
  event: "card_created",
7002
7664
  payload: msg.payload ?? {}
@@ -7006,29 +7668,29 @@ class Watcher {
7006
7668
  const cardId = payload.card_id;
7007
7669
  const command = payload.command;
7008
7670
  if (cardId && command) {
7009
- log.info(TAG31, `Broadcast: agent_command ${command} for ${cardId}`);
7671
+ log.info(TAG32, `Broadcast: agent_command ${command} for ${cardId}`);
7010
7672
  this.onAgentCommand?.({ cardId, command });
7011
7673
  }
7012
7674
  }).subscribe((status) => {
7013
7675
  if (status === "SUBSCRIBED") {
7014
7676
  this.connected = true;
7015
7677
  if (!isPretty() || !this.suppressStartupLogs) {
7016
- log.info(TAG31, "Broadcast subscription active");
7678
+ log.info(TAG32, "Broadcast subscription active");
7017
7679
  }
7018
7680
  this.maybeResolveReady();
7019
7681
  } else if (status === "CHANNEL_ERROR") {
7020
7682
  this.connected = false;
7021
- log.error(TAG31, "Broadcast channel error — will rely on reconciliation");
7683
+ log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
7022
7684
  } else if (status === "TIMED_OUT") {
7023
7685
  this.connected = false;
7024
- log.warn(TAG31, "Broadcast subscription timed out — retrying...");
7686
+ log.warn(TAG32, "Broadcast subscription timed out — retrying...");
7025
7687
  } else if (status === "CLOSED") {
7026
7688
  this.connected = false;
7027
7689
  }
7028
7690
  });
7029
7691
  this.channel = channel;
7030
7692
  presenceChannel.on("presence", { event: "sync" }, () => {
7031
- log.debug(TAG31, "Presence sync");
7693
+ log.debug(TAG32, "Presence sync");
7032
7694
  }).subscribe(async (status) => {
7033
7695
  if (status === "SUBSCRIBED") {
7034
7696
  await presenceChannel.track({
@@ -7041,7 +7703,7 @@ class Watcher {
7041
7703
  agentName: this.identity.agentName
7042
7704
  });
7043
7705
  if (!isPretty() || !this.suppressStartupLogs) {
7044
- log.info(TAG31, "Presence tracked on board-presence channel");
7706
+ log.info(TAG32, "Presence tracked on board-presence channel");
7045
7707
  }
7046
7708
  this.presenceTracked = true;
7047
7709
  this.maybeResolveReady();
@@ -7063,10 +7725,10 @@ class Watcher {
7063
7725
  this.supabase = null;
7064
7726
  }
7065
7727
  this.connected = false;
7066
- log.info(TAG31, "Broadcast subscription stopped");
7728
+ log.info(TAG32, "Broadcast subscription stopped");
7067
7729
  }
7068
7730
  }
7069
- var TAG31 = "watcher";
7731
+ var TAG32 = "watcher";
7070
7732
  var init_watcher = __esm(() => {
7071
7733
  init_log();
7072
7734
  });
@@ -7153,10 +7815,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
7153
7815
  });
7154
7816
  } catch {}
7155
7817
  if (result.removed.length > 0) {
7156
- log.info(TAG32, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
7818
+ log.info(TAG33, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
7157
7819
  }
7158
7820
  if (result.errors.length > 0) {
7159
- log.warn(TAG32, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
7821
+ log.warn(TAG33, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
7160
7822
  }
7161
7823
  return result;
7162
7824
  }
@@ -7186,7 +7848,7 @@ function pruneFailedRemoteBranches(opts) {
7186
7848
  } catch (err) {
7187
7849
  const detail = gitErrorDetail(err);
7188
7850
  if (isTransientGitNetworkError(detail)) {
7189
- log.debug(TAG32, `Remote branch GC skipped — remote unreachable: ${detail}`);
7851
+ log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
7190
7852
  return result;
7191
7853
  }
7192
7854
  result.errors.push({ ref: "fetch", error: detail });
@@ -7225,7 +7887,7 @@ function pruneFailedRemoteBranches(opts) {
7225
7887
  continue;
7226
7888
  }
7227
7889
  if (clock() > sweepDeadline) {
7228
- log.debug(TAG32, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7890
+ log.debug(TAG33, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7229
7891
  break;
7230
7892
  }
7231
7893
  try {
@@ -7238,17 +7900,17 @@ function pruneFailedRemoteBranches(opts) {
7238
7900
  } catch (err) {
7239
7901
  const detail = gitErrorDetail(err);
7240
7902
  if (isTransientGitNetworkError(detail)) {
7241
- log.debug(TAG32, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7903
+ log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7242
7904
  break;
7243
7905
  }
7244
7906
  result.errors.push({ ref, error: detail });
7245
7907
  }
7246
7908
  }
7247
7909
  if (result.removed.length > 0) {
7248
- log.info(TAG32, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7910
+ log.info(TAG33, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7249
7911
  }
7250
7912
  if (result.errors.length > 0) {
7251
- log.warn(TAG32, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
7913
+ log.warn(TAG33, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
7252
7914
  }
7253
7915
  return result;
7254
7916
  }
@@ -7279,13 +7941,13 @@ class WorktreeGc {
7279
7941
  try {
7280
7942
  runWorktreeGc(this.basePath, this.store);
7281
7943
  } catch (err) {
7282
- log.warn(TAG32, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7944
+ log.warn(TAG33, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7283
7945
  }
7284
7946
  if (this.remoteOpts) {
7285
7947
  try {
7286
7948
  pruneFailedRemoteBranches(this.remoteOpts);
7287
7949
  } catch (err) {
7288
- log.warn(TAG32, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7950
+ log.warn(TAG33, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7289
7951
  }
7290
7952
  }
7291
7953
  }
@@ -7299,7 +7961,7 @@ function getRepoRoot2() {
7299
7961
  return null;
7300
7962
  }
7301
7963
  }
7302
- var TAG32 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
7964
+ var TAG33 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
7303
7965
  var init_worktree_gc = __esm(() => {
7304
7966
  init_log();
7305
7967
  init_worktree();
@@ -7403,7 +8065,7 @@ async function main() {
7403
8065
  } catch (err) {
7404
8066
  if (err instanceof ConfigValidationError) {
7405
8067
  banner.fail();
7406
- log.error(TAG33, err.message);
8068
+ log.error(TAG34, err.message);
7407
8069
  process.exit(1);
7408
8070
  }
7409
8071
  throw err;
@@ -7471,6 +8133,7 @@ async function main() {
7471
8133
  daemonPid: process.pid,
7472
8134
  startedAt,
7473
8135
  uptimeMs: Date.now() - startedAt,
8136
+ runner: config.agent.runner,
7474
8137
  workers: pool.snapshotWorkers().map((w) => ({
7475
8138
  ...w,
7476
8139
  phase: null
@@ -7512,7 +8175,7 @@ async function main() {
7512
8175
  if (shuttingDown)
7513
8176
  return;
7514
8177
  shuttingDown = true;
7515
- log.info(TAG33, `Received ${signal}, shutting down gracefully...`);
8178
+ log.info(TAG34, `Received ${signal}, shutting down gracefully...`);
7516
8179
  reconciler.stop();
7517
8180
  mergeMonitor?.stop();
7518
8181
  worktreeGc.stop();
@@ -7522,18 +8185,18 @@ async function main() {
7522
8185
  }
7523
8186
  await watcher.stop();
7524
8187
  await pool.shutdown();
7525
- log.info(TAG33, "Daemon stopped.");
8188
+ log.info(TAG34, "Daemon stopped.");
7526
8189
  process.exit(exitCode);
7527
8190
  };
7528
8191
  process.on("SIGINT", () => shutdown("SIGINT"));
7529
8192
  process.on("SIGTERM", () => shutdown("SIGTERM"));
7530
8193
  process.on("uncaughtException", (err) => {
7531
- log.error(TAG33, `Uncaught exception: ${err.message}`);
8194
+ log.error(TAG34, `Uncaught exception: ${err.message}`);
7532
8195
  exitCode = 1;
7533
8196
  shutdown("uncaughtException");
7534
8197
  });
7535
8198
  process.on("unhandledRejection", (reason) => {
7536
- log.error(TAG33, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
8199
+ log.error(TAG34, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
7537
8200
  exitCode = 1;
7538
8201
  shutdown("unhandledRejection");
7539
8202
  });
@@ -7586,35 +8249,35 @@ async function handleBroadcast(event, client, pool, config, agentId) {
7586
8249
  if (assignedAgentId === undefined)
7587
8250
  return;
7588
8251
  if (assignedAgentId === agentId) {
7589
- log.info(TAG33, `Broadcast: card ${cardId} assigned to agent`);
8252
+ log.info(TAG34, `Broadcast: card ${cardId} assigned to agent`);
7590
8253
  try {
7591
8254
  await pool.resetAttemptsForReassign(cardId);
7592
8255
  await tryEnqueueCard(cardId, client, pool, config, agentId);
7593
8256
  } catch (err) {
7594
- log.error(TAG33, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
8257
+ log.error(TAG34, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
7595
8258
  }
7596
8259
  } else if (pool.isCardKnown(cardId)) {
7597
- log.info(TAG33, `Broadcast: card ${cardId} unassigned from agent`);
8260
+ log.info(TAG34, `Broadcast: card ${cardId} unassigned from agent`);
7598
8261
  await pool.removeCard(cardId);
7599
8262
  }
7600
8263
  }
7601
8264
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7602
8265
  const { card } = await client.getCard(cardId);
7603
8266
  if (card.assigned_agent_id !== agentId) {
7604
- log.debug(TAG33, `Card ${cardId} no longer assigned to agent — skipping`);
8267
+ log.debug(TAG34, `Card ${cardId} no longer assigned to agent — skipping`);
7605
8268
  return;
7606
8269
  }
7607
8270
  const board = await client.getBoard(config.projectId, { summary: true });
7608
8271
  const columns = board.columns;
7609
8272
  const column = columns.find((c) => c.id === card.column_id);
7610
8273
  if (!column) {
7611
- log.warn(TAG33, `Column not found for card ${cardId}`);
8274
+ log.warn(TAG34, `Column not found for card ${cardId}`);
7612
8275
  return;
7613
8276
  }
7614
8277
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7615
8278
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7616
8279
  if (!isPickupColumn && !isReviewColumn) {
7617
- log.info(TAG33, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
8280
+ log.info(TAG34, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
7618
8281
  return;
7619
8282
  }
7620
8283
  const mode = isReviewColumn ? "review" : "implement";
@@ -7622,16 +8285,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7622
8285
  const cardLabels = resolveCardLabels(card, labelMap);
7623
8286
  const subtasks = card.subtasks ?? [];
7624
8287
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
7625
- log.debug(TAG33, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
8288
+ log.debug(TAG34, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
7626
8289
  return;
7627
8290
  }
7628
8291
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
7629
- log.info(TAG33, `Card #${card.short_id} has no branch reference — skipping auto-review`);
8292
+ log.info(TAG34, `Card #${card.short_id} has no branch reference — skipping auto-review`);
7630
8293
  return;
7631
8294
  }
7632
8295
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
7633
8296
  }
7634
- var TAG33 = "daemon", PKG_VERSION;
8297
+ var TAG34 = "daemon", PKG_VERSION;
7635
8298
  var init_src = __esm(() => {
7636
8299
  init_board_helpers();
7637
8300
  init_config();