@pourkit/cli 0.0.0-next-20260529180337 → 0.0.0-next-20260531183322

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.
package/dist/cli.js CHANGED
@@ -301,17 +301,17 @@ function parseWorktreeListPorcelain(text2, branch) {
301
301
  const entries = text2.trim().split("\n\n");
302
302
  for (const entry of entries) {
303
303
  const lines = entry.trim().split("\n");
304
- let path7 = "";
304
+ let path9 = "";
305
305
  let entryBranch = "";
306
306
  for (const line of lines) {
307
307
  if (line.startsWith("worktree ")) {
308
- path7 = line.slice("worktree ".length);
308
+ path9 = line.slice("worktree ".length);
309
309
  } else if (line.startsWith("branch refs/heads/")) {
310
310
  entryBranch = line.slice("branch refs/heads/".length);
311
311
  }
312
312
  }
313
- if (entryBranch === branch && path7) {
314
- return path7;
313
+ if (entryBranch === branch && path9) {
314
+ return path9;
315
315
  }
316
316
  }
317
317
  return null;
@@ -334,7 +334,7 @@ var init_common = __esm({
334
334
  });
335
335
 
336
336
  // cli.ts
337
- import path6 from "path";
337
+ import path8 from "path";
338
338
  import { realpathSync } from "fs";
339
339
  import { pathToFileURL } from "url";
340
340
  import { Command, Option, CommanderError } from "commander";
@@ -368,6 +368,13 @@ var VerificationCommandSchema = z.object({
368
368
  var QueueConfigSchema = z.object({
369
369
  loop: z.boolean().optional()
370
370
  }).strict();
371
+ var FailureResolutionConfigSchema = z.object({
372
+ agent: NonEmptyString,
373
+ model: NonEmptyString,
374
+ promptTemplate: NonEmptyString,
375
+ maxAttemptsPerFailure: z.number().int().positive(),
376
+ failureLimits: z.record(z.string(), z.number().int().positive()).optional()
377
+ }).strict();
371
378
  var ReviewRefactorLoopStrategySchema = z.object({
372
379
  type: z.literal("review-refactor-loop"),
373
380
  implement: z.object({
@@ -379,6 +386,7 @@ var ReviewRefactorLoopStrategySchema = z.object({
379
386
  promptTemplate: NonEmptyString,
380
387
  maxAttempts: z.number().int().positive()
381
388
  }).strict().optional(),
389
+ failureResolution: FailureResolutionConfigSchema,
382
390
  review: z.object({
383
391
  reviewer: ReviewerConfigSchema,
384
392
  refactor: StageAgentConfigSchema,
@@ -398,6 +406,10 @@ var ReviewRefactorLoopStrategySchema = z.object({
398
406
  maxAttempts: z.number().int().positive()
399
407
  }).strict()
400
408
  }).strict();
409
+ var TargetSerenaConfigSchema = z.object({
410
+ enabled: z.boolean().optional(),
411
+ required: z.boolean().optional()
412
+ }).strict();
401
413
  var TargetSchema = z.object({
402
414
  name: NonEmptyString,
403
415
  baseBranch: z.preprocess(
@@ -414,6 +426,7 @@ var TargetSchema = z.object({
414
426
  z.boolean().default(true)
415
427
  ),
416
428
  queue: QueueConfigSchema.optional(),
429
+ serena: TargetSerenaConfigSchema.optional(),
417
430
  strategy: ReviewRefactorLoopStrategySchema
418
431
  }).strict();
419
432
  var LabelsSchema = z.object({
@@ -453,12 +466,21 @@ var CleanupConfigSchema = z.object({
453
466
  worktreeRetentionDays: z.number().int().positive().default(14),
454
467
  logRetentionDays: z.number().int().positive().default(30)
455
468
  }).strict();
469
+ var SerenaConfigSchema = z.object({
470
+ enabled: z.boolean().default(false),
471
+ required: z.boolean().default(false),
472
+ mcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
473
+ sandboxMcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
474
+ dataDir: z.string().default(".pourkit/serena/"),
475
+ autoStart: z.boolean().default(false)
476
+ }).strict();
456
477
  var PourkitConfigSchema = z.object({
457
478
  targets: z.array(TargetSchema).min(1),
458
479
  labels: LabelsSchema,
459
480
  sandbox: SandboxSchema,
460
481
  checks: ChecksSchema,
461
- cleanup: CleanupConfigSchema.optional()
482
+ cleanup: CleanupConfigSchema.optional(),
483
+ serena: SerenaConfigSchema.default({})
462
484
  }).strict();
463
485
  var removedFieldReplacements = {
464
486
  "config.implementor": "targets[].strategy.implement.builder",
@@ -523,10 +545,10 @@ function checkRemovedFields(raw) {
523
545
  }
524
546
  }
525
547
  }
526
- function formatZodPath(path7) {
527
- if (path7.length === 0) return "";
548
+ function formatZodPath(path9) {
549
+ if (path9.length === 0) return "";
528
550
  let result = "";
529
- for (const segment of path7) {
551
+ for (const segment of path9) {
530
552
  if (typeof segment === "number") {
531
553
  result += `[${segment}]`;
532
554
  } else {
@@ -537,8 +559,8 @@ function formatZodPath(path7) {
537
559
  }
538
560
  function formatFirstZodError(err) {
539
561
  const issue = err.issues[0];
540
- const path7 = formatZodPath(issue.path);
541
- if (path7 === "targets" && (issue.code === "too_small" || issue.code === "invalid_type")) {
562
+ const path9 = formatZodPath(issue.path);
563
+ if (path9 === "targets" && (issue.code === "too_small" || issue.code === "invalid_type")) {
542
564
  return "Config must have at least one target";
543
565
  }
544
566
  if (issue.path.length >= 3 && issue.path[0] === "targets" && typeof issue.path[1] === "number" && issue.path[2] === "name" && issue.code === z.ZodIssueCode.too_small) {
@@ -547,37 +569,37 @@ function formatFirstZodError(err) {
547
569
  switch (issue.code) {
548
570
  case z.ZodIssueCode.invalid_type: {
549
571
  if (issue.expected === "object") {
550
- return path7 ? `${path7} must be an object` : "Config must be an object";
572
+ return path9 ? `${path9} must be an object` : "Config must be an object";
551
573
  }
552
574
  if (issue.expected === "integer") {
553
- return `${path7} must be an integer`;
575
+ return `${path9} must be an integer`;
554
576
  }
555
577
  if (issue.expected === "string") {
556
- return `${path7} must be a string`;
578
+ return `${path9} must be a string`;
557
579
  }
558
580
  if (issue.expected === "number") {
559
- return `${path7} must be a number`;
581
+ return `${path9} must be a number`;
560
582
  }
561
583
  return issue.message;
562
584
  }
563
585
  case z.ZodIssueCode.too_small:
564
586
  if (issue.type === "string" && issue.minimum === 1) {
565
- return `${path7} must be a non-empty string`;
587
+ return `${path9} must be a non-empty string`;
566
588
  }
567
589
  if (issue.type === "array" && issue.minimum === 1) {
568
- return `${path7} must not be empty`;
590
+ return `${path9} must not be empty`;
569
591
  }
570
592
  if (issue.type === "number") {
571
- return `${path7} must be a positive number`;
593
+ return `${path9} must be a positive number`;
572
594
  }
573
595
  return issue.message;
574
596
  case z.ZodIssueCode.invalid_literal:
575
- return `${path7} must be ${issue.expected}`;
597
+ return `${path9} must be ${issue.expected}`;
576
598
  case z.ZodIssueCode.unrecognized_keys:
577
- const keyPath = path7 ? `${path7}.${issue.keys[0]}` : issue.keys[0];
599
+ const keyPath = path9 ? `${path9}.${issue.keys[0]}` : issue.keys[0];
578
600
  return `${keyPath} is not supported`;
579
601
  case z.ZodIssueCode.custom:
580
- return path7 ? `${path7} ${issue.message}` : issue.message;
602
+ return path9 ? `${path9} ${issue.message}` : issue.message;
581
603
  default:
582
604
  return issue.message;
583
605
  }
@@ -604,9 +626,19 @@ function parseConfig(raw) {
604
626
  "setupCommands",
605
627
  "autoMerge",
606
628
  "queue",
629
+ "serena",
607
630
  "strategy"
608
631
  ]);
609
632
  }
633
+ for (let i = 0; i < rawTargets.length; i++) {
634
+ const t = rawTargets[i];
635
+ const strategy = t?.strategy;
636
+ if (strategy && typeof strategy === "object" && "conflictResolution" in strategy) {
637
+ throw new Error(
638
+ `targets[${i}].strategy.conflictResolution has been removed; use targets[${i}].strategy.failureResolution`
639
+ );
640
+ }
641
+ }
610
642
  if (config.sandbox && typeof config.sandbox === "object") {
611
643
  assertKnownKeys(config.sandbox, "sandbox", [
612
644
  "provider",
@@ -637,17 +669,17 @@ function parseConfig(raw) {
637
669
  setupCommands,
638
670
  autoMerge: t.autoMerge,
639
671
  queue: t.queue,
672
+ serena: t.serena,
640
673
  strategy: {
641
674
  type: "review-refactor-loop",
642
675
  implement: { builder: t.strategy.implement.builder },
643
- ...t.strategy.conflictResolution ? {
644
- conflictResolution: {
645
- agent: t.strategy.conflictResolution.agent,
646
- model: t.strategy.conflictResolution.model,
647
- promptTemplate: t.strategy.conflictResolution.promptTemplate,
648
- maxAttempts: t.strategy.conflictResolution.maxAttempts
649
- }
650
- } : {},
676
+ failureResolution: {
677
+ agent: t.strategy.failureResolution.agent,
678
+ model: t.strategy.failureResolution.model,
679
+ promptTemplate: t.strategy.failureResolution.promptTemplate,
680
+ maxAttemptsPerFailure: t.strategy.failureResolution.maxAttemptsPerFailure,
681
+ failureLimits: t.strategy.failureResolution.failureLimits
682
+ },
651
683
  review: {
652
684
  reviewer: t.strategy.review.reviewer,
653
685
  refactor: t.strategy.review.refactor,
@@ -662,6 +694,19 @@ function parseConfig(raw) {
662
694
  }
663
695
  };
664
696
  });
697
+ const serena = {
698
+ ...data.serena,
699
+ mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? data.serena.mcpUrl,
700
+ sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? data.serena.sandboxMcpUrl
701
+ };
702
+ if (serena.mcpUrl.trim() === "") {
703
+ throw new Error("POURKIT_SERENA_MCP_URL must be a non-empty string");
704
+ }
705
+ if (serena.sandboxMcpUrl.trim() === "") {
706
+ throw new Error(
707
+ "POURKIT_SERENA_SANDBOX_MCP_URL must be a non-empty string"
708
+ );
709
+ }
665
710
  return {
666
711
  targets,
667
712
  labels: data.labels,
@@ -680,6 +725,7 @@ function parseConfig(raw) {
680
725
  pollIntervalSeconds: data.checks.pollIntervalSeconds ?? 15,
681
726
  issueListLimit: data.checks.issueListLimit ?? 50
682
727
  },
728
+ serena,
683
729
  cleanup: {
684
730
  enabled: data.cleanup?.enabled ?? true,
685
731
  worktreeRetentionDays: data.cleanup?.worktreeRetentionDays ?? 14,
@@ -687,10 +733,10 @@ function parseConfig(raw) {
687
733
  }
688
734
  };
689
735
  }
690
- function assertKnownKeys(value, path7, knownKeys) {
736
+ function assertKnownKeys(value, path9, knownKeys) {
691
737
  for (const key of Object.keys(value)) {
692
738
  if (!knownKeys.includes(key)) {
693
- throw new Error(`${path7}.${key} is not supported`);
739
+ throw new Error(`${path9}.${key} is not supported`);
694
740
  }
695
741
  }
696
742
  }
@@ -698,14 +744,14 @@ function getVerificationCommands(target) {
698
744
  return target.strategy.verify?.commands ?? [];
699
745
  }
700
746
  async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
701
- const { existsSync: existsSync8 } = await import("fs");
702
- const { readFile: readFile4, writeFile: writeFile3, rm } = await import("fs/promises");
747
+ const { existsSync: existsSync10 } = await import("fs");
748
+ const { readFile: readFile5, writeFile: writeFile3, rm } = await import("fs/promises");
703
749
  const { tmpdir } = await import("os");
704
750
  const { join: pjoin, basename } = await import("path");
705
751
  const { pathToFileURL: pathToFileURL2 } = await import("url");
706
752
  const { build } = await import("esbuild");
707
753
  const configPath = pjoin(repoRoot2, configFileName);
708
- if (!existsSync8(configPath)) {
754
+ if (!existsSync10(configPath)) {
709
755
  throw new Error(
710
756
  `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
711
757
  );
@@ -836,16 +882,16 @@ function parseWorktreeListPorcelain2(text2) {
836
882
  const entries = text2.trim().split("\n\n");
837
883
  return entries.map((entry) => {
838
884
  const lines = entry.trim().split("\n");
839
- let path7 = "";
885
+ let path9 = "";
840
886
  let branch = "";
841
887
  for (const line of lines) {
842
888
  if (line.startsWith("worktree ")) {
843
- path7 = line.slice("worktree ".length);
889
+ path9 = line.slice("worktree ".length);
844
890
  } else if (line.startsWith("branch refs/heads/")) {
845
891
  branch = line.slice("branch refs/heads/".length);
846
892
  }
847
893
  }
848
- return { path: path7, branch: branch || void 0 };
894
+ return { path: path9, branch: branch || void 0 };
849
895
  }).filter((e) => e.path);
850
896
  }
851
897
  async function listCleanupCandidates(repoRoot2, retentionDays) {
@@ -885,7 +931,7 @@ async function removeStaleWorktree(candidate, repoRoot2, logger) {
885
931
  }
886
932
  }
887
933
  async function removeExpiredFiles(dirPath, retentionDays) {
888
- const { readdir: readdir2, stat, unlink, access } = await import("fs/promises");
934
+ const { readdir: readdir2, stat, unlink, access: access2 } = await import("fs/promises");
889
935
  let entries;
890
936
  try {
891
937
  entries = await readdir2(dirPath);
@@ -899,7 +945,7 @@ async function removeExpiredFiles(dirPath, retentionDays) {
899
945
  try {
900
946
  const stats = await stat(entryPath);
901
947
  if (stats.isFile() && now - stats.mtimeMs > retentionMs) {
902
- await access(entryPath, 4);
948
+ await access2(entryPath, 4);
903
949
  await unlink(entryPath);
904
950
  }
905
951
  } catch {
@@ -950,8 +996,8 @@ async function cleanupRepository(options) {
950
996
  }
951
997
 
952
998
  // commands/issue-run.ts
953
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
954
- import { join as join8 } from "path";
999
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1000
+ import { join as join10 } from "path";
955
1001
 
956
1002
  // pr/templates.ts
957
1003
  init_common();
@@ -1014,6 +1060,13 @@ var STAGE_SECTIONS = {
1014
1060
  "branch",
1015
1061
  "verification-commands",
1016
1062
  "artifacts"
1063
+ ],
1064
+ failureResolution: [
1065
+ "issue",
1066
+ "comments",
1067
+ "branch",
1068
+ "verification-commands",
1069
+ "artifacts"
1017
1070
  ]
1018
1071
  };
1019
1072
  function buildRunContextArtifact(options) {
@@ -1207,361 +1260,837 @@ function invalidateAfterBaseRefresh(state) {
1207
1260
  };
1208
1261
  }
1209
1262
 
1210
- // commands/conflict-resolution.ts
1211
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1212
- import { join as join4 } from "path";
1213
- init_common();
1263
+ // failure-resolution/effect-runtime.ts
1264
+ import { Effect } from "effect";
1214
1265
 
1215
- // conflicts/conflict-resolution-artifact.ts
1216
- var ConflictResolutionArtifactProtocolError = class extends Error {
1217
- constructor(message) {
1218
- super(message);
1219
- this.name = "ConflictResolutionArtifactProtocolError";
1266
+ // failure-resolution/types.ts
1267
+ var RebaseConflict = class extends Error {
1268
+ _tag = "RebaseConflict";
1269
+ conflictedPaths;
1270
+ message;
1271
+ constructor(args) {
1272
+ super(args.message);
1273
+ this.name = "RebaseConflict";
1274
+ this.conflictedPaths = args.conflictedPaths;
1275
+ this.message = args.message;
1220
1276
  }
1221
1277
  };
1222
- var VALID_STATUSES = [
1223
- "resolved",
1224
- "ambiguous"
1225
- ];
1226
- var SECTION_HEADING_PATTERN = /^## (Status|Summary|Files|Verification)\s*$/gm;
1227
- function extractSections(output) {
1228
- const sections = [];
1229
- let match;
1230
- const re = new RegExp(SECTION_HEADING_PATTERN);
1231
- while ((match = re.exec(output)) !== null) {
1232
- const heading = match[1];
1233
- sections.push({
1234
- heading,
1235
- startIndex: match.index,
1236
- endIndex: match.index + match[0].length
1278
+ var PublishedHistoryRisk = class extends Error {
1279
+ _tag = "PublishedHistoryRisk";
1280
+ prNumber;
1281
+ prState;
1282
+ constructor(args) {
1283
+ super(`Published PR #${args.prNumber} is ${args.prState}`);
1284
+ this.name = "PublishedHistoryRisk";
1285
+ this.prNumber = args.prNumber;
1286
+ this.prState = args.prState;
1287
+ }
1288
+ };
1289
+ var RecoveryArtifactInvalid = class extends Error {
1290
+ _tag = "RecoveryArtifactInvalid";
1291
+ reason;
1292
+ constructor(args) {
1293
+ super(args.reason);
1294
+ this.name = "RecoveryArtifactInvalid";
1295
+ this.reason = args.reason;
1296
+ }
1297
+ };
1298
+ var SUPPORTED_DECISIONS = /* @__PURE__ */ new Set([
1299
+ "RETRY_STAGE",
1300
+ "HANDOFF_TO_HUMAN",
1301
+ "FAIL_RUN"
1302
+ ]);
1303
+ function isSupportedRecoveryDecision(decision) {
1304
+ return SUPPORTED_DECISIONS.has(decision);
1305
+ }
1306
+ var JSON_BLOCK_RE = /```json\n([\s\S]*?)```/;
1307
+ function parseRecoveryArtifact(markdown, artifactPath) {
1308
+ const match = JSON_BLOCK_RE.exec(markdown);
1309
+ if (!match) {
1310
+ throw new RecoveryArtifactInvalid({
1311
+ reason: `No JSON code block found in artifact at ${artifactPath}`
1237
1312
  });
1238
1313
  }
1239
- return sections;
1240
- }
1241
- function contentAfter(output, sections, section) {
1242
- const index = sections.indexOf(section);
1243
- const nextSection = sections[index + 1];
1244
- const start = section.endIndex;
1245
- const end = nextSection?.startIndex ?? output.length;
1246
- return output.slice(start, end).trim();
1247
- }
1248
- function extractMarker(output) {
1249
- const matches = output.matchAll(
1250
- /<conflict-resolution>\s*(resolved|ambiguous)\s*<\/conflict-resolution>/g
1251
- );
1252
- const results = Array.from(matches);
1253
- if (results.length > 1) {
1254
- throw new ConflictResolutionArtifactProtocolError(
1255
- "Duplicate <conflict-resolution>...</conflict-resolution> markers"
1256
- );
1314
+ let parsed;
1315
+ try {
1316
+ parsed = JSON.parse(match[1]);
1317
+ } catch {
1318
+ throw new RecoveryArtifactInvalid({
1319
+ reason: `Malformed JSON in artifact at ${artifactPath}`
1320
+ });
1257
1321
  }
1258
- return results.length === 1 ? results[0][1] : null;
1259
- }
1260
- function parseFileList(filesContent) {
1261
- return filesContent.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => {
1262
- const rest = line.slice(2).trim();
1263
- const codeMatch = rest.match(/^`([^`]+)`/);
1264
- return codeMatch ? codeMatch[1] : rest;
1265
- });
1322
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1323
+ throw new RecoveryArtifactInvalid({
1324
+ reason: `Expected a JSON object in artifact at ${artifactPath}`
1325
+ });
1326
+ }
1327
+ const json = parsed;
1328
+ if (typeof json.recoveryDecision !== "string" || json.recoveryDecision === "") {
1329
+ throw new RecoveryArtifactInvalid({
1330
+ reason: `Missing or empty recoveryDecision in artifact at ${artifactPath}`
1331
+ });
1332
+ }
1333
+ if (typeof json.summary !== "string" || json.summary === "") {
1334
+ throw new RecoveryArtifactInvalid({
1335
+ reason: `Missing or empty summary in artifact at ${artifactPath}`
1336
+ });
1337
+ }
1338
+ const changedFiles = json.changedFiles;
1339
+ if (!Array.isArray(changedFiles)) {
1340
+ throw new RecoveryArtifactInvalid({
1341
+ reason: `changedFiles must be an array in artifact at ${artifactPath}`
1342
+ });
1343
+ }
1344
+ if (!changedFiles.every((f) => typeof f === "string")) {
1345
+ throw new RecoveryArtifactInvalid({
1346
+ reason: `changedFiles must be an array of strings in artifact at ${artifactPath}`
1347
+ });
1348
+ }
1349
+ if (json.verificationSummary !== void 0 && typeof json.verificationSummary !== "string") {
1350
+ throw new RecoveryArtifactInvalid({
1351
+ reason: `verificationSummary must be a string in artifact at ${artifactPath}`
1352
+ });
1353
+ }
1354
+ if (json.notes !== void 0 && typeof json.notes !== "string") {
1355
+ throw new RecoveryArtifactInvalid({
1356
+ reason: `notes must be a string in artifact at ${artifactPath}`
1357
+ });
1358
+ }
1359
+ if (json.verificationCommands !== void 0) {
1360
+ if (!Array.isArray(json.verificationCommands)) {
1361
+ throw new RecoveryArtifactInvalid({
1362
+ reason: `verificationCommands must be an array in artifact at ${artifactPath}`
1363
+ });
1364
+ }
1365
+ if (!json.verificationCommands.every((c) => typeof c === "string")) {
1366
+ throw new RecoveryArtifactInvalid({
1367
+ reason: `verificationCommands must be an array of strings in artifact at ${artifactPath}`
1368
+ });
1369
+ }
1370
+ }
1371
+ return {
1372
+ raw: markdown,
1373
+ json: {
1374
+ recoveryDecision: json.recoveryDecision,
1375
+ summary: json.summary,
1376
+ changedFiles,
1377
+ verificationSummary: json.verificationSummary,
1378
+ verificationCommands: json.verificationCommands,
1379
+ notes: json.notes
1380
+ },
1381
+ path: artifactPath
1382
+ };
1266
1383
  }
1267
- function parseVerificationTable(content) {
1268
- const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
1269
- const tableLines = lines.filter((l) => l.startsWith("|") && l.endsWith("|"));
1270
- const dataRows = tableLines.slice(2);
1271
- return dataRows.map((row) => {
1272
- const cells = row.split("|").slice(1, -1).map((c) => c.trim());
1384
+ function validateRecoveryDecision(artifact, allowedDecisions) {
1385
+ const decision = artifact.json.recoveryDecision;
1386
+ if (!allowedDecisions.includes(decision)) {
1273
1387
  return {
1274
- command: cells[0] ?? "",
1275
- result: cells[1] ?? "",
1276
- notes: cells[2] ?? ""
1388
+ valid: false,
1389
+ reason: `Decision "${decision}" is not in allowed list: ${allowedDecisions.join(", ")}`
1277
1390
  };
1278
- });
1279
- }
1280
- function parseConflictResolutionArtifact(output) {
1281
- if (!output.trim()) {
1282
- throw new ConflictResolutionArtifactProtocolError(
1283
- "Empty conflict resolution artifact output"
1284
- );
1285
1391
  }
1286
- const sections = extractSections(output);
1287
- const statusSections = sections.filter((s) => s.heading === "Status");
1288
- const summarySections = sections.filter((s) => s.heading === "Summary");
1289
- const filesSections = sections.filter((s) => s.heading === "Files");
1290
- if (statusSections.length > 1) {
1291
- throw new ConflictResolutionArtifactProtocolError(
1292
- 'Duplicate "## Status" sections'
1293
- );
1392
+ if (!isSupportedRecoveryDecision(decision)) {
1393
+ return {
1394
+ valid: false,
1395
+ reason: `Decision "${decision}" is not supported in this slice`
1396
+ };
1294
1397
  }
1295
- if (summarySections.length > 1) {
1296
- throw new ConflictResolutionArtifactProtocolError(
1297
- 'Duplicate "## Summary" sections'
1298
- );
1398
+ return { valid: true, decision };
1399
+ }
1400
+
1401
+ // shared/attempt-log.ts
1402
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2 } from "fs";
1403
+ import { dirname as dirname2, join as join4 } from "path";
1404
+ var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
1405
+ function writeAttemptLog(worktreePath, entry) {
1406
+ const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
1407
+ mkdirSync3(dirname2(logPath), { recursive: true });
1408
+ appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
1409
+ }
1410
+ function readAttemptLog(worktreePath) {
1411
+ const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
1412
+ if (!existsSync2(logPath)) {
1413
+ return [];
1299
1414
  }
1300
- if (filesSections.length > 1) {
1301
- throw new ConflictResolutionArtifactProtocolError(
1302
- 'Duplicate "## Files" sections'
1303
- );
1415
+ const raw = readFileSync2(logPath, "utf-8");
1416
+ const lines = raw.split("\n").filter((l) => l.length > 0);
1417
+ const entries = [];
1418
+ for (const line of lines) {
1419
+ try {
1420
+ const parsed = JSON.parse(line);
1421
+ if (isValidAttemptLogEntry(parsed)) {
1422
+ entries.push(parsed);
1423
+ }
1424
+ } catch {
1425
+ }
1304
1426
  }
1305
- const verificationSections = sections.filter(
1306
- (s) => s.heading === "Verification"
1427
+ return entries;
1428
+ }
1429
+ function isValidAttemptLogEntry(raw) {
1430
+ if (typeof raw !== "object" || raw === null) return false;
1431
+ const obj = raw;
1432
+ if (obj.attemptType !== "stage" && obj.attemptType !== "recovery")
1433
+ return false;
1434
+ if (typeof obj.fingerprint !== "string") return false;
1435
+ if (typeof obj.timestamp !== "string") return false;
1436
+ if (typeof obj.stage !== "string") return false;
1437
+ if (obj.outcome !== "success" && obj.outcome !== "failure" && obj.outcome !== "handoff")
1438
+ return false;
1439
+ return true;
1440
+ }
1441
+ function recoveryBudgetForFailure(worktreePath, fingerprint, maxAttempts) {
1442
+ const entries = readAttemptLog(worktreePath);
1443
+ const used = entries.filter(
1444
+ (e) => e.attemptType === "recovery" && e.fingerprint === fingerprint
1445
+ ).length;
1446
+ const remaining = Math.max(0, maxAttempts - used);
1447
+ return {
1448
+ used,
1449
+ remaining,
1450
+ exhausted: used >= maxAttempts
1451
+ };
1452
+ }
1453
+ function computeFailureFingerprint(stage, failureType) {
1454
+ return `${stage.toLowerCase()}:${failureType}`;
1455
+ }
1456
+
1457
+ // failure-resolution/stage-attempt.ts
1458
+ function createStageAttemptId() {
1459
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1460
+ }
1461
+ function recordStageAttempt(worktreePath, record) {
1462
+ writeAttemptLog(worktreePath, {
1463
+ attemptType: "stage",
1464
+ fingerprint: record.failureFingerprint ?? `${record.stage}:success`,
1465
+ timestamp: record.completedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1466
+ stage: record.stage,
1467
+ outcome: record.outcome === "handoff" ? "handoff" : record.outcome === "success" ? "success" : "failure"
1468
+ });
1469
+ }
1470
+
1471
+ // failure-resolution/effect-runtime.ts
1472
+ function baseRefreshEffect(options) {
1473
+ return Effect.promise(() => refreshStaleIssueBranch(options)).pipe(
1474
+ Effect.flatMap(
1475
+ (result) => {
1476
+ switch (result.status) {
1477
+ case "refreshed":
1478
+ return Effect.succeed({ status: "refreshed" });
1479
+ case "skipped-current":
1480
+ return Effect.succeed({ status: "skipped-current" });
1481
+ case "conflicted":
1482
+ return Effect.fail(
1483
+ new RebaseConflict({
1484
+ conflictedPaths: result.conflictedPaths,
1485
+ message: result.message
1486
+ })
1487
+ );
1488
+ case "refused-published-history":
1489
+ return Effect.fail(
1490
+ new PublishedHistoryRisk({
1491
+ prNumber: result.prNumber,
1492
+ prState: result.prState
1493
+ })
1494
+ );
1495
+ }
1496
+ }
1497
+ )
1307
1498
  );
1308
- const statusSection = statusSections[0];
1309
- const summarySection = summarySections[0];
1310
- const filesSection = filesSections[0];
1311
- if (!statusSection) {
1312
- throw new ConflictResolutionArtifactProtocolError(
1313
- 'Missing required section "## Status"'
1314
- );
1499
+ }
1500
+ async function runBaseRefreshAttempt(options) {
1501
+ const attemptId = createStageAttemptId();
1502
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1503
+ const program = baseRefreshEffect(options).pipe(
1504
+ Effect.tapBoth({
1505
+ onSuccess: (success) => Effect.sync(() => {
1506
+ recordStageAttempt(options.worktreePath, {
1507
+ id: attemptId,
1508
+ stage: "baseRefresh",
1509
+ startedAt,
1510
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1511
+ outcome: "success"
1512
+ });
1513
+ }),
1514
+ onFailure: (failure) => Effect.sync(() => {
1515
+ recordStageAttempt(options.worktreePath, {
1516
+ id: attemptId,
1517
+ stage: "baseRefresh",
1518
+ startedAt,
1519
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1520
+ outcome: "failure",
1521
+ failureFingerprint: computeFailureFingerprint(
1522
+ "baseRefresh",
1523
+ failure._tag
1524
+ ),
1525
+ failureType: failure._tag
1526
+ });
1527
+ })
1528
+ })
1529
+ );
1530
+ return Effect.runPromiseExit(program);
1531
+ }
1532
+
1533
+ // commands/conflict-resolution.ts
1534
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1535
+ import { join as join5 } from "path";
1536
+ init_common();
1537
+ var CONFLICT_MARKER_PATTERN = /<<<<<<<|=======|>>>>>>>/m;
1538
+ async function hasUnresolvedConflictMarkers(worktreePath, files) {
1539
+ for (const file of files) {
1540
+ const filePath = join5(worktreePath, file);
1541
+ try {
1542
+ const content = readFileSync3(filePath, "utf-8");
1543
+ if (CONFLICT_MARKER_PATTERN.test(content)) {
1544
+ return true;
1545
+ }
1546
+ } catch {
1547
+ }
1315
1548
  }
1316
- if (!summarySection) {
1317
- throw new ConflictResolutionArtifactProtocolError(
1318
- 'Missing required section "## Summary"'
1319
- );
1549
+ return false;
1550
+ }
1551
+
1552
+ // commands/issue-run.ts
1553
+ import { Exit as Exit2 } from "effect";
1554
+
1555
+ // serena/baseline.ts
1556
+ init_common();
1557
+ import path3 from "path";
1558
+ import { access, mkdir as mkdir2 } from "fs/promises";
1559
+ function resolveSerenaPaths(repoRoot2, dataDir = ".pourkit/serena/") {
1560
+ const rootDir = path3.isAbsolute(dataDir) ? path3.normalize(dataDir) : path3.resolve(repoRoot2, dataDir);
1561
+ return {
1562
+ rootDir,
1563
+ baselineWorktreePath: path3.join(rootDir, "baseline", "active-repo"),
1564
+ dataDir: path3.join(rootDir, "data")
1565
+ };
1566
+ }
1567
+ async function pathExists(dirPath) {
1568
+ try {
1569
+ await access(dirPath);
1570
+ return true;
1571
+ } catch {
1572
+ return false;
1320
1573
  }
1321
- if (!filesSection) {
1322
- throw new ConflictResolutionArtifactProtocolError(
1323
- 'Missing required section "## Files"'
1324
- );
1574
+ }
1575
+ async function isGitRepoRoot(repoPath) {
1576
+ try {
1577
+ const result = await execCapture("git", ["rev-parse", "--show-toplevel"], {
1578
+ cwd: repoPath,
1579
+ label: "git rev-parse --show-toplevel"
1580
+ });
1581
+ return path3.resolve(result.stdout.trim()) === path3.resolve(repoPath);
1582
+ } catch {
1583
+ return false;
1325
1584
  }
1326
- const statusRaw = contentAfter(output, sections, statusSection);
1327
- const summary = contentAfter(output, sections, summarySection);
1328
- const filesContent = contentAfter(output, sections, filesSection);
1329
- if (!statusRaw) {
1330
- throw new ConflictResolutionArtifactProtocolError(
1331
- '"## Status" section is empty'
1585
+ }
1586
+ async function ensureBaselineWorktree(options) {
1587
+ const paths = resolveSerenaPaths(options.repoRoot, options.dataDir);
1588
+ await mkdir2(paths.rootDir, { recursive: true });
1589
+ await mkdir2(paths.dataDir, { recursive: true });
1590
+ if (!await pathExists(paths.baselineWorktreePath)) {
1591
+ await mkdir2(path3.dirname(paths.baselineWorktreePath), { recursive: true });
1592
+ await execCapture(
1593
+ "git",
1594
+ ["clone", options.repoRoot, paths.baselineWorktreePath],
1595
+ {
1596
+ cwd: options.repoRoot,
1597
+ label: "git clone baseline worktree"
1598
+ }
1332
1599
  );
1600
+ return paths;
1333
1601
  }
1334
- if (!summary) {
1335
- throw new ConflictResolutionArtifactProtocolError(
1336
- '"## Summary" section is empty'
1602
+ if (!await isGitRepoRoot(paths.baselineWorktreePath)) {
1603
+ throw new Error(
1604
+ `Serena baseline worktree exists but is not a git repo: ${paths.baselineWorktreePath}`
1337
1605
  );
1338
1606
  }
1339
- if (!VALID_STATUSES.includes(statusRaw)) {
1340
- throw new ConflictResolutionArtifactProtocolError(
1341
- `Unsupported status "${statusRaw}". Allowed statuses: ${VALID_STATUSES.join(", ")}`
1342
- );
1607
+ return paths;
1608
+ }
1609
+ async function getSerenaBaselineStatus(options) {
1610
+ const remoteName = options.remoteName ?? "origin";
1611
+ const paths = resolveSerenaPaths(options.repoRoot, options.dataDir);
1612
+ const expectedRef = `${remoteName}/${options.baseBranch}`;
1613
+ if (!await pathExists(paths.baselineWorktreePath)) {
1614
+ return {
1615
+ exists: false,
1616
+ baselineWorktreePath: paths.baselineWorktreePath,
1617
+ expectedRef,
1618
+ fresh: false
1619
+ };
1343
1620
  }
1344
- const markerStatus = extractMarker(output);
1345
- if (!markerStatus) {
1346
- throw new ConflictResolutionArtifactProtocolError(
1347
- "Missing <conflict-resolution>...</conflict-resolution> marker"
1348
- );
1621
+ if (!await isGitRepoRoot(paths.baselineWorktreePath)) {
1622
+ return {
1623
+ exists: false,
1624
+ baselineWorktreePath: paths.baselineWorktreePath,
1625
+ expectedRef,
1626
+ fresh: false
1627
+ };
1349
1628
  }
1350
- if (markerStatus !== statusRaw) {
1351
- throw new ConflictResolutionArtifactProtocolError(
1352
- `Conflict resolution status "${statusRaw}" does not match marker "${markerStatus}"`
1353
- );
1629
+ let currentCommit;
1630
+ try {
1631
+ const currentResult = await execCapture("git", ["rev-parse", "HEAD"], {
1632
+ cwd: paths.baselineWorktreePath,
1633
+ label: "git rev-parse HEAD"
1634
+ });
1635
+ currentCommit = currentResult.stdout.trim() || void 0;
1636
+ } catch {
1637
+ return {
1638
+ exists: true,
1639
+ baselineWorktreePath: paths.baselineWorktreePath,
1640
+ expectedRef,
1641
+ fresh: false
1642
+ };
1354
1643
  }
1355
- let verification;
1356
- if (verificationSections.length > 0) {
1357
- const verificationContent = contentAfter(
1358
- output,
1359
- sections,
1360
- verificationSections[0]
1644
+ let expectedCommit;
1645
+ try {
1646
+ const expectedResult = await execCapture(
1647
+ "git",
1648
+ ["rev-parse", expectedRef],
1649
+ {
1650
+ cwd: paths.baselineWorktreePath,
1651
+ label: `git rev-parse ${expectedRef}`
1652
+ }
1361
1653
  );
1362
- verification = parseVerificationTable(verificationContent);
1654
+ expectedCommit = expectedResult.stdout.trim() || void 0;
1655
+ } catch {
1656
+ return {
1657
+ exists: true,
1658
+ baselineWorktreePath: paths.baselineWorktreePath,
1659
+ currentCommit,
1660
+ expectedRef,
1661
+ fresh: false
1662
+ };
1363
1663
  }
1364
1664
  return {
1365
- status: statusRaw,
1366
- summary,
1367
- files: parseFileList(filesContent),
1368
- verification,
1369
- raw: output
1665
+ exists: true,
1666
+ baselineWorktreePath: paths.baselineWorktreePath,
1667
+ currentCommit,
1668
+ expectedRef,
1669
+ fresh: currentCommit === expectedCommit
1370
1670
  };
1371
1671
  }
1672
+ async function refreshSerenaBaseline(options) {
1673
+ const remoteName = options.remoteName ?? "origin";
1674
+ const paths = await ensureBaselineWorktree(options);
1675
+ await execCapture("git", ["fetch", remoteName, options.baseBranch], {
1676
+ cwd: paths.baselineWorktreePath,
1677
+ label: "git fetch baseline branch"
1678
+ });
1679
+ await execCapture(
1680
+ "git",
1681
+ ["checkout", "--detach", `${remoteName}/${options.baseBranch}`],
1682
+ {
1683
+ cwd: paths.baselineWorktreePath,
1684
+ label: "git checkout detached baseline branch"
1685
+ }
1686
+ );
1687
+ return getSerenaBaselineStatus(options);
1688
+ }
1372
1689
 
1373
- // commands/conflict-resolution.ts
1374
- function loadConflictResolutionPrompt(repoRoot2, promptTemplate, artifactPath) {
1375
- const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
1376
- const promptBody = existsSync2(promptPath) ? readFileSync2(promptPath, "utf-8") : promptTemplate;
1377
- return `${promptBody}
1378
-
1379
- ## Shared Run Context
1690
+ // serena/container.ts
1691
+ init_common();
1692
+ import { mkdir as mkdir3 } from "fs/promises";
1693
+ import path4 from "path";
1694
+ var DEFAULT_CONTAINER_NAME = "pourkit-serena-sidecar";
1695
+ var MCP_CONTAINER_PORT = 9121;
1696
+ var DASHBOARD_CONTAINER_PORT = 24282;
1697
+ var SERENA_DATA_MOUNT = "/workspaces/serena-data";
1698
+ function resolveSidecarUrls(options) {
1699
+ return {
1700
+ containerName: options.containerName ?? DEFAULT_CONTAINER_NAME,
1701
+ mcpUrl: options.mcpUrl ?? `http://localhost:${options.mcpPort}/mcp`,
1702
+ dashboardUrl: `http://localhost:${options.dashboardPort}`
1703
+ };
1704
+ }
1705
+ async function inspectSidecarContainer(containerName) {
1706
+ try {
1707
+ const result = await execCapture("docker", ["inspect", containerName]);
1708
+ const parsed = JSON.parse(result.stdout);
1709
+ return {
1710
+ exists: true,
1711
+ running: Boolean(parsed[0]?.State?.Running)
1712
+ };
1713
+ } catch {
1714
+ return {
1715
+ exists: false,
1716
+ running: false
1717
+ };
1718
+ }
1719
+ }
1720
+ async function readSidecarStatus(options) {
1721
+ const { containerName, mcpUrl, dashboardUrl } = resolveSidecarUrls(options);
1722
+ const container = await inspectSidecarContainer(containerName);
1723
+ return {
1724
+ running: container.running,
1725
+ mcpUrl,
1726
+ dashboardUrl,
1727
+ containerName
1728
+ };
1729
+ }
1730
+ function buildStartArgs(options, containerName) {
1731
+ return [
1732
+ "run",
1733
+ "-d",
1734
+ "--name",
1735
+ containerName,
1736
+ "--restart",
1737
+ "unless-stopped",
1738
+ "-p",
1739
+ `${options.mcpPort}:${MCP_CONTAINER_PORT}`,
1740
+ "-p",
1741
+ `${options.dashboardPort}:${DASHBOARD_CONTAINER_PORT}`,
1742
+ "-v",
1743
+ `${options.baselineWorktreePath}:/workspaces/pourkit`,
1744
+ "-v",
1745
+ `${options.dataDir}:${SERENA_DATA_MOUNT}`,
1746
+ "-e",
1747
+ `SERENA_HOME=${SERENA_DATA_MOUNT}/config`,
1748
+ options.image,
1749
+ "serena",
1750
+ "start-mcp-server",
1751
+ "--transport",
1752
+ "streamable-http",
1753
+ "--port",
1754
+ String(MCP_CONTAINER_PORT),
1755
+ "--host",
1756
+ "0.0.0.0"
1757
+ ];
1758
+ }
1759
+ async function getSerenaSidecarStatus(options) {
1760
+ return readSidecarStatus(options);
1761
+ }
1762
+ async function startSerenaSidecar(options) {
1763
+ const { containerName } = resolveSidecarUrls(options);
1764
+ const container = await inspectSidecarContainer(containerName);
1765
+ if (container.exists) {
1766
+ if (!container.running) {
1767
+ await execCapture("docker", ["start", containerName]);
1768
+ }
1769
+ return readSidecarStatus(options);
1770
+ }
1771
+ await execCapture("docker", buildStartArgs(options, containerName));
1772
+ return readSidecarStatus(options);
1773
+ }
1774
+ async function indexSerenaProject(options) {
1775
+ const { containerName } = resolveSidecarUrls(options);
1776
+ await execCapture("docker", [
1777
+ "exec",
1778
+ containerName,
1779
+ "serena",
1780
+ "project",
1781
+ "create",
1782
+ "--language",
1783
+ "typescript",
1784
+ "--index",
1785
+ "/workspaces/pourkit"
1786
+ ]);
1787
+ }
1788
+ async function stopSerenaSidecar(options) {
1789
+ const { containerName } = resolveSidecarUrls(options);
1790
+ try {
1791
+ await execCapture("docker", ["stop", containerName]);
1792
+ } catch {
1793
+ }
1794
+ return readSidecarStatus(options);
1795
+ }
1796
+ async function prepareSerenaSidecarConfig(options) {
1797
+ const configDir = path4.join(options.dataDir, "config");
1798
+ await mkdir3(configDir, { recursive: true });
1799
+ }
1380
1800
 
1381
- Read the selected issue requirements, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
1801
+ // serena/preflight.ts
1802
+ var SERENA_MCP_PORT = 9121;
1803
+ var SERENA_DASHBOARD_PORT = 24282;
1804
+ var SERENA_IMAGE = "ghcr.io/oraios/serena:latest";
1805
+ function sidecarOptions(paths, mcpUrl) {
1806
+ return {
1807
+ baselineWorktreePath: paths.baselineWorktreePath,
1808
+ dataDir: paths.dataDir,
1809
+ mcpPort: SERENA_MCP_PORT,
1810
+ dashboardPort: SERENA_DASHBOARD_PORT,
1811
+ image: SERENA_IMAGE,
1812
+ mcpUrl
1813
+ };
1814
+ }
1815
+ async function canReachMcp(url) {
1816
+ for (let attempt = 0; attempt < 10; attempt += 1) {
1817
+ try {
1818
+ await fetch(url, { method: "GET", signal: AbortSignal.timeout(500) });
1819
+ return true;
1820
+ } catch {
1821
+ if (attempt < 9) {
1822
+ await new Promise((resolve) => setTimeout(resolve, 100));
1823
+ }
1824
+ }
1825
+ }
1826
+ return false;
1827
+ }
1828
+ function formatError(error) {
1829
+ return error instanceof Error ? error.message : String(error);
1830
+ }
1831
+ async function prepareSerenaForTarget(options) {
1832
+ if (!options.enabled) {
1833
+ return { enabled: false };
1834
+ }
1835
+ try {
1836
+ const paths = await ensureBaselineWorktree({
1837
+ repoRoot: options.repoRoot,
1838
+ dataDir: options.dataDir
1839
+ });
1840
+ await prepareSerenaSidecarConfig({
1841
+ baselineWorktreePath: paths.baselineWorktreePath,
1842
+ dataDir: paths.dataDir
1843
+ });
1844
+ const status = options.autoStart ? await startSerenaSidecar(sidecarOptions(paths, options.mcpUrl)) : await getSerenaSidecarStatus(sidecarOptions(paths, options.mcpUrl));
1845
+ const mcpReachable = await canReachMcp(options.mcpUrl);
1846
+ if (!mcpReachable) {
1847
+ return {
1848
+ enabled: true,
1849
+ available: false,
1850
+ error: status.running ? `Serena MCP is not reachable at ${options.mcpUrl}` : `Serena sidecar is not running for target ${options.targetName}`
1851
+ };
1852
+ }
1853
+ await refreshSerenaBaseline({
1854
+ repoRoot: options.repoRoot,
1855
+ dataDir: options.dataDir,
1856
+ baseBranch: options.baseBranch
1857
+ });
1858
+ return {
1859
+ enabled: true,
1860
+ available: true,
1861
+ mcpUrl: options.mcpUrl
1862
+ };
1863
+ } catch (error) {
1864
+ return {
1865
+ enabled: true,
1866
+ available: false,
1867
+ error: formatError(error)
1868
+ };
1869
+ }
1870
+ }
1382
1871
 
1383
- ## Output
1872
+ // failure-resolution/failure-resolution-agent.ts
1873
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1874
+ import { join as join6 } from "path";
1384
1875
 
1385
- Write your resolution to: ${artifactPath}
1876
+ // failure-resolution/recovery-policy.ts
1877
+ function isSecuritySensitiveFailure(failure) {
1878
+ return failure instanceof PublishedHistoryRisk;
1879
+ }
1880
+ async function evaluateRecoveryPolicy(params) {
1881
+ if (isSecuritySensitiveFailure(params.failure)) {
1882
+ return {
1883
+ decision: "HANDOFF_TO_HUMAN",
1884
+ reason: "Security-sensitive failure \u2014 AI recovery bypassed"
1885
+ };
1886
+ }
1887
+ const budget = recoveryBudgetForFailure(
1888
+ params.worktreePath,
1889
+ params.fingerprint,
1890
+ params.maxAttempts
1891
+ );
1892
+ if (budget.exhausted) {
1893
+ return {
1894
+ decision: "HANDOFF_TO_HUMAN",
1895
+ reason: `Recovery budget exhausted (${budget.used}/${params.maxAttempts})`
1896
+ };
1897
+ }
1898
+ if (!params.allowedDecisions.includes(params.agentRecommendedDecision)) {
1899
+ return {
1900
+ decision: "HANDOFF_TO_HUMAN",
1901
+ reason: `Agent recommended ${params.agentRecommendedDecision} which is not allowed`
1902
+ };
1903
+ }
1904
+ if (params.agentRecommendedDecision === "FAIL_RUN") {
1905
+ return { decision: "FAIL_RUN", reason: "Agent recommended FAIL_RUN" };
1906
+ }
1907
+ return {
1908
+ decision: params.agentRecommendedDecision,
1909
+ reason: "Agent recommendation accepted"
1910
+ };
1911
+ }
1386
1912
 
1387
- Do not provide a separate chat response. The runner only reads the file above.`;
1913
+ // failure-resolution/failure-resolution-agent.ts
1914
+ function constructFailureResolutionPacket(failure, context) {
1915
+ return {
1916
+ failureType: "RebaseConflict",
1917
+ stageName: context.stageName,
1918
+ attemptNumber: context.attemptNumber,
1919
+ worktreePath: context.worktreePath,
1920
+ branchName: context.branchName,
1921
+ baseBranch: context.baseBranch,
1922
+ conflictedPaths: failure.conflictedPaths,
1923
+ failureSummary: failure.message,
1924
+ maxAttempts: context.maxAttempts,
1925
+ allowedDecisions: context.allowedDecisions,
1926
+ artifactTarget: context.artifactTarget
1927
+ };
1388
1928
  }
1389
- async function runConflictResolutionOnce(options) {
1929
+ async function runFailureResolutionAgent(options) {
1390
1930
  const {
1391
1931
  executionProvider,
1392
1932
  config,
1393
1933
  target,
1394
- issue,
1395
- branchName,
1934
+ failure,
1935
+ packet,
1396
1936
  worktreePath,
1397
1937
  repoRoot: repoRoot2,
1398
- conflictedPaths,
1399
- attempt,
1400
1938
  logger
1401
1939
  } = options;
1402
- const strategyCr = target.strategy.conflictResolution;
1403
- if (!strategyCr) {
1404
- return { status: "failed", message: "No conflictResolution configured" };
1405
- }
1406
- const artifactPath = `.pourkit/.tmp/conflict-resolution/attempt-${attempt}.md`;
1407
- const prompt = loadConflictResolutionPrompt(
1408
- repoRoot2,
1409
- strategyCr.promptTemplate,
1410
- artifactPath
1940
+ const frConfig = target.strategy.failureResolution;
1941
+ const artifactPath = packet.artifactTarget;
1942
+ const fullArtifactPath = join6(worktreePath, artifactPath);
1943
+ const fingerprint = computeFailureFingerprint(
1944
+ "baseRefresh",
1945
+ "RebaseConflict"
1411
1946
  );
1412
- const runContextArtifact = buildRunContextArtifact({
1413
- issue,
1414
- target,
1415
- branchName,
1416
- sections: STAGE_SECTIONS.conflictResolution
1417
- });
1947
+ const prompt = [
1948
+ `# Failure Resolution: ${packet.failureType}`,
1949
+ "",
1950
+ "## Failure Context",
1951
+ "",
1952
+ "```json",
1953
+ JSON.stringify(packet, null, 2),
1954
+ "```",
1955
+ "",
1956
+ "## Instructions",
1957
+ "",
1958
+ `Write your resolution to: ${artifactPath}`,
1959
+ "Include a ```json block with: recoveryDecision, summary, changedFiles, verificationSummary (optional), verificationCommands (optional), notes (optional).",
1960
+ "",
1961
+ "Allowed decisions: " + packet.allowedDecisions.join(", ")
1962
+ ].join("\n");
1418
1963
  const executionResult = await executionProvider.execute({
1419
- stage: "conflictResolution",
1420
- agent: strategyCr.agent,
1421
- model: strategyCr.model,
1964
+ stage: "failureResolution",
1965
+ agent: frConfig.agent,
1966
+ model: frConfig.model,
1422
1967
  prompt,
1423
1968
  target,
1424
1969
  repoRoot: repoRoot2,
1425
- branchName,
1970
+ branchName: packet.branchName,
1426
1971
  sandbox: config.sandbox,
1427
1972
  autoApprove: true,
1428
1973
  worktreePath,
1429
1974
  artifactPath,
1430
- artifacts: [runContextArtifact],
1975
+ artifacts: [],
1431
1976
  logger
1432
1977
  });
1433
1978
  if (!executionResult.success) {
1979
+ await writeRecoveryAttempt(
1980
+ worktreePath,
1981
+ "failure",
1982
+ fingerprint,
1983
+ `Agent execution failed: ${executionResult.error}`,
1984
+ void 0,
1985
+ "HANDOFF_TO_HUMAN"
1986
+ );
1434
1987
  return {
1435
- status: "failed",
1436
- message: executionResult.error ?? "Conflict resolution agent execution failed"
1988
+ status: "handoff",
1989
+ decision: "HANDOFF_TO_HUMAN",
1990
+ reason: `Agent execution failed: ${executionResult.error}`
1437
1991
  };
1438
1992
  }
1439
- const fullArtifactPath = join4(worktreePath, artifactPath);
1440
- if (!existsSync2(fullArtifactPath)) {
1993
+ if (!existsSync4(fullArtifactPath)) {
1994
+ await writeRecoveryAttempt(
1995
+ worktreePath,
1996
+ "failure",
1997
+ fingerprint,
1998
+ "Agent did not write artifact",
1999
+ void 0,
2000
+ "HANDOFF_TO_HUMAN"
2001
+ );
1441
2002
  return {
1442
- status: "failed",
1443
- artifactPath,
1444
- message: "Conflict resolution agent completed but did not write artifact"
2003
+ status: "handoff",
2004
+ decision: "HANDOFF_TO_HUMAN",
2005
+ reason: "Agent did not write artifact"
1445
2006
  };
1446
2007
  }
1447
- let artifactContent;
2008
+ let artifact;
1448
2009
  try {
1449
- artifactContent = readFileSync2(fullArtifactPath, "utf-8");
2010
+ const md = readFileSync4(fullArtifactPath, "utf-8");
2011
+ artifact = parseRecoveryArtifact(md, artifactPath);
1450
2012
  } catch (error) {
2013
+ const reason = error instanceof Error ? error.message : "Failed to parse artifact";
2014
+ await writeRecoveryAttempt(
2015
+ worktreePath,
2016
+ "failure",
2017
+ fingerprint,
2018
+ reason,
2019
+ void 0,
2020
+ "HANDOFF_TO_HUMAN"
2021
+ );
2022
+ return { status: "handoff", decision: "HANDOFF_TO_HUMAN", reason };
2023
+ }
2024
+ const validation = validateRecoveryDecision(
2025
+ artifact,
2026
+ packet.allowedDecisions
2027
+ );
2028
+ if (!validation.valid) {
2029
+ await writeRecoveryAttempt(
2030
+ worktreePath,
2031
+ "failure",
2032
+ fingerprint,
2033
+ validation.reason,
2034
+ void 0,
2035
+ "HANDOFF_TO_HUMAN"
2036
+ );
1451
2037
  return {
1452
- status: "failed",
1453
- artifactPath,
1454
- message: `Failed to read conflict resolution artifact: ${error instanceof Error ? error.message : String(error)}`
2038
+ status: "handoff",
2039
+ decision: "HANDOFF_TO_HUMAN",
2040
+ reason: validation.reason
1455
2041
  };
1456
2042
  }
1457
- let parsed;
1458
- try {
1459
- parsed = parseConflictResolutionArtifact(artifactContent);
1460
- } catch (error) {
1461
- if (error instanceof ConflictResolutionArtifactProtocolError) {
1462
- return {
1463
- status: "failed",
1464
- artifactPath,
1465
- message: `Invalid conflict resolution artifact: ${error.message}`
1466
- };
1467
- }
1468
- throw error;
2043
+ const policyResult = await evaluateRecoveryPolicy({
2044
+ failure,
2045
+ worktreePath,
2046
+ fingerprint,
2047
+ maxAttempts: packet.maxAttempts,
2048
+ agentRecommendedDecision: validation.decision,
2049
+ allowedDecisions: packet.allowedDecisions
2050
+ });
2051
+ await writeRecoveryAttempt(
2052
+ worktreePath,
2053
+ policyResult.decision === "HANDOFF_TO_HUMAN" ? "handoff" : policyResult.decision === "FAIL_RUN" ? "failure" : "success",
2054
+ fingerprint,
2055
+ policyResult.reason,
2056
+ artifactPath,
2057
+ policyResult.decision
2058
+ );
2059
+ if (policyResult.decision === "HANDOFF_TO_HUMAN") {
2060
+ return {
2061
+ status: "handoff",
2062
+ decision: "HANDOFF_TO_HUMAN",
2063
+ reason: policyResult.reason
2064
+ };
1469
2065
  }
1470
- if (parsed.status === "ambiguous") {
2066
+ if (policyResult.decision === "FAIL_RUN") {
1471
2067
  return {
1472
- status: "ambiguous",
1473
- artifactPath,
1474
- message: parsed.summary
2068
+ status: "fail-run",
2069
+ decision: "FAIL_RUN",
2070
+ reason: policyResult.reason
1475
2071
  };
1476
2072
  }
1477
2073
  return {
1478
- status: "resolved",
1479
- artifactPath,
1480
- files: parsed.files
2074
+ status: "recovered",
2075
+ decision: policyResult.decision,
2076
+ artifact
1481
2077
  };
1482
2078
  }
1483
- var CONFLICT_MARKER_PATTERN = /<<<<<<<|=======|>>>>>>>/m;
1484
- async function hasUnresolvedConflictMarkers(worktreePath, files) {
1485
- for (const file of files) {
1486
- const filePath = join4(worktreePath, file);
1487
- try {
1488
- const content = readFileSync2(filePath, "utf-8");
1489
- if (CONFLICT_MARKER_PATTERN.test(content)) {
1490
- return true;
1491
- }
1492
- } catch {
1493
- }
1494
- }
1495
- return false;
1496
- }
1497
- async function runConflictResolutionLoop(options) {
1498
- const { worktreePath, maxAttempts, logger, initialConflictedPaths } = options;
1499
- let attempt = 0;
1500
- let conflictedPaths = initialConflictedPaths;
1501
- while (attempt < maxAttempts && conflictedPaths.length > 0) {
1502
- attempt++;
1503
- const crResult = await runConflictResolutionOnce({
1504
- ...options,
1505
- conflictedPaths,
1506
- attempt
1507
- });
1508
- if (crResult.status !== "resolved") {
1509
- const message = crResult.status === "ambiguous" ? crResult.message : crResult.message ?? "Conflict resolution agent execution failed";
1510
- return { status: crResult.status, attempts: attempt, message };
1511
- }
1512
- const markersRemain = await hasUnresolvedConflictMarkers(
1513
- worktreePath,
1514
- conflictedPaths
1515
- );
1516
- if (markersRemain) {
1517
- return {
1518
- status: "ambiguous",
1519
- attempts: attempt,
1520
- message: "Conflict resolution agent resolved artifact but conflict markers remain in files"
1521
- };
1522
- }
1523
- await execCapture("git", ["add", ...conflictedPaths], {
1524
- cwd: worktreePath,
1525
- logger,
1526
- label: "git add conflicted paths"
1527
- });
1528
- try {
1529
- await execCapture("git", ["rebase", "--continue"], {
1530
- cwd: worktreePath,
1531
- logger,
1532
- label: "git rebase --continue"
1533
- });
1534
- conflictedPaths = [];
1535
- } catch (error) {
1536
- const statusResult = await execCapture("git", ["status", "--porcelain"], {
1537
- cwd: worktreePath,
1538
- logger,
1539
- label: "git status"
1540
- });
1541
- conflictedPaths = statusResult.stdout.split("\n").filter((line) => /^(AA|DD|UU|AU|UA|DU|UD)\s/.test(line)).map((line) => line.slice(3).trim()).filter(Boolean);
1542
- if (conflictedPaths.length === 0) {
1543
- const rebaseErrorMessage = error instanceof Error ? error.message : String(error);
1544
- return {
1545
- status: "failed",
1546
- attempts: attempt,
1547
- message: `git rebase --continue failed with no remaining conflicts: ${rebaseErrorMessage}`
1548
- };
1549
- }
1550
- }
1551
- }
1552
- if (conflictedPaths.length > 0) {
1553
- return {
1554
- status: "exhausted",
1555
- attempts: attempt,
1556
- message: `Conflict resolution maxAttempts (${maxAttempts}) exhausted with remaining conflicts`
1557
- };
1558
- }
1559
- return { status: "completed", attempts: attempt };
2079
+ async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary, artifactRef, decision) {
2080
+ writeAttemptLog(worktreePath, {
2081
+ attemptType: "recovery",
2082
+ fingerprint,
2083
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2084
+ stage: "baseRefresh",
2085
+ outcome,
2086
+ artifactRef,
2087
+ decision: decision ?? (outcome === "handoff" ? "HANDOFF_TO_HUMAN" : outcome === "success" ? "RETRY_STAGE" : void 0)
2088
+ });
1560
2089
  }
1561
2090
 
1562
2091
  // commands/pr-description-agent.ts
1563
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1564
- import { dirname as dirname2, join as join6 } from "path";
2092
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "fs";
2093
+ import { dirname as dirname3, join as join8 } from "path";
1565
2094
 
1566
2095
  // pr/pr-description.ts
1567
2096
  var CONVENTIONAL_TITLE_PATTERN = /^(feat|fix|perf|refactor|docs|test|chore|ci|build)(\([^)]+\))?!?:\s+\S/;
@@ -1571,11 +2100,11 @@ var PrDescriptionProtocolError = class extends Error {
1571
2100
  this.name = "PrDescriptionProtocolError";
1572
2101
  }
1573
2102
  };
1574
- var SECTION_HEADING_PATTERN2 = /^## (PR Title|PR Body)\s*$/gm;
1575
- function extractSections2(output) {
2103
+ var SECTION_HEADING_PATTERN = /^## (PR Title|PR Body)\s*$/gm;
2104
+ function extractSections(output) {
1576
2105
  const sections = [];
1577
2106
  let match;
1578
- const re = new RegExp(SECTION_HEADING_PATTERN2);
2107
+ const re = new RegExp(SECTION_HEADING_PATTERN);
1579
2108
  while ((match = re.exec(output)) !== null) {
1580
2109
  const heading = match[1];
1581
2110
  sections.push({
@@ -1586,7 +2115,7 @@ function extractSections2(output) {
1586
2115
  }
1587
2116
  return sections;
1588
2117
  }
1589
- function contentAfter2(output, sections, section) {
2118
+ function contentAfter(output, sections, section) {
1590
2119
  const index = sections.indexOf(section);
1591
2120
  const nextSection = sections[index + 1];
1592
2121
  const start = section.endIndex;
@@ -1594,7 +2123,7 @@ function contentAfter2(output, sections, section) {
1594
2123
  return output.slice(start, end).trim();
1595
2124
  }
1596
2125
  function parsePrDescription(output) {
1597
- const sections = extractSections2(output);
2126
+ const sections = extractSections(output);
1598
2127
  const titleSections = sections.filter((s) => s.heading === "PR Title");
1599
2128
  const bodySections = sections.filter((s) => s.heading === "PR Body");
1600
2129
  if (titleSections.length === 0) {
@@ -1617,8 +2146,8 @@ function parsePrDescription(output) {
1617
2146
  `Duplicate "## PR Body" sections found (${bodySections.length})`
1618
2147
  );
1619
2148
  }
1620
- const title = contentAfter2(output, sections, titleSections[0]);
1621
- const body = contentAfter2(output, sections, bodySections[0]);
2149
+ const title = contentAfter(output, sections, titleSections[0]);
2150
+ const body = contentAfter(output, sections, bodySections[0]);
1622
2151
  if (title.length === 0) {
1623
2152
  throw new PrDescriptionProtocolError('"## PR Title" section is empty');
1624
2153
  }
@@ -1649,7 +2178,7 @@ function inferConventionalType(commitSummaries) {
1649
2178
 
1650
2179
  // pr/pr-description-context.ts
1651
2180
  init_common();
1652
- import { join as join5 } from "path";
2181
+ import { join as join7 } from "path";
1653
2182
  import { readFile } from "fs/promises";
1654
2183
  async function collectFinalizerContext(options) {
1655
2184
  const { targetBase, branchName, worktreePath, reviewArtifactPath, logger } = options;
@@ -1710,7 +2239,7 @@ async function readReviewArtifact(artifactPath) {
1710
2239
  return content;
1711
2240
  }
1712
2241
  function buildFinalizerPrompt(context, promptTemplate) {
1713
- const artifactPathInWorktree = join5(
2242
+ const artifactPathInWorktree = join7(
1714
2243
  ".pourkit",
1715
2244
  ".tmp",
1716
2245
  "finalizer",
@@ -1788,13 +2317,13 @@ async function runFinalizerAgent(options) {
1788
2317
  finalizer.promptTemplate
1789
2318
  );
1790
2319
  const prompt = buildFinalizerPrompt(context, resolvedPrompt);
1791
- const artifactPathInWorktree = join6(
2320
+ const artifactPathInWorktree = join8(
1792
2321
  ".pourkit",
1793
2322
  ".tmp",
1794
2323
  "finalizer",
1795
2324
  "agent-output.md"
1796
2325
  );
1797
- const artifactPath = join6(worktreePath, artifactPathInWorktree);
2326
+ const artifactPath = join8(worktreePath, artifactPathInWorktree);
1798
2327
  prepareArtifactPath(artifactPath);
1799
2328
  let output = "";
1800
2329
  let parsed;
@@ -1859,24 +2388,24 @@ async function runFinalizerAgent(options) {
1859
2388
  }
1860
2389
  function loadFinalizerPrompt(repoRoot2, promptTemplate) {
1861
2390
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
1862
- if (existsSync3(promptPath)) {
1863
- return readFileSync3(promptPath, "utf-8");
2391
+ if (existsSync5(promptPath)) {
2392
+ return readFileSync5(promptPath, "utf-8");
1864
2393
  }
1865
2394
  return promptTemplate;
1866
2395
  }
1867
2396
  function prepareArtifactPath(artifactPath) {
1868
- mkdirSync3(dirname2(artifactPath), { recursive: true });
1869
- if (existsSync3(artifactPath)) {
2397
+ mkdirSync4(dirname3(artifactPath), { recursive: true });
2398
+ if (existsSync5(artifactPath)) {
1870
2399
  rmSync(artifactPath);
1871
2400
  }
1872
2401
  }
1873
2402
  function readAgentOutput(artifactPath) {
1874
- if (!existsSync3(artifactPath)) {
2403
+ if (!existsSync5(artifactPath)) {
1875
2404
  throw new Error(
1876
2405
  `Finalizer agent did not produce output at ${artifactPath}`
1877
2406
  );
1878
2407
  }
1879
- const output = readFileSync3(artifactPath, "utf-8");
2408
+ const output = readFileSync5(artifactPath, "utf-8");
1880
2409
  if (!output.trim()) {
1881
2410
  throw new Error(`Finalizer agent produced empty output at ${artifactPath}`);
1882
2411
  }
@@ -1884,9 +2413,9 @@ function readAgentOutput(artifactPath) {
1884
2413
  }
1885
2414
  async function persistGeneratedArtifact(worktreePath, output) {
1886
2415
  try {
1887
- const dir = join6(worktreePath, ".pourkit", ".tmp", "finalizer");
1888
- mkdirSync3(dir, { recursive: true });
1889
- writeFileSync2(join6(dir, "generated.md"), output, "utf-8");
2416
+ const dir = join8(worktreePath, ".pourkit", ".tmp", "finalizer");
2417
+ mkdirSync4(dir, { recursive: true });
2418
+ writeFileSync2(join8(dir, "generated.md"), output, "utf-8");
1890
2419
  } catch {
1891
2420
  }
1892
2421
  }
@@ -2124,14 +2653,14 @@ async function runMergeCoordinator(options) {
2124
2653
 
2125
2654
  // commands/review.ts
2126
2655
  import {
2127
- existsSync as existsSync4,
2128
- mkdirSync as mkdirSync4,
2129
- readFileSync as readFileSync4,
2656
+ existsSync as existsSync6,
2657
+ mkdirSync as mkdirSync5,
2658
+ readFileSync as readFileSync6,
2130
2659
  readdirSync,
2131
2660
  rmSync as rmSync2,
2132
2661
  writeFileSync as writeFileSync3
2133
2662
  } from "fs";
2134
- import { dirname as dirname3, join as join7 } from "path";
2663
+ import { dirname as dirname4, join as join9 } from "path";
2135
2664
 
2136
2665
  // pr/review-verdict.ts
2137
2666
  var ReviewVerdictProtocolError = class extends Error {
@@ -2213,12 +2742,12 @@ function extractLatestFindingIds(reviewOutput, iteration) {
2213
2742
  return ids;
2214
2743
  }
2215
2744
  function validateRefactorArtifact(artifactPath, findingIds) {
2216
- if (!existsSync4(artifactPath)) {
2745
+ if (!existsSync6(artifactPath)) {
2217
2746
  throw new RefactorArtifactValidationError(
2218
2747
  `Refactor artifact missing at ${artifactPath}`
2219
2748
  );
2220
2749
  }
2221
- const content = readFileSync4(artifactPath, "utf-8");
2750
+ const content = readFileSync6(artifactPath, "utf-8");
2222
2751
  if (!content.trim()) {
2223
2752
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
2224
2753
  }
@@ -2363,13 +2892,13 @@ async function runReviewCommand(options) {
2363
2892
  if (!reviewer) {
2364
2893
  throw new Error("No reviewer config found");
2365
2894
  }
2366
- const artifactPathInWorktree = join7(
2895
+ const artifactPathInWorktree = join9(
2367
2896
  ".pourkit",
2368
2897
  ".tmp",
2369
2898
  "reviewers",
2370
2899
  `iteration-${iteration ?? 1}.md`
2371
2900
  );
2372
- const artifactPath = join7(worktreePath, artifactPathInWorktree);
2901
+ const artifactPath = join9(worktreePath, artifactPathInWorktree);
2373
2902
  prepareReviewArtifactPath(artifactPath);
2374
2903
  const prompt = buildReviewerPrompt(
2375
2904
  repoRoot2,
@@ -2476,8 +3005,8 @@ ${entry.trimEnd()}`).join("\n\n")}
2476
3005
  `;
2477
3006
  }
2478
3007
  function renderPriorRefactorArtifacts(worktreePath, currentIteration) {
2479
- const refactorsDir = join7(worktreePath, ".pourkit", ".tmp", "refactors");
2480
- if (!existsSync4(refactorsDir)) {
3008
+ const refactorsDir = join9(worktreePath, ".pourkit", ".tmp", "refactors");
3009
+ if (!existsSync6(refactorsDir)) {
2481
3010
  return "";
2482
3011
  }
2483
3012
  const files = readdirSync(refactorsDir);
@@ -2487,9 +3016,9 @@ function renderPriorRefactorArtifacts(worktreePath, currentIteration) {
2487
3016
  if (match) {
2488
3017
  const num = parseInt(match[1], 10);
2489
3018
  if (num < currentIteration) {
2490
- const filePath = join7(refactorsDir, file);
3019
+ const filePath = join9(refactorsDir, file);
2491
3020
  try {
2492
- const content = readFileSync4(filePath, "utf-8");
3021
+ const content = readFileSync6(filePath, "utf-8");
2493
3022
  if (content.trim()) {
2494
3023
  iterationFiles.push({ num, content });
2495
3024
  }
@@ -2514,8 +3043,8 @@ ${iterationsBlocks}
2514
3043
  `;
2515
3044
  }
2516
3045
  function renderPriorReviewerArtifacts(worktreePath, currentIteration) {
2517
- const reviewersDir = join7(worktreePath, ".pourkit", ".tmp", "reviewers");
2518
- if (!existsSync4(reviewersDir)) {
3046
+ const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
3047
+ if (!existsSync6(reviewersDir)) {
2519
3048
  return "";
2520
3049
  }
2521
3050
  const files = readdirSync(reviewersDir);
@@ -2525,9 +3054,9 @@ function renderPriorReviewerArtifacts(worktreePath, currentIteration) {
2525
3054
  if (match) {
2526
3055
  const num = parseInt(match[1], 10);
2527
3056
  if (num < currentIteration) {
2528
- const filePath = join7(reviewersDir, file);
3057
+ const filePath = join9(reviewersDir, file);
2529
3058
  try {
2530
- const content = readFileSync4(filePath, "utf-8");
3059
+ const content = readFileSync6(filePath, "utf-8");
2531
3060
  if (content.trim()) {
2532
3061
  iterationFiles.push({ num, content });
2533
3062
  }
@@ -2556,7 +3085,7 @@ function loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock) {
2556
3085
  repoRoot2,
2557
3086
  promptTemplate
2558
3087
  );
2559
- const promptBody = existsSync4(promptTemplatePath) ? readFileSync4(promptTemplatePath, "utf-8") : promptTemplate;
3088
+ const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
2560
3089
  const hasCriteriaPlaceholder = promptBody.includes("{{REVIEW_CRITERIA}}");
2561
3090
  return {
2562
3091
  content: promptBody.replace(/\{\{REVIEW_CRITERIA\}\}/g, criteriaBlock),
@@ -2565,29 +3094,29 @@ function loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock) {
2565
3094
  }
2566
3095
  function renderReviewCriteria(repoRoot2, criteria) {
2567
3096
  return criteria.map((criterion) => {
2568
- const snippetPath = join7(
3097
+ const snippetPath = join9(
2569
3098
  repoRoot2,
2570
3099
  ".pourkit",
2571
3100
  "prompts",
2572
3101
  `reviewer-${criterion}.snippet.md`
2573
3102
  );
2574
- if (existsSync4(snippetPath)) {
2575
- return readFileSync4(snippetPath, "utf-8").trimEnd();
3103
+ if (existsSync6(snippetPath)) {
3104
+ return readFileSync6(snippetPath, "utf-8").trimEnd();
2576
3105
  }
2577
3106
  return `- ${criterion}`;
2578
3107
  }).join("\n\n");
2579
3108
  }
2580
3109
  function prepareReviewArtifactPath(artifactPath) {
2581
- mkdirSync4(dirname3(artifactPath), { recursive: true });
2582
- if (existsSync4(artifactPath)) {
3110
+ mkdirSync5(dirname4(artifactPath), { recursive: true });
3111
+ if (existsSync6(artifactPath)) {
2583
3112
  rmSync2(artifactPath);
2584
3113
  }
2585
3114
  }
2586
3115
  function recoverReviewOutputFromLog(logPath) {
2587
- if (!existsSync4(logPath)) {
3116
+ if (!existsSync6(logPath)) {
2588
3117
  return null;
2589
3118
  }
2590
- const logContent = readFileSync4(logPath, "utf-8");
3119
+ const logContent = readFileSync6(logPath, "utf-8");
2591
3120
  const startIndex = logContent.indexOf("## Findings");
2592
3121
  if (startIndex === -1) {
2593
3122
  return null;
@@ -2602,8 +3131,8 @@ function recoverReviewOutputFromLog(logPath) {
2602
3131
  return recoveredOutput.length > 0 ? recoveredOutput : null;
2603
3132
  }
2604
3133
  function readReviewArtifact2(artifactPath, logPath) {
2605
- if (existsSync4(artifactPath)) {
2606
- const output = readFileSync4(artifactPath, "utf-8");
3134
+ if (existsSync6(artifactPath)) {
3135
+ const output = readFileSync6(artifactPath, "utf-8");
2607
3136
  if (output.trim()) {
2608
3137
  return output;
2609
3138
  }
@@ -2613,7 +3142,7 @@ function readReviewArtifact2(artifactPath, logPath) {
2613
3142
  writeFileSync3(artifactPath, recoveredOutput, "utf-8");
2614
3143
  return recoveredOutput;
2615
3144
  }
2616
- if (!existsSync4(artifactPath)) {
3145
+ if (!existsSync6(artifactPath)) {
2617
3146
  throw new Error(`Reviewer did not produce output at ${artifactPath}`);
2618
3147
  }
2619
3148
  throw new Error(`Reviewer produced empty output at ${artifactPath}`);
@@ -2629,7 +3158,8 @@ async function runReviewWithRefactorLoop(options) {
2629
3158
  repoRoot: repoRoot2,
2630
3159
  logger,
2631
3160
  startingLifetimeIteration = 0,
2632
- humanHandoffResolved
3161
+ humanHandoffResolved,
3162
+ serena
2633
3163
  } = options;
2634
3164
  const strategy = target.strategy;
2635
3165
  const reviewer = strategy.review.reviewer;
@@ -2644,7 +3174,7 @@ async function runReviewWithRefactorLoop(options) {
2644
3174
  const passWithNotesRefactorAttempts = strategy.review.passWithNotesRefactorAttempts;
2645
3175
  let resolvedStartingIteration = startingLifetimeIteration;
2646
3176
  {
2647
- const reviewersDir = join7(worktreePath, ".pourkit", ".tmp", "reviewers");
3177
+ const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
2648
3178
  try {
2649
3179
  const files = readdirSync(reviewersDir);
2650
3180
  let maxExistingIteration = 0;
@@ -2752,7 +3282,7 @@ async function runReviewWithRefactorLoop(options) {
2752
3282
  }
2753
3283
  if (reviewResult.verdict === "NEEDS_REFACTOR" || reviewResult.verdict === "PASS_WITH_NOTES" || reviewResult.verdict === "FAIL") {
2754
3284
  logger.step("info", "Running refactor agent");
2755
- const refactorArtifactPathInWorktree = join7(
3285
+ const refactorArtifactPathInWorktree = join9(
2756
3286
  ".pourkit",
2757
3287
  ".tmp",
2758
3288
  "refactors",
@@ -2786,6 +3316,7 @@ async function runReviewWithRefactorLoop(options) {
2786
3316
  sections: STAGE_SECTIONS.refactor
2787
3317
  })
2788
3318
  ],
3319
+ ...serena ? { serena } : {},
2789
3320
  logger
2790
3321
  });
2791
3322
  if (!refactorResult.success) {
@@ -2808,7 +3339,7 @@ async function runReviewWithRefactorLoop(options) {
2808
3339
  reviewResult.output,
2809
3340
  lifetimeIteration
2810
3341
  );
2811
- const refactorArtifactPath = join7(
3342
+ const refactorArtifactPath = join9(
2812
3343
  worktreePath,
2813
3344
  refactorArtifactPathInWorktree
2814
3345
  );
@@ -2858,9 +3389,9 @@ async function runReviewWithRefactorLoop(options) {
2858
3389
  }
2859
3390
  async function writeArtifact(worktreePath, filename, output) {
2860
3391
  try {
2861
- const dir = join7(worktreePath, ".pourkit", ".tmp", "reviewers");
2862
- mkdirSync4(dir, { recursive: true });
2863
- writeFileSync3(join7(dir, filename), output, "utf-8");
3392
+ const dir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
3393
+ mkdirSync5(dir, { recursive: true });
3394
+ writeFileSync3(join9(dir, filename), output, "utf-8");
2864
3395
  } catch {
2865
3396
  }
2866
3397
  }
@@ -2872,7 +3403,7 @@ function buildRefactorPrompt(repoRoot2, promptTemplate, latestReview, artifactPa
2872
3403
  repoRoot2,
2873
3404
  promptTemplate
2874
3405
  );
2875
- const promptBody = existsSync4(promptTemplatePath) ? readFileSync4(promptTemplatePath, "utf-8") : promptTemplate;
3406
+ const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
2876
3407
  return appendProtectedWorkGuidance(`${promptBody}
2877
3408
 
2878
3409
  ## Shared Run Context
@@ -3050,6 +3581,16 @@ function checkIssueGates(issue, config, force) {
3050
3581
  }
3051
3582
  return { allowed: true, gates };
3052
3583
  }
3584
+ function resolveSerenaRuntimeConfig(config, target) {
3585
+ return {
3586
+ enabled: target.serena?.enabled ?? config.serena.enabled,
3587
+ required: target.serena?.required ?? config.serena.required,
3588
+ autoStart: config.serena.autoStart,
3589
+ dataDir: config.serena.dataDir,
3590
+ mcpUrl: config.serena.mcpUrl,
3591
+ sandboxMcpUrl: config.serena.sandboxMcpUrl
3592
+ };
3593
+ }
3053
3594
  async function startIssueRun(options) {
3054
3595
  const {
3055
3596
  issueNumber,
@@ -3070,6 +3611,35 @@ async function startIssueRun(options) {
3070
3611
  const target = resolveTarget(config, targetName);
3071
3612
  const branchName = renderBranchName(target.branchTemplate, issue);
3072
3613
  const strategy = target.strategy;
3614
+ const serenaRuntimeConfig = resolveSerenaRuntimeConfig(config, target);
3615
+ const shouldPrepareSerena = serenaRuntimeConfig.enabled || serenaRuntimeConfig.required;
3616
+ let serenaExecutionContext;
3617
+ if (shouldPrepareSerena) {
3618
+ const serenaPreflight = await prepareSerenaForTarget({
3619
+ repoRoot: ROOT,
3620
+ targetName: target.name,
3621
+ baseBranch: target.baseBranch,
3622
+ dataDir: serenaRuntimeConfig.dataDir,
3623
+ mcpUrl: serenaRuntimeConfig.mcpUrl,
3624
+ enabled: shouldPrepareSerena,
3625
+ required: serenaRuntimeConfig.required,
3626
+ autoStart: serenaRuntimeConfig.autoStart,
3627
+ logger
3628
+ });
3629
+ if (serenaPreflight.enabled && serenaPreflight.available) {
3630
+ serenaExecutionContext = {
3631
+ available: true,
3632
+ sandboxMcpUrl: serenaRuntimeConfig.sandboxMcpUrl
3633
+ };
3634
+ }
3635
+ if (serenaPreflight.enabled && !serenaPreflight.available) {
3636
+ const message = `Serena preflight unavailable for target ${target.name}: ${serenaPreflight.error}`;
3637
+ if (serenaRuntimeConfig.required) {
3638
+ throw new Error(message);
3639
+ }
3640
+ logger.step("warn", message);
3641
+ }
3642
+ }
3073
3643
  if (options.resetWorktree) {
3074
3644
  const existingPr = await prProvider.getPr(branchName);
3075
3645
  if (existingPr && existingPr.state === "OPEN") {
@@ -3093,7 +3663,7 @@ async function startIssueRun(options) {
3093
3663
  const worktreeState = resolution.worktreePath ? readWorktreeRunState(resolution.worktreePath) : null;
3094
3664
  if (resolution.mode !== "new") {
3095
3665
  const existingPr = await prProvider.getPr(branchName);
3096
- const refreshResult = await refreshStaleIssueBranch({
3666
+ const exit = await runBaseRefreshAttempt({
3097
3667
  worktreePath: resolution.worktreePath,
3098
3668
  baseBranch: target.baseBranch,
3099
3669
  localGitBaseRef: resolution.baseRef,
@@ -3101,113 +3671,85 @@ async function startIssueRun(options) {
3101
3671
  prNumber: existingPr?.number,
3102
3672
  prState: existingPr?.state
3103
3673
  });
3104
- if (refreshResult.status === "refreshed") {
3105
- if (worktreeState?.completedStages.builder) {
3106
- const invalidatedState = invalidateAfterBaseRefresh(worktreeState);
3107
- writeWorktreeRunState(resolution.worktreePath, invalidatedState);
3108
- }
3109
- } else if (refreshResult.status === "conflicted") {
3110
- if (strategy.conflictResolution && resolution.worktreePath) {
3111
- const crLoopResult = await runConflictResolutionLoop({
3112
- executionProvider,
3113
- config,
3114
- target,
3115
- issue,
3116
- branchName,
3117
- worktreePath: resolution.worktreePath,
3118
- repoRoot: ROOT,
3119
- initialConflictedPaths: refreshResult.conflictedPaths,
3120
- maxAttempts: strategy.conflictResolution.maxAttempts,
3121
- logger
3122
- });
3123
- if (crLoopResult.status === "completed") {
3124
- if (strategy.verify?.commands) {
3125
- for (const cmd of strategy.verify.commands) {
3126
- await execCapture("bash", ["-lc", cmd.command], {
3127
- cwd: resolution.worktreePath,
3128
- logger,
3129
- label: `verify ${cmd.label}`
3130
- });
3131
- }
3132
- }
3133
- if (worktreeState?.completedStages.builder) {
3134
- const invalidatedState = invalidateAfterBaseRefresh(worktreeState);
3135
- writeWorktreeRunState(resolution.worktreePath, invalidatedState);
3136
- }
3137
- } else {
3138
- const failureMessage = crLoopResult.status === "ambiguous" ? `Conflict resolution ambiguous: ${crLoopResult.message}` : crLoopResult.status === "exhausted" ? `Conflict resolution maxAttempts (${strategy.conflictResolution.maxAttempts}) exhausted: ${crLoopResult.message}` : `Conflict resolution failed: ${crLoopResult.message}`;
3139
- const failureStage = "conflictResolution";
3140
- if (worktreeState) {
3141
- updateWorktreeRunState(resolution.worktreePath, {
3142
- lastFailure: {
3143
- stage: failureStage,
3144
- message: failureMessage
3145
- }
3674
+ if (Exit2.isSuccess(exit)) {
3675
+ const refreshResult = exit.value;
3676
+ if (refreshResult.status === "refreshed") {
3677
+ if (worktreeState?.completedStages.builder) {
3678
+ const invalidatedState = invalidateAfterBaseRefresh(worktreeState);
3679
+ writeWorktreeRunState(resolution.worktreePath, invalidatedState);
3680
+ }
3681
+ }
3682
+ } else {
3683
+ const cause = exit.cause;
3684
+ if (cause._tag === "Fail") {
3685
+ const failure = cause.error;
3686
+ if (failure instanceof RebaseConflict) {
3687
+ if (strategy.failureResolution && resolution.worktreePath) {
3688
+ await handleRebaseConflict(failure, {
3689
+ worktreePath: resolution.worktreePath,
3690
+ branchName,
3691
+ target,
3692
+ config,
3693
+ issueNumber,
3694
+ issueProvider,
3695
+ executionProvider,
3696
+ repoRoot: ROOT,
3697
+ worktreeState,
3698
+ logger
3146
3699
  });
3147
3700
  } else {
3148
- writeWorktreeRunState(resolution.worktreePath, {
3149
- issueNumber,
3150
- targetName: target.name,
3151
- branchName,
3152
- baseBranch: target.baseBranch,
3153
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3154
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3155
- completedStages: {},
3156
- review: { lifetimeIterations: 0 },
3157
- lastFailure: {
3158
- stage: failureStage,
3159
- message: failureMessage
3701
+ if (resolution.worktreePath) {
3702
+ if (worktreeState) {
3703
+ updateWorktreeRunState(resolution.worktreePath, {
3704
+ lastFailure: {
3705
+ stage: "baseRefresh",
3706
+ message: `Base refresh conflict detected. Handing off to human: ${failure.message}`
3707
+ }
3708
+ });
3709
+ } else {
3710
+ writeWorktreeRunState(resolution.worktreePath, {
3711
+ issueNumber,
3712
+ targetName: target.name,
3713
+ branchName,
3714
+ baseBranch: target.baseBranch,
3715
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3716
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3717
+ completedStages: {},
3718
+ review: { lifetimeIterations: 0 },
3719
+ lastFailure: {
3720
+ stage: "baseRefresh",
3721
+ message: `Base refresh conflict detected. Handing off to human: ${failure.message}`
3722
+ }
3723
+ });
3160
3724
  }
3161
- });
3725
+ }
3726
+ await transitionIssueToFailureState(
3727
+ issueProvider,
3728
+ issueNumber,
3729
+ config,
3730
+ `Base refresh conflicted: ${failure.message}. Worktree preserved at ${resolution.worktreePath}.`,
3731
+ logger
3732
+ );
3733
+ throw new Error(`Base refresh conflicted: ${failure.message}`);
3162
3734
  }
3163
- await transitionIssueToFailureState(
3164
- issueProvider,
3165
- issueNumber,
3735
+ } else if (failure instanceof PublishedHistoryRisk) {
3736
+ await handlePublishedHistoryRisk(failure, {
3737
+ worktreePath: resolution.worktreePath,
3738
+ branchName,
3739
+ target,
3166
3740
  config,
3167
- failureMessage,
3741
+ issueNumber,
3742
+ issueProvider,
3743
+ worktreeState,
3168
3744
  logger
3169
- );
3170
- throw new Error(failureMessage);
3745
+ });
3746
+ } else {
3747
+ throw new Error(`Base refresh failed: ${failure.message}`);
3171
3748
  }
3172
3749
  } else {
3173
- if (resolution.worktreePath) {
3174
- if (worktreeState) {
3175
- updateWorktreeRunState(resolution.worktreePath, {
3176
- lastFailure: {
3177
- stage: "baseRefresh",
3178
- message: `Base refresh conflict detected. Handing off to human: ${refreshResult.message}`
3179
- }
3180
- });
3181
- } else {
3182
- writeWorktreeRunState(resolution.worktreePath, {
3183
- issueNumber,
3184
- targetName: target.name,
3185
- branchName,
3186
- baseBranch: target.baseBranch,
3187
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3188
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3189
- completedStages: {},
3190
- review: { lifetimeIterations: 0 },
3191
- lastFailure: {
3192
- stage: "baseRefresh",
3193
- message: `Base refresh conflict detected. Handing off to human: ${refreshResult.message}`
3194
- }
3195
- });
3196
- }
3197
- }
3198
- await transitionIssueToFailureState(
3199
- issueProvider,
3200
- issueNumber,
3201
- config,
3202
- `Base refresh conflicted: ${refreshResult.message}. Worktree preserved at ${resolution.worktreePath}.`,
3203
- logger
3204
- );
3205
- throw new Error(`Base refresh conflicted: ${refreshResult.message}`);
3750
+ const defectMessage = cause._tag === "Die" ? `Base refresh failed with unexpected error: ${cause.defect}` : `Base refresh failed with unhandled cause: ${cause._tag}`;
3751
+ throw new Error(defectMessage);
3206
3752
  }
3207
- } else if (refreshResult.status === "refused-published-history") {
3208
- throw new Error(
3209
- `Cannot auto-refresh published history: PR #${refreshResult.prNumber} (${refreshResult.prState}) exists for branch ${branchName}`
3210
- );
3211
3753
  }
3212
3754
  }
3213
3755
  const runContextArtifact = buildRunContextArtifact({
@@ -3232,6 +3774,7 @@ async function startIssueRun(options) {
3232
3774
  branchName,
3233
3775
  ...resolution.mode === "new" ? { baseRef: resolution.baseRef } : {},
3234
3776
  sandbox: config.sandbox,
3777
+ ...serenaExecutionContext ? { serena: serenaExecutionContext } : {},
3235
3778
  autoApprove: true,
3236
3779
  timeoutMs: EXECUTION_TIMEOUT_MS,
3237
3780
  ...resolution.worktreePath ? { worktreePath: resolution.worktreePath } : {},
@@ -3274,7 +3817,8 @@ async function startIssueRun(options) {
3274
3817
  target,
3275
3818
  branchName,
3276
3819
  worktreeState: finalWorktreeState,
3277
- executionResult
3820
+ executionResult,
3821
+ ...serenaExecutionContext ? { serena: serenaExecutionContext } : {}
3278
3822
  };
3279
3823
  }
3280
3824
  async function advanceIssueRunReview(options) {
@@ -3353,12 +3897,12 @@ async function completeIssueRun(options) {
3353
3897
  prTitle = finalizerFromState.title;
3354
3898
  prBody = finalizerFromState.body;
3355
3899
  } else if (finalizerFromState.artifactPath) {
3356
- if (!existsSync5(finalizerFromState.artifactPath)) {
3900
+ if (!existsSync7(finalizerFromState.artifactPath)) {
3357
3901
  throw new Error(
3358
3902
  `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
3359
3903
  );
3360
3904
  }
3361
- const artifactContent = readFileSync5(
3905
+ const artifactContent = readFileSync7(
3362
3906
  finalizerFromState.artifactPath,
3363
3907
  "utf-8"
3364
3908
  );
@@ -3445,6 +3989,13 @@ async function completeIssueRun(options) {
3445
3989
  if (prFromState && prFromState.state === "OPEN") {
3446
3990
  pr = prFromState;
3447
3991
  } else {
3992
+ await guardFinalPublishContent({
3993
+ worktreePath: executionResult.worktreePath,
3994
+ baseRef: `origin/${target.baseBranch}`,
3995
+ title: prTitle,
3996
+ body: finalBody,
3997
+ logger
3998
+ });
3448
3999
  await execCapture("git", ["push", "-u", "origin", branchName], {
3449
4000
  cwd: executionResult.worktreePath,
3450
4001
  logger,
@@ -3644,12 +4195,106 @@ async function finalizeWorktreeCommit(options) {
3644
4195
  logger,
3645
4196
  label: "git add"
3646
4197
  });
4198
+ await guardFinalCommitContent({
4199
+ worktreePath,
4200
+ title,
4201
+ body,
4202
+ logger
4203
+ });
3647
4204
  await execCapture("git", ["commit", "--no-verify", "-m", title, "-m", body], {
3648
4205
  cwd: worktreePath,
3649
4206
  logger,
3650
4207
  label: "git commit"
3651
4208
  });
3652
4209
  }
4210
+ async function guardFinalCommitContent(options) {
4211
+ const { worktreePath, title, body, logger } = options;
4212
+ const stagedDiff = await execCapture("git", ["diff", "--cached"], {
4213
+ cwd: worktreePath,
4214
+ logger,
4215
+ label: "git diff --cached secret guard"
4216
+ });
4217
+ assertNoSecretLikeContent([
4218
+ { source: "staged diff", content: stagedDiff.stdout },
4219
+ { source: "commit title", content: title },
4220
+ { source: "commit body", content: body }
4221
+ ]);
4222
+ }
4223
+ async function guardFinalPublishContent(options) {
4224
+ const { worktreePath, baseRef, title, body, logger } = options;
4225
+ const finalDiff = await execCapture("git", ["diff", `${baseRef}...HEAD`], {
4226
+ cwd: worktreePath,
4227
+ logger,
4228
+ label: "git diff final secret guard"
4229
+ });
4230
+ assertNoSecretLikeContent([
4231
+ { source: "final diff", content: finalDiff.stdout },
4232
+ { source: "PR title", content: title },
4233
+ { source: "PR body", content: body }
4234
+ ]);
4235
+ }
4236
+ function assertNoSecretLikeContent(inputs) {
4237
+ const findings = inputs.flatMap(
4238
+ ({ source, content }) => findSecretLikeContent(source, content ?? "")
4239
+ );
4240
+ if (findings.length === 0) {
4241
+ return;
4242
+ }
4243
+ const summary = findings.slice(0, 5).map((finding) => `${finding.source}: ${finding.kind}`).join("; ");
4244
+ throw new Error(
4245
+ `Secret-like content detected before finalization (${summary}). Redact it before committing, pushing, or creating a PR.`
4246
+ );
4247
+ }
4248
+ function findSecretLikeContent(source, content) {
4249
+ const findings = [];
4250
+ const patterns = [
4251
+ {
4252
+ kind: "private key block",
4253
+ regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/gi
4254
+ },
4255
+ {
4256
+ kind: "GitHub token",
4257
+ regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g
4258
+ },
4259
+ {
4260
+ kind: "GitHub fine-grained token",
4261
+ regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g
4262
+ },
4263
+ { kind: "npm token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g },
4264
+ { kind: "OpenAI key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
4265
+ { kind: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
4266
+ {
4267
+ kind: "authorization bearer token",
4268
+ regex: /\bAuthorization\s*:\s*Bearer\s+[^\s`'\"]{12,}/gi
4269
+ },
4270
+ {
4271
+ kind: "secret assignment",
4272
+ regex: /\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|DATABASE[_-]?URL)[A-Z0-9_]*\s*[:=]\s*["']?[^\s"'`]{8,}/gi
4273
+ }
4274
+ ];
4275
+ for (const { kind, regex } of patterns) {
4276
+ for (const match of content.matchAll(regex)) {
4277
+ if (isAllowedSecretPlaceholder(match[0])) {
4278
+ continue;
4279
+ }
4280
+ findings.push({ source, kind });
4281
+ break;
4282
+ }
4283
+ }
4284
+ return findings;
4285
+ }
4286
+ function isAllowedSecretPlaceholder(value) {
4287
+ const normalized = value.toLowerCase();
4288
+ return [
4289
+ "<redacted>",
4290
+ "<example",
4291
+ "example-",
4292
+ "dummy-",
4293
+ "test-",
4294
+ "placeholder",
4295
+ "xxxxx"
4296
+ ].some((allowed) => normalized.includes(allowed));
4297
+ }
3653
4298
  async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
3654
4299
  const remoteBase = parseRemoteBaseRef(baseRef);
3655
4300
  if (!remoteBase) {
@@ -3865,7 +4510,7 @@ async function resolveIssueWorktree(root, branchName, baseBranch, logger) {
3865
4510
  return { mode: "new", branchName, baseRef };
3866
4511
  }
3867
4512
  function issueWorktreePath(root, branchName) {
3868
- return join8(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
4513
+ return join10(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
3869
4514
  }
3870
4515
  function resolveRegisteredIssueWorktreePath(worktreeListPorcelain, root, branchName) {
3871
4516
  const branchWorktreePath = parseWorktreeListPorcelain(
@@ -3893,12 +4538,251 @@ async function syncTargetBranch(root, baseBranch, logger) {
3893
4538
  }
3894
4539
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
3895
4540
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
3896
- const promptBody = existsSync5(promptPath) ? readFileSync5(promptPath, "utf-8") : promptTemplate;
4541
+ const promptBody = existsSync7(promptPath) ? readFileSync7(promptPath, "utf-8") : promptTemplate;
3897
4542
  return appendProtectedWorkGuidance(`${promptBody}
3898
4543
 
3899
4544
  ## Shared Run Context
3900
4545
 
3901
- Read the selected issue requirements, comments, branch context, validation commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
4546
+ Read the selected issue requirements, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
4547
+ }
4548
+ async function handleRebaseConflict(failure, context) {
4549
+ const frConfig = context.target.strategy.failureResolution;
4550
+ if (!frConfig) {
4551
+ await transitionIssueToFailureState(
4552
+ context.issueProvider,
4553
+ context.issueNumber,
4554
+ context.config,
4555
+ "No failureResolution configured",
4556
+ context.logger
4557
+ );
4558
+ throw new Error("Base refresh conflicted: no failureResolution configured");
4559
+ }
4560
+ const maxAttempts = frConfig.failureLimits?.RebaseConflict ?? frConfig.maxAttemptsPerFailure;
4561
+ let attemptNumber = 0;
4562
+ let currentFailure = failure;
4563
+ while (attemptNumber < maxAttempts) {
4564
+ attemptNumber++;
4565
+ const packet = constructFailureResolutionPacket(currentFailure, {
4566
+ stageName: "baseRefresh",
4567
+ attemptNumber,
4568
+ worktreePath: context.worktreePath,
4569
+ branchName: context.branchName,
4570
+ baseBranch: context.target.baseBranch,
4571
+ maxAttempts,
4572
+ allowedDecisions: ["RETRY_STAGE", "HANDOFF_TO_HUMAN", "FAIL_RUN"],
4573
+ artifactTarget: `.pourkit/.tmp/failure-resolution/attempt-${attemptNumber}.md`
4574
+ });
4575
+ const result = await runFailureResolutionAgent({
4576
+ executionProvider: context.executionProvider,
4577
+ config: context.config,
4578
+ target: context.target,
4579
+ failure: currentFailure,
4580
+ packet,
4581
+ worktreePath: context.worktreePath,
4582
+ repoRoot: context.repoRoot,
4583
+ logger: context.logger
4584
+ });
4585
+ if (result.status === "handoff") {
4586
+ if (context.worktreeState) {
4587
+ updateWorktreeRunState(context.worktreePath, {
4588
+ lastFailure: { stage: "baseRefresh", message: result.reason }
4589
+ });
4590
+ } else {
4591
+ writeWorktreeRunState(context.worktreePath, {
4592
+ issueNumber: context.issueNumber,
4593
+ targetName: context.target.name,
4594
+ branchName: context.branchName,
4595
+ baseBranch: context.target.baseBranch,
4596
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4597
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4598
+ completedStages: {},
4599
+ review: { lifetimeIterations: 0 },
4600
+ lastFailure: { stage: "baseRefresh", message: result.reason }
4601
+ });
4602
+ }
4603
+ await transitionIssueToFailureState(
4604
+ context.issueProvider,
4605
+ context.issueNumber,
4606
+ context.config,
4607
+ result.reason,
4608
+ context.logger
4609
+ );
4610
+ throw new Error(result.reason);
4611
+ }
4612
+ if (result.status === "fail-run") {
4613
+ if (context.worktreeState) {
4614
+ updateWorktreeRunState(context.worktreePath, {
4615
+ lastFailure: { stage: "baseRefresh", message: result.reason }
4616
+ });
4617
+ } else {
4618
+ writeWorktreeRunState(context.worktreePath, {
4619
+ issueNumber: context.issueNumber,
4620
+ targetName: context.target.name,
4621
+ branchName: context.branchName,
4622
+ baseBranch: context.target.baseBranch,
4623
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4624
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4625
+ completedStages: {},
4626
+ review: { lifetimeIterations: 0 },
4627
+ lastFailure: { stage: "baseRefresh", message: result.reason }
4628
+ });
4629
+ }
4630
+ throw new Error(result.reason);
4631
+ }
4632
+ const changedPaths = packet.conflictedPaths ?? currentFailure.conflictedPaths;
4633
+ const markersRemain = await hasUnresolvedConflictMarkers(
4634
+ context.worktreePath,
4635
+ changedPaths
4636
+ );
4637
+ if (markersRemain) {
4638
+ const message = "Conflict resolution agent resolved artifact but conflict markers remain in files";
4639
+ if (context.worktreeState) {
4640
+ updateWorktreeRunState(context.worktreePath, {
4641
+ lastFailure: { stage: "baseRefresh", message }
4642
+ });
4643
+ } else {
4644
+ writeWorktreeRunState(context.worktreePath, {
4645
+ issueNumber: context.issueNumber,
4646
+ targetName: context.target.name,
4647
+ branchName: context.branchName,
4648
+ baseBranch: context.target.baseBranch,
4649
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4650
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4651
+ completedStages: {},
4652
+ review: { lifetimeIterations: 0 },
4653
+ lastFailure: { stage: "baseRefresh", message }
4654
+ });
4655
+ }
4656
+ await transitionIssueToFailureState(
4657
+ context.issueProvider,
4658
+ context.issueNumber,
4659
+ context.config,
4660
+ message,
4661
+ context.logger
4662
+ );
4663
+ throw new Error(message);
4664
+ }
4665
+ await execCapture("git", ["add", ...changedPaths], {
4666
+ cwd: context.worktreePath,
4667
+ logger: context.logger,
4668
+ label: "git add conflicted paths"
4669
+ });
4670
+ try {
4671
+ await execCapture("git", ["rebase", "--continue"], {
4672
+ cwd: context.worktreePath,
4673
+ env: { ...process.env, GIT_EDITOR: "true" },
4674
+ logger: context.logger,
4675
+ label: "git rebase --continue"
4676
+ });
4677
+ if (context.worktreeState?.completedStages.builder) {
4678
+ const invalidatedState = invalidateAfterBaseRefresh(
4679
+ context.worktreeState
4680
+ );
4681
+ writeWorktreeRunState(context.worktreePath, invalidatedState);
4682
+ }
4683
+ return;
4684
+ } catch {
4685
+ const statusResult = await execCapture("git", ["status", "--porcelain"], {
4686
+ cwd: context.worktreePath,
4687
+ logger: context.logger,
4688
+ label: "git status"
4689
+ });
4690
+ const newConflictedPaths = statusResult.stdout.split("\n").filter((line) => /^(AA|DD|UU|AU|UA|DU|UD)\s/.test(line)).map((line) => line.slice(3).trim()).filter(Boolean);
4691
+ if (newConflictedPaths.length === 0) {
4692
+ const message = "git rebase --continue failed with no remaining conflicts";
4693
+ if (context.worktreeState) {
4694
+ updateWorktreeRunState(context.worktreePath, {
4695
+ lastFailure: { stage: "baseRefresh", message }
4696
+ });
4697
+ } else {
4698
+ writeWorktreeRunState(context.worktreePath, {
4699
+ issueNumber: context.issueNumber,
4700
+ targetName: context.target.name,
4701
+ branchName: context.branchName,
4702
+ baseBranch: context.target.baseBranch,
4703
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4704
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4705
+ completedStages: {},
4706
+ review: { lifetimeIterations: 0 },
4707
+ lastFailure: { stage: "baseRefresh", message }
4708
+ });
4709
+ }
4710
+ await transitionIssueToFailureState(
4711
+ context.issueProvider,
4712
+ context.issueNumber,
4713
+ context.config,
4714
+ message,
4715
+ context.logger
4716
+ );
4717
+ throw new Error(message);
4718
+ }
4719
+ currentFailure = new RebaseConflict({
4720
+ conflictedPaths: newConflictedPaths,
4721
+ message: `Rebase conflict in: ${newConflictedPaths.join(", ")}`
4722
+ });
4723
+ }
4724
+ }
4725
+ if (context.worktreeState) {
4726
+ updateWorktreeRunState(context.worktreePath, {
4727
+ lastFailure: {
4728
+ stage: "baseRefresh",
4729
+ message: `Base refresh recovery exhausted after ${maxAttempts} attempts`
4730
+ }
4731
+ });
4732
+ } else {
4733
+ writeWorktreeRunState(context.worktreePath, {
4734
+ issueNumber: context.issueNumber,
4735
+ targetName: context.target.name,
4736
+ branchName: context.branchName,
4737
+ baseBranch: context.target.baseBranch,
4738
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4739
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4740
+ completedStages: {},
4741
+ review: { lifetimeIterations: 0 },
4742
+ lastFailure: {
4743
+ stage: "baseRefresh",
4744
+ message: `Base refresh recovery exhausted after ${maxAttempts} attempts`
4745
+ }
4746
+ });
4747
+ }
4748
+ await transitionIssueToFailureState(
4749
+ context.issueProvider,
4750
+ context.issueNumber,
4751
+ context.config,
4752
+ `Base refresh recovery exhausted after ${maxAttempts} attempts`,
4753
+ context.logger
4754
+ );
4755
+ throw new Error(
4756
+ `Base refresh recovery exhausted after ${maxAttempts} attempts`
4757
+ );
4758
+ }
4759
+ async function handlePublishedHistoryRisk(failure, context) {
4760
+ const message = `Cannot auto-refresh published history: PR #${failure.prNumber} (${failure.prState}) exists for branch ${context.branchName}`;
4761
+ if (context.worktreeState) {
4762
+ updateWorktreeRunState(context.worktreePath, {
4763
+ lastFailure: { stage: "baseRefresh", message }
4764
+ });
4765
+ } else {
4766
+ writeWorktreeRunState(context.worktreePath, {
4767
+ issueNumber: context.issueNumber,
4768
+ targetName: context.target.name,
4769
+ branchName: context.branchName,
4770
+ baseBranch: context.target.baseBranch,
4771
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4772
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4773
+ completedStages: {},
4774
+ review: { lifetimeIterations: 0 },
4775
+ lastFailure: { stage: "baseRefresh", message }
4776
+ });
4777
+ }
4778
+ await transitionIssueToFailureState(
4779
+ context.issueProvider,
4780
+ context.issueNumber,
4781
+ context.config,
4782
+ message,
4783
+ context.logger
4784
+ );
4785
+ throw new Error(message);
3902
4786
  }
3903
4787
 
3904
4788
  // commands/issue.ts
@@ -3941,7 +4825,8 @@ async function runIssueCommand(options) {
3941
4825
  repoRoot: ROOT,
3942
4826
  logger,
3943
4827
  startingLifetimeIteration: lifetimeIterationsFromState,
3944
- humanHandoffResolved
4828
+ humanHandoffResolved,
4829
+ serena: startResult.serena
3945
4830
  });
3946
4831
  if (reviewResult.exhaustedMaxIterations) {
3947
4832
  throw new Error(
@@ -3986,6 +4871,95 @@ async function runIssueCommand(options) {
3986
4871
  }
3987
4872
  }
3988
4873
 
4874
+ // commands/issue-create.ts
4875
+ import { readFile as readFile3 } from "fs/promises";
4876
+ function isFlag(value) {
4877
+ return value.startsWith("--");
4878
+ }
4879
+ function requireFlagValue(flag, value) {
4880
+ if (!value || isFlag(value)) {
4881
+ throw new Error(`${flag} requires a value`);
4882
+ }
4883
+ return value;
4884
+ }
4885
+ function parseIssueCreateArgs(args) {
4886
+ let title;
4887
+ let body;
4888
+ let bodyFile;
4889
+ const labels = [];
4890
+ const assignees = [];
4891
+ const remaining = [];
4892
+ let i = 0;
4893
+ while (i < args.length) {
4894
+ const arg = args[i];
4895
+ if (arg === "--title") {
4896
+ title = requireFlagValue("--title", args[i + 1]);
4897
+ i += 2;
4898
+ } else if (arg === "--body") {
4899
+ body = requireFlagValue("--body", args[i + 1]);
4900
+ i += 2;
4901
+ } else if (arg === "--body-file") {
4902
+ bodyFile = requireFlagValue("--body-file", args[i + 1]);
4903
+ i += 2;
4904
+ } else if (arg === "--label") {
4905
+ const label = requireFlagValue("--label", args[i + 1]);
4906
+ labels.push(label);
4907
+ i += 2;
4908
+ } else if (arg === "--assignee") {
4909
+ const assignee = requireFlagValue("--assignee", args[i + 1]);
4910
+ assignees.push(assignee);
4911
+ i += 2;
4912
+ } else {
4913
+ remaining.push(arg);
4914
+ i++;
4915
+ }
4916
+ }
4917
+ return {
4918
+ options: {
4919
+ title,
4920
+ body,
4921
+ bodyFile,
4922
+ labels,
4923
+ assignees
4924
+ },
4925
+ remaining
4926
+ };
4927
+ }
4928
+ function validateIssueCreateOptions(options) {
4929
+ const errors = [];
4930
+ if (!options.title) {
4931
+ errors.push("--title is required");
4932
+ }
4933
+ if (options.body && options.bodyFile) {
4934
+ errors.push("--body and --body-file cannot be used together");
4935
+ }
4936
+ if (errors.length > 0) {
4937
+ throw new Error(errors.join("; "));
4938
+ }
4939
+ }
4940
+ async function runIssueCreateCommand(args, issueProvider, logger) {
4941
+ const { options, remaining } = parseIssueCreateArgs(args);
4942
+ if (remaining.length > 0) {
4943
+ throw new Error(`Unsupported arguments: ${remaining.join(" ")}`);
4944
+ }
4945
+ validateIssueCreateOptions(options);
4946
+ const renderedBody = options.bodyFile ? await readFile3(options.bodyFile, "utf-8") : options.body ?? "";
4947
+ const createOptions = {
4948
+ title: options.title,
4949
+ body: renderedBody,
4950
+ labels: options.labels.length > 0 ? options.labels : void 0,
4951
+ assignees: options.assignees.length > 0 ? options.assignees : void 0
4952
+ };
4953
+ const created = await issueProvider.createIssue(createOptions);
4954
+ return {
4955
+ options,
4956
+ renderedBody,
4957
+ issueNumber: created.number,
4958
+ issueUrl: created.url,
4959
+ issueTitle: created.title
4960
+ };
4961
+ }
4962
+
3989
4963
  // commands/queue.ts
3990
4964
  init_common();
3991
4965
 
@@ -4423,11 +5397,11 @@ async function runPrWorkflow(options) {
4423
5397
  }
4424
5398
 
4425
5399
  // commands/pr-create.ts
4426
- function isFlag(value) {
5400
+ function isFlag2(value) {
4427
5401
  return value.startsWith("--");
4428
5402
  }
4429
- function requireFlagValue(flag, value) {
4430
- if (!value || isFlag(value)) {
5403
+ function requireFlagValue2(flag, value) {
5404
+ if (!value || isFlag2(value)) {
4431
5405
  throw new Error(`${flag} requires a value`);
4432
5406
  }
4433
5407
  return value;
@@ -4445,25 +5419,25 @@ function parsePrCreateArgs(args) {
4445
5419
  while (i < args.length) {
4446
5420
  const arg = args[i];
4447
5421
  if (arg === "--target") {
4448
- target = requireFlagValue("--target", args[i + 1]);
5422
+ target = requireFlagValue2("--target", args[i + 1]);
4449
5423
  i += 2;
4450
5424
  } else if (arg === "--title") {
4451
- title = requireFlagValue("--title", args[i + 1]);
5425
+ title = requireFlagValue2("--title", args[i + 1]);
4452
5426
  i += 2;
4453
5427
  } else if (arg === "--base") {
4454
- base = requireFlagValue("--base", args[i + 1]);
5428
+ base = requireFlagValue2("--base", args[i + 1]);
4455
5429
  i += 2;
4456
5430
  } else if (arg === "--head") {
4457
- head = requireFlagValue("--head", args[i + 1]);
5431
+ head = requireFlagValue2("--head", args[i + 1]);
4458
5432
  i += 2;
4459
5433
  } else if (arg === "--body") {
4460
- body = requireFlagValue("--body", args[i + 1]);
5434
+ body = requireFlagValue2("--body", args[i + 1]);
4461
5435
  i += 2;
4462
5436
  } else if (arg === "--body-file") {
4463
- bodyFile = requireFlagValue("--body-file", args[i + 1]);
5437
+ bodyFile = requireFlagValue2("--body-file", args[i + 1]);
4464
5438
  i += 2;
4465
5439
  } else if (arg === "--issue") {
4466
- const raw = requireFlagValue("--issue", args[i + 1]);
5440
+ const raw = requireFlagValue2("--issue", args[i + 1]);
4467
5441
  if (!/^\d+$/.test(raw)) {
4468
5442
  throw new Error(`Invalid issue number: ${raw}`);
4469
5443
  }
@@ -4561,17 +5535,17 @@ async function runPrCreateCommand(args, logger, prProvider, config, repoRoot2) {
4561
5535
 
4562
5536
  // commands/pr-merge.ts
4563
5537
  init_common();
4564
- function isFlag2(value) {
5538
+ function isFlag3(value) {
4565
5539
  return value.startsWith("--");
4566
5540
  }
4567
- function requireFlagValue2(flag, value) {
4568
- if (!value || isFlag2(value)) {
5541
+ function requireFlagValue3(flag, value) {
5542
+ if (!value || isFlag3(value)) {
4569
5543
  throw new Error(`${flag} requires a value`);
4570
5544
  }
4571
5545
  return value;
4572
5546
  }
4573
5547
  function parsePrNumber(raw) {
4574
- if (!raw || isFlag2(raw) || !/^\d+$/.test(raw)) {
5548
+ if (!raw || isFlag3(raw) || !/^\d+$/.test(raw)) {
4575
5549
  throw new Error("PR number is required");
4576
5550
  }
4577
5551
  const prNumber = parseInt(raw, 10);
@@ -4591,10 +5565,10 @@ function parsePrMergeArgs(args) {
4591
5565
  while (i < args.length) {
4592
5566
  const arg = args[i];
4593
5567
  if (arg === "--target") {
4594
- target = requireFlagValue2("--target", args[i + 1]);
5568
+ target = requireFlagValue3("--target", args[i + 1]);
4595
5569
  i += 2;
4596
5570
  } else if (arg === "--method") {
4597
- const raw = requireFlagValue2("--method", args[i + 1]);
5571
+ const raw = requireFlagValue3("--method", args[i + 1]);
4598
5572
  if (raw !== "merge" && raw !== "squash" && raw !== "rebase") {
4599
5573
  throw new Error(`Invalid merge method: ${raw}`);
4600
5574
  }
@@ -4707,17 +5681,17 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
4707
5681
  }
4708
5682
 
4709
5683
  // commands/init.ts
4710
- import { existsSync as existsSync6, statSync } from "fs";
5684
+ import { existsSync as existsSync8, statSync } from "fs";
4711
5685
  import {
4712
5686
  copyFile,
4713
- mkdir as mkdir2,
4714
- readFile as readFile3,
5687
+ mkdir as mkdir4,
5688
+ readFile as readFile4,
4715
5689
  readdir,
4716
5690
  rename,
4717
5691
  writeFile
4718
5692
  } from "fs/promises";
4719
5693
  import { createHash, randomUUID } from "crypto";
4720
- import path3 from "path";
5694
+ import path5 from "path";
4721
5695
  import { execFile as execFile2 } from "child_process";
4722
5696
  import { promisify as promisify2 } from "util";
4723
5697
  import { confirm, isCancel, log, select, text } from "@clack/prompts";
@@ -4942,7 +5916,7 @@ function generateConfigTemplate(options) {
4942
5916
  labels: maybeLabels
4943
5917
  } = options;
4944
5918
  const labels = maybeLabels ?? DEFAULT_RUNNER_LABELS;
4945
- const relPath = path3.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
5919
+ const relPath = path5.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
4946
5920
  const importPath = relPath || ".";
4947
5921
  const setupCommand = `${packageManager} install`;
4948
5922
  let setupSection;
@@ -5190,9 +6164,9 @@ _Avoid_: Alternative terms
5190
6164
  `;
5191
6165
  }
5192
6166
  async function generateManagedAgentInstructions(options) {
5193
- const sourcePath = path3.join(options.sourceRoot, "AGENTS.md");
6167
+ const sourcePath = path5.join(options.sourceRoot, "AGENTS.md");
5194
6168
  try {
5195
- return await readFile3(sourcePath, "utf-8");
6169
+ return await readFile4(sourcePath, "utf-8");
5196
6170
  } catch {
5197
6171
  return `## Agent Skills
5198
6172
 
@@ -5220,7 +6194,7 @@ async function walkDir(dir) {
5220
6194
  const files = [];
5221
6195
  const entries = await readdir(dir, { withFileTypes: true });
5222
6196
  for (const entry of entries) {
5223
- const full = path3.join(dir, entry.name);
6197
+ const full = path5.join(dir, entry.name);
5224
6198
  if (entry.isDirectory()) {
5225
6199
  files.push(...await walkDir(full));
5226
6200
  } else {
@@ -5230,11 +6204,11 @@ async function walkDir(dir) {
5230
6204
  return files;
5231
6205
  }
5232
6206
  async function computeFileChecksum(filePath) {
5233
- const content = await readFile3(filePath);
6207
+ const content = await readFile4(filePath);
5234
6208
  return createHash("sha256").update(content).digest("hex");
5235
6209
  }
5236
6210
  function lockfileExists(root, name) {
5237
- return existsSync6(path3.join(root, name));
6211
+ return existsSync8(path5.join(root, name));
5238
6212
  }
5239
6213
  function detectPackageManager(root) {
5240
6214
  if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
@@ -5277,8 +6251,8 @@ async function discoverLocalSource(sourcePath) {
5277
6251
  }
5278
6252
  async function discoverReadme(root) {
5279
6253
  for (const name of ["README.md", "readme.md"]) {
5280
- const p = path3.join(root, name);
5281
- if (existsSync6(p)) {
6254
+ const p = path5.join(root, name);
6255
+ if (existsSync8(p)) {
5282
6256
  return p;
5283
6257
  }
5284
6258
  }
@@ -5287,25 +6261,25 @@ async function discoverReadme(root) {
5287
6261
  async function discoverAgentFiles(root) {
5288
6262
  const files = [];
5289
6263
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
5290
- const p = path3.join(root, name);
5291
- if (existsSync6(p)) {
6264
+ const p = path5.join(root, name);
6265
+ if (existsSync8(p)) {
5292
6266
  files.push(p);
5293
6267
  }
5294
6268
  }
5295
6269
  return files;
5296
6270
  }
5297
6271
  async function discoverMerlleState(root) {
5298
- const p = path3.join(root, ".pourkit", "state.json");
5299
- return existsSync6(p) ? p : null;
6272
+ const p = path5.join(root, ".pourkit", "state.json");
6273
+ return existsSync8(p) ? p : null;
5300
6274
  }
5301
6275
  async function discoverAgentSkills(root) {
5302
6276
  const dirs = [
5303
- path3.join(root, ".agents", "skills"),
5304
- path3.join(root, ".opencode", "skills")
6277
+ path5.join(root, ".agents", "skills"),
6278
+ path5.join(root, ".opencode", "skills")
5305
6279
  ];
5306
6280
  const found = [];
5307
6281
  for (const d of dirs) {
5308
- if (existsSync6(d)) {
6282
+ if (existsSync8(d)) {
5309
6283
  found.push(d);
5310
6284
  }
5311
6285
  }
@@ -5314,17 +6288,17 @@ async function discoverAgentSkills(root) {
5314
6288
  async function discoverRootDomainDocs(root) {
5315
6289
  const docs = [];
5316
6290
  for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
5317
- const p = path3.join(root, name);
5318
- if (existsSync6(p)) {
6291
+ const p = path5.join(root, name);
6292
+ if (existsSync8(p)) {
5319
6293
  docs.push(p);
5320
6294
  }
5321
6295
  }
5322
- const adrDir = path3.join(root, "docs", "adr");
5323
- if (existsSync6(adrDir)) {
6296
+ const adrDir = path5.join(root, "docs", "adr");
6297
+ if (existsSync8(adrDir)) {
5324
6298
  const entries = await readdir(adrDir, { withFileTypes: true });
5325
6299
  for (const entry of entries) {
5326
6300
  if (entry.isFile() && entry.name.endsWith(".md")) {
5327
- docs.push(path3.join(adrDir, entry.name));
6301
+ docs.push(path5.join(adrDir, entry.name));
5328
6302
  }
5329
6303
  }
5330
6304
  }
@@ -5412,7 +6386,7 @@ async function planInit(options) {
5412
6386
  for (const f of agentFiles) {
5413
6387
  if (sourceRoot) {
5414
6388
  const agentFileMode = options.conflictPolicy?.agentFile ?? "both";
5415
- const basename = path3.basename(f);
6389
+ const basename = path5.basename(f);
5416
6390
  if (agentFileMode === "skip" || agentFileMode === "agents" && basename !== "AGENTS.md" || agentFileMode === "claude" && basename !== "CLAUDE.md") {
5417
6391
  operations.push({
5418
6392
  kind: "skip",
@@ -5438,7 +6412,7 @@ async function planInit(options) {
5438
6412
  kind: "skip",
5439
6413
  path: f,
5440
6414
  ownership: "project-owned",
5441
- reason: `Existing agent file: ${path3.basename(f)}`,
6415
+ reason: `Existing agent file: ${path5.basename(f)}`,
5442
6416
  requiresConfirmation: false,
5443
6417
  destructive: false
5444
6418
  });
@@ -5461,15 +6435,15 @@ async function planInit(options) {
5461
6435
  if (s.includes(".opencode") && options.legacySkills) {
5462
6436
  const skillFiles = await walkDir(s);
5463
6437
  for (const file of skillFiles) {
5464
- const relPath = path3.relative(s, file);
5465
- const destPath = path3.join(targetRoot, ".agents", "skills", relPath);
5466
- if (!existsSync6(destPath)) {
6438
+ const relPath = path5.relative(s, file);
6439
+ const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
6440
+ if (!existsSync8(destPath)) {
5467
6441
  operations.push({
5468
6442
  kind: "copy",
5469
6443
  sourcePath: file,
5470
6444
  path: destPath,
5471
6445
  ownership: "project-owned",
5472
- reason: `Migrate legacy skill: ${path3.join(".opencode/skills", relPath)}`,
6446
+ reason: `Migrate legacy skill: ${path5.join(".opencode/skills", relPath)}`,
5473
6447
  requiresConfirmation: false,
5474
6448
  destructive: false
5475
6449
  });
@@ -5526,7 +6500,7 @@ async function planInit(options) {
5526
6500
  });
5527
6501
  }
5528
6502
  if (sourceRoot) {
5529
- if (!existsSync6(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
6503
+ if (!existsSync8(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
5530
6504
  warnings.push(
5531
6505
  `--from-local path does not exist or is not a directory: ${sourceRoot}`
5532
6506
  );
@@ -5587,17 +6561,17 @@ async function planInit(options) {
5587
6561
  continue;
5588
6562
  }
5589
6563
  const targetDirName = ".agents/skills";
5590
- const targetPath = path3.join(targetRoot, targetDirName);
6564
+ const targetPath = path5.join(targetRoot, targetDirName);
5591
6565
  const skillFiles = await walkDir(s);
5592
6566
  for (const file of skillFiles) {
5593
- const relPath = path3.relative(s, file);
5594
- const destPath = path3.join(targetPath, relPath);
6567
+ const relPath = path5.relative(s, file);
6568
+ const destPath = path5.join(targetPath, relPath);
5595
6569
  if (plannedSkillDests.has(destPath)) {
5596
6570
  operations.push({
5597
6571
  kind: "skip",
5598
6572
  path: destPath,
5599
6573
  ownership: "project-owned",
5600
- reason: `Skill destination conflict, skipping source copy: ${path3.join(targetDirName, relPath)}`,
6574
+ reason: `Skill destination conflict, skipping source copy: ${path5.join(targetDirName, relPath)}`,
5601
6575
  requiresConfirmation: false,
5602
6576
  destructive: false,
5603
6577
  conflict: "destination already planned"
@@ -5610,7 +6584,7 @@ async function planInit(options) {
5610
6584
  sourcePath: file,
5611
6585
  path: destPath,
5612
6586
  ownership: "copied-customizable",
5613
- reason: `Copy skill: ${path3.join(targetDirName, relPath)}`,
6587
+ reason: `Copy skill: ${path5.join(targetDirName, relPath)}`,
5614
6588
  requiresConfirmation: false,
5615
6589
  destructive: false,
5616
6590
  checksum
@@ -5622,7 +6596,7 @@ async function planInit(options) {
5622
6596
  operations.push({
5623
6597
  kind: "copy",
5624
6598
  sourcePath: srcReadme,
5625
- path: path3.join(targetRoot, "README.md"),
6599
+ path: path5.join(targetRoot, "README.md"),
5626
6600
  ownership: "project-owned",
5627
6601
  reason: "Copy README.md from source",
5628
6602
  requiresConfirmation: false,
@@ -5633,8 +6607,8 @@ async function planInit(options) {
5633
6607
  const rootDocs = await discoverRootDomainDocs(targetRoot);
5634
6608
  const merleDestPaths = /* @__PURE__ */ new Set();
5635
6609
  for (const docPath of rootDocs) {
5636
- const relPath = path3.relative(targetRoot, docPath);
5637
- const destPath = path3.join(targetRoot, ".pourkit", relPath);
6610
+ const relPath = path5.relative(targetRoot, docPath);
6611
+ const destPath = path5.join(targetRoot, ".pourkit", relPath);
5638
6612
  merleDestPaths.add(destPath);
5639
6613
  if (docsMigration === "skip") {
5640
6614
  operations.push({
@@ -5645,7 +6619,7 @@ async function planInit(options) {
5645
6619
  requiresConfirmation: false,
5646
6620
  destructive: false
5647
6621
  });
5648
- } else if (existsSync6(destPath)) {
6622
+ } else if (existsSync8(destPath)) {
5649
6623
  operations.push({
5650
6624
  kind: "skip",
5651
6625
  path: destPath,
@@ -5680,8 +6654,8 @@ async function planInit(options) {
5680
6654
  let packageScripts = {};
5681
6655
  let hasPackageJson = true;
5682
6656
  try {
5683
- const pkgContent = await readFile3(
5684
- path3.join(targetRoot, "package.json"),
6657
+ const pkgContent = await readFile4(
6658
+ path5.join(targetRoot, "package.json"),
5685
6659
  "utf-8"
5686
6660
  );
5687
6661
  const pkg = JSON.parse(pkgContent);
@@ -5702,8 +6676,8 @@ async function planInit(options) {
5702
6676
  } catch {
5703
6677
  }
5704
6678
  }
5705
- const contextPath = path3.join(targetRoot, ".pourkit", "CONTEXT.md");
5706
- if (!existsSync6(contextPath) && !merleDestPaths.has(contextPath)) {
6679
+ const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
6680
+ if (!existsSync8(contextPath) && !merleDestPaths.has(contextPath)) {
5707
6681
  operations.push({
5708
6682
  kind: "create",
5709
6683
  path: contextPath,
@@ -5714,14 +6688,14 @@ async function planInit(options) {
5714
6688
  content: generateContextScaffold()
5715
6689
  });
5716
6690
  }
5717
- const adrGitkeep = path3.join(
6691
+ const adrGitkeep = path5.join(
5718
6692
  targetRoot,
5719
6693
  ".pourkit",
5720
6694
  "docs",
5721
6695
  "adr",
5722
6696
  ".gitkeep"
5723
6697
  );
5724
- if (!existsSync6(adrGitkeep)) {
6698
+ if (!existsSync8(adrGitkeep)) {
5725
6699
  operations.push({
5726
6700
  kind: "create",
5727
6701
  path: adrGitkeep,
@@ -5731,16 +6705,16 @@ async function planInit(options) {
5731
6705
  destructive: false
5732
6706
  });
5733
6707
  }
5734
- const srcDocAgents = path3.join(sourceRoot, ".pourkit", "docs", "agents");
5735
- const tgtDocAgents = path3.join(targetRoot, ".pourkit", "docs", "agents");
5736
- if (existsSync6(srcDocAgents) && !existsSync6(tgtDocAgents)) {
6708
+ const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
6709
+ const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
6710
+ if (existsSync8(srcDocAgents) && !existsSync8(tgtDocAgents)) {
5737
6711
  const docFiles = await walkDir(srcDocAgents);
5738
6712
  for (const file of docFiles) {
5739
- const relPath = path3.relative(srcDocAgents, file);
6713
+ const relPath = path5.relative(srcDocAgents, file);
5740
6714
  if (relPath === "triage-labels.md") {
5741
6715
  operations.push({
5742
6716
  kind: "create",
5743
- path: path3.join(tgtDocAgents, relPath),
6717
+ path: path5.join(tgtDocAgents, relPath),
5744
6718
  ownership: "managed",
5745
6719
  reason: "Init triage labels doc",
5746
6720
  requiresConfirmation: false,
@@ -5755,7 +6729,7 @@ async function planInit(options) {
5755
6729
  operations.push({
5756
6730
  kind: "copy",
5757
6731
  sourcePath: file,
5758
- path: path3.join(tgtDocAgents, relPath),
6732
+ path: path5.join(tgtDocAgents, relPath),
5759
6733
  ownership: "managed",
5760
6734
  reason: `Copy agent doc: ${relPath}`,
5761
6735
  requiresConfirmation: false,
@@ -5764,17 +6738,17 @@ async function planInit(options) {
5764
6738
  });
5765
6739
  }
5766
6740
  }
5767
- const srcPrompts = path3.join(sourceRoot, ".pourkit", "prompts");
5768
- const tgtPrompts = path3.join(targetRoot, ".pourkit", "prompts");
5769
- if (existsSync6(srcPrompts) && !existsSync6(tgtPrompts)) {
6741
+ const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
6742
+ const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
6743
+ if (existsSync8(srcPrompts) && !existsSync8(tgtPrompts)) {
5770
6744
  const promptFiles = await walkDir(srcPrompts);
5771
6745
  for (const file of promptFiles) {
5772
- const relPath = path3.relative(srcPrompts, file);
6746
+ const relPath = path5.relative(srcPrompts, file);
5773
6747
  const checksum = await computeFileChecksum(file);
5774
6748
  operations.push({
5775
6749
  kind: "copy",
5776
6750
  sourcePath: file,
5777
- path: path3.join(tgtPrompts, relPath),
6751
+ path: path5.join(tgtPrompts, relPath),
5778
6752
  ownership: "managed",
5779
6753
  reason: `Copy prompt: ${relPath}`,
5780
6754
  requiresConfirmation: false,
@@ -5783,17 +6757,17 @@ async function planInit(options) {
5783
6757
  });
5784
6758
  }
5785
6759
  }
5786
- const srcSandboxDockerfile = path3.join(
6760
+ const srcSandboxDockerfile = path5.join(
5787
6761
  sourceRoot,
5788
6762
  ".sandcastle",
5789
6763
  "Dockerfile"
5790
6764
  );
5791
- const tgtSandboxDockerfile = path3.join(
6765
+ const tgtSandboxDockerfile = path5.join(
5792
6766
  targetRoot,
5793
6767
  ".sandcastle",
5794
6768
  "Dockerfile"
5795
6769
  );
5796
- if (existsSync6(tgtSandboxDockerfile)) {
6770
+ if (existsSync8(tgtSandboxDockerfile)) {
5797
6771
  operations.push({
5798
6772
  kind: "skip",
5799
6773
  path: tgtSandboxDockerfile,
@@ -5802,7 +6776,7 @@ async function planInit(options) {
5802
6776
  requiresConfirmation: false,
5803
6777
  destructive: false
5804
6778
  });
5805
- } else if (existsSync6(srcSandboxDockerfile)) {
6779
+ } else if (existsSync8(srcSandboxDockerfile)) {
5806
6780
  const checksum = await computeFileChecksum(srcSandboxDockerfile);
5807
6781
  operations.push({
5808
6782
  kind: "copy",
@@ -5815,8 +6789,8 @@ async function planInit(options) {
5815
6789
  checksum
5816
6790
  });
5817
6791
  }
5818
- const configTsPath = path3.join(targetRoot, "pourkit.config.ts");
5819
- if (!existsSync6(configTsPath)) {
6792
+ const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
6793
+ if (!existsSync8(configTsPath)) {
5820
6794
  const verifyCommands = inferVerificationCommands(
5821
6795
  packageScripts,
5822
6796
  pm || "npm"
@@ -5853,10 +6827,10 @@ async function planInit(options) {
5853
6827
  const hasExistingAgents = operations.some(
5854
6828
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
5855
6829
  );
5856
- if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync6(path3.join(targetRoot, "AGENTS.md"))) {
6830
+ if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync8(path5.join(targetRoot, "AGENTS.md"))) {
5857
6831
  operations.push({
5858
6832
  kind: "create",
5859
- path: path3.join(targetRoot, "AGENTS.md"),
6833
+ path: path5.join(targetRoot, "AGENTS.md"),
5860
6834
  ownership: "managed",
5861
6835
  reason: "Init AGENTS.md with Pourkit managed block",
5862
6836
  requiresConfirmation: false,
@@ -5869,10 +6843,10 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
5869
6843
  const hasExistingClaude = operations.some(
5870
6844
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
5871
6845
  );
5872
- if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync6(path3.join(targetRoot, "CLAUDE.md"))) {
6846
+ if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync8(path5.join(targetRoot, "CLAUDE.md"))) {
5873
6847
  operations.push({
5874
6848
  kind: "create",
5875
- path: path3.join(targetRoot, "CLAUDE.md"),
6849
+ path: path5.join(targetRoot, "CLAUDE.md"),
5876
6850
  ownership: "managed",
5877
6851
  reason: "Init CLAUDE.md with Pourkit managed block",
5878
6852
  requiresConfirmation: false,
@@ -5882,9 +6856,9 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
5882
6856
  `
5883
6857
  });
5884
6858
  }
5885
- const gitignoreTarget = path3.join(targetRoot, ".gitignore");
6859
+ const gitignoreTarget = path5.join(targetRoot, ".gitignore");
5886
6860
  const gitignoreContent = generateGitignoreBlock();
5887
- if (!existsSync6(gitignoreTarget)) {
6861
+ if (!existsSync8(gitignoreTarget)) {
5888
6862
  operations.push({
5889
6863
  kind: "create",
5890
6864
  path: gitignoreTarget,
@@ -5907,8 +6881,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
5907
6881
  content: gitignoreContent
5908
6882
  });
5909
6883
  }
5910
- const openCodePath = path3.join(targetRoot, "opencode.json");
5911
- if (!existsSync6(openCodePath)) {
6884
+ const openCodePath = path5.join(targetRoot, "opencode.json");
6885
+ if (!existsSync8(openCodePath)) {
5912
6886
  operations.push({
5913
6887
  kind: "create",
5914
6888
  path: openCodePath,
@@ -5920,7 +6894,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
5920
6894
  });
5921
6895
  } else {
5922
6896
  try {
5923
- const existingContent = await readFile3(openCodePath, "utf-8");
6897
+ const existingContent = await readFile4(openCodePath, "utf-8");
5924
6898
  const existingConfig = JSON.parse(existingContent);
5925
6899
  if (typeof existingConfig !== "object" || existingConfig === null || Array.isArray(existingConfig)) {
5926
6900
  warnings.push(
@@ -5964,8 +6938,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
5964
6938
  });
5965
6939
  }
5966
6940
  }
5967
- const manifestPath = path3.join(targetRoot, ".pourkit", "manifest.json");
5968
- if (existsSync6(manifestPath)) {
6941
+ const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
6942
+ if (existsSync8(manifestPath)) {
5969
6943
  operations.push({
5970
6944
  kind: "skip",
5971
6945
  path: manifestPath,
@@ -6284,13 +7258,13 @@ async function updateManagedBlock(filePath, content) {
6284
7258
  const blockContent = `${MANAGED_BLOCK_BEGIN}
6285
7259
  ${content}${MANAGED_BLOCK_END}
6286
7260
  `;
6287
- if (!existsSync6(filePath)) {
6288
- const dir = path3.dirname(filePath);
6289
- await mkdir2(dir, { recursive: true });
7261
+ if (!existsSync8(filePath)) {
7262
+ const dir = path5.dirname(filePath);
7263
+ await mkdir4(dir, { recursive: true });
6290
7264
  await writeFileAtomic(filePath, blockContent);
6291
7265
  return;
6292
7266
  }
6293
- const existing = await readFile3(filePath, "utf-8");
7267
+ const existing = await readFile4(filePath, "utf-8");
6294
7268
  const beginIdx = existing.indexOf(MANAGED_BLOCK_BEGIN);
6295
7269
  const endIdx = existing.indexOf(MANAGED_BLOCK_END);
6296
7270
  if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
@@ -6303,17 +7277,17 @@ ${content}${MANAGED_BLOCK_END}
6303
7277
  }
6304
7278
  }
6305
7279
  async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
6306
- const manifestDir = path3.join(plan.targetRoot, ".pourkit");
6307
- const manifestPath = path3.join(manifestDir, "manifest.json");
7280
+ const manifestDir = path5.join(plan.targetRoot, ".pourkit");
7281
+ const manifestPath = path5.join(manifestDir, "manifest.json");
6308
7282
  const assets = {};
6309
7283
  for (const op of plan.operations) {
6310
7284
  if (!op.path) continue;
6311
7285
  if (op.kind !== "create" && op.kind !== "copy" && op.kind !== "update" && op.kind !== "move")
6312
7286
  continue;
6313
7287
  if (op.requiresConfirmation) continue;
6314
- const relPath = path3.relative(plan.targetRoot, op.path);
7288
+ const relPath = path5.relative(plan.targetRoot, op.path);
6315
7289
  if (relPath === ".pourkit/manifest.json") continue;
6316
- if (existsSync6(op.path)) {
7290
+ if (existsSync8(op.path)) {
6317
7291
  const sha256 = await computeFileChecksum(op.path);
6318
7292
  assets[relPath] = {
6319
7293
  ownership: op.ownership || "managed",
@@ -6337,7 +7311,7 @@ async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
6337
7311
  packageManager,
6338
7312
  assets
6339
7313
  };
6340
- await mkdir2(manifestDir, { recursive: true });
7314
+ await mkdir4(manifestDir, { recursive: true });
6341
7315
  await writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
6342
7316
  return manifest;
6343
7317
  }
@@ -6358,12 +7332,12 @@ async function applyInitPlan(plan, options) {
6358
7332
  skipped++;
6359
7333
  continue;
6360
7334
  }
6361
- if (existsSync6(op.path) && !op.destructive) {
7335
+ if (existsSync8(op.path) && !op.destructive) {
6362
7336
  skipped++;
6363
7337
  continue;
6364
7338
  }
6365
- const dir = path3.dirname(op.path);
6366
- await mkdir2(dir, { recursive: true });
7339
+ const dir = path5.dirname(op.path);
7340
+ await mkdir4(dir, { recursive: true });
6367
7341
  await writeFileAtomic(op.path, op.content ?? "");
6368
7342
  applied++;
6369
7343
  break;
@@ -6373,7 +7347,7 @@ async function applyInitPlan(plan, options) {
6373
7347
  skipped++;
6374
7348
  continue;
6375
7349
  }
6376
- if (existsSync6(op.path)) {
7350
+ if (existsSync8(op.path)) {
6377
7351
  skipped++;
6378
7352
  continue;
6379
7353
  }
@@ -6381,8 +7355,8 @@ async function applyInitPlan(plan, options) {
6381
7355
  if (srcStat.isDirectory()) {
6382
7356
  skipped++;
6383
7357
  } else {
6384
- const dir = path3.dirname(op.path);
6385
- await mkdir2(dir, { recursive: true });
7358
+ const dir = path5.dirname(op.path);
7359
+ await mkdir4(dir, { recursive: true });
6386
7360
  await copyFile(op.sourcePath, op.path);
6387
7361
  applied++;
6388
7362
  }
@@ -6402,12 +7376,12 @@ async function applyInitPlan(plan, options) {
6402
7376
  skipped++;
6403
7377
  continue;
6404
7378
  }
6405
- if (existsSync6(op.path)) {
7379
+ if (existsSync8(op.path)) {
6406
7380
  skipped++;
6407
7381
  continue;
6408
7382
  }
6409
- const dir = path3.dirname(op.path);
6410
- await mkdir2(dir, { recursive: true });
7383
+ const dir = path5.dirname(op.path);
7384
+ await mkdir4(dir, { recursive: true });
6411
7385
  await rename(op.sourcePath, op.path);
6412
7386
  applied++;
6413
7387
  break;
@@ -6537,13 +7511,13 @@ async function applyInitFromSource(options) {
6537
7511
  let manifestWritten = false;
6538
7512
  if (result.errors.length === 0) {
6539
7513
  const manifestSkipped = plan.operations.some(
6540
- (op) => op.kind === "skip" && op.path === path3.join(targetRoot, ".pourkit", "manifest.json")
7514
+ (op) => op.kind === "skip" && op.path === path5.join(targetRoot, ".pourkit", "manifest.json")
6541
7515
  );
6542
7516
  if (!manifestSkipped) {
6543
7517
  const agentFiles = [];
6544
7518
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
6545
- if (existsSync6(path3.join(targetRoot, name))) {
6546
- agentFiles.push(path3.join(targetRoot, name));
7519
+ if (existsSync8(path5.join(targetRoot, name))) {
7520
+ agentFiles.push(path5.join(targetRoot, name));
6547
7521
  }
6548
7522
  }
6549
7523
  const pm = detectPackageManager(targetRoot);
@@ -6718,6 +7692,121 @@ Init applied: ${result.applied} operations applied, ${result.skipped} skipped.`
6718
7692
  }
6719
7693
  }
6720
7694
 
7695
+ // commands/serena.ts
7696
+ init_common();
7697
+ var SERENA_MCP_PORT2 = 9121;
7698
+ var SERENA_DASHBOARD_PORT2 = 24282;
7699
+ var SERENA_IMAGE2 = "ghcr.io/oraios/serena:latest";
7700
+ async function resolveSerenaCommandContext(options) {
7701
+ const repoRootPath = options.cwd ? repoRoot(options.cwd) : repoRoot();
7702
+ const config = await loadRepoConfig(repoRootPath);
7703
+ const target = resolveTarget(config, options.target);
7704
+ return {
7705
+ repoRootPath,
7706
+ config,
7707
+ target
7708
+ };
7709
+ }
7710
+ async function resolveSerenaLifecycleContext(options) {
7711
+ const repoRootPath = options.cwd ? repoRoot(options.cwd) : repoRoot();
7712
+ const config = await loadRepoConfig(repoRootPath);
7713
+ return {
7714
+ repoRootPath,
7715
+ config,
7716
+ paths: resolveSerenaPaths(repoRootPath, config.serena.dataDir)
7717
+ };
7718
+ }
7719
+ function buildSerenaSidecarOptions(paths, mcpUrl) {
7720
+ return {
7721
+ baselineWorktreePath: paths.baselineWorktreePath,
7722
+ dataDir: paths.dataDir,
7723
+ mcpPort: SERENA_MCP_PORT2,
7724
+ dashboardPort: SERENA_DASHBOARD_PORT2,
7725
+ image: SERENA_IMAGE2,
7726
+ mcpUrl
7727
+ };
7728
+ }
7729
+ function logSerenaSidecarStatus(heading, status, baselineFreshness) {
7730
+ console.log(`${heading}:`);
7731
+ console.log(` running: ${status.running ? "yes" : "no"}`);
7732
+ console.log(` mcpUrl: ${status.mcpUrl}`);
7733
+ console.log(` dashboardUrl: ${status.dashboardUrl}`);
7734
+ console.log(` containerName: ${status.containerName}`);
7735
+ if (baselineFreshness) {
7736
+ console.log(` Baseline freshness: ${baselineFreshness}`);
7737
+ }
7738
+ }
7739
+ async function runSerenaInitCommand(options) {
7740
+ const { repoRootPath, config, target } = await resolveSerenaCommandContext(options);
7741
+ const paths = await ensureBaselineWorktree({
7742
+ repoRoot: repoRootPath,
7743
+ dataDir: config.serena.dataDir
7744
+ });
7745
+ await refreshSerenaBaseline({
7746
+ repoRoot: repoRootPath,
7747
+ dataDir: config.serena.dataDir,
7748
+ baseBranch: target.baseBranch
7749
+ });
7750
+ await prepareSerenaSidecarConfig({
7751
+ baselineWorktreePath: paths.baselineWorktreePath,
7752
+ dataDir: paths.dataDir
7753
+ });
7754
+ const sidecarOptions2 = buildSerenaSidecarOptions(paths, config.serena.mcpUrl);
7755
+ await startSerenaSidecar(sidecarOptions2);
7756
+ await indexSerenaProject(sidecarOptions2);
7757
+ }
7758
+ async function runSerenaRefreshCommand(options) {
7759
+ const { repoRootPath, config, target } = await resolveSerenaCommandContext(options);
7760
+ await refreshSerenaBaseline({
7761
+ repoRoot: repoRootPath,
7762
+ dataDir: config.serena.dataDir,
7763
+ baseBranch: target.baseBranch
7764
+ });
7765
+ }
7766
+ async function runSerenaStartCommand(options) {
7767
+ const { repoRootPath, config } = await resolveSerenaLifecycleContext(options);
7768
+ const ensuredPaths = await ensureBaselineWorktree({
7769
+ repoRoot: repoRootPath,
7770
+ dataDir: config.serena.dataDir
7771
+ });
7772
+ await prepareSerenaSidecarConfig({
7773
+ baselineWorktreePath: ensuredPaths.baselineWorktreePath,
7774
+ dataDir: ensuredPaths.dataDir
7775
+ });
7776
+ const status = await startSerenaSidecar(
7777
+ buildSerenaSidecarOptions(ensuredPaths, config.serena.mcpUrl)
7778
+ );
7779
+ logSerenaSidecarStatus("Serena sidecar started", status);
7780
+ }
7781
+ async function runSerenaStopCommand(options) {
7782
+ const { config, paths } = await resolveSerenaLifecycleContext(options);
7783
+ const status = await stopSerenaSidecar(
7784
+ buildSerenaSidecarOptions(paths, config.serena.mcpUrl)
7785
+ );
7786
+ logSerenaSidecarStatus("Serena sidecar stopped", status);
7787
+ }
7788
+ async function runSerenaStatusCommand(options) {
7789
+ const { repoRootPath, config, paths } = await resolveSerenaLifecycleContext(options);
7790
+ const status = await getSerenaSidecarStatus(
7791
+ buildSerenaSidecarOptions(paths, config.serena.mcpUrl)
7792
+ );
7793
+ if (options.target) {
7794
+ const target = resolveTarget(config, options.target);
7795
+ const baseline = await getSerenaBaselineStatus({
7796
+ repoRoot: repoRootPath,
7797
+ dataDir: config.serena.dataDir,
7798
+ baseBranch: target.baseBranch
7799
+ });
7800
+ logSerenaSidecarStatus(
7801
+ "Serena sidecar status",
7802
+ status,
7803
+ baseline.fresh ? "fresh" : "stale"
7804
+ );
7805
+ return;
7806
+ }
7807
+ logSerenaSidecarStatus("Serena sidecar status", status);
7808
+ }
7809
+
6721
7810
  // providers/github-provider.ts
6722
7811
  var GitHubIssueProvider = class {
6723
7812
  client;
@@ -6730,6 +7819,17 @@ var GitHubIssueProvider = class {
6730
7819
  this.blockedLabel = options?.blockedLabel ?? "blocked";
6731
7820
  this.issueListLimit = options?.issueListLimit ?? 50;
6732
7821
  }
7822
+ async createIssue(options) {
7823
+ const { data } = await this.client.octokit.rest.issues.create({
7824
+ owner: this.client.owner,
7825
+ repo: this.client.repo,
7826
+ title: options.title,
7827
+ body: options.body ?? "",
7828
+ labels: options.labels,
7829
+ assignees: options.assignees
7830
+ });
7831
+ return { number: data.number, url: data.html_url, title: data.title };
7832
+ }
6733
7833
  async fetchIssue(number) {
6734
7834
  const { data } = await this.client.octokit.rest.issues.get({
6735
7835
  owner: this.client.owner,
@@ -7234,47 +8334,47 @@ function formatChecks2(checks) {
7234
8334
  init_common();
7235
8335
 
7236
8336
  // execution/sandcastle-execution.ts
7237
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
7238
- import { join as join10 } from "path";
8337
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
8338
+ import { join as join12 } from "path";
7239
8339
  import { createWorktree, opencode } from "@ai-hero/sandcastle";
7240
8340
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
7241
8341
 
7242
8342
  // execution/execution-provider.ts
7243
8343
  init_common();
7244
8344
  import { writeFile as writeFile2 } from "fs/promises";
7245
- import { dirname as dirname4, join as join9 } from "path";
8345
+ import { dirname as dirname5, join as join11 } from "path";
7246
8346
  async function writeExecutionArtifacts(worktreePath, artifacts) {
7247
8347
  for (const artifact of artifacts) {
7248
- const filePath = join9(worktreePath, artifact.path);
7249
- await ensureDir(dirname4(filePath));
8348
+ const filePath = join11(worktreePath, artifact.path);
8349
+ await ensureDir(dirname5(filePath));
7250
8350
  await writeFile2(filePath, artifact.content, "utf-8");
7251
8351
  }
7252
8352
  }
7253
8353
 
7254
8354
  // execution/sandbox-image-build.ts
7255
8355
  init_common();
7256
- import path5 from "path";
8356
+ import path7 from "path";
7257
8357
 
7258
8358
  // execution/sandbox-image.ts
7259
8359
  import { createHash as createHash2 } from "crypto";
7260
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
7261
- import path4 from "path";
8360
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
8361
+ import path6 from "path";
7262
8362
  function sandboxImageName(repoRoot2) {
7263
- const dirName = path4.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
8363
+ const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
7264
8364
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
7265
8365
  const baseName = sanitized || "local";
7266
- const dockerfilePath = path4.join(repoRoot2, ".sandcastle", "Dockerfile");
7267
- if (!existsSync7(dockerfilePath)) {
8366
+ const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
8367
+ if (!existsSync9(dockerfilePath)) {
7268
8368
  return `sandcastle:${baseName}`;
7269
8369
  }
7270
- const fingerprint = createHash2("sha256").update(readFileSync6(dockerfilePath)).digest("hex").slice(0, 8);
8370
+ const fingerprint = createHash2("sha256").update(readFileSync8(dockerfilePath)).digest("hex").slice(0, 8);
7271
8371
  return `sandcastle:${baseName}-${fingerprint}`;
7272
8372
  }
7273
8373
 
7274
8374
  // execution/sandbox-image-build.ts
7275
8375
  async function ensureSandboxImageBuilt(repoRoot2, options) {
7276
8376
  const imageName = sandboxImageName(repoRoot2);
7277
- const dockerfilePath = path5.join(repoRoot2, ".sandcastle", "Dockerfile");
8377
+ const dockerfilePath = path7.join(repoRoot2, ".sandcastle", "Dockerfile");
7278
8378
  if (!options?.force) {
7279
8379
  try {
7280
8380
  await execCapture("docker", ["image", "inspect", imageName]);
@@ -7312,6 +8412,25 @@ function buildSandboxOptions(repoRoot2, sandbox) {
7312
8412
  };
7313
8413
  }
7314
8414
 
8415
+ // execution/opencode-config.ts
8416
+ function isSerenaEligibleStage(stage) {
8417
+ return stage === "builder" || stage === "refactor";
8418
+ }
8419
+ function buildSerenaOpenCodeConfig(stage, serena) {
8420
+ if (!serena?.available || !isSerenaEligibleStage(stage)) {
8421
+ return void 0;
8422
+ }
8423
+ return {
8424
+ mcp: {
8425
+ serena: {
8426
+ type: "remote",
8427
+ url: serena.sandboxMcpUrl,
8428
+ enabled: true
8429
+ }
8430
+ }
8431
+ };
8432
+ }
8433
+
7315
8434
  // execution/sandcastle-execution.ts
7316
8435
  var SandcastleExecutionProvider = class {
7317
8436
  async createSession() {
@@ -7364,6 +8483,10 @@ var SandcastleExecutionSession = class {
7364
8483
  if (autoApprove) {
7365
8484
  env.OPENCODE_AUTO_APPROVE = "true";
7366
8485
  }
8486
+ const serenaConfig = buildSerenaOpenCodeConfig(stage, options.serena);
8487
+ if (serenaConfig) {
8488
+ env.OPENCODE_CONFIG_CONTENT = JSON.stringify(serenaConfig);
8489
+ }
7367
8490
  const logPath = `${repoRoot2}/.pourkit/logs/${sanitizeBranch(branchName)}-${Date.now()}.log`;
7368
8491
  await ensureSandboxImageBuilt(repoRoot2, { force: sandbox.forceRebuild });
7369
8492
  try {
@@ -7482,12 +8605,12 @@ function sanitizeBranch(branchName) {
7482
8605
  return branchName.replace(/[^A-Za-z0-9._-]/g, "-");
7483
8606
  }
7484
8607
  function savePromptToFile(repoRoot2, stage, iteration, prompt) {
7485
- const promptsDir = join10(repoRoot2, ".pourkit", ".tmp", "prompts");
7486
- mkdirSync5(promptsDir, { recursive: true });
8608
+ const promptsDir = join12(repoRoot2, ".pourkit", ".tmp", "prompts");
8609
+ mkdirSync6(promptsDir, { recursive: true });
7487
8610
  const timestamp2 = Date.now();
7488
8611
  const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
7489
8612
  const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
7490
- const filePath = join10(promptsDir, filename);
8613
+ const filePath = join12(promptsDir, filename);
7491
8614
  writeFileSync4(filePath, prompt, "utf-8");
7492
8615
  }
7493
8616
 
@@ -7553,19 +8676,91 @@ async function handleError(logger, error) {
7553
8676
  function createCliProgram(version) {
7554
8677
  const program = new Command();
7555
8678
  program.name("pourkit").version(version).exitOverride().description("AI-driven issue-to-PR workflow for GitHub repositories.");
7556
- program.command("issue").argument("<number>", "issue number").requiredOption("--target <name>", "target name").option("--force", "bypass issue gates").option(
8679
+ const issueCommand = program.command("issue");
8680
+ issueCommand.command("create").description("Create a new GitHub issue").requiredOption("--title <title>", "issue title").option("--body <body>", "issue body text").option("--body-file <path>", "file to read issue body from").option(
8681
+ "--label <label>",
8682
+ "label to apply (repeatable)",
8683
+ (value, previous) => {
8684
+ previous.push(value);
8685
+ return previous;
8686
+ },
8687
+ []
8688
+ ).option(
8689
+ "--assignee <login>",
8690
+ "assignee login (repeatable)",
8691
+ (value, previous) => {
8692
+ previous.push(value);
8693
+ return previous;
8694
+ },
8695
+ []
8696
+ ).option("--cwd <path>", "target repository directory").action(
8697
+ async (options) => {
8698
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
8699
+ const logPath = path8.join(
8700
+ targetRepoRoot,
8701
+ ".pourkit",
8702
+ "logs",
8703
+ "issue-create.log"
8704
+ );
8705
+ const logger = createLogger("pourkit", logPath);
8706
+ const client = await requireGitHubClient({ cwd: targetRepoRoot });
8707
+ const issueProvider = new GitHubIssueProvider(client);
8708
+ try {
8709
+ const args = ["--title", options.title];
8710
+ if (options.body) {
8711
+ args.push("--body", options.body);
8712
+ }
8713
+ if (options.bodyFile) {
8714
+ const bodyFileArg = options.cwd && !path8.isAbsolute(options.bodyFile) ? path8.resolve(options.cwd, options.bodyFile) : options.bodyFile;
8715
+ args.push("--body-file", bodyFileArg);
8716
+ }
8717
+ for (const label of options.label) {
8718
+ args.push("--label", label);
8719
+ }
8720
+ for (const assignee of options.assignee) {
8721
+ args.push("--assignee", assignee);
8722
+ }
8723
+ const result = await runIssueCreateCommand(
8724
+ args,
8725
+ issueProvider,
8726
+ logger
8727
+ );
8728
+ logger.raw("Issue created successfully:");
8729
+ logger.raw(` Issue Number: #${result.issueNumber}`);
8730
+ logger.raw(` Issue Title: ${result.issueTitle}`);
8731
+ logger.raw(` Issue URL: ${result.issueUrl}`);
8732
+ await logger.close();
8733
+ } catch (error) {
8734
+ await handleError(logger, error);
8735
+ }
8736
+ }
8737
+ );
8738
+ issueCommand.argument("[value]", "issue number or subcommand").option("--target <name>", "target name").option("--force", "bypass issue gates").option(
7557
8739
  "--reset-worktree",
7558
8740
  "delete local issue worktree and branch before starting"
7559
8741
  ).option("--cwd <path>", "target repository directory").action(
7560
- async (issueNumberRaw, options) => {
8742
+ async (value, options) => {
8743
+ if (!value) {
8744
+ console.error(
8745
+ "Invalid command. Use 'issue <number>' or 'issue create --title ...'"
8746
+ );
8747
+ process.exit(1);
8748
+ }
8749
+ const issueNumberRaw = value;
7561
8750
  const issueNumber = Number.parseInt(issueNumberRaw, 10);
7562
8751
  if (Number.isNaN(issueNumber)) {
7563
8752
  console.error(`Invalid issue number: ${issueNumberRaw}`);
7564
8753
  process.exit(1);
7565
8754
  }
8755
+ if (!options.target) {
8756
+ console.error(
8757
+ "error: required option '--target <name>' not specified"
8758
+ );
8759
+ process.exit(1);
8760
+ }
7566
8761
  const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
7567
8762
  const config = await loadRepoConfig(targetRepoRoot);
7568
- const logPath = path6.join(
8763
+ const logPath = path8.join(
7569
8764
  targetRepoRoot,
7570
8765
  ".pourkit",
7571
8766
  "logs",
@@ -7624,7 +8819,7 @@ function createCliProgram(version) {
7624
8819
  const prdRef = options.prd ? normalizePrdRef(options.prd) : void 0;
7625
8820
  const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
7626
8821
  const config = await loadRepoConfig(targetRepoRoot);
7627
- const logPath = path6.join(
8822
+ const logPath = path8.join(
7628
8823
  targetRepoRoot,
7629
8824
  ".pourkit",
7630
8825
  "logs",
@@ -7721,6 +8916,35 @@ function createCliProgram(version) {
7721
8916
  await runInitCommand(initOptions);
7722
8917
  }
7723
8918
  );
8919
+ const serena = program.command("serena").description("Serena baseline and sidecar commands");
8920
+ serena.command("init").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
8921
+ await runSerenaInitCommand({
8922
+ target: options.target,
8923
+ cwd: options.cwd
8924
+ });
8925
+ });
8926
+ serena.command("refresh").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
8927
+ await runSerenaRefreshCommand({
8928
+ target: options.target,
8929
+ cwd: options.cwd
8930
+ });
8931
+ });
8932
+ serena.command("start").option("--cwd <path>", "target repository directory").action(async (options) => {
8933
+ await runSerenaStartCommand({
8934
+ cwd: options.cwd
8935
+ });
8936
+ });
8937
+ serena.command("stop").option("--cwd <path>", "target repository directory").action(async (options) => {
8938
+ await runSerenaStopCommand({
8939
+ cwd: options.cwd
8940
+ });
8941
+ });
8942
+ serena.command("status").option("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
8943
+ await runSerenaStatusCommand({
8944
+ target: options.target,
8945
+ cwd: options.cwd
8946
+ });
8947
+ });
7724
8948
  const pr = program.command("pr").description("Pull request commands");
7725
8949
  pr.command("create").requiredOption("--target <name>", "target name").requiredOption("--title <title>", "PR title").option("--base <base>", "base branch").option("--head <branch>", "head branch").option("--body <body>", "PR body").option("--body-file <path>", "file to read PR body from").option(
7726
8950
  "--issue <number>",
@@ -7734,7 +8958,7 @@ function createCliProgram(version) {
7734
8958
  async (options) => {
7735
8959
  const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
7736
8960
  const config = await loadRepoConfig(targetRepoRoot);
7737
- const logPath = path6.join(
8961
+ const logPath = path8.join(
7738
8962
  targetRepoRoot,
7739
8963
  ".pourkit",
7740
8964
  "logs",
@@ -7780,7 +9004,7 @@ function createCliProgram(version) {
7780
9004
  async (prNumber, options) => {
7781
9005
  const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
7782
9006
  const config = await loadRepoConfig(targetRepoRoot);
7783
- const logPath = path6.join(
9007
+ const logPath = path8.join(
7784
9008
  targetRepoRoot,
7785
9009
  ".pourkit",
7786
9010
  "logs",
@@ -7817,11 +9041,11 @@ function createCliProgram(version) {
7817
9041
  return program;
7818
9042
  }
7819
9043
  async function resolveCliVersion() {
7820
- if (isPackageVersion("0.0.0-next-20260529180337")) {
7821
- return "0.0.0-next-20260529180337";
9044
+ if (isPackageVersion("0.0.0-next-20260531183322")) {
9045
+ return "0.0.0-next-20260531183322";
7822
9046
  }
7823
- if (isReleaseVersion("0.0.0-next-20260529180337")) {
7824
- return "0.0.0-next-20260529180337";
9047
+ if (isReleaseVersion("0.0.0-next-20260531183322")) {
9048
+ return "0.0.0-next-20260531183322";
7825
9049
  }
7826
9050
  try {
7827
9051
  const root = repoRoot();