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