@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 +1842 -618
- package/dist/cli.js.map +1 -1
- package/dist/e2e/run-live-e2e.js +1394 -510
- package/dist/e2e/run-live-e2e.js.map +1 -1
- package/package.json +3 -2
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
|
|
304
|
+
let path9 = "";
|
|
305
305
|
let entryBranch = "";
|
|
306
306
|
for (const line of lines) {
|
|
307
307
|
if (line.startsWith("worktree ")) {
|
|
308
|
-
|
|
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 &&
|
|
314
|
-
return
|
|
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
|
|
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(
|
|
527
|
-
if (
|
|
548
|
+
function formatZodPath(path9) {
|
|
549
|
+
if (path9.length === 0) return "";
|
|
528
550
|
let result = "";
|
|
529
|
-
for (const segment of
|
|
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
|
|
541
|
-
if (
|
|
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
|
|
572
|
+
return path9 ? `${path9} must be an object` : "Config must be an object";
|
|
551
573
|
}
|
|
552
574
|
if (issue.expected === "integer") {
|
|
553
|
-
return `${
|
|
575
|
+
return `${path9} must be an integer`;
|
|
554
576
|
}
|
|
555
577
|
if (issue.expected === "string") {
|
|
556
|
-
return `${
|
|
578
|
+
return `${path9} must be a string`;
|
|
557
579
|
}
|
|
558
580
|
if (issue.expected === "number") {
|
|
559
|
-
return `${
|
|
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 `${
|
|
587
|
+
return `${path9} must be a non-empty string`;
|
|
566
588
|
}
|
|
567
589
|
if (issue.type === "array" && issue.minimum === 1) {
|
|
568
|
-
return `${
|
|
590
|
+
return `${path9} must not be empty`;
|
|
569
591
|
}
|
|
570
592
|
if (issue.type === "number") {
|
|
571
|
-
return `${
|
|
593
|
+
return `${path9} must be a positive number`;
|
|
572
594
|
}
|
|
573
595
|
return issue.message;
|
|
574
596
|
case z.ZodIssueCode.invalid_literal:
|
|
575
|
-
return `${
|
|
597
|
+
return `${path9} must be ${issue.expected}`;
|
|
576
598
|
case z.ZodIssueCode.unrecognized_keys:
|
|
577
|
-
const keyPath =
|
|
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
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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,
|
|
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(`${
|
|
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:
|
|
702
|
-
const { readFile:
|
|
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 (!
|
|
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
|
|
885
|
+
let path9 = "";
|
|
840
886
|
let branch = "";
|
|
841
887
|
for (const line of lines) {
|
|
842
888
|
if (line.startsWith("worktree ")) {
|
|
843
|
-
|
|
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:
|
|
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
|
|
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
|
|
954
|
-
import { join as
|
|
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
|
-
//
|
|
1211
|
-
import {
|
|
1212
|
-
import { join as join4 } from "path";
|
|
1213
|
-
init_common();
|
|
1263
|
+
// failure-resolution/effect-runtime.ts
|
|
1264
|
+
import { Effect } from "effect";
|
|
1214
1265
|
|
|
1215
|
-
//
|
|
1216
|
-
var
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
|
1223
|
-
"
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
|
1268
|
-
const
|
|
1269
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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 (!
|
|
1335
|
-
throw new
|
|
1336
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
//
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1929
|
+
async function runFailureResolutionAgent(options) {
|
|
1390
1930
|
const {
|
|
1391
1931
|
executionProvider,
|
|
1392
1932
|
config,
|
|
1393
1933
|
target,
|
|
1394
|
-
|
|
1395
|
-
|
|
1934
|
+
failure,
|
|
1935
|
+
packet,
|
|
1396
1936
|
worktreePath,
|
|
1397
1937
|
repoRoot: repoRoot2,
|
|
1398
|
-
conflictedPaths,
|
|
1399
|
-
attempt,
|
|
1400
1938
|
logger
|
|
1401
1939
|
} = options;
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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: "
|
|
1420
|
-
agent:
|
|
1421
|
-
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: [
|
|
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: "
|
|
1436
|
-
|
|
1988
|
+
status: "handoff",
|
|
1989
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
1990
|
+
reason: `Agent execution failed: ${executionResult.error}`
|
|
1437
1991
|
};
|
|
1438
1992
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
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: "
|
|
1443
|
-
|
|
1444
|
-
|
|
2003
|
+
status: "handoff",
|
|
2004
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2005
|
+
reason: "Agent did not write artifact"
|
|
1445
2006
|
};
|
|
1446
2007
|
}
|
|
1447
|
-
let
|
|
2008
|
+
let artifact;
|
|
1448
2009
|
try {
|
|
1449
|
-
|
|
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: "
|
|
1453
|
-
|
|
1454
|
-
|
|
2038
|
+
status: "handoff",
|
|
2039
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2040
|
+
reason: validation.reason
|
|
1455
2041
|
};
|
|
1456
2042
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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 (
|
|
2066
|
+
if (policyResult.decision === "FAIL_RUN") {
|
|
1471
2067
|
return {
|
|
1472
|
-
status: "
|
|
1473
|
-
|
|
1474
|
-
|
|
2068
|
+
status: "fail-run",
|
|
2069
|
+
decision: "FAIL_RUN",
|
|
2070
|
+
reason: policyResult.reason
|
|
1475
2071
|
};
|
|
1476
2072
|
}
|
|
1477
2073
|
return {
|
|
1478
|
-
status: "
|
|
1479
|
-
|
|
1480
|
-
|
|
2074
|
+
status: "recovered",
|
|
2075
|
+
decision: policyResult.decision,
|
|
2076
|
+
artifact
|
|
1481
2077
|
};
|
|
1482
2078
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1564
|
-
import { dirname as
|
|
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
|
|
1575
|
-
function
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
1621
|
-
const body =
|
|
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
|
|
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 =
|
|
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 =
|
|
2320
|
+
const artifactPathInWorktree = join8(
|
|
1792
2321
|
".pourkit",
|
|
1793
2322
|
".tmp",
|
|
1794
2323
|
"finalizer",
|
|
1795
2324
|
"agent-output.md"
|
|
1796
2325
|
);
|
|
1797
|
-
const artifactPath =
|
|
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 (
|
|
1863
|
-
return
|
|
2391
|
+
if (existsSync5(promptPath)) {
|
|
2392
|
+
return readFileSync5(promptPath, "utf-8");
|
|
1864
2393
|
}
|
|
1865
2394
|
return promptTemplate;
|
|
1866
2395
|
}
|
|
1867
2396
|
function prepareArtifactPath(artifactPath) {
|
|
1868
|
-
|
|
1869
|
-
if (
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
1888
|
-
|
|
1889
|
-
writeFileSync2(
|
|
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
|
|
2128
|
-
mkdirSync as
|
|
2129
|
-
readFileSync as
|
|
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
|
|
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 (!
|
|
2745
|
+
if (!existsSync6(artifactPath)) {
|
|
2217
2746
|
throw new RefactorArtifactValidationError(
|
|
2218
2747
|
`Refactor artifact missing at ${artifactPath}`
|
|
2219
2748
|
);
|
|
2220
2749
|
}
|
|
2221
|
-
const content =
|
|
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 =
|
|
2895
|
+
const artifactPathInWorktree = join9(
|
|
2367
2896
|
".pourkit",
|
|
2368
2897
|
".tmp",
|
|
2369
2898
|
"reviewers",
|
|
2370
2899
|
`iteration-${iteration ?? 1}.md`
|
|
2371
2900
|
);
|
|
2372
|
-
const artifactPath =
|
|
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 =
|
|
2480
|
-
if (!
|
|
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 =
|
|
3019
|
+
const filePath = join9(refactorsDir, file);
|
|
2491
3020
|
try {
|
|
2492
|
-
const content =
|
|
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 =
|
|
2518
|
-
if (!
|
|
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 =
|
|
3057
|
+
const filePath = join9(reviewersDir, file);
|
|
2529
3058
|
try {
|
|
2530
|
-
const content =
|
|
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 =
|
|
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 =
|
|
3097
|
+
const snippetPath = join9(
|
|
2569
3098
|
repoRoot2,
|
|
2570
3099
|
".pourkit",
|
|
2571
3100
|
"prompts",
|
|
2572
3101
|
`reviewer-${criterion}.snippet.md`
|
|
2573
3102
|
);
|
|
2574
|
-
if (
|
|
2575
|
-
return
|
|
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
|
-
|
|
2582
|
-
if (
|
|
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 (!
|
|
3116
|
+
if (!existsSync6(logPath)) {
|
|
2588
3117
|
return null;
|
|
2589
3118
|
}
|
|
2590
|
-
const logContent =
|
|
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 (
|
|
2606
|
-
const output =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2862
|
-
|
|
2863
|
-
writeFileSync3(
|
|
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 =
|
|
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
|
|
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 (
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
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
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3735
|
+
} else if (failure instanceof PublishedHistoryRisk) {
|
|
3736
|
+
await handlePublishedHistoryRisk(failure, {
|
|
3737
|
+
worktreePath: resolution.worktreePath,
|
|
3738
|
+
branchName,
|
|
3739
|
+
target,
|
|
3166
3740
|
config,
|
|
3167
|
-
|
|
3741
|
+
issueNumber,
|
|
3742
|
+
issueProvider,
|
|
3743
|
+
worktreeState,
|
|
3168
3744
|
logger
|
|
3169
|
-
);
|
|
3170
|
-
|
|
3745
|
+
});
|
|
3746
|
+
} else {
|
|
3747
|
+
throw new Error(`Base refresh failed: ${failure.message}`);
|
|
3171
3748
|
}
|
|
3172
3749
|
} else {
|
|
3173
|
-
|
|
3174
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
5400
|
+
function isFlag2(value) {
|
|
4427
5401
|
return value.startsWith("--");
|
|
4428
5402
|
}
|
|
4429
|
-
function
|
|
4430
|
-
if (!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 =
|
|
5422
|
+
target = requireFlagValue2("--target", args[i + 1]);
|
|
4449
5423
|
i += 2;
|
|
4450
5424
|
} else if (arg === "--title") {
|
|
4451
|
-
title =
|
|
5425
|
+
title = requireFlagValue2("--title", args[i + 1]);
|
|
4452
5426
|
i += 2;
|
|
4453
5427
|
} else if (arg === "--base") {
|
|
4454
|
-
base =
|
|
5428
|
+
base = requireFlagValue2("--base", args[i + 1]);
|
|
4455
5429
|
i += 2;
|
|
4456
5430
|
} else if (arg === "--head") {
|
|
4457
|
-
head =
|
|
5431
|
+
head = requireFlagValue2("--head", args[i + 1]);
|
|
4458
5432
|
i += 2;
|
|
4459
5433
|
} else if (arg === "--body") {
|
|
4460
|
-
body =
|
|
5434
|
+
body = requireFlagValue2("--body", args[i + 1]);
|
|
4461
5435
|
i += 2;
|
|
4462
5436
|
} else if (arg === "--body-file") {
|
|
4463
|
-
bodyFile =
|
|
5437
|
+
bodyFile = requireFlagValue2("--body-file", args[i + 1]);
|
|
4464
5438
|
i += 2;
|
|
4465
5439
|
} else if (arg === "--issue") {
|
|
4466
|
-
const raw =
|
|
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
|
|
5538
|
+
function isFlag3(value) {
|
|
4565
5539
|
return value.startsWith("--");
|
|
4566
5540
|
}
|
|
4567
|
-
function
|
|
4568
|
-
if (!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 ||
|
|
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 =
|
|
5568
|
+
target = requireFlagValue3("--target", args[i + 1]);
|
|
4595
5569
|
i += 2;
|
|
4596
5570
|
} else if (arg === "--method") {
|
|
4597
|
-
const raw =
|
|
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
|
|
5684
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
4711
5685
|
import {
|
|
4712
5686
|
copyFile,
|
|
4713
|
-
mkdir as
|
|
4714
|
-
readFile as
|
|
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
|
|
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 =
|
|
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 =
|
|
6167
|
+
const sourcePath = path5.join(options.sourceRoot, "AGENTS.md");
|
|
5194
6168
|
try {
|
|
5195
|
-
return await
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
5281
|
-
if (
|
|
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 =
|
|
5291
|
-
if (
|
|
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 =
|
|
5299
|
-
return
|
|
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
|
-
|
|
5304
|
-
|
|
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 (
|
|
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 =
|
|
5318
|
-
if (
|
|
6291
|
+
const p = path5.join(root, name);
|
|
6292
|
+
if (existsSync8(p)) {
|
|
5319
6293
|
docs.push(p);
|
|
5320
6294
|
}
|
|
5321
6295
|
}
|
|
5322
|
-
const adrDir =
|
|
5323
|
-
if (
|
|
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(
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
5465
|
-
const destPath =
|
|
5466
|
-
if (!
|
|
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: ${
|
|
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 (!
|
|
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 =
|
|
6564
|
+
const targetPath = path5.join(targetRoot, targetDirName);
|
|
5591
6565
|
const skillFiles = await walkDir(s);
|
|
5592
6566
|
for (const file of skillFiles) {
|
|
5593
|
-
const relPath =
|
|
5594
|
-
const destPath =
|
|
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: ${
|
|
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: ${
|
|
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:
|
|
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 =
|
|
5637
|
-
const destPath =
|
|
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 (
|
|
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
|
|
5684
|
-
|
|
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 =
|
|
5706
|
-
if (!
|
|
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 =
|
|
6691
|
+
const adrGitkeep = path5.join(
|
|
5718
6692
|
targetRoot,
|
|
5719
6693
|
".pourkit",
|
|
5720
6694
|
"docs",
|
|
5721
6695
|
"adr",
|
|
5722
6696
|
".gitkeep"
|
|
5723
6697
|
);
|
|
5724
|
-
if (!
|
|
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 =
|
|
5735
|
-
const tgtDocAgents =
|
|
5736
|
-
if (
|
|
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 =
|
|
6713
|
+
const relPath = path5.relative(srcDocAgents, file);
|
|
5740
6714
|
if (relPath === "triage-labels.md") {
|
|
5741
6715
|
operations.push({
|
|
5742
6716
|
kind: "create",
|
|
5743
|
-
path:
|
|
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:
|
|
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 =
|
|
5768
|
-
const tgtPrompts =
|
|
5769
|
-
if (
|
|
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 =
|
|
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:
|
|
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 =
|
|
6760
|
+
const srcSandboxDockerfile = path5.join(
|
|
5787
6761
|
sourceRoot,
|
|
5788
6762
|
".sandcastle",
|
|
5789
6763
|
"Dockerfile"
|
|
5790
6764
|
);
|
|
5791
|
-
const tgtSandboxDockerfile =
|
|
6765
|
+
const tgtSandboxDockerfile = path5.join(
|
|
5792
6766
|
targetRoot,
|
|
5793
6767
|
".sandcastle",
|
|
5794
6768
|
"Dockerfile"
|
|
5795
6769
|
);
|
|
5796
|
-
if (
|
|
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 (
|
|
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 =
|
|
5819
|
-
if (!
|
|
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 && !
|
|
6830
|
+
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync8(path5.join(targetRoot, "AGENTS.md"))) {
|
|
5857
6831
|
operations.push({
|
|
5858
6832
|
kind: "create",
|
|
5859
|
-
path:
|
|
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 && !
|
|
6846
|
+
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync8(path5.join(targetRoot, "CLAUDE.md"))) {
|
|
5873
6847
|
operations.push({
|
|
5874
6848
|
kind: "create",
|
|
5875
|
-
path:
|
|
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 =
|
|
6859
|
+
const gitignoreTarget = path5.join(targetRoot, ".gitignore");
|
|
5886
6860
|
const gitignoreContent = generateGitignoreBlock();
|
|
5887
|
-
if (!
|
|
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 =
|
|
5911
|
-
if (!
|
|
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
|
|
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 =
|
|
5968
|
-
if (
|
|
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 (!
|
|
6288
|
-
const dir =
|
|
6289
|
-
await
|
|
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
|
|
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 =
|
|
6307
|
-
const manifestPath =
|
|
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 =
|
|
7288
|
+
const relPath = path5.relative(plan.targetRoot, op.path);
|
|
6315
7289
|
if (relPath === ".pourkit/manifest.json") continue;
|
|
6316
|
-
if (
|
|
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
|
|
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 (
|
|
7335
|
+
if (existsSync8(op.path) && !op.destructive) {
|
|
6362
7336
|
skipped++;
|
|
6363
7337
|
continue;
|
|
6364
7338
|
}
|
|
6365
|
-
const dir =
|
|
6366
|
-
await
|
|
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 (
|
|
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 =
|
|
6385
|
-
await
|
|
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 (
|
|
7379
|
+
if (existsSync8(op.path)) {
|
|
6406
7380
|
skipped++;
|
|
6407
7381
|
continue;
|
|
6408
7382
|
}
|
|
6409
|
-
const dir =
|
|
6410
|
-
await
|
|
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 ===
|
|
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 (
|
|
6546
|
-
agentFiles.push(
|
|
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
|
|
7238
|
-
import { join as
|
|
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
|
|
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 =
|
|
7249
|
-
await ensureDir(
|
|
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
|
|
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
|
|
7261
|
-
import
|
|
8360
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
8361
|
+
import path6 from "path";
|
|
7262
8362
|
function sandboxImageName(repoRoot2) {
|
|
7263
|
-
const dirName =
|
|
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 =
|
|
7267
|
-
if (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
7486
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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-
|
|
7821
|
-
return "0.0.0-next-
|
|
9044
|
+
if (isPackageVersion("0.0.0-next-20260531183322")) {
|
|
9045
|
+
return "0.0.0-next-20260531183322";
|
|
7822
9046
|
}
|
|
7823
|
-
if (isReleaseVersion("0.0.0-next-
|
|
7824
|
-
return "0.0.0-next-
|
|
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();
|