@nilsr0711/drydock 0.1.8 → 0.1.9

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 (119) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +4 -4
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/adrs/page.js +2 -2
  18. package/.next/standalone/.next/server/app/adrs/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/adrs/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/analytics/page.js +1 -1
  21. package/.next/standalone/.next/server/app/analytics/page.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/api/control/shutdown/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/control/shutdown/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/cost/export/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/health/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/health/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/sse/dashboard/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/sse/jobs/[id]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/webhooks/[repoId]/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/webhooks/[repoId]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/costs/page.js +1 -1
  33. package/.next/standalone/.next/server/app/costs/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/costs/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/jobs/[id]/page.js +3 -3
  36. package/.next/standalone/.next/server/app/jobs/[id]/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/jobs/[id]/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/jobs/page.js +2 -2
  39. package/.next/standalone/.next/server/app/jobs/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/jobs/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/needs-human/page.js +2 -2
  42. package/.next/standalone/.next/server/app/needs-human/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/needs-human/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/page.js +2 -2
  45. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/prompts/page.js +2 -2
  48. package/.next/standalone/.next/server/app/prompts/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/prompts/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/repos/[id]/page.js +2 -2
  51. package/.next/standalone/.next/server/app/repos/[id]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/repos/[id]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  54. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app-paths-manifest.json +4 -4
  57. package/.next/standalone/.next/server/chunks/152.js +3 -0
  58. package/.next/standalone/.next/server/chunks/304.js +1 -1
  59. package/.next/standalone/.next/server/chunks/387.js +14 -14
  60. package/.next/standalone/.next/server/chunks/40.js +1 -1
  61. package/.next/standalone/.next/server/chunks/475.js +19 -0
  62. package/.next/standalone/.next/server/chunks/50.js +1 -0
  63. package/.next/standalone/.next/server/chunks/521.js +1 -1
  64. package/.next/standalone/.next/server/chunks/614.js +2 -2
  65. package/.next/standalone/.next/server/chunks/668.js +1 -0
  66. package/.next/standalone/.next/server/chunks/786.js +1 -1
  67. package/.next/standalone/.next/server/chunks/908.js +1 -1
  68. package/.next/standalone/.next/server/chunks/{685.js → 944.js} +1 -1
  69. package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  70. package/.next/standalone/.next/server/pages/500.html +1 -1
  71. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  73. package/.next/standalone/.next/static/chunks/6634-74f0b66587b57037.js +1 -0
  74. package/.next/standalone/.next/static/chunks/8382-5201fc3dd1f64e60.js +1 -0
  75. package/.next/standalone/.next/static/chunks/app/adrs/page-ca11e90e9404af90.js +1 -0
  76. package/.next/standalone/.next/static/chunks/app/jobs/[id]/page-dfb41b4785fd7cbb.js +1 -0
  77. package/.next/{static/chunks/app/layout-402d4ab34259b89b.js → standalone/.next/static/chunks/app/layout-134301ff0a9b828d.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/app/needs-human/page-569fc0e6aefc7677.js +1 -0
  79. package/.next/standalone/.next/static/chunks/app/page-504585be128c823c.js +1 -0
  80. package/.next/standalone/.next/static/chunks/app/prompts/page-c6bf6a4c782035ac.js +1 -0
  81. package/.next/standalone/.next/static/chunks/app/repos/[id]/page-c0c4531c2af46526.js +1 -0
  82. package/.next/standalone/.next/static/chunks/app/settings/page-84a44ab52a0ff49d.js +1 -0
  83. package/.next/standalone/drizzle/0036_adopt_claude_mem.sql +1 -0
  84. package/.next/standalone/mcp-server.cjs +304 -81
  85. package/.next/standalone/package.json +1 -1
  86. package/.next/static/chunks/6634-74f0b66587b57037.js +1 -0
  87. package/.next/static/chunks/8382-5201fc3dd1f64e60.js +1 -0
  88. package/.next/static/chunks/app/adrs/page-ca11e90e9404af90.js +1 -0
  89. package/.next/static/chunks/app/jobs/[id]/page-dfb41b4785fd7cbb.js +1 -0
  90. package/.next/{standalone/.next/static/chunks/app/layout-402d4ab34259b89b.js → static/chunks/app/layout-134301ff0a9b828d.js} +1 -1
  91. package/.next/static/chunks/app/needs-human/page-569fc0e6aefc7677.js +1 -0
  92. package/.next/static/chunks/app/page-504585be128c823c.js +1 -0
  93. package/.next/static/chunks/app/prompts/page-c6bf6a4c782035ac.js +1 -0
  94. package/.next/static/chunks/app/repos/[id]/page-c0c4531c2af46526.js +1 -0
  95. package/.next/static/chunks/app/settings/page-84a44ab52a0ff49d.js +1 -0
  96. package/README.md +11 -5
  97. package/drizzle/0036_adopt_claude_mem.sql +1 -0
  98. package/package.json +1 -1
  99. package/.next/standalone/.next/server/chunks/99.js +0 -21
  100. package/.next/standalone/.next/static/chunks/1298-0d60333e56ac8b2c.js +0 -1
  101. package/.next/standalone/.next/static/chunks/app/adrs/page-0c221fc18d1e89af.js +0 -1
  102. package/.next/standalone/.next/static/chunks/app/jobs/[id]/page-6ef8bc39b5181817.js +0 -1
  103. package/.next/standalone/.next/static/chunks/app/needs-human/page-b69ac22856c0a0b1.js +0 -1
  104. package/.next/standalone/.next/static/chunks/app/page-ccefa99d40734a8c.js +0 -1
  105. package/.next/standalone/.next/static/chunks/app/prompts/page-bcd1063eb5d64aa5.js +0 -1
  106. package/.next/standalone/.next/static/chunks/app/repos/[id]/page-2bd4b35a0b28042e.js +0 -1
  107. package/.next/standalone/.next/static/chunks/app/settings/page-13a04ecac139b74c.js +0 -1
  108. package/.next/static/chunks/1298-0d60333e56ac8b2c.js +0 -1
  109. package/.next/static/chunks/app/adrs/page-0c221fc18d1e89af.js +0 -1
  110. package/.next/static/chunks/app/jobs/[id]/page-6ef8bc39b5181817.js +0 -1
  111. package/.next/static/chunks/app/needs-human/page-b69ac22856c0a0b1.js +0 -1
  112. package/.next/static/chunks/app/page-ccefa99d40734a8c.js +0 -1
  113. package/.next/static/chunks/app/prompts/page-bcd1063eb5d64aa5.js +0 -1
  114. package/.next/static/chunks/app/repos/[id]/page-2bd4b35a0b28042e.js +0 -1
  115. package/.next/static/chunks/app/settings/page-13a04ecac139b74c.js +0 -1
  116. /package/.next/standalone/.next/static/{qSEvPCWIslg9lcxt6XQhl → gBXyOslZ8CoJrAVnOEHDm}/_buildManifest.js +0 -0
  117. /package/.next/standalone/.next/static/{qSEvPCWIslg9lcxt6XQhl → gBXyOslZ8CoJrAVnOEHDm}/_ssgManifest.js +0 -0
  118. /package/.next/static/{qSEvPCWIslg9lcxt6XQhl → gBXyOslZ8CoJrAVnOEHDm}/_buildManifest.js +0 -0
  119. /package/.next/static/{qSEvPCWIslg9lcxt6XQhl → gBXyOslZ8CoJrAVnOEHDm}/_ssgManifest.js +0 -0
@@ -8092,7 +8092,7 @@ var init_sql = __esm({
8092
8092
  return new SQL([new StringChunk(str)]);
8093
8093
  }
8094
8094
  sql2.raw = raw;
8095
- function join17(chunks, separator) {
8095
+ function join18(chunks, separator) {
8096
8096
  const result = [];
8097
8097
  for (const [i, chunk] of chunks.entries()) {
8098
8098
  if (i > 0 && separator !== void 0) {
@@ -8102,7 +8102,7 @@ var init_sql = __esm({
8102
8102
  }
8103
8103
  return new SQL(result);
8104
8104
  }
8105
- sql2.join = join17;
8105
+ sql2.join = join18;
8106
8106
  function identifier(value) {
8107
8107
  return new Name(value);
8108
8108
  }
@@ -11027,7 +11027,7 @@ var init_select2 = __esm({
11027
11027
  const baseTableName = this.tableName;
11028
11028
  const tableName = getTableLikeName(table);
11029
11029
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
11030
- if (typeof tableName === "string" && this.config.joins?.some((join17) => join17.alias === tableName)) {
11030
+ if (typeof tableName === "string" && this.config.joins?.some((join18) => join18.alias === tableName)) {
11031
11031
  throw new Error(`Alias "${tableName}" is already used in this query`);
11032
11032
  }
11033
11033
  if (!this.isPartialSelect) {
@@ -11917,7 +11917,7 @@ var init_update = __esm({
11917
11917
  createJoin(joinType) {
11918
11918
  return (table, on) => {
11919
11919
  const tableName = getTableLikeName(table);
11920
- if (typeof tableName === "string" && this.config.joins.some((join17) => join17.alias === tableName)) {
11920
+ if (typeof tableName === "string" && this.config.joins.some((join18) => join18.alias === tableName)) {
11921
11921
  throw new Error(`Alias "${tableName}" is already used in this query`);
11922
11922
  }
11923
11923
  if (typeof on === "function") {
@@ -13463,6 +13463,12 @@ var init_schema = __esm({
13463
13463
  sandboxAllowNetwork: integer("sandbox_allow_network", { mode: "boolean" }).notNull().default(false),
13464
13464
  sandboxCpus: text("sandbox_cpus"),
13465
13465
  sandboxMemory: text("sandbox_memory"),
13466
+ // Opt-in claude-mem worktree adoption (issue #274, default off). When on, a
13467
+ // settling job triggers claude-mem's `adopt` for its worktree right before
13468
+ // Drydock removes it, consolidating the per-worktree memory into the parent
13469
+ // project while the worktree still exists. Best-effort and depends on the
13470
+ // external claude-mem plugin being installed, so it is off by default.
13471
+ adoptClaudeMem: integer("adopt_claude_mem", { mode: "boolean" }).notNull().default(false),
13466
13472
  createdAt: integer("created_at").notNull().default(sql`(unixepoch())`)
13467
13473
  });
13468
13474
  promptTemplates = sqliteTable("prompt_templates", {
@@ -13498,6 +13504,11 @@ var init_schema = __esm({
13498
13504
  // template (no saved repo version). Lets analytics slice outcomes by the
13499
13505
  // exact prompt revision a job ran with, alongside model and agent.
13500
13506
  implementPromptVersion: integer("implement_prompt_version"),
13507
+ // Per-job turn budget; 0 means unlimited (issue #254). The effective default
13508
+ // for new jobs comes from the global maxTurns setting (createJob always seeds
13509
+ // an explicit value), so this column default is dead for real inserts — it
13510
+ // stays at the frozen migration value (40) to avoid a no-op SQLite table
13511
+ // rebuild just to realign a fallback that is never hit.
13501
13512
  maxTurns: integer("max_turns").notNull().default(40),
13502
13513
  totalInputTokens: integer("total_input_tokens").notNull().default(0),
13503
13514
  totalOutputTokens: integer("total_output_tokens").notNull().default(0),
@@ -13987,9 +13998,11 @@ var init_service = __esm({
13987
13998
  // Per-job turn budget (issue #254). 0 is off (unlimited): the runner drops the
13988
13999
  // CLI `--max-turns` flag and the OpenRouter loop skips its turn check, so a
13989
14000
  // long task is bounded only by maxJobMinutes / the per-job cost cap. Defaults
13990
- // high (200) so normal tasks finish the live test job hit the old 40-turn
13991
- // wall on an ordinary issue. The value seeds each new job's budget.
13992
- maxTurns: import_zod3.z.number().int().nonnegative().default(200),
14001
+ // to 0 (unlimited) so a fresh install is fully autonomous out of the box
14002
+ // ordinary issues routinely exceed any fixed turn wall and a max-turns abort
14003
+ // would otherwise escalate to needs_human. The value seeds each new job's
14004
+ // budget; set a positive ceiling here or per-call to cap turns.
14005
+ maxTurns: import_zod3.z.number().int().nonnegative().default(0),
13993
14006
  // Hard wall-clock timeout per agent session in minutes (issue #47). A hung
13994
14007
  // agent (network stall, MCP deadlock, stdin prompt) is aborted after this so
13995
14008
  // it never holds a job slot forever. Defaults to 120 so long autonomous tasks
@@ -14015,6 +14028,15 @@ var init_service = __esm({
14015
14028
  // Auto-wait on Codex usage limits (issue #167, ADR 030): the same park-and-
14016
14029
  // resume treatment for OpenAI/ChatGPT-plan limits hit by the Codex CLI.
14017
14030
  codexLimitAutoWait: import_zod3.z.boolean().default(true),
14031
+ // Auto-resume a job that exhausts its positive turn budget (issue #277). The
14032
+ // CLI aborts with an `error_max_turns` result and no provider-limit signal;
14033
+ // when on, Drydock resumes the stored session to continue the work (a bounded
14034
+ // number of times) instead of parking it in needs_human as "exited non-zero".
14035
+ // On by default per the autonomous model — a turn wall is recoverable, not an
14036
+ // operator decision. Off restores the plain escalation (with the clear
14037
+ // turn-budget reason either way). Only fires when a positive turn budget is
14038
+ // set; the default unlimited budget (0) never hits the wall.
14039
+ maxTurnsAutoResume: import_zod3.z.boolean().default(true),
14018
14040
  // Global kill-switch for opt-in release management (issue #59, ADR 028). Off by
14019
14041
  // default; both this and a repo's own `releaseEnabled` must be on for the
14020
14042
  // release pipeline to run for that repo. Cutting a public release is hard to
@@ -26397,6 +26419,7 @@ function toParsed(event) {
26397
26419
  } else {
26398
26420
  base.sessionId = event.session_id;
26399
26421
  base.resultText = event.result;
26422
+ base.resultSubtype = event.subtype;
26400
26423
  base.costUsd = event.total_cost_usd;
26401
26424
  base.inputTokens = event.usage?.input_tokens ?? 0;
26402
26425
  base.outputTokens = event.usage?.output_tokens ?? 0;
@@ -26499,6 +26522,8 @@ var init_parser = __esm({
26499
26522
  costUsd = 0;
26500
26523
  /** Final result text from the stream's result event, when it carried one. */
26501
26524
  resultText;
26525
+ /** Result event subtype from the stream, e.g. `error_max_turns` (issue #277). */
26526
+ resultSubtype;
26502
26527
  /** Whether the stream's result event was flagged as an error (issue #166). */
26503
26528
  resultIsError = false;
26504
26529
  /** Latest subscription rate-limit snapshot seen in the stream (issue #188). */
@@ -26547,6 +26572,7 @@ var init_parser = __esm({
26547
26572
  this.totalCacheReadInputTokens = parsed.cacheReadInputTokens;
26548
26573
  if (parsed.costUsd !== void 0) this.costUsd = parsed.costUsd;
26549
26574
  if (parsed.resultText !== void 0) this.resultText = parsed.resultText;
26575
+ if (parsed.resultSubtype !== void 0) this.resultSubtype = parsed.resultSubtype;
26550
26576
  this.resultIsError = parsed.isError;
26551
26577
  } else {
26552
26578
  this.totalInputTokens += parsed.inputTokens;
@@ -28993,6 +29019,10 @@ function requireRepo(repoId, db) {
28993
29019
  function listIssues(repoId, db = getDb()) {
28994
29020
  return db.select().from(issues).where(eq(issues.repoId, repoId)).orderBy(asc(issues.priority), asc(issues.number)).all();
28995
29021
  }
29022
+ function getIssueTitle(repoId, number2, db = getDb()) {
29023
+ const row = db.select({ title: issues.title }).from(issues).where(and(eq(issues.repoId, repoId), eq(issues.number, number2))).get();
29024
+ return row?.title ?? null;
29025
+ }
28996
29026
  function syncIssuesFromGh(repoId, fetched, db = getDb()) {
28997
29027
  const existing = db.select().from(issues).where(eq(issues.repoId, repoId)).all();
28998
29028
  const existingByNumber = new Map(existing.map((i) => [i.number, i]));
@@ -30137,6 +30167,13 @@ function worktreeHome() {
30137
30167
  function sanitize(name) {
30138
30168
  return name.replace(/[^a-zA-Z0-9._-]/g, "-");
30139
30169
  }
30170
+ function slugifyTitle(title) {
30171
+ return title.normalize("NFKD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_SLUG_LENGTH).replace(/-+$/g, "");
30172
+ }
30173
+ function issueBranchLabel(issueNumber, title) {
30174
+ const slug = title ? slugifyTitle(title) : "";
30175
+ return slug ? `issue-${issueNumber}-${slug}` : `issue-${issueNumber}`;
30176
+ }
30140
30177
  function repoWorktreesDir(repoName) {
30141
30178
  return (0, import_node_path.join)(worktreeHome(), "worktrees", sanitize(repoName));
30142
30179
  }
@@ -30147,7 +30184,7 @@ function stripAiAttribution(message) {
30147
30184
  function nonEmptyCommitMessage(cleaned) {
30148
30185
  return cleaned.trim() === "" ? STRIPPED_MESSAGE_FALLBACK : cleaned;
30149
30186
  }
30150
- var import_node_fs, import_node_os2, import_node_path, AI_ASSISTANT_NAMES, AI_ATTRIBUTION_LINE, STRIPPED_MESSAGE_FALLBACK, WorktreeError, EmptyCommitError, repoLocks, WorktreeManager;
30187
+ var import_node_fs, import_node_os2, import_node_path, MAX_SLUG_LENGTH, AI_ASSISTANT_NAMES, AI_ATTRIBUTION_LINE, STRIPPED_MESSAGE_FALLBACK, WorktreeError, EmptyCommitError, repoLocks, WorktreeManager;
30151
30188
  var init_worktree = __esm({
30152
30189
  "src/lib/git/worktree.ts"() {
30153
30190
  "use strict";
@@ -30155,6 +30192,7 @@ var init_worktree = __esm({
30155
30192
  import_node_os2 = require("node:os");
30156
30193
  import_node_path = require("node:path");
30157
30194
  init_runner();
30195
+ MAX_SLUG_LENGTH = 50;
30158
30196
  AI_ASSISTANT_NAMES = "claude|anthropic|codex|openai|chatgpt|copilot|gemini|cursor|devin";
30159
30197
  AI_ATTRIBUTION_LINE = new RegExp(
30160
30198
  `^(?:co-authored-by:.*\\b(?:${AI_ASSISTANT_NAMES})\\b|\u{1F916}?\\s*generated (?:with|by)\\b.*\\b(?:${AI_ASSISTANT_NAMES})\\b)`,
@@ -30403,6 +30441,10 @@ var init_defaults = __esm({
30403
30441
  ciFix: "ci-fix",
30404
30442
  plan: "plan",
30405
30443
  limitResume: "limit-resume",
30444
+ // Continuation prompt for a session resumed after exhausting its turn budget
30445
+ // (issue #277): unlike the limit-resume path, the worktree is kept intact, so
30446
+ // the prior session's uncommitted work is still present.
30447
+ turnResume: "turn-resume",
30406
30448
  // Continuation prompt for a needs_human job an operator unblocked with typed
30407
30449
  // guidance (issue #257); the human's instruction is injected via $INSTRUCTION.
30408
30450
  humanResume: "human-resume",
@@ -30536,6 +30578,31 @@ var init_defaults = __esm({
30536
30578
  "",
30537
30579
  "Drydock appends `Closes #$ISSUE_NUM` and removes the file \u2014 do not commit it."
30538
30580
  ].join("\n"),
30581
+ // Continuation prompt for a session resumed after hitting its turn budget
30582
+ // (issue #277): the conversation context survives via --resume AND the worktree
30583
+ // is kept, so any uncommitted edits are still in place. The agent simply
30584
+ // continues where it left off rather than re-applying lost work.
30585
+ "turn-resume": [
30586
+ `Your previous session on issue #$ISSUE_NUM in "$REPO_NAME" was paused because it reached its`,
30587
+ `turn budget. You are resuming on branch "$BRANCH" with your conversation context and any`,
30588
+ "uncommitted edits from the interrupted session still in place \u2014 just continue from where you",
30589
+ "left off and finish implementing the issue. Follow the repo's conventions (`CLAUDE.md`/",
30590
+ "`AGENTS.md`, neighbouring code) and keep working test-first: a failing test that captures the",
30591
+ "requirement, then the code to make it green. Before you finish, verify: run the repo's tests,",
30592
+ "typecheck, lint, and build, and do not finish on a red signal. Never weaken or delete a test",
30593
+ "to make the suite pass. Keep the change focused. Split your work into focused, thematic",
30594
+ "commits, each with a clear Conventional Commit subject (`type(scope): summary`) grouped by",
30595
+ "concern \u2014 not one mega-commit. Never add AI attribution to a commit: no `Co-Authored-By`",
30596
+ "trailer naming an assistant, no `Generated with Claude Code` line, and no mention of the tool",
30597
+ "or model in the message. Do not push or open a pull request yourself; Drydock pushes and opens",
30598
+ "the PR, committing anything you leave uncommitted.",
30599
+ "Before finishing, write `.drydock/PR.md`: first line a Conventional Commit subject (used as",
30600
+ "the commit message and PR title), then a blank line, then a body in this format:",
30601
+ "",
30602
+ "$PR_FORMAT",
30603
+ "",
30604
+ "Drydock appends `Closes #$ISSUE_NUM` and removes the file \u2014 do not commit it."
30605
+ ].join("\n"),
30539
30606
  // Continuation prompt for a session resumed with human guidance (issue #257):
30540
30607
  // a needs_human job an operator unblocked by typing how to proceed. The
30541
30608
  // conversation context survives via --resume and the prior commits are
@@ -30999,7 +31066,10 @@ async function runOpenRouterJobSession(job, prompt, cwd, deps = {}) {
30999
31066
  inputTokens: 0,
31000
31067
  outputTokens: 0,
31001
31068
  timedOut: false,
31002
- costExceeded: false
31069
+ costExceeded: false,
31070
+ // The HTTP tool-loop enforces its own turn budget and never emits the CLI's
31071
+ // `error_max_turns` result, so it never sets this max-turns signal (issue #277).
31072
+ maxTurnsReached: false
31003
31073
  };
31004
31074
  const latch = agentLimitBlocked("openrouter", db);
31005
31075
  if (latch) {
@@ -31192,7 +31262,16 @@ async function runOpenRouterJobSession(job, prompt, cwd, deps = {}) {
31192
31262
  } else {
31193
31263
  db.update(jobs).set({ totalInputTokens: inputTokens, totalOutputTokens: outputTokens, costUsd }).where(eq(jobs.id, job.id)).run();
31194
31264
  }
31195
- return { exitCode, costUsd, inputTokens, outputTokens, timedOut, costExceeded, limit };
31265
+ return {
31266
+ exitCode,
31267
+ costUsd,
31268
+ inputTokens,
31269
+ outputTokens,
31270
+ timedOut,
31271
+ costExceeded,
31272
+ maxTurnsReached: false,
31273
+ limit
31274
+ };
31196
31275
  }
31197
31276
  var TIMED_OUT_EXIT, COST_EXCEEDED_EXIT, LIMIT_BLOCKED_EXIT, SYSTEM_PROMPT;
31198
31277
  var init_session3 = __esm({
@@ -31227,6 +31306,10 @@ var init_session3 = __esm({
31227
31306
  });
31228
31307
 
31229
31308
  // src/lib/orchestrator/agent-session.ts
31309
+ function reachedMaxTurns(parser, outcome) {
31310
+ if (outcome.timedOut || outcome.costExceeded) return false;
31311
+ return parser.resultSubtype === MAX_TURNS_RESULT_SUBTYPE;
31312
+ }
31230
31313
  function limitGateResult(provider, job, broker, db) {
31231
31314
  const latch = agentLimitBlocked(provider.id, db);
31232
31315
  if (!latch) return void 0;
@@ -31243,6 +31326,7 @@ function limitGateResult(provider, job, broker, db) {
31243
31326
  outputTokens: 0,
31244
31327
  timedOut: false,
31245
31328
  costExceeded: false,
31329
+ maxTurnsReached: false,
31246
31330
  limit: {
31247
31331
  agent: provider.id,
31248
31332
  kind: latch.kind,
@@ -31466,6 +31550,7 @@ async function spawnAgentSession(job, prompt, cwd, deps = {}) {
31466
31550
  outputTokens: parser.totalOutputTokens,
31467
31551
  timedOut,
31468
31552
  costExceeded,
31553
+ maxTurnsReached: reachedMaxTurns(parser, { timedOut, costExceeded }),
31469
31554
  spawnError,
31470
31555
  limit: classifySessionFailure(provider, parser, stderrTail, {
31471
31556
  exitCode,
@@ -31595,6 +31680,7 @@ async function resumeAgentSession(job, sessionId, failedLog, cwd, deps = {}) {
31595
31680
  outputTokens: parser.totalOutputTokens,
31596
31681
  timedOut,
31597
31682
  costExceeded,
31683
+ maxTurnsReached: reachedMaxTurns(parser, { timedOut, costExceeded }),
31598
31684
  spawnError,
31599
31685
  limit: classifySessionFailure(provider, parser, stderrTail, {
31600
31686
  exitCode,
@@ -31603,7 +31689,7 @@ async function resumeAgentSession(job, sessionId, failedLog, cwd, deps = {}) {
31603
31689
  })
31604
31690
  };
31605
31691
  }
31606
- var TIMED_OUT_EXIT2, COST_EXCEEDED_EXIT2, LIMIT_BLOCKED_EXIT2, STDERR_TAIL_MAX, ZERO_USAGE;
31692
+ var MAX_TURNS_RESULT_SUBTYPE, TIMED_OUT_EXIT2, COST_EXCEEDED_EXIT2, LIMIT_BLOCKED_EXIT2, STDERR_TAIL_MAX, ZERO_USAGE;
31607
31693
  var init_agent_session = __esm({
31608
31694
  "src/lib/orchestrator/agent-session.ts"() {
31609
31695
  "use strict";
@@ -31620,6 +31706,7 @@ var init_agent_session = __esm({
31620
31706
  init_provider_limit();
31621
31707
  init_provider_usage();
31622
31708
  init_singleton();
31709
+ MAX_TURNS_RESULT_SUBTYPE = "error_max_turns";
31623
31710
  TIMED_OUT_EXIT2 = -1;
31624
31711
  COST_EXCEEDED_EXIT2 = -2;
31625
31712
  LIMIT_BLOCKED_EXIT2 = -3;
@@ -34212,6 +34299,89 @@ var init_ci_babysitter = __esm({
34212
34299
  }
34213
34300
  });
34214
34301
 
34302
+ // src/lib/orchestrator/claude-mem-adopt.ts
34303
+ function defaultConfigDir() {
34304
+ return process.env.CLAUDE_CONFIG_DIR ?? (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".claude");
34305
+ }
34306
+ function compareVersionDesc(a, b) {
34307
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
34308
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
34309
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
34310
+ const diff = (pb[i] ?? 0) - (pa[i] ?? 0);
34311
+ if (diff !== 0) return diff;
34312
+ }
34313
+ return b.localeCompare(a);
34314
+ }
34315
+ function normalizeRoot(root) {
34316
+ return (0, import_node_fs4.existsSync)((0, import_node_path8.join)(root, "plugin", "scripts")) ? (0, import_node_path8.join)(root, "plugin") : root;
34317
+ }
34318
+ function hasWorkerScripts(pluginDir) {
34319
+ return (0, import_node_fs4.existsSync)((0, import_node_path8.join)(pluginDir, "scripts", "bun-runner.js")) && (0, import_node_fs4.existsSync)((0, import_node_path8.join)(pluginDir, "scripts", "worker-service.cjs"));
34320
+ }
34321
+ function resolveClaudeMemPlugin(configDir = defaultConfigDir()) {
34322
+ const candidates = [];
34323
+ const envRoot = process.env.CLAUDE_PLUGIN_ROOT ?? process.env.PLUGIN_ROOT;
34324
+ if (envRoot) candidates.push(envRoot);
34325
+ const cacheDir = (0, import_node_path8.join)(configDir, "plugins", "cache", "thedotmack", "claude-mem");
34326
+ if ((0, import_node_fs4.existsSync)(cacheDir)) {
34327
+ let versions = [];
34328
+ try {
34329
+ versions = (0, import_node_fs4.readdirSync)(cacheDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d/.test(e.name)).map((e) => e.name).sort(compareVersionDesc);
34330
+ } catch {
34331
+ versions = [];
34332
+ }
34333
+ for (const version2 of versions) candidates.push((0, import_node_path8.join)(cacheDir, version2));
34334
+ }
34335
+ candidates.push((0, import_node_path8.join)(configDir, "plugins", "marketplaces", "thedotmack", "plugin"));
34336
+ for (const root of candidates) {
34337
+ const pluginDir = normalizeRoot(root);
34338
+ if (hasWorkerScripts(pluginDir)) return pluginDir;
34339
+ }
34340
+ return null;
34341
+ }
34342
+ async function adoptWorktreeMemory(input, opts = {}) {
34343
+ const resolvePlugin = opts.resolvePlugin ?? resolveClaudeMemPlugin;
34344
+ const run = opts.run ?? spawnRunner;
34345
+ try {
34346
+ const pluginDir = resolvePlugin(opts.configDir);
34347
+ if (!pluginDir) {
34348
+ logError(`[claude-mem] adoption skipped for ${input.branch}: plugin not installed`);
34349
+ return;
34350
+ }
34351
+ const args = [
34352
+ (0, import_node_path8.join)(pluginDir, "scripts", "bun-runner.js"),
34353
+ (0, import_node_path8.join)(pluginDir, "scripts", "worker-service.cjs"),
34354
+ "adopt",
34355
+ "--branch",
34356
+ input.branch,
34357
+ "--cwd",
34358
+ input.cwd
34359
+ ];
34360
+ const result = await run("node", args, input.cwd, {
34361
+ timeoutMs: opts.timeoutMs ?? ADOPT_TIMEOUT_MS
34362
+ });
34363
+ if (result.exitCode !== 0) {
34364
+ logError(
34365
+ `[claude-mem] adoption for ${input.branch} exited ${result.exitCode}: ${result.stderr.trim()}`
34366
+ );
34367
+ }
34368
+ } catch (err) {
34369
+ logError(`[claude-mem] adoption failed for ${input.branch}`, err);
34370
+ }
34371
+ }
34372
+ var import_node_fs4, import_node_os5, import_node_path8, ADOPT_TIMEOUT_MS;
34373
+ var init_claude_mem_adopt = __esm({
34374
+ "src/lib/orchestrator/claude-mem-adopt.ts"() {
34375
+ "use strict";
34376
+ import_node_fs4 = require("node:fs");
34377
+ import_node_os5 = require("node:os");
34378
+ import_node_path8 = require("node:path");
34379
+ init_runner();
34380
+ init_logger2();
34381
+ ADOPT_TIMEOUT_MS = 6e4;
34382
+ }
34383
+ });
34384
+
34215
34385
  // src/lib/orchestrator/followups-metadata.ts
34216
34386
  function parseFollowups(raw) {
34217
34387
  const entries = [];
@@ -34245,7 +34415,7 @@ function parseFollowups(raw) {
34245
34415
  function readFollowups(worktreePath) {
34246
34416
  let raw;
34247
34417
  try {
34248
- raw = (0, import_node_fs4.readFileSync)((0, import_node_path8.join)(worktreePath, FOLLOWUPS_METADATA_PATH), "utf8");
34418
+ raw = (0, import_node_fs5.readFileSync)((0, import_node_path9.join)(worktreePath, FOLLOWUPS_METADATA_PATH), "utf8");
34249
34419
  } catch {
34250
34420
  return [];
34251
34421
  }
@@ -34253,16 +34423,16 @@ function readFollowups(worktreePath) {
34253
34423
  }
34254
34424
  function consumeFollowups(worktreePath) {
34255
34425
  const entries = readFollowups(worktreePath);
34256
- (0, import_node_fs4.rmSync)((0, import_node_path8.join)(worktreePath, FOLLOWUPS_METADATA_PATH), { force: true });
34426
+ (0, import_node_fs5.rmSync)((0, import_node_path9.join)(worktreePath, FOLLOWUPS_METADATA_PATH), { force: true });
34257
34427
  return entries;
34258
34428
  }
34259
- var import_node_fs4, import_node_path8, FOLLOWUPS_METADATA_PATH, TITLE_MAX_CHARS, BODY_MAX_CHARS, MAX_ENTRIES, HEADING;
34429
+ var import_node_fs5, import_node_path9, FOLLOWUPS_METADATA_PATH, TITLE_MAX_CHARS, BODY_MAX_CHARS, MAX_ENTRIES, HEADING;
34260
34430
  var init_followups_metadata = __esm({
34261
34431
  "src/lib/orchestrator/followups-metadata.ts"() {
34262
34432
  "use strict";
34263
- import_node_fs4 = require("node:fs");
34264
- import_node_path8 = require("node:path");
34265
- FOLLOWUPS_METADATA_PATH = (0, import_node_path8.join)(".drydock", "FOLLOWUPS.md");
34433
+ import_node_fs5 = require("node:fs");
34434
+ import_node_path9 = require("node:path");
34435
+ FOLLOWUPS_METADATA_PATH = (0, import_node_path9.join)(".drydock", "FOLLOWUPS.md");
34266
34436
  TITLE_MAX_CHARS = 300;
34267
34437
  BODY_MAX_CHARS = 6e4;
34268
34438
  MAX_ENTRIES = 20;
@@ -34657,7 +34827,7 @@ async function runPrAuditPass(deps) {
34657
34827
  };
34658
34828
  let generate = deps.generate;
34659
34829
  if (!generate) {
34660
- tmp = await (0, import_promises3.mkdtemp)((0, import_node_path9.join)((0, import_node_os5.tmpdir)(), "drydock-pr-audit-"));
34830
+ tmp = await (0, import_promises3.mkdtemp)((0, import_node_path10.join)((0, import_node_os6.tmpdir)(), "drydock-pr-audit-"));
34661
34831
  const provider = getAgentProvider(config.agent);
34662
34832
  generate = buildPrAuditGenerator({
34663
34833
  provider,
@@ -34748,13 +34918,13 @@ function startPrAudit(jobId, db = getDb(), opts = {}) {
34748
34918
  });
34749
34919
  return { job, prNumber: job.prNumber, done };
34750
34920
  }
34751
- var import_promises3, import_node_os5, import_node_path9, PR_AUDIT_TIMEOUT_MS;
34921
+ var import_promises3, import_node_os6, import_node_path10, PR_AUDIT_TIMEOUT_MS;
34752
34922
  var init_pr_audit_driver = __esm({
34753
34923
  "src/lib/orchestrator/pr-audit-driver.ts"() {
34754
34924
  "use strict";
34755
34925
  import_promises3 = require("node:fs/promises");
34756
- import_node_os5 = require("node:os");
34757
- import_node_path9 = require("node:path");
34926
+ import_node_os6 = require("node:os");
34927
+ import_node_path10 = require("node:path");
34758
34928
  init_registry();
34759
34929
  init_types2();
34760
34930
  init_client2();
@@ -34790,7 +34960,7 @@ function parsePrMetadata(raw) {
34790
34960
  function readPrMetadata(worktreePath) {
34791
34961
  let raw;
34792
34962
  try {
34793
- raw = (0, import_node_fs5.readFileSync)((0, import_node_path10.join)(worktreePath, PR_METADATA_PATH), "utf8");
34963
+ raw = (0, import_node_fs6.readFileSync)((0, import_node_path11.join)(worktreePath, PR_METADATA_PATH), "utf8");
34794
34964
  } catch {
34795
34965
  return null;
34796
34966
  }
@@ -34798,16 +34968,16 @@ function readPrMetadata(worktreePath) {
34798
34968
  }
34799
34969
  function consumePrMetadata(worktreePath) {
34800
34970
  const meta = readPrMetadata(worktreePath);
34801
- (0, import_node_fs5.rmSync)((0, import_node_path10.join)(worktreePath, PR_METADATA_PATH), { force: true });
34971
+ (0, import_node_fs6.rmSync)((0, import_node_path11.join)(worktreePath, PR_METADATA_PATH), { force: true });
34802
34972
  return meta;
34803
34973
  }
34804
- var import_node_fs5, import_node_path10, PR_METADATA_PATH, TITLE_MAX_CHARS2, BODY_MAX_CHARS2;
34974
+ var import_node_fs6, import_node_path11, PR_METADATA_PATH, TITLE_MAX_CHARS2, BODY_MAX_CHARS2;
34805
34975
  var init_pr_metadata = __esm({
34806
34976
  "src/lib/orchestrator/pr-metadata.ts"() {
34807
34977
  "use strict";
34808
- import_node_fs5 = require("node:fs");
34809
- import_node_path10 = require("node:path");
34810
- PR_METADATA_PATH = (0, import_node_path10.join)(".drydock", "PR.md");
34978
+ import_node_fs6 = require("node:fs");
34979
+ import_node_path11 = require("node:path");
34980
+ PR_METADATA_PATH = (0, import_node_path11.join)(".drydock", "PR.md");
34811
34981
  TITLE_MAX_CHARS2 = 300;
34812
34982
  BODY_MAX_CHARS2 = 6e4;
34813
34983
  }
@@ -34855,7 +35025,7 @@ function parseQuestions(raw) {
34855
35025
  function readQuestions(worktreePath) {
34856
35026
  let raw;
34857
35027
  try {
34858
- raw = (0, import_node_fs6.readFileSync)((0, import_node_path11.join)(worktreePath, QUESTIONS_METADATA_PATH), "utf8");
35028
+ raw = (0, import_node_fs7.readFileSync)((0, import_node_path12.join)(worktreePath, QUESTIONS_METADATA_PATH), "utf8");
34859
35029
  } catch {
34860
35030
  return null;
34861
35031
  }
@@ -34863,16 +35033,16 @@ function readQuestions(worktreePath) {
34863
35033
  }
34864
35034
  function consumeQuestions(worktreePath) {
34865
35035
  const questions = readQuestions(worktreePath);
34866
- (0, import_node_fs6.rmSync)((0, import_node_path11.join)(worktreePath, QUESTIONS_METADATA_PATH), { force: true });
35036
+ (0, import_node_fs7.rmSync)((0, import_node_path12.join)(worktreePath, QUESTIONS_METADATA_PATH), { force: true });
34867
35037
  return questions;
34868
35038
  }
34869
- var import_node_fs6, import_node_path11, QUESTIONS_METADATA_PATH, QUESTIONS_MAX_CHARS;
35039
+ var import_node_fs7, import_node_path12, QUESTIONS_METADATA_PATH, QUESTIONS_MAX_CHARS;
34870
35040
  var init_questions_metadata = __esm({
34871
35041
  "src/lib/orchestrator/questions-metadata.ts"() {
34872
35042
  "use strict";
34873
- import_node_fs6 = require("node:fs");
34874
- import_node_path11 = require("node:path");
34875
- QUESTIONS_METADATA_PATH = (0, import_node_path11.join)(".drydock", "QUESTIONS.md");
35043
+ import_node_fs7 = require("node:fs");
35044
+ import_node_path12 = require("node:path");
35045
+ QUESTIONS_METADATA_PATH = (0, import_node_path12.join)(".drydock", "QUESTIONS.md");
34876
35046
  QUESTIONS_MAX_CHARS = 6e4;
34877
35047
  }
34878
35048
  });
@@ -34905,7 +35075,7 @@ function parseReleaseMetadata(raw) {
34905
35075
  function readReleaseMetadata(worktreePath) {
34906
35076
  let raw;
34907
35077
  try {
34908
- raw = (0, import_node_fs7.readFileSync)((0, import_node_path12.join)(worktreePath, RELEASE_METADATA_PATH), "utf8");
35078
+ raw = (0, import_node_fs8.readFileSync)((0, import_node_path13.join)(worktreePath, RELEASE_METADATA_PATH), "utf8");
34909
35079
  } catch {
34910
35080
  return null;
34911
35081
  }
@@ -34913,16 +35083,16 @@ function readReleaseMetadata(worktreePath) {
34913
35083
  }
34914
35084
  function consumeReleaseMetadata(worktreePath) {
34915
35085
  const meta = readReleaseMetadata(worktreePath);
34916
- (0, import_node_fs7.rmSync)((0, import_node_path12.join)(worktreePath, RELEASE_METADATA_PATH), { force: true });
35086
+ (0, import_node_fs8.rmSync)((0, import_node_path13.join)(worktreePath, RELEASE_METADATA_PATH), { force: true });
34917
35087
  return meta;
34918
35088
  }
34919
- var import_node_fs7, import_node_path12, RELEASE_METADATA_PATH, TITLE_MAX_CHARS3, NOTES_MAX_CHARS, TAG_LINE, VERSION_TITLE;
35089
+ var import_node_fs8, import_node_path13, RELEASE_METADATA_PATH, TITLE_MAX_CHARS3, NOTES_MAX_CHARS, TAG_LINE, VERSION_TITLE;
34920
35090
  var init_release_metadata = __esm({
34921
35091
  "src/lib/orchestrator/release-metadata.ts"() {
34922
35092
  "use strict";
34923
- import_node_fs7 = require("node:fs");
34924
- import_node_path12 = require("node:path");
34925
- RELEASE_METADATA_PATH = (0, import_node_path12.join)(".drydock", "RELEASE.md");
35093
+ import_node_fs8 = require("node:fs");
35094
+ import_node_path13 = require("node:path");
35095
+ RELEASE_METADATA_PATH = (0, import_node_path13.join)(".drydock", "RELEASE.md");
34926
35096
  TITLE_MAX_CHARS3 = 300;
34927
35097
  NOTES_MAX_CHARS = 6e4;
34928
35098
  TAG_LINE = /^tag:\s*(\S+)\s*$/i;
@@ -35287,7 +35457,7 @@ async function runVerificationPass(deps) {
35287
35457
  };
35288
35458
  let generate = deps.generate;
35289
35459
  if (!generate) {
35290
- tmp = await (0, import_promises4.mkdtemp)((0, import_node_path13.join)((0, import_node_os6.tmpdir)(), "drydock-verify-"));
35460
+ tmp = await (0, import_promises4.mkdtemp)((0, import_node_path14.join)((0, import_node_os7.tmpdir)(), "drydock-verify-"));
35291
35461
  generate = buildVerificationGenerator({
35292
35462
  provider,
35293
35463
  command,
@@ -35330,13 +35500,13 @@ async function runVerificationPass(deps) {
35330
35500
  }
35331
35501
  }
35332
35502
  }
35333
- var import_promises4, import_node_os6, import_node_path13, VERIFY_TIMEOUT_MS, COMMENT_HEADER;
35503
+ var import_promises4, import_node_os7, import_node_path14, VERIFY_TIMEOUT_MS, COMMENT_HEADER;
35334
35504
  var init_verify_driver = __esm({
35335
35505
  "src/lib/orchestrator/verify-driver.ts"() {
35336
35506
  "use strict";
35337
35507
  import_promises4 = require("node:fs/promises");
35338
- import_node_os6 = require("node:os");
35339
- import_node_path13 = require("node:path");
35508
+ import_node_os7 = require("node:os");
35509
+ import_node_path14 = require("node:path");
35340
35510
  init_client2();
35341
35511
  init_subtasks();
35342
35512
  init_verify();
@@ -35521,6 +35691,7 @@ async function runJobCore(jobId, deps, send) {
35521
35691
  const consumeQuestions2 = deps.consumeQuestions ?? consumeQuestions;
35522
35692
  const consumeFollowups2 = deps.consumeFollowups ?? consumeFollowups;
35523
35693
  const markNeedsHuman = deps.markNeedsHuman ?? ((issueNumber) => markIssueNeedsHuman(repo.id, issueNumber, db));
35694
+ const adoptClaudeMem = deps.adoptClaudeMem ?? adoptWorktreeMemory;
35524
35695
  const resumeStoredSession = (j, prompt, cwd) => {
35525
35696
  if (!j.sessionId) throw new Error(`job ${j.id} has no session id to resume`);
35526
35697
  return resumeAgentSession(j, j.sessionId, "", cwd, {
@@ -35649,7 +35820,11 @@ async function runJobCore(jobId, deps, send) {
35649
35820
  const humanInstruction = job.humanInstruction;
35650
35821
  const instructionResume = !!humanInstruction && !!job.sessionId && provider.supportsResume;
35651
35822
  const resumeOnExistingBranch = !!humanInstruction && !!job.branch;
35652
- wt = resumeOnExistingBranch ? await worktrees.prepareResume(repo, job.id, job.branch) : await worktrees.prepare(repo, job.id, job.issueNumber);
35823
+ const branchLabel = issueBranchLabel(
35824
+ job.issueNumber,
35825
+ getIssueTitle(repo.id, job.issueNumber, db)
35826
+ );
35827
+ wt = resumeOnExistingBranch ? await worktrees.prepareResume(repo, job.id, job.branch) : await worktrees.prepare(repo, job.id, job.issueNumber, branchLabel);
35653
35828
  recordEvent(job.id, "worktree", { path: wt.path, branch: wt.branch }, db);
35654
35829
  if (sandboxRequested) {
35655
35830
  const prepared = await prepareSandbox({
@@ -35803,6 +35978,36 @@ ${planText}`
35803
35978
  if (repo.autoDecompose) markSubtasksParked(repo.id, job.issueNumber, db);
35804
35979
  return afterSession;
35805
35980
  }
35981
+ let turnResumeAttempts = 0;
35982
+ while (session.maxTurnsReached && getSettings(db).maxTurnsAutoResume && session.sessionId && provider.supportsResume && turnResumeAttempts < MAX_TURN_RESUMES) {
35983
+ turnResumeAttempts += 1;
35984
+ recordEvent(
35985
+ job.id,
35986
+ "status",
35987
+ {
35988
+ reason: `turn budget (${job.maxTurns}) reached, resuming`,
35989
+ attempt: turnResumeAttempts,
35990
+ sessionId: session.sessionId
35991
+ },
35992
+ db
35993
+ );
35994
+ if (repo.autoDecompose) markSubtasksWorking(repo.id, job.issueNumber, db);
35995
+ const resumePrompt = renderTemplate(
35996
+ resolveTemplateContent(repo.id, TEMPLATE_NAMES.turnResume, db),
35997
+ {
35998
+ ISSUE_NUM: job.issueNumber,
35999
+ BRANCH: wt.branch,
36000
+ REPO_NAME: repo.name,
36001
+ PR_FORMAT: resolveTemplateContent(repo.id, TEMPLATE_NAMES.prFormat, db)
36002
+ }
36003
+ );
36004
+ session = await resumeLimitSession(getJob(job.id, db), resumePrompt, wt.path);
36005
+ const afterResume = getJob(job.id, db);
36006
+ if (afterResume.status === "aborted" || afterResume.status === "interrupted") {
36007
+ if (repo.autoDecompose) markSubtasksParked(repo.id, job.issueNumber, db);
36008
+ return afterResume;
36009
+ }
36010
+ }
35806
36011
  if (session.timedOut) {
35807
36012
  return await parkForHuman(`${provider.label} timed out after ${maxJobMinutes} minutes`);
35808
36013
  }
@@ -35825,6 +36030,9 @@ ${planText}`
35825
36030
  }
35826
36031
  return await parkOnLimit(limit);
35827
36032
  }
36033
+ if (session.maxTurnsReached) {
36034
+ return await parkForHuman(`turn budget (${job.maxTurns}) reached`);
36035
+ }
35828
36036
  if (session.exitCode !== 0) {
35829
36037
  return await parkForHuman(`${provider.label} exited non-zero`);
35830
36038
  }
@@ -35942,6 +36150,13 @@ ${questions}`
35942
36150
  return current;
35943
36151
  } finally {
35944
36152
  if (wt && !preserveWorktree) {
36153
+ if (repo.adoptClaudeMem) {
36154
+ try {
36155
+ await adoptClaudeMem({ branch: wt.branch, cwd: wt.path });
36156
+ } catch (adoptErr) {
36157
+ logError(`[run-job] claude-mem adoption failed for job ${job.id}`, adoptErr);
36158
+ }
36159
+ }
35945
36160
  try {
35946
36161
  await worktrees.remove(wt, repo.path);
35947
36162
  } catch (cleanupErr) {
@@ -35950,7 +36165,7 @@ ${questions}`
35950
36165
  }
35951
36166
  }
35952
36167
  }
35953
- var PLAN_MAX_CHARS, ISSUE_TITLE_MAX_CHARS, ISSUE_BODY_MAX_CHARS, HUMAN_INSTRUCTION_MAX_CHARS;
36168
+ var MAX_TURN_RESUMES, PLAN_MAX_CHARS, ISSUE_TITLE_MAX_CHARS, ISSUE_BODY_MAX_CHARS, HUMAN_INSTRUCTION_MAX_CHARS;
35954
36169
  var init_run_job = __esm({
35955
36170
  "src/lib/orchestrator/run-job.ts"() {
35956
36171
  "use strict";
@@ -35975,6 +36190,7 @@ var init_run_job = __esm({
35975
36190
  init_agent_command();
35976
36191
  init_agent_session();
35977
36192
  init_ci_babysitter();
36193
+ init_claude_mem_adopt();
35978
36194
  init_followups_metadata();
35979
36195
  init_jobs();
35980
36196
  init_needs_human();
@@ -35988,6 +36204,7 @@ var init_run_job = __esm({
35988
36204
  init_state_machine();
35989
36205
  init_subtask_driver();
35990
36206
  init_verify_driver();
36207
+ MAX_TURN_RESUMES = 3;
35991
36208
  PLAN_MAX_CHARS = 1e4;
35992
36209
  ISSUE_TITLE_MAX_CHARS = 500;
35993
36210
  ISSUE_BODY_MAX_CHARS = 2e4;
@@ -36022,7 +36239,7 @@ function waitForIdle(timeoutMs = 3e4, pollMs = 100) {
36022
36239
  });
36023
36240
  }
36024
36241
  function lockPath() {
36025
- return (0, import_node_path14.join)(worktreeHome(), "instance.lock");
36242
+ return (0, import_node_path15.join)(worktreeHome(), "instance.lock");
36026
36243
  }
36027
36244
  function pidAlive(pid) {
36028
36245
  try {
@@ -36042,17 +36259,17 @@ function parseLock(text2) {
36042
36259
  }
36043
36260
  function readLock(path2) {
36044
36261
  try {
36045
- return parseLock((0, import_node_fs8.readFileSync)(path2, "utf8"));
36262
+ return parseLock((0, import_node_fs9.readFileSync)(path2, "utf8"));
36046
36263
  } catch {
36047
36264
  return { pid: null, ts: null };
36048
36265
  }
36049
36266
  }
36050
36267
  function writeLock(path2) {
36051
- const fd = (0, import_node_fs8.openSync)(path2, "wx");
36268
+ const fd = (0, import_node_fs9.openSync)(path2, "wx");
36052
36269
  try {
36053
- (0, import_node_fs8.writeSync)(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
36270
+ (0, import_node_fs9.writeSync)(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
36054
36271
  } finally {
36055
- (0, import_node_fs8.closeSync)(fd);
36272
+ (0, import_node_fs9.closeSync)(fd);
36056
36273
  }
36057
36274
  }
36058
36275
  function lockIsStale(record2, now) {
@@ -36064,7 +36281,7 @@ function lockIsStale(record2, now) {
36064
36281
  }
36065
36282
  function acquireInstanceLock() {
36066
36283
  const path2 = lockPath();
36067
- (0, import_node_fs8.mkdirSync)((0, import_node_path14.dirname)(path2), { recursive: true });
36284
+ (0, import_node_fs9.mkdirSync)((0, import_node_path15.dirname)(path2), { recursive: true });
36068
36285
  try {
36069
36286
  writeLock(path2);
36070
36287
  return true;
@@ -36072,7 +36289,7 @@ function acquireInstanceLock() {
36072
36289
  }
36073
36290
  if (!lockIsStale(readLock(path2), Date.now())) return false;
36074
36291
  try {
36075
- (0, import_node_fs8.unlinkSync)(path2);
36292
+ (0, import_node_fs9.unlinkSync)(path2);
36076
36293
  writeLock(path2);
36077
36294
  return true;
36078
36295
  } catch {
@@ -36084,16 +36301,16 @@ function refreshInstanceLock() {
36084
36301
  if (readLock(path2).pid !== process.pid) return false;
36085
36302
  const tmp = `${path2}.${process.pid}.tmp`;
36086
36303
  try {
36087
- (0, import_node_fs8.writeFileSync)(tmp, JSON.stringify({ pid: process.pid, ts: Date.now() }));
36304
+ (0, import_node_fs9.writeFileSync)(tmp, JSON.stringify({ pid: process.pid, ts: Date.now() }));
36088
36305
  if (readLock(path2).pid !== process.pid) {
36089
- (0, import_node_fs8.unlinkSync)(tmp);
36306
+ (0, import_node_fs9.unlinkSync)(tmp);
36090
36307
  return false;
36091
36308
  }
36092
- (0, import_node_fs8.renameSync)(tmp, path2);
36309
+ (0, import_node_fs9.renameSync)(tmp, path2);
36093
36310
  return true;
36094
36311
  } catch {
36095
36312
  try {
36096
- (0, import_node_fs8.unlinkSync)(tmp);
36313
+ (0, import_node_fs9.unlinkSync)(tmp);
36097
36314
  } catch {
36098
36315
  }
36099
36316
  return false;
@@ -36117,16 +36334,16 @@ function releaseInstanceLock() {
36117
36334
  const path2 = lockPath();
36118
36335
  if (readLock(path2).pid !== process.pid) return;
36119
36336
  try {
36120
- (0, import_node_fs8.unlinkSync)(path2);
36337
+ (0, import_node_fs9.unlinkSync)(path2);
36121
36338
  } catch {
36122
36339
  }
36123
36340
  }
36124
- var import_node_fs8, import_node_path14, draining, activeJobs, LOCK_HEARTBEAT_MS, LOCK_TTL_MS, heartbeatTimer;
36341
+ var import_node_fs9, import_node_path15, draining, activeJobs, LOCK_HEARTBEAT_MS, LOCK_TTL_MS, heartbeatTimer;
36125
36342
  var init_runtime2 = __esm({
36126
36343
  "src/lib/orchestrator/runtime.ts"() {
36127
36344
  "use strict";
36128
- import_node_fs8 = require("node:fs");
36129
- import_node_path14 = require("node:path");
36345
+ import_node_fs9 = require("node:fs");
36346
+ import_node_path15 = require("node:path");
36130
36347
  init_worktree();
36131
36348
  draining = false;
36132
36349
  activeJobs = /* @__PURE__ */ new Set();
@@ -36514,7 +36731,7 @@ async function reapOrphanedWorktrees(deps = {}) {
36514
36731
  const dir = repoWorktreesDir(repo.name);
36515
36732
  let entries;
36516
36733
  try {
36517
- entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
36734
+ entries = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
36518
36735
  } catch {
36519
36736
  continue;
36520
36737
  }
@@ -36524,12 +36741,12 @@ async function reapOrphanedWorktrees(deps = {}) {
36524
36741
  const jobId = Number(match[1]);
36525
36742
  if (live.has(jobId)) continue;
36526
36743
  if (isLiveJob(jobId, db)) continue;
36527
- const path2 = (0, import_node_path15.join)(dir, entry);
36744
+ const path2 = (0, import_node_path16.join)(dir, entry);
36528
36745
  await run("git", ["-C", repo.path, "worktree", "remove", "--force", path2]).catch(
36529
36746
  () => void 0
36530
36747
  );
36531
36748
  try {
36532
- (0, import_node_fs9.rmSync)(path2, { recursive: true, force: true });
36749
+ (0, import_node_fs10.rmSync)(path2, { recursive: true, force: true });
36533
36750
  reaped++;
36534
36751
  } catch (err) {
36535
36752
  logError(`[worktree-reaper] failed to remove ${path2}`, err);
@@ -36538,12 +36755,12 @@ async function reapOrphanedWorktrees(deps = {}) {
36538
36755
  }
36539
36756
  return reaped;
36540
36757
  }
36541
- var import_node_fs9, import_node_path15, JOB_DIR, FB_DIR, DH_DIR;
36758
+ var import_node_fs10, import_node_path16, JOB_DIR, FB_DIR, DH_DIR;
36542
36759
  var init_worktree_reaper = __esm({
36543
36760
  "src/lib/orchestrator/worktree-reaper.ts"() {
36544
36761
  "use strict";
36545
- import_node_fs9 = require("node:fs");
36546
- import_node_path15 = require("node:path");
36762
+ import_node_fs10 = require("node:fs");
36763
+ import_node_path16 = require("node:path");
36547
36764
  init_drizzle_orm();
36548
36765
  init_client2();
36549
36766
  init_queries();
@@ -36666,7 +36883,7 @@ function startOrchestrator() {
36666
36883
  process.once("SIGINT", onSignal);
36667
36884
  process.once("SIGTERM", onSignal);
36668
36885
  }
36669
- var started, abortHandles, DEFAULT_ABORT_GRACE_MS, IDLE_WAIT_MS, PRUNE_INTERVAL_MS;
36886
+ var started, ABORT_HANDLES_KEY, globalWithAbort, abortHandles, DEFAULT_ABORT_GRACE_MS, IDLE_WAIT_MS, PRUNE_INTERVAL_MS;
36670
36887
  var init_singleton = __esm({
36671
36888
  "src/lib/orchestrator/singleton.ts"() {
36672
36889
  "use strict";
@@ -36682,7 +36899,10 @@ var init_singleton = __esm({
36682
36899
  init_runtime2();
36683
36900
  init_worktree_reaper();
36684
36901
  started = false;
36685
- abortHandles = /* @__PURE__ */ new Map();
36902
+ ABORT_HANDLES_KEY = Symbol.for("drydock.orchestrator.abort-handles");
36903
+ globalWithAbort = globalThis;
36904
+ globalWithAbort[ABORT_HANDLES_KEY] ??= /* @__PURE__ */ new Map();
36905
+ abortHandles = globalWithAbort[ABORT_HANDLES_KEY];
36686
36906
  DEFAULT_ABORT_GRACE_MS = 5e3;
36687
36907
  IDLE_WAIT_MS = DEFAULT_ABORT_GRACE_MS + 3e3;
36688
36908
  PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -36691,13 +36911,13 @@ var init_singleton = __esm({
36691
36911
 
36692
36912
  // src/lib/db/client.ts
36693
36913
  function resolveMigrationsDir() {
36694
- return process.env.DRYDOCK_MIGRATIONS ?? (0, import_node_path16.resolve)(process.cwd(), "drizzle");
36914
+ return process.env.DRYDOCK_MIGRATIONS ?? (0, import_node_path17.resolve)(process.cwd(), "drizzle");
36695
36915
  }
36696
36916
  function applyMigrations(sqlite) {
36697
36917
  const migrationsFolder = resolveMigrationsDir();
36698
36918
  let files;
36699
36919
  try {
36700
- files = (0, import_node_fs10.readdirSync)(migrationsFolder).filter((f) => f.endsWith(".sql")).sort();
36920
+ files = (0, import_node_fs11.readdirSync)(migrationsFolder).filter((f) => f.endsWith(".sql")).sort();
36701
36921
  } catch {
36702
36922
  return;
36703
36923
  }
@@ -36708,7 +36928,7 @@ function applyMigrations(sqlite) {
36708
36928
  const record2 = sqlite.prepare("INSERT INTO __migrations (name) VALUES (?)");
36709
36929
  for (const file of files) {
36710
36930
  if (applied.has(file)) continue;
36711
- const sql2 = (0, import_node_fs10.readFileSync)((0, import_node_path16.join)(migrationsFolder, file), "utf8");
36931
+ const sql2 = (0, import_node_fs11.readFileSync)((0, import_node_path17.join)(migrationsFolder, file), "utf8");
36712
36932
  sqlite.pragma("foreign_keys = OFF");
36713
36933
  try {
36714
36934
  const run = sqlite.transaction(() => {
@@ -36733,7 +36953,7 @@ function applyMigrations(sqlite) {
36733
36953
  }
36734
36954
  function createDb(dbPath) {
36735
36955
  if (dbPath !== ":memory:") {
36736
- (0, import_node_fs10.mkdirSync)((0, import_node_path16.dirname)(dbPath), { recursive: true });
36956
+ (0, import_node_fs11.mkdirSync)((0, import_node_path17.dirname)(dbPath), { recursive: true });
36737
36957
  }
36738
36958
  const sqlite = new import_better_sqlite32.default(dbPath);
36739
36959
  try {
@@ -36749,7 +36969,7 @@ function createDb(dbPath) {
36749
36969
  function getDb() {
36750
36970
  if (singletonError) throw singletonError;
36751
36971
  if (!singleton2) {
36752
- const path2 = process.env.DRYDOCK_DB ?? (0, import_node_path16.resolve)(process.cwd(), "data/drydock.db");
36972
+ const path2 = process.env.DRYDOCK_DB ?? (0, import_node_path17.resolve)(process.cwd(), "data/drydock.db");
36753
36973
  try {
36754
36974
  singleton2 = createDb(path2);
36755
36975
  } catch (err) {
@@ -36760,12 +36980,12 @@ function getDb() {
36760
36980
  }
36761
36981
  return singleton2;
36762
36982
  }
36763
- var import_node_fs10, import_node_path16, import_better_sqlite32, FOREIGN_KEYS_PRAGMA, singleton2, singletonError;
36983
+ var import_node_fs11, import_node_path17, import_better_sqlite32, FOREIGN_KEYS_PRAGMA, singleton2, singletonError;
36764
36984
  var init_client2 = __esm({
36765
36985
  "src/lib/db/client.ts"() {
36766
36986
  "use strict";
36767
- import_node_fs10 = require("node:fs");
36768
- import_node_path16 = require("node:path");
36987
+ import_node_fs11 = require("node:fs");
36988
+ import_node_path17 = require("node:path");
36769
36989
  import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
36770
36990
  init_better_sqlite3();
36771
36991
  init_logger2();
@@ -42496,15 +42716,15 @@ function resumeJobWithInstruction(jobId, instruction, db = getDb()) {
42496
42716
  }
42497
42717
 
42498
42718
  // src/lib/repos/path.ts
42499
- var import_node_fs11 = require("node:fs");
42500
- var import_node_path17 = require("node:path");
42719
+ var import_node_fs12 = require("node:fs");
42720
+ var import_node_path18 = require("node:path");
42501
42721
  function isGitRepoPath(path2) {
42502
42722
  try {
42503
- if (!(0, import_node_fs11.statSync)(path2).isDirectory()) return false;
42723
+ if (!(0, import_node_fs12.statSync)(path2).isDirectory()) return false;
42504
42724
  } catch {
42505
42725
  return false;
42506
42726
  }
42507
- return (0, import_node_fs11.existsSync)((0, import_node_path17.join)(path2, ".git"));
42727
+ return (0, import_node_fs12.existsSync)((0, import_node_path18.join)(path2, ".git"));
42508
42728
  }
42509
42729
 
42510
42730
  // src/lib/repos/service.ts
@@ -42617,7 +42837,10 @@ var repoInputSchema = import_zod18.z.object({
42617
42837
  sandboxImage: import_zod18.z.string().nullish(),
42618
42838
  sandboxAllowNetwork: import_zod18.z.boolean().default(false),
42619
42839
  sandboxCpus: import_zod18.z.string().nullish(),
42620
- sandboxMemory: import_zod18.z.string().nullish()
42840
+ sandboxMemory: import_zod18.z.string().nullish(),
42841
+ // Opt-in claude-mem worktree adoption (issue #274). Off by default: it depends
42842
+ // on the external claude-mem plugin being installed. See the schema column.
42843
+ adoptClaudeMem: import_zod18.z.boolean().default(false)
42621
42844
  });
42622
42845
  function assertModelAllowedForAgent(agent, model, db) {
42623
42846
  if (agent === "openrouter") {