@gethmy/agent 1.10.9 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +867 -194
  2. package/dist/index.js +867 -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);
@@ -5659,6 +6163,16 @@ class Worker {
5659
6163
  } catch {}
5660
6164
  await this.recordOutcome(card.id, "failure");
5661
6165
  } else if (this.runId && this.aborted) {
6166
+ try {
6167
+ await this.client.updateCard(card.id, { assignedAgentId: null });
6168
+ } catch (err) {
6169
+ log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
6170
+ }
6171
+ try {
6172
+ await runTransition(this.client, card, { removeLabels: ["agent"] });
6173
+ } catch (tErr) {
6174
+ log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
6175
+ }
5662
6176
  try {
5663
6177
  await this.stateStore.endRun(this.runId, "paused", {
5664
6178
  errorMessage: "cancelled",
@@ -5674,6 +6188,26 @@ class Worker {
5674
6188
  } catch {}
5675
6189
  await this.recordOutcome(card.id, "failure");
5676
6190
  }
6191
+ if (this.cliRunner) {
6192
+ if (succeeded) {
6193
+ this.cliRunner.recordFinished({ status: "completed" });
6194
+ } else if (this.timedOut) {
6195
+ this.cliRunner.recordFinished({
6196
+ status: "failed",
6197
+ failureReason: "timeout"
6198
+ });
6199
+ } else if (this.aborted) {
6200
+ this.cliRunner.recordFinished({
6201
+ status: "stopped",
6202
+ stopReason: "user_requested"
6203
+ });
6204
+ } else {
6205
+ this.cliRunner.recordFinished({ status: "failed" });
6206
+ }
6207
+ try {
6208
+ await this.cliRunner.flushFinal();
6209
+ } catch {}
6210
+ }
5677
6211
  this.cleanup();
5678
6212
  this.state = "idle";
5679
6213
  this.onDone(this);
@@ -5763,7 +6297,9 @@ class Worker {
5763
6297
  this.aborted = true;
5764
6298
  this.state = "cancelling";
5765
6299
  log.info(this.tag, `Cancelling work on ${this.cardId}`);
5766
- if (this.process && !this.process.killed) {
6300
+ if (this.sdkRunner) {
6301
+ await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
6302
+ } else if (this.process && !this.process.killed) {
5767
6303
  await terminateGroup(this.process, {
5768
6304
  sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT2,
5769
6305
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
@@ -5798,7 +6334,9 @@ class Worker {
5798
6334
  const planTimeout = setTimeout(() => {
5799
6335
  planTimedOut = true;
5800
6336
  log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
5801
- if (this.process && !this.process.killed) {
6337
+ if (this.sdkRunner) {
6338
+ this.sdkRunner.stop("timeout").catch(() => {});
6339
+ } else if (this.process && !this.process.killed) {
5802
6340
  terminateGroup(this.process, {
5803
6341
  sigintTimeoutMs: 1e4,
5804
6342
  sigtermTimeoutMs: 5000
@@ -5892,7 +6430,40 @@ class Worker {
5892
6430
  }
5893
6431
  return false;
5894
6432
  }
5895
- async spawnClaude(prompt, card, subtasks, opts = {}) {
6433
+ async drainSteeringMessages(card, subtasks) {
6434
+ if (!this.cliSessionId || !this.sessionId || !this.cardId)
6435
+ return;
6436
+ for (let i = 0;i < MAX_STEERING_ITERATIONS && !this.aborted; i++) {
6437
+ let messages;
6438
+ try {
6439
+ const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
6440
+ messages = res.messages ?? [];
6441
+ } catch (err) {
6442
+ log.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
6443
+ return;
6444
+ }
6445
+ if (messages.length === 0)
6446
+ return;
6447
+ this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
6448
+ log.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
6449
+ this.state = "running";
6450
+ await this.recordPhase("running");
6451
+ try {
6452
+ await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
6453
+ model: this.selectImplementModel(card),
6454
+ maxTurns: STEERING_MAX_TURNS,
6455
+ resumeSessionId: this.cliSessionId
6456
+ });
6457
+ } catch (err) {
6458
+ log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
6459
+ return;
6460
+ }
6461
+ }
6462
+ }
6463
+ spawnClaude(prompt, card, subtasks, opts = {}) {
6464
+ return this.config.runner === "sdk" ? this.spawnClaudeSdk(prompt, card, subtasks, opts) : this.spawnClaudeCli(prompt, card, subtasks, opts);
6465
+ }
6466
+ async spawnClaudeCli(prompt, card, subtasks, opts = {}) {
5896
6467
  const model = opts.model ?? this.config.claude.model;
5897
6468
  const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
5898
6469
  const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
@@ -5909,6 +6480,7 @@ class Worker {
5909
6480
  String(maxTurns),
5910
6481
  "--allowedTools",
5911
6482
  allowedTools,
6483
+ ...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
5912
6484
  ...this.config.claude.additionalArgs,
5913
6485
  "--",
5914
6486
  prompt
@@ -5928,10 +6500,11 @@ class Worker {
5928
6500
  });
5929
6501
  const parser = new StreamParser;
5930
6502
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
5931
- if (this.sessionId) {
5932
- this.progressTracker.setSessionId(this.sessionId);
5933
- }
5934
6503
  this.progressTracker.attach(parser);
6504
+ this.cliRunner?.attach(parser);
6505
+ if (this.cliRunner) {
6506
+ this.progressTracker.setRunEventSink(this.cliRunner);
6507
+ }
5935
6508
  if (this.process.stdout) {
5936
6509
  parser.attach(this.process.stdout);
5937
6510
  if (runLog) {
@@ -5955,14 +6528,16 @@ class Worker {
5955
6528
  reject(new Error(`Failed to spawn claude: ${err.message}`));
5956
6529
  });
5957
6530
  this.process.on("close", (code) => {
6531
+ const leaderPid = this.process?.pid;
5958
6532
  this.process = null;
6533
+ if (parser.sessionId)
6534
+ this.cliSessionId = parser.sessionId;
5959
6535
  this.lastSessionStats = this.progressTracker?.stats;
5960
6536
  const spawnCost = this.lastSessionStats?.cost;
5961
6537
  if (spawnCost) {
5962
6538
  this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
5963
6539
  this.runTurns += spawnCost.numTurns;
5964
6540
  }
5965
- this.progressTracker?.flushFinal();
5966
6541
  this.progressTracker?.stop();
5967
6542
  this.progressTracker = null;
5968
6543
  if (runLog) {
@@ -5972,6 +6547,7 @@ class Worker {
5972
6547
  `);
5973
6548
  runLog.stream.end();
5974
6549
  }
6550
+ reapGroup(leaderPid);
5975
6551
  if (this.aborted) {
5976
6552
  resolve3();
5977
6553
  } else if (code === 0) {
@@ -5984,11 +6560,101 @@ class Worker {
5984
6560
  });
5985
6561
  });
5986
6562
  }
6563
+ async spawnClaudeSdk(prompt, card, subtasks, opts = {}) {
6564
+ const model = opts.model ?? this.config.claude.model;
6565
+ const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
6566
+ const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
6567
+ const initialPhase = opts.initialPhase ?? "exploring";
6568
+ const sdkCfg = this.config.sdk;
6569
+ log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
6570
+ const runLog = openRunLog(this.tag, this.runId, card.short_id);
6571
+ if (runLog) {
6572
+ log.info(this.tag, `Run log: ${runLog.path}`);
6573
+ runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
6574
+ ` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
6575
+
6576
+ `);
6577
+ }
6578
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
6579
+ if (this.cliRunner) {
6580
+ this.progressTracker.setRunEventSink(this.cliRunner);
6581
+ }
6582
+ const runner = new SdkAgentRunner({
6583
+ model,
6584
+ maxTurns,
6585
+ allowedTools,
6586
+ maxBudgetUsd: sdkCfg?.maxBudgetUsd,
6587
+ settingSources: sdkCfg?.settingSources,
6588
+ mcpServers: sdkCfg?.mcpServers,
6589
+ strictMcpConfig: sdkCfg?.strictMcpConfig,
6590
+ onSpawn: (child) => {
6591
+ this.process = child;
6592
+ }
6593
+ });
6594
+ this.sdkRunner = runner;
6595
+ const baseInput = {
6596
+ sessionId: this.sessionId ?? "",
6597
+ cardId: card.id,
6598
+ workspaceId: this.workspaceId,
6599
+ prompt,
6600
+ cwd: this.worktreePath,
6601
+ model
6602
+ };
6603
+ const stream = opts.resumeSessionId ? runner.resume({ ...baseInput, resumeSessionId: opts.resumeSessionId }) : runner.start(baseInput);
6604
+ let failure = null;
6605
+ let failureKind = null;
6606
+ try {
6607
+ for await (const ev of stream) {
6608
+ this.progressTracker?.ingest(ev);
6609
+ if (ev.source === "agent")
6610
+ this.cliRunner?.record(ev);
6611
+ if (ev.kind === "error") {
6612
+ failure = ev.payload.message;
6613
+ if (ev.payload.errorKind != null)
6614
+ failureKind = ev.payload.errorKind;
6615
+ }
6616
+ runLog?.stream.write(`[${ev.kind}] ${sdkDraftLogLine(ev)}
6617
+ `);
6618
+ }
6619
+ } finally {
6620
+ this.cliSessionId = runner.sessionId ?? this.cliSessionId;
6621
+ this.lastSessionStats = this.progressTracker?.stats;
6622
+ const spawnCost = this.lastSessionStats?.cost;
6623
+ if (spawnCost) {
6624
+ this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
6625
+ this.runTurns += spawnCost.numTurns;
6626
+ }
6627
+ if (runLog) {
6628
+ const stats = this.lastSessionStats;
6629
+ runLog.stream.write(`
6630
+ # runner=sdk aborted=${this.aborted} ` + `toolCalls=${stats?.toolCalls ?? 0} filesEdited=${stats?.filesEdited ?? 0} ` + `cost=$${stats?.cost?.totalCostUsd.toFixed(4) ?? "0"} ` + `ended=${new Date().toISOString()}
6631
+ `);
6632
+ runLog.stream.end();
6633
+ }
6634
+ this.progressTracker?.stop();
6635
+ this.progressTracker = null;
6636
+ this.process = null;
6637
+ this.sdkRunner = null;
6638
+ }
6639
+ if (this.aborted)
6640
+ return;
6641
+ if (failure) {
6642
+ const err = new Error(failure);
6643
+ err.stderr = runner.capturedStderrText;
6644
+ err.errorKind = failureKind;
6645
+ throw err;
6646
+ }
6647
+ }
5987
6648
  cleanup() {
5988
6649
  if (this.progressTracker) {
5989
6650
  this.progressTracker.stop();
5990
6651
  this.progressTracker = null;
5991
6652
  }
6653
+ if (this.cliRunner) {
6654
+ this.cliRunner.stop();
6655
+ this.cliRunner = null;
6656
+ }
6657
+ this.sdkRunner = null;
5992
6658
  this.stopHeartbeat();
5993
6659
  this.lastSessionStats = undefined;
5994
6660
  if (this.timeoutTimer) {
@@ -6013,9 +6679,10 @@ class Worker {
6013
6679
  this.runTurns = 0;
6014
6680
  }
6015
6681
  }
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;
6682
+ 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
6683
  var init_worker = __esm(() => {
6018
6684
  init_board_helpers();
6685
+ init_cli_agent_runner();
6019
6686
  init_completion();
6020
6687
  init_error_classifier();
6021
6688
  init_log();
@@ -6025,6 +6692,7 @@ var init_worker = __esm(() => {
6025
6692
  init_progress_tracker();
6026
6693
  init_prompt();
6027
6694
  init_run_log();
6695
+ init_sdk_agent_runner();
6028
6696
  init_state_store();
6029
6697
  init_stream_parser();
6030
6698
  init_transitions();
@@ -6083,39 +6751,39 @@ class Pool {
6083
6751
  }
6084
6752
  async enqueue(card, column, labels, subtasks, mode = "implement") {
6085
6753
  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`);
6754
+ log.debug(TAG27, `Card ${card.id} already queued or active, skipping`);
6087
6755
  return;
6088
6756
  }
6089
6757
  if (mode === "implement") {
6090
6758
  if (this.authPaused) {
6091
- log.debug(TAG26, `#${card.short_id} held — agent paused (auth error)`);
6759
+ log.debug(TAG27, `#${card.short_id} held — agent paused (auth error)`);
6092
6760
  await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
6093
6761
  return;
6094
6762
  }
6095
6763
  const cooldownMs = this.apiCooldownRemainingMs();
6096
6764
  if (cooldownMs > 0) {
6097
- log.debug(TAG26, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
6765
+ log.debug(TAG27, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
6098
6766
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
6099
6767
  return;
6100
6768
  }
6101
6769
  const decision = this.budget.check(card.id);
6102
6770
  if (!decision.allow) {
6103
6771
  if (decision.reason === "daily_budget") {
6104
- log.warn(TAG26, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
6772
+ log.warn(TAG27, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
6105
6773
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
6106
6774
  } else {
6107
- log.debug(TAG26, `#${card.short_id} gave up: ${decision.detail}`);
6775
+ log.debug(TAG27, `#${card.short_id} gave up: ${decision.detail}`);
6108
6776
  }
6109
6777
  return;
6110
6778
  }
6111
6779
  const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
6112
6780
  if (blockers === null) {
6113
- log.warn(TAG26, `#${card.short_id} blocker check failed — deferring to next tick`);
6781
+ log.warn(TAG27, `#${card.short_id} blocker check failed — deferring to next tick`);
6114
6782
  return;
6115
6783
  }
6116
6784
  if (blockers.length > 0) {
6117
6785
  const list = blockers.map((b) => `#${b.shortId}`).join(", ");
6118
- log.info(TAG26, `#${card.short_id} blocked by ${list} — waiting`);
6786
+ log.info(TAG27, `#${card.short_id} blocked by ${list} — waiting`);
6119
6787
  await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
6120
6788
  return;
6121
6789
  }
@@ -6144,7 +6812,7 @@ class Pool {
6144
6812
  });
6145
6813
  this.lastWaitingEmit.set(cardId, currentTask);
6146
6814
  } catch (err) {
6147
- log.debug(TAG26, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6815
+ log.debug(TAG27, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6148
6816
  }
6149
6817
  }
6150
6818
  noteApiError(err) {
@@ -6152,7 +6820,7 @@ class Pool {
6152
6820
  return;
6153
6821
  if (err.kind === "auth") {
6154
6822
  if (!this.authPaused) {
6155
- log.error(TAG26, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6823
+ log.error(TAG27, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6156
6824
  }
6157
6825
  this.authPaused = true;
6158
6826
  return;
@@ -6161,7 +6829,7 @@ class Pool {
6161
6829
  const until = Date.now() + cooldownMs;
6162
6830
  if (until > this.apiCooldownUntil) {
6163
6831
  this.apiCooldownUntil = until;
6164
- log.warn(TAG26, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6832
+ log.warn(TAG27, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6165
6833
  }
6166
6834
  }
6167
6835
  apiCooldownRemainingMs() {
@@ -6174,13 +6842,13 @@ class Pool {
6174
6842
  const removed = queue.remove(cardId);
6175
6843
  if (removed) {
6176
6844
  this.cardDataCache.delete(cardId);
6177
- log.info(TAG26, `Removed #${removed.shortId} from ${removed.mode} queue`);
6845
+ log.info(TAG27, `Removed #${removed.shortId} from ${removed.mode} queue`);
6178
6846
  return;
6179
6847
  }
6180
6848
  }
6181
6849
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
6182
6850
  if (worker) {
6183
- log.info(TAG26, `Cancelling worker ${worker.id} for card ${cardId}`);
6851
+ log.info(TAG27, `Cancelling worker ${worker.id} for card ${cardId}`);
6184
6852
  await worker.cancel();
6185
6853
  }
6186
6854
  }
@@ -6213,10 +6881,10 @@ class Pool {
6213
6881
  async handleAgentCommand(cardId, command) {
6214
6882
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
6215
6883
  if (!worker) {
6216
- log.debug(TAG26, `No active worker for card ${cardId}, ignoring ${command}`);
6884
+ log.debug(TAG27, `No active worker for card ${cardId}, ignoring ${command}`);
6217
6885
  return;
6218
6886
  }
6219
- log.info(TAG26, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6887
+ log.info(TAG27, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6220
6888
  switch (command) {
6221
6889
  case "pause":
6222
6890
  await worker.pause();
@@ -6264,7 +6932,7 @@ class Pool {
6264
6932
  };
6265
6933
  }
6266
6934
  async shutdown() {
6267
- log.info(TAG26, "Shutting down pool...");
6935
+ log.info(TAG27, "Shutting down pool...");
6268
6936
  this.shuttingDown = true;
6269
6937
  const active = [
6270
6938
  ...this.implWorkers.filter((w) => w.isActive),
@@ -6272,7 +6940,7 @@ class Pool {
6272
6940
  ];
6273
6941
  await Promise.all(active.map((w) => w.cancel()));
6274
6942
  this.sleepGuard.stop();
6275
- log.info(TAG26, "Pool shutdown complete");
6943
+ log.info(TAG27, "Pool shutdown complete");
6276
6944
  }
6277
6945
  cardDataCache = new Map;
6278
6946
  tryDispatchFor(workers, queue, label) {
@@ -6280,7 +6948,7 @@ class Pool {
6280
6948
  return false;
6281
6949
  const idle = workers.find((w) => w.isIdle);
6282
6950
  if (!idle) {
6283
- log.debug(TAG26, `No idle ${label} workers (queue: ${queue.length})`);
6951
+ log.debug(TAG27, `No idle ${label} workers (queue: ${queue.length})`);
6284
6952
  return false;
6285
6953
  }
6286
6954
  const next = queue.dequeue();
@@ -6288,18 +6956,18 @@ class Pool {
6288
6956
  return false;
6289
6957
  const data = this.cardDataCache.get(next.cardId);
6290
6958
  if (!data) {
6291
- log.warn(TAG26, `No cached data for card ${next.cardId}, skipping`);
6959
+ log.warn(TAG27, `No cached data for card ${next.cardId}, skipping`);
6292
6960
  return false;
6293
6961
  }
6294
6962
  this.cardDataCache.delete(next.cardId);
6295
6963
  this.lastWaitingEmit.delete(next.cardId);
6296
- log.info(TAG26, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6964
+ log.info(TAG27, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6297
6965
  this.sleepGuard.acquire();
6298
6966
  idle.run(data.card, data.column, data.labels, data.subtasks);
6299
6967
  return true;
6300
6968
  }
6301
6969
  }
6302
- var TAG26 = "pool";
6970
+ var TAG27 = "pool";
6303
6971
  var init_pool = __esm(() => {
6304
6972
  init_error_classifier();
6305
6973
  init_log();
@@ -6341,7 +7009,7 @@ function load(path) {
6341
7009
  return parsed;
6342
7010
  return {};
6343
7011
  } catch (err) {
6344
- log.warn(TAG27, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
7012
+ log.warn(TAG28, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
6345
7013
  return {};
6346
7014
  }
6347
7015
  }
@@ -6359,7 +7027,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
6359
7027
  registry[projectId] = { ...entry, updatedAt: Date.now() };
6360
7028
  save(path, registry);
6361
7029
  } catch (err) {
6362
- log.warn(TAG27, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7030
+ log.warn(TAG28, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6363
7031
  }
6364
7032
  }
6365
7033
  function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
@@ -6375,10 +7043,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
6375
7043
  delete registry[projectId];
6376
7044
  save(path, registry);
6377
7045
  } catch (err) {
6378
- log.warn(TAG27, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7046
+ log.warn(TAG28, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6379
7047
  }
6380
7048
  }
6381
- var TAG27 = "port-registry";
7049
+ var TAG28 = "port-registry";
6382
7050
  var init_port_registry = __esm(() => {
6383
7051
  init_log();
6384
7052
  });
@@ -6399,7 +7067,7 @@ async function fetchCardSafely(client, cardId) {
6399
7067
  const { card } = await client.getCard(cardId);
6400
7068
  return card;
6401
7069
  } catch (err) {
6402
- log.warn(TAG28, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
7070
+ log.warn(TAG29, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
6403
7071
  return null;
6404
7072
  }
6405
7073
  }
@@ -6409,7 +7077,7 @@ async function recoverOrphans(store, client, config) {
6409
7077
  return [];
6410
7078
  }
6411
7079
  const outcomes = [];
6412
- log.info(TAG28, `recovering ${active.length} orphan run(s) from prior daemon`);
7080
+ log.info(TAG29, `recovering ${active.length} orphan run(s) from prior daemon`);
6413
7081
  for (const run of active) {
6414
7082
  const outcome = {
6415
7083
  runId: run.runId,
@@ -6421,11 +7089,11 @@ async function recoverOrphans(store, client, config) {
6421
7089
  };
6422
7090
  outcomes.push(outcome);
6423
7091
  if (isProcessAlive(run.daemonPid, process.pid)) {
6424
- log.warn(TAG28, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
7092
+ log.warn(TAG29, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
6425
7093
  outcome.actions.push("skipped: daemon pid still alive");
6426
7094
  continue;
6427
7095
  }
6428
- log.info(TAG28, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
7096
+ log.info(TAG29, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
6429
7097
  await recoverRun(run, store, client, config, outcome);
6430
7098
  }
6431
7099
  return outcomes;
@@ -6443,7 +7111,7 @@ async function recoverRun(run, store, client, config, outcome) {
6443
7111
  } catch (err) {
6444
7112
  const msg = err instanceof Error ? err.message : String(err);
6445
7113
  outcome.errors.push(`endAgentSession: ${msg}`);
6446
- log.warn(TAG28, `endAgentSession failed for ${run.cardId}: ${msg}`);
7114
+ log.warn(TAG29, `endAgentSession failed for ${run.cardId}: ${msg}`);
6447
7115
  }
6448
7116
  const card = await fetchCardSafely(client, run.cardId);
6449
7117
  if (card) {
@@ -6486,9 +7154,9 @@ async function recoverRun(run, store, client, config, outcome) {
6486
7154
  const msg = err instanceof Error ? err.message : String(err);
6487
7155
  outcome.errors.push(`endRun: ${msg}`);
6488
7156
  }
6489
- log.info(TAG28, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
7157
+ log.info(TAG29, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
6490
7158
  }
6491
- var TAG28 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
7159
+ var TAG29 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
6492
7160
  var init_recovery = __esm(() => {
6493
7161
  init_board_helpers();
6494
7162
  init_log();
@@ -6536,7 +7204,7 @@ class Reconciler {
6536
7204
  clearInterval(this.timer);
6537
7205
  this.timer = null;
6538
7206
  }
6539
- log.info(TAG29, "Heartbeat stopped");
7207
+ log.info(TAG30, "Heartbeat stopped");
6540
7208
  }
6541
7209
  async recoverStaleRuns() {
6542
7210
  if (!this.stateStore || !this.agentConfig)
@@ -6553,7 +7221,7 @@ class Reconciler {
6553
7221
  if (!daemonDead && !(heartbeatStale && ourZombie))
6554
7222
  continue;
6555
7223
  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`);
7224
+ log.warn(TAG30, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
6557
7225
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
6558
7226
  runId: run.runId,
6559
7227
  cardId: run.cardId,
@@ -6580,11 +7248,11 @@ class Reconciler {
6580
7248
  const stalledAt = Date.parse(card.updated_at ?? "");
6581
7249
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
6582
7250
  continue;
6583
- log.warn(TAG29, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
7251
+ log.warn(TAG30, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
6584
7252
  try {
6585
7253
  await this.client.moveCard(card.id, pickupCol.id);
6586
7254
  } catch (err) {
6587
- log.error(TAG29, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7255
+ log.error(TAG30, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6588
7256
  }
6589
7257
  }
6590
7258
  }
@@ -6607,11 +7275,11 @@ class Reconciler {
6607
7275
  const parkedAt = Date.parse(card.updated_at ?? "");
6608
7276
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
6609
7277
  continue;
6610
- log.warn(TAG29, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
7278
+ log.warn(TAG30, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
6611
7279
  try {
6612
7280
  await this.client.moveCard(card.id, pickupCol.id);
6613
7281
  } catch (err) {
6614
- log.error(TAG29, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7282
+ log.error(TAG30, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6615
7283
  }
6616
7284
  }
6617
7285
  }
@@ -6640,18 +7308,18 @@ class Reconciler {
6640
7308
  const subtasks = card.subtasks ?? [];
6641
7309
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
6642
7310
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
6643
- log.debug(TAG29, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
7311
+ log.debug(TAG30, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
6644
7312
  continue;
6645
7313
  }
6646
7314
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
6647
- log.debug(TAG29, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
7315
+ log.debug(TAG30, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
6648
7316
  continue;
6649
7317
  }
6650
7318
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6651
- log.debug(TAG29, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
7319
+ log.debug(TAG30, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
6652
7320
  continue;
6653
7321
  }
6654
- log.info(TAG29, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
7322
+ log.info(TAG30, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
6655
7323
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
6656
7324
  }
6657
7325
  }
@@ -6661,18 +7329,18 @@ class Reconciler {
6661
7329
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
6662
7330
  for (const knownId of knownCardIds) {
6663
7331
  if (!allAgentCardIds.has(knownId)) {
6664
- log.info(TAG29, `Missed unassign: ${knownId} — removing`);
7332
+ log.info(TAG30, `Missed unassign: ${knownId} — removing`);
6665
7333
  await this.pool.removeCard(knownId);
6666
7334
  }
6667
7335
  }
6668
7336
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
6669
- log.debug(TAG29, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
7337
+ log.debug(TAG30, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
6670
7338
  } catch (err) {
6671
- log.error(TAG29, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
7339
+ log.error(TAG30, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
6672
7340
  }
6673
7341
  }
6674
7342
  }
6675
- var TAG29 = "reconcile";
7343
+ var TAG30 = "reconcile";
6676
7344
  var init_reconcile = __esm(() => {
6677
7345
  init_board_helpers();
6678
7346
  init_log();
@@ -6710,7 +7378,7 @@ function prettyBanner(config, version) {
6710
7378
  checks.push({ kind: "ok", message });
6711
7379
  },
6712
7380
  warn(message) {
6713
- log.warn(TAG30, message);
7381
+ log.warn(TAG31, message);
6714
7382
  checks.push({ kind: "warn", message: message.split(`
6715
7383
  `, 1)[0] });
6716
7384
  },
@@ -6735,25 +7403,25 @@ function prettyBanner(config, version) {
6735
7403
  };
6736
7404
  }
6737
7405
  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(", ")}`);
7406
+ log.info(TAG31, `Harmony Agent Daemon v${version} starting...`);
7407
+ 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
7408
  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}`);
7409
+ log.info(TAG31, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
6742
7410
  }
6743
7411
  let failed = false;
6744
7412
  return {
6745
7413
  setProjectName(_name) {},
6746
7414
  setGitProvider(provider) {
6747
- log.info(TAG30, `Git provider: ${provider}`);
7415
+ log.info(TAG31, `Git provider: ${provider}`);
6748
7416
  },
6749
7417
  setHttpPort(port) {
6750
- log.info(TAG30, `HTTP server on port ${port}`);
7418
+ log.info(TAG31, `HTTP server on port ${port}`);
6751
7419
  },
6752
7420
  check(message) {
6753
- log.info(TAG30, message);
7421
+ log.info(TAG31, message);
6754
7422
  },
6755
7423
  warn(message) {
6756
- log.warn(TAG30, message);
7424
+ log.warn(TAG31, message);
6757
7425
  },
6758
7426
  fail() {
6759
7427
  failed = true;
@@ -6761,7 +7429,7 @@ function jsonBanner(config, version) {
6761
7429
  async ready(message) {
6762
7430
  if (failed)
6763
7431
  return;
6764
- log.info(TAG30, message);
7432
+ log.info(TAG31, message);
6765
7433
  }
6766
7434
  };
6767
7435
  }
@@ -6807,6 +7475,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
6807
7475
  label: "Model",
6808
7476
  value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
6809
7477
  });
7478
+ rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
6810
7479
  const tail = [];
6811
7480
  if (gitProvider)
6812
7481
  tail.push(gitProvider);
@@ -6824,6 +7493,9 @@ function titleRule(title) {
6824
7493
  const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
6825
7494
  return dim(`${prefix}${title}${surround}${suffix}`);
6826
7495
  }
7496
+ function runnerDesc(runner) {
7497
+ return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
7498
+ }
6827
7499
  function shortenId(id) {
6828
7500
  if (id.length <= 8)
6829
7501
  return id;
@@ -6838,7 +7510,7 @@ function cyan(s) {
6838
7510
  function yellow(s) {
6839
7511
  return `${ANSI.yellow}${s}${ANSI.reset}`;
6840
7512
  }
6841
- var TAG30 = "daemon", RULE_WIDTH = 70, ANSI;
7513
+ var TAG31 = "daemon", RULE_WIDTH = 70, ANSI;
6842
7514
  var init_startup_banner = __esm(() => {
6843
7515
  init_log();
6844
7516
  ANSI = {
@@ -6985,18 +7657,18 @@ class Watcher {
6985
7657
  }
6986
7658
  async start() {
6987
7659
  if (!isPretty()) {
6988
- log.info(TAG31, "Connecting to Supabase realtime (broadcast)...");
7660
+ log.info(TAG32, "Connecting to Supabase realtime (broadcast)...");
6989
7661
  }
6990
7662
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
6991
7663
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
6992
7664
  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)}`);
7665
+ log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
6994
7666
  this.onCardBroadcast({
6995
7667
  event: "card_update",
6996
7668
  payload: msg.payload ?? {}
6997
7669
  });
6998
7670
  }).on("broadcast", { event: "card_created" }, (msg) => {
6999
- log.debug(TAG31, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
7671
+ log.debug(TAG32, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
7000
7672
  this.onCardBroadcast({
7001
7673
  event: "card_created",
7002
7674
  payload: msg.payload ?? {}
@@ -7006,29 +7678,29 @@ class Watcher {
7006
7678
  const cardId = payload.card_id;
7007
7679
  const command = payload.command;
7008
7680
  if (cardId && command) {
7009
- log.info(TAG31, `Broadcast: agent_command ${command} for ${cardId}`);
7681
+ log.info(TAG32, `Broadcast: agent_command ${command} for ${cardId}`);
7010
7682
  this.onAgentCommand?.({ cardId, command });
7011
7683
  }
7012
7684
  }).subscribe((status) => {
7013
7685
  if (status === "SUBSCRIBED") {
7014
7686
  this.connected = true;
7015
7687
  if (!isPretty() || !this.suppressStartupLogs) {
7016
- log.info(TAG31, "Broadcast subscription active");
7688
+ log.info(TAG32, "Broadcast subscription active");
7017
7689
  }
7018
7690
  this.maybeResolveReady();
7019
7691
  } else if (status === "CHANNEL_ERROR") {
7020
7692
  this.connected = false;
7021
- log.error(TAG31, "Broadcast channel error — will rely on reconciliation");
7693
+ log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
7022
7694
  } else if (status === "TIMED_OUT") {
7023
7695
  this.connected = false;
7024
- log.warn(TAG31, "Broadcast subscription timed out — retrying...");
7696
+ log.warn(TAG32, "Broadcast subscription timed out — retrying...");
7025
7697
  } else if (status === "CLOSED") {
7026
7698
  this.connected = false;
7027
7699
  }
7028
7700
  });
7029
7701
  this.channel = channel;
7030
7702
  presenceChannel.on("presence", { event: "sync" }, () => {
7031
- log.debug(TAG31, "Presence sync");
7703
+ log.debug(TAG32, "Presence sync");
7032
7704
  }).subscribe(async (status) => {
7033
7705
  if (status === "SUBSCRIBED") {
7034
7706
  await presenceChannel.track({
@@ -7041,7 +7713,7 @@ class Watcher {
7041
7713
  agentName: this.identity.agentName
7042
7714
  });
7043
7715
  if (!isPretty() || !this.suppressStartupLogs) {
7044
- log.info(TAG31, "Presence tracked on board-presence channel");
7716
+ log.info(TAG32, "Presence tracked on board-presence channel");
7045
7717
  }
7046
7718
  this.presenceTracked = true;
7047
7719
  this.maybeResolveReady();
@@ -7063,10 +7735,10 @@ class Watcher {
7063
7735
  this.supabase = null;
7064
7736
  }
7065
7737
  this.connected = false;
7066
- log.info(TAG31, "Broadcast subscription stopped");
7738
+ log.info(TAG32, "Broadcast subscription stopped");
7067
7739
  }
7068
7740
  }
7069
- var TAG31 = "watcher";
7741
+ var TAG32 = "watcher";
7070
7742
  var init_watcher = __esm(() => {
7071
7743
  init_log();
7072
7744
  });
@@ -7153,10 +7825,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
7153
7825
  });
7154
7826
  } catch {}
7155
7827
  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(", ")}`);
7828
+ log.info(TAG33, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
7157
7829
  }
7158
7830
  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("; ")}`);
7831
+ log.warn(TAG33, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
7160
7832
  }
7161
7833
  return result;
7162
7834
  }
@@ -7186,7 +7858,7 @@ function pruneFailedRemoteBranches(opts) {
7186
7858
  } catch (err) {
7187
7859
  const detail = gitErrorDetail(err);
7188
7860
  if (isTransientGitNetworkError(detail)) {
7189
- log.debug(TAG32, `Remote branch GC skipped — remote unreachable: ${detail}`);
7861
+ log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
7190
7862
  return result;
7191
7863
  }
7192
7864
  result.errors.push({ ref: "fetch", error: detail });
@@ -7225,7 +7897,7 @@ function pruneFailedRemoteBranches(opts) {
7225
7897
  continue;
7226
7898
  }
7227
7899
  if (clock() > sweepDeadline) {
7228
- log.debug(TAG32, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7900
+ log.debug(TAG33, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7229
7901
  break;
7230
7902
  }
7231
7903
  try {
@@ -7238,17 +7910,17 @@ function pruneFailedRemoteBranches(opts) {
7238
7910
  } catch (err) {
7239
7911
  const detail = gitErrorDetail(err);
7240
7912
  if (isTransientGitNetworkError(detail)) {
7241
- log.debug(TAG32, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7913
+ log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7242
7914
  break;
7243
7915
  }
7244
7916
  result.errors.push({ ref, error: detail });
7245
7917
  }
7246
7918
  }
7247
7919
  if (result.removed.length > 0) {
7248
- log.info(TAG32, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7920
+ log.info(TAG33, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7249
7921
  }
7250
7922
  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("; ")}`);
7923
+ log.warn(TAG33, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
7252
7924
  }
7253
7925
  return result;
7254
7926
  }
@@ -7279,13 +7951,13 @@ class WorktreeGc {
7279
7951
  try {
7280
7952
  runWorktreeGc(this.basePath, this.store);
7281
7953
  } catch (err) {
7282
- log.warn(TAG32, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7954
+ log.warn(TAG33, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7283
7955
  }
7284
7956
  if (this.remoteOpts) {
7285
7957
  try {
7286
7958
  pruneFailedRemoteBranches(this.remoteOpts);
7287
7959
  } catch (err) {
7288
- log.warn(TAG32, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7960
+ log.warn(TAG33, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7289
7961
  }
7290
7962
  }
7291
7963
  }
@@ -7299,7 +7971,7 @@ function getRepoRoot2() {
7299
7971
  return null;
7300
7972
  }
7301
7973
  }
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;
7974
+ 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
7975
  var init_worktree_gc = __esm(() => {
7304
7976
  init_log();
7305
7977
  init_worktree();
@@ -7403,7 +8075,7 @@ async function main() {
7403
8075
  } catch (err) {
7404
8076
  if (err instanceof ConfigValidationError) {
7405
8077
  banner.fail();
7406
- log.error(TAG33, err.message);
8078
+ log.error(TAG34, err.message);
7407
8079
  process.exit(1);
7408
8080
  }
7409
8081
  throw err;
@@ -7471,6 +8143,7 @@ async function main() {
7471
8143
  daemonPid: process.pid,
7472
8144
  startedAt,
7473
8145
  uptimeMs: Date.now() - startedAt,
8146
+ runner: config.agent.runner,
7474
8147
  workers: pool.snapshotWorkers().map((w) => ({
7475
8148
  ...w,
7476
8149
  phase: null
@@ -7512,7 +8185,7 @@ async function main() {
7512
8185
  if (shuttingDown)
7513
8186
  return;
7514
8187
  shuttingDown = true;
7515
- log.info(TAG33, `Received ${signal}, shutting down gracefully...`);
8188
+ log.info(TAG34, `Received ${signal}, shutting down gracefully...`);
7516
8189
  reconciler.stop();
7517
8190
  mergeMonitor?.stop();
7518
8191
  worktreeGc.stop();
@@ -7522,18 +8195,18 @@ async function main() {
7522
8195
  }
7523
8196
  await watcher.stop();
7524
8197
  await pool.shutdown();
7525
- log.info(TAG33, "Daemon stopped.");
8198
+ log.info(TAG34, "Daemon stopped.");
7526
8199
  process.exit(exitCode);
7527
8200
  };
7528
8201
  process.on("SIGINT", () => shutdown("SIGINT"));
7529
8202
  process.on("SIGTERM", () => shutdown("SIGTERM"));
7530
8203
  process.on("uncaughtException", (err) => {
7531
- log.error(TAG33, `Uncaught exception: ${err.message}`);
8204
+ log.error(TAG34, `Uncaught exception: ${err.message}`);
7532
8205
  exitCode = 1;
7533
8206
  shutdown("uncaughtException");
7534
8207
  });
7535
8208
  process.on("unhandledRejection", (reason) => {
7536
- log.error(TAG33, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
8209
+ log.error(TAG34, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
7537
8210
  exitCode = 1;
7538
8211
  shutdown("unhandledRejection");
7539
8212
  });
@@ -7586,35 +8259,35 @@ async function handleBroadcast(event, client, pool, config, agentId) {
7586
8259
  if (assignedAgentId === undefined)
7587
8260
  return;
7588
8261
  if (assignedAgentId === agentId) {
7589
- log.info(TAG33, `Broadcast: card ${cardId} assigned to agent`);
8262
+ log.info(TAG34, `Broadcast: card ${cardId} assigned to agent`);
7590
8263
  try {
7591
8264
  await pool.resetAttemptsForReassign(cardId);
7592
8265
  await tryEnqueueCard(cardId, client, pool, config, agentId);
7593
8266
  } catch (err) {
7594
- log.error(TAG33, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
8267
+ log.error(TAG34, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
7595
8268
  }
7596
8269
  } else if (pool.isCardKnown(cardId)) {
7597
- log.info(TAG33, `Broadcast: card ${cardId} unassigned from agent`);
8270
+ log.info(TAG34, `Broadcast: card ${cardId} unassigned from agent`);
7598
8271
  await pool.removeCard(cardId);
7599
8272
  }
7600
8273
  }
7601
8274
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7602
8275
  const { card } = await client.getCard(cardId);
7603
8276
  if (card.assigned_agent_id !== agentId) {
7604
- log.debug(TAG33, `Card ${cardId} no longer assigned to agent — skipping`);
8277
+ log.debug(TAG34, `Card ${cardId} no longer assigned to agent — skipping`);
7605
8278
  return;
7606
8279
  }
7607
8280
  const board = await client.getBoard(config.projectId, { summary: true });
7608
8281
  const columns = board.columns;
7609
8282
  const column = columns.find((c) => c.id === card.column_id);
7610
8283
  if (!column) {
7611
- log.warn(TAG33, `Column not found for card ${cardId}`);
8284
+ log.warn(TAG34, `Column not found for card ${cardId}`);
7612
8285
  return;
7613
8286
  }
7614
8287
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7615
8288
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7616
8289
  if (!isPickupColumn && !isReviewColumn) {
7617
- log.info(TAG33, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
8290
+ log.info(TAG34, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
7618
8291
  return;
7619
8292
  }
7620
8293
  const mode = isReviewColumn ? "review" : "implement";
@@ -7622,16 +8295,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7622
8295
  const cardLabels = resolveCardLabels(card, labelMap);
7623
8296
  const subtasks = card.subtasks ?? [];
7624
8297
  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`);
8298
+ log.debug(TAG34, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
7626
8299
  return;
7627
8300
  }
7628
8301
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
7629
- log.info(TAG33, `Card #${card.short_id} has no branch reference — skipping auto-review`);
8302
+ log.info(TAG34, `Card #${card.short_id} has no branch reference — skipping auto-review`);
7630
8303
  return;
7631
8304
  }
7632
8305
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
7633
8306
  }
7634
- var TAG33 = "daemon", PKG_VERSION;
8307
+ var TAG34 = "daemon", PKG_VERSION;
7635
8308
  var init_src = __esm(() => {
7636
8309
  init_board_helpers();
7637
8310
  init_config();