@pourkit/cli 0.0.0-next-20260530000923 → 0.0.0-next-20260531213458
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 +1880 -621
- package/dist/cli.js.map +1 -1
- package/dist/e2e/run-live-e2e.js +1418 -509
- package/dist/e2e/run-live-e2e.js.map +1 -1
- package/package.json +2 -1
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,20 +744,21 @@ 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 {
|
|
703
|
-
const { tmpdir } = await import("os");
|
|
747
|
+
const { existsSync: existsSync10 } = await import("fs");
|
|
748
|
+
const { mkdir: mkdir5, writeFile: writeFile3, rm } = await import("fs/promises");
|
|
704
749
|
const { join: pjoin, basename } = await import("path");
|
|
705
750
|
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
706
751
|
const { build } = await import("esbuild");
|
|
707
752
|
const configPath = pjoin(repoRoot2, configFileName);
|
|
708
|
-
if (!
|
|
753
|
+
if (!existsSync10(configPath)) {
|
|
709
754
|
throw new Error(
|
|
710
755
|
`No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
|
|
711
756
|
);
|
|
712
757
|
}
|
|
758
|
+
const tmpDir = pjoin(repoRoot2, ".pourkit", ".tmp", "config");
|
|
759
|
+
await mkdir5(tmpDir, { recursive: true });
|
|
713
760
|
const tmpFile = pjoin(
|
|
714
|
-
|
|
761
|
+
tmpDir,
|
|
715
762
|
`pourkit-config-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
|
|
716
763
|
);
|
|
717
764
|
try {
|
|
@@ -836,16 +883,16 @@ function parseWorktreeListPorcelain2(text2) {
|
|
|
836
883
|
const entries = text2.trim().split("\n\n");
|
|
837
884
|
return entries.map((entry) => {
|
|
838
885
|
const lines = entry.trim().split("\n");
|
|
839
|
-
let
|
|
886
|
+
let path9 = "";
|
|
840
887
|
let branch = "";
|
|
841
888
|
for (const line of lines) {
|
|
842
889
|
if (line.startsWith("worktree ")) {
|
|
843
|
-
|
|
890
|
+
path9 = line.slice("worktree ".length);
|
|
844
891
|
} else if (line.startsWith("branch refs/heads/")) {
|
|
845
892
|
branch = line.slice("branch refs/heads/".length);
|
|
846
893
|
}
|
|
847
894
|
}
|
|
848
|
-
return { path:
|
|
895
|
+
return { path: path9, branch: branch || void 0 };
|
|
849
896
|
}).filter((e) => e.path);
|
|
850
897
|
}
|
|
851
898
|
async function listCleanupCandidates(repoRoot2, retentionDays) {
|
|
@@ -885,7 +932,7 @@ async function removeStaleWorktree(candidate, repoRoot2, logger) {
|
|
|
885
932
|
}
|
|
886
933
|
}
|
|
887
934
|
async function removeExpiredFiles(dirPath, retentionDays) {
|
|
888
|
-
const { readdir: readdir2, stat, unlink, access } = await import("fs/promises");
|
|
935
|
+
const { readdir: readdir2, stat, unlink, access: access2 } = await import("fs/promises");
|
|
889
936
|
let entries;
|
|
890
937
|
try {
|
|
891
938
|
entries = await readdir2(dirPath);
|
|
@@ -899,7 +946,7 @@ async function removeExpiredFiles(dirPath, retentionDays) {
|
|
|
899
946
|
try {
|
|
900
947
|
const stats = await stat(entryPath);
|
|
901
948
|
if (stats.isFile() && now - stats.mtimeMs > retentionMs) {
|
|
902
|
-
await
|
|
949
|
+
await access2(entryPath, 4);
|
|
903
950
|
await unlink(entryPath);
|
|
904
951
|
}
|
|
905
952
|
} catch {
|
|
@@ -950,8 +997,8 @@ async function cleanupRepository(options) {
|
|
|
950
997
|
}
|
|
951
998
|
|
|
952
999
|
// commands/issue-run.ts
|
|
953
|
-
import { existsSync as
|
|
954
|
-
import { join as
|
|
1000
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
1001
|
+
import { join as join10 } from "path";
|
|
955
1002
|
|
|
956
1003
|
// pr/templates.ts
|
|
957
1004
|
init_common();
|
|
@@ -1014,6 +1061,13 @@ var STAGE_SECTIONS = {
|
|
|
1014
1061
|
"branch",
|
|
1015
1062
|
"verification-commands",
|
|
1016
1063
|
"artifacts"
|
|
1064
|
+
],
|
|
1065
|
+
failureResolution: [
|
|
1066
|
+
"issue",
|
|
1067
|
+
"comments",
|
|
1068
|
+
"branch",
|
|
1069
|
+
"verification-commands",
|
|
1070
|
+
"artifacts"
|
|
1017
1071
|
]
|
|
1018
1072
|
};
|
|
1019
1073
|
function buildRunContextArtifact(options) {
|
|
@@ -1207,361 +1261,871 @@ function invalidateAfterBaseRefresh(state) {
|
|
|
1207
1261
|
};
|
|
1208
1262
|
}
|
|
1209
1263
|
|
|
1210
|
-
//
|
|
1211
|
-
import {
|
|
1212
|
-
import { join as join4 } from "path";
|
|
1213
|
-
init_common();
|
|
1264
|
+
// failure-resolution/effect-runtime.ts
|
|
1265
|
+
import { Effect } from "effect";
|
|
1214
1266
|
|
|
1215
|
-
//
|
|
1216
|
-
var
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1267
|
+
// failure-resolution/types.ts
|
|
1268
|
+
var RebaseConflict = class extends Error {
|
|
1269
|
+
_tag = "RebaseConflict";
|
|
1270
|
+
conflictedPaths;
|
|
1271
|
+
message;
|
|
1272
|
+
constructor(args) {
|
|
1273
|
+
super(args.message);
|
|
1274
|
+
this.name = "RebaseConflict";
|
|
1275
|
+
this.conflictedPaths = args.conflictedPaths;
|
|
1276
|
+
this.message = args.message;
|
|
1220
1277
|
}
|
|
1221
1278
|
};
|
|
1222
|
-
var
|
|
1223
|
-
"
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1279
|
+
var PublishedHistoryRisk = class extends Error {
|
|
1280
|
+
_tag = "PublishedHistoryRisk";
|
|
1281
|
+
prNumber;
|
|
1282
|
+
prState;
|
|
1283
|
+
constructor(args) {
|
|
1284
|
+
super(`Published PR #${args.prNumber} is ${args.prState}`);
|
|
1285
|
+
this.name = "PublishedHistoryRisk";
|
|
1286
|
+
this.prNumber = args.prNumber;
|
|
1287
|
+
this.prState = args.prState;
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var RecoveryArtifactInvalid = class extends Error {
|
|
1291
|
+
_tag = "RecoveryArtifactInvalid";
|
|
1292
|
+
reason;
|
|
1293
|
+
constructor(args) {
|
|
1294
|
+
super(args.reason);
|
|
1295
|
+
this.name = "RecoveryArtifactInvalid";
|
|
1296
|
+
this.reason = args.reason;
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
var ConfigFailure = class extends Error {
|
|
1300
|
+
_tag = "ConfigFailure";
|
|
1301
|
+
configKey;
|
|
1302
|
+
expectedType;
|
|
1303
|
+
message;
|
|
1304
|
+
constructor(args) {
|
|
1305
|
+
super(args.message);
|
|
1306
|
+
this.name = "ConfigFailure";
|
|
1307
|
+
this.configKey = args.configKey;
|
|
1308
|
+
this.expectedType = args.expectedType;
|
|
1309
|
+
this.message = args.message;
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
var SafetyFailure = class extends Error {
|
|
1313
|
+
_tag = "SafetyFailure";
|
|
1314
|
+
sensitivityKind;
|
|
1315
|
+
message;
|
|
1316
|
+
constructor(args) {
|
|
1317
|
+
super(args.message);
|
|
1318
|
+
this.name = "SafetyFailure";
|
|
1319
|
+
this.sensitivityKind = args.sensitivityKind;
|
|
1320
|
+
this.message = args.message;
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
var SUPPORTED_DECISIONS = /* @__PURE__ */ new Set([
|
|
1324
|
+
"RETRY_STAGE",
|
|
1325
|
+
"HANDOFF_TO_HUMAN",
|
|
1326
|
+
"FAIL_RUN"
|
|
1327
|
+
]);
|
|
1328
|
+
function isSupportedRecoveryDecision(decision) {
|
|
1329
|
+
return SUPPORTED_DECISIONS.has(decision);
|
|
1330
|
+
}
|
|
1331
|
+
var JSON_BLOCK_RE = /```json\n([\s\S]*?)```/;
|
|
1332
|
+
function parseRecoveryArtifact(markdown, artifactPath) {
|
|
1333
|
+
const match = JSON_BLOCK_RE.exec(markdown);
|
|
1334
|
+
if (!match) {
|
|
1335
|
+
throw new RecoveryArtifactInvalid({
|
|
1336
|
+
reason: `No JSON code block found in artifact at ${artifactPath}`
|
|
1237
1337
|
});
|
|
1238
1338
|
}
|
|
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
|
-
);
|
|
1339
|
+
let parsed;
|
|
1340
|
+
try {
|
|
1341
|
+
parsed = JSON.parse(match[1]);
|
|
1342
|
+
} catch {
|
|
1343
|
+
throw new RecoveryArtifactInvalid({
|
|
1344
|
+
reason: `Malformed JSON in artifact at ${artifactPath}`
|
|
1345
|
+
});
|
|
1257
1346
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1347
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1348
|
+
throw new RecoveryArtifactInvalid({
|
|
1349
|
+
reason: `Expected a JSON object in artifact at ${artifactPath}`
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
const json = parsed;
|
|
1353
|
+
if (typeof json.recoveryDecision !== "string" || json.recoveryDecision === "") {
|
|
1354
|
+
throw new RecoveryArtifactInvalid({
|
|
1355
|
+
reason: `Missing or empty recoveryDecision in artifact at ${artifactPath}`
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (typeof json.summary !== "string" || json.summary === "") {
|
|
1359
|
+
throw new RecoveryArtifactInvalid({
|
|
1360
|
+
reason: `Missing or empty summary in artifact at ${artifactPath}`
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
const changedFiles = json.changedFiles;
|
|
1364
|
+
if (!Array.isArray(changedFiles)) {
|
|
1365
|
+
throw new RecoveryArtifactInvalid({
|
|
1366
|
+
reason: `changedFiles must be an array in artifact at ${artifactPath}`
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (!changedFiles.every((f) => typeof f === "string")) {
|
|
1370
|
+
throw new RecoveryArtifactInvalid({
|
|
1371
|
+
reason: `changedFiles must be an array of strings in artifact at ${artifactPath}`
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
if (json.verificationSummary !== void 0 && typeof json.verificationSummary !== "string") {
|
|
1375
|
+
throw new RecoveryArtifactInvalid({
|
|
1376
|
+
reason: `verificationSummary must be a string in artifact at ${artifactPath}`
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
if (json.notes !== void 0 && typeof json.notes !== "string") {
|
|
1380
|
+
throw new RecoveryArtifactInvalid({
|
|
1381
|
+
reason: `notes must be a string in artifact at ${artifactPath}`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
if (json.verificationCommands !== void 0) {
|
|
1385
|
+
if (!Array.isArray(json.verificationCommands)) {
|
|
1386
|
+
throw new RecoveryArtifactInvalid({
|
|
1387
|
+
reason: `verificationCommands must be an array in artifact at ${artifactPath}`
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
if (!json.verificationCommands.every((c) => typeof c === "string")) {
|
|
1391
|
+
throw new RecoveryArtifactInvalid({
|
|
1392
|
+
reason: `verificationCommands must be an array of strings in artifact at ${artifactPath}`
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
raw: markdown,
|
|
1398
|
+
json: {
|
|
1399
|
+
recoveryDecision: json.recoveryDecision,
|
|
1400
|
+
summary: json.summary,
|
|
1401
|
+
changedFiles,
|
|
1402
|
+
verificationSummary: json.verificationSummary,
|
|
1403
|
+
verificationCommands: json.verificationCommands,
|
|
1404
|
+
notes: json.notes
|
|
1405
|
+
},
|
|
1406
|
+
path: artifactPath
|
|
1407
|
+
};
|
|
1266
1408
|
}
|
|
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());
|
|
1409
|
+
function validateRecoveryDecision(artifact, allowedDecisions) {
|
|
1410
|
+
const decision = artifact.json.recoveryDecision;
|
|
1411
|
+
if (!allowedDecisions.includes(decision)) {
|
|
1273
1412
|
return {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
notes: cells[2] ?? ""
|
|
1413
|
+
valid: false,
|
|
1414
|
+
reason: `Decision "${decision}" is not in allowed list: ${allowedDecisions.join(", ")}`
|
|
1277
1415
|
};
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
function parseConflictResolutionArtifact(output) {
|
|
1281
|
-
if (!output.trim()) {
|
|
1282
|
-
throw new ConflictResolutionArtifactProtocolError(
|
|
1283
|
-
"Empty conflict resolution artifact output"
|
|
1284
|
-
);
|
|
1285
1416
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
throw new ConflictResolutionArtifactProtocolError(
|
|
1292
|
-
'Duplicate "## Status" sections'
|
|
1293
|
-
);
|
|
1417
|
+
if (!isSupportedRecoveryDecision(decision)) {
|
|
1418
|
+
return {
|
|
1419
|
+
valid: false,
|
|
1420
|
+
reason: `Decision "${decision}" is not supported in this slice`
|
|
1421
|
+
};
|
|
1294
1422
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1423
|
+
return { valid: true, decision };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// shared/attempt-log.ts
|
|
1427
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2 } from "fs";
|
|
1428
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1429
|
+
var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
|
|
1430
|
+
function writeAttemptLog(worktreePath, entry) {
|
|
1431
|
+
const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
|
|
1432
|
+
mkdirSync3(dirname2(logPath), { recursive: true });
|
|
1433
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
1434
|
+
}
|
|
1435
|
+
function readAttemptLog(worktreePath) {
|
|
1436
|
+
const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
|
|
1437
|
+
if (!existsSync2(logPath)) {
|
|
1438
|
+
return [];
|
|
1299
1439
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1440
|
+
const raw = readFileSync2(logPath, "utf-8");
|
|
1441
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
1442
|
+
const entries = [];
|
|
1443
|
+
for (const line of lines) {
|
|
1444
|
+
try {
|
|
1445
|
+
const parsed = JSON.parse(line);
|
|
1446
|
+
if (isValidAttemptLogEntry(parsed)) {
|
|
1447
|
+
entries.push(parsed);
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1304
1451
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1452
|
+
return entries;
|
|
1453
|
+
}
|
|
1454
|
+
function isValidAttemptLogEntry(raw) {
|
|
1455
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
1456
|
+
const obj = raw;
|
|
1457
|
+
if (obj.attemptType !== "stage" && obj.attemptType !== "recovery")
|
|
1458
|
+
return false;
|
|
1459
|
+
if (typeof obj.fingerprint !== "string") return false;
|
|
1460
|
+
if (typeof obj.timestamp !== "string") return false;
|
|
1461
|
+
if (typeof obj.stage !== "string") return false;
|
|
1462
|
+
if (obj.outcome !== "success" && obj.outcome !== "failure" && obj.outcome !== "handoff")
|
|
1463
|
+
return false;
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
function recoveryBudgetForFailure(worktreePath, fingerprint, maxAttempts) {
|
|
1467
|
+
const entries = readAttemptLog(worktreePath);
|
|
1468
|
+
const used = entries.filter(
|
|
1469
|
+
(e) => e.attemptType === "recovery" && e.fingerprint === fingerprint
|
|
1470
|
+
).length;
|
|
1471
|
+
const remaining = Math.max(0, maxAttempts - used);
|
|
1472
|
+
return {
|
|
1473
|
+
used,
|
|
1474
|
+
remaining,
|
|
1475
|
+
exhausted: used >= maxAttempts
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function computeFailureFingerprint(stage, failureType) {
|
|
1479
|
+
return `${stage.toLowerCase()}:${failureType}`;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// failure-resolution/stage-attempt.ts
|
|
1483
|
+
function createStageAttemptId() {
|
|
1484
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1485
|
+
}
|
|
1486
|
+
function recordStageAttempt(worktreePath, record) {
|
|
1487
|
+
writeAttemptLog(worktreePath, {
|
|
1488
|
+
attemptType: "stage",
|
|
1489
|
+
fingerprint: record.failureFingerprint ?? `${record.stage}:success`,
|
|
1490
|
+
timestamp: record.completedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1491
|
+
stage: record.stage,
|
|
1492
|
+
outcome: record.outcome === "handoff" ? "handoff" : record.outcome === "success" ? "success" : "failure"
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// failure-resolution/effect-runtime.ts
|
|
1497
|
+
function baseRefreshEffect(options) {
|
|
1498
|
+
return Effect.promise(() => refreshStaleIssueBranch(options)).pipe(
|
|
1499
|
+
Effect.flatMap(
|
|
1500
|
+
(result) => {
|
|
1501
|
+
switch (result.status) {
|
|
1502
|
+
case "refreshed":
|
|
1503
|
+
return Effect.succeed({ status: "refreshed" });
|
|
1504
|
+
case "skipped-current":
|
|
1505
|
+
return Effect.succeed({ status: "skipped-current" });
|
|
1506
|
+
case "conflicted":
|
|
1507
|
+
return Effect.fail(
|
|
1508
|
+
new RebaseConflict({
|
|
1509
|
+
conflictedPaths: result.conflictedPaths,
|
|
1510
|
+
message: result.message
|
|
1511
|
+
})
|
|
1512
|
+
);
|
|
1513
|
+
case "refused-published-history":
|
|
1514
|
+
return Effect.fail(
|
|
1515
|
+
new PublishedHistoryRisk({
|
|
1516
|
+
prNumber: result.prNumber,
|
|
1517
|
+
prState: result.prState
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
)
|
|
1307
1523
|
);
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1524
|
+
}
|
|
1525
|
+
async function runBaseRefreshAttempt(options) {
|
|
1526
|
+
const attemptId = createStageAttemptId();
|
|
1527
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1528
|
+
const program = baseRefreshEffect(options).pipe(
|
|
1529
|
+
Effect.tapBoth({
|
|
1530
|
+
onSuccess: (success) => Effect.sync(() => {
|
|
1531
|
+
recordStageAttempt(options.worktreePath, {
|
|
1532
|
+
id: attemptId,
|
|
1533
|
+
stage: "baseRefresh",
|
|
1534
|
+
startedAt,
|
|
1535
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1536
|
+
outcome: "success"
|
|
1537
|
+
});
|
|
1538
|
+
}),
|
|
1539
|
+
onFailure: (failure) => Effect.sync(() => {
|
|
1540
|
+
recordStageAttempt(options.worktreePath, {
|
|
1541
|
+
id: attemptId,
|
|
1542
|
+
stage: "baseRefresh",
|
|
1543
|
+
startedAt,
|
|
1544
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1545
|
+
outcome: "failure",
|
|
1546
|
+
failureFingerprint: computeFailureFingerprint(
|
|
1547
|
+
"baseRefresh",
|
|
1548
|
+
failure._tag
|
|
1549
|
+
),
|
|
1550
|
+
failureType: failure._tag
|
|
1551
|
+
});
|
|
1552
|
+
})
|
|
1553
|
+
})
|
|
1554
|
+
);
|
|
1555
|
+
return Effect.runPromiseExit(program);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// commands/conflict-resolution.ts
|
|
1559
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1560
|
+
import { join as join5 } from "path";
|
|
1561
|
+
init_common();
|
|
1562
|
+
var CONFLICT_MARKER_PATTERN = /<<<<<<<|=======|>>>>>>>/m;
|
|
1563
|
+
async function hasUnresolvedConflictMarkers(worktreePath, files) {
|
|
1564
|
+
for (const file of files) {
|
|
1565
|
+
const filePath = join5(worktreePath, file);
|
|
1566
|
+
try {
|
|
1567
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1568
|
+
if (CONFLICT_MARKER_PATTERN.test(content)) {
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
} catch {
|
|
1572
|
+
}
|
|
1315
1573
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// commands/issue-run.ts
|
|
1578
|
+
import { Exit as Exit2 } from "effect";
|
|
1579
|
+
|
|
1580
|
+
// serena/baseline.ts
|
|
1581
|
+
init_common();
|
|
1582
|
+
import path3 from "path";
|
|
1583
|
+
import { access, mkdir as mkdir2 } from "fs/promises";
|
|
1584
|
+
function resolveSerenaPaths(repoRoot2, dataDir = ".pourkit/serena/") {
|
|
1585
|
+
const rootDir = path3.isAbsolute(dataDir) ? path3.normalize(dataDir) : path3.resolve(repoRoot2, dataDir);
|
|
1586
|
+
return {
|
|
1587
|
+
rootDir,
|
|
1588
|
+
baselineWorktreePath: path3.join(rootDir, "baseline", "active-repo"),
|
|
1589
|
+
dataDir: path3.join(rootDir, "data")
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async function pathExists(dirPath) {
|
|
1593
|
+
try {
|
|
1594
|
+
await access(dirPath);
|
|
1595
|
+
return true;
|
|
1596
|
+
} catch {
|
|
1597
|
+
return false;
|
|
1320
1598
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1599
|
+
}
|
|
1600
|
+
async function isGitRepoRoot(repoPath) {
|
|
1601
|
+
try {
|
|
1602
|
+
const result = await execCapture("git", ["rev-parse", "--show-toplevel"], {
|
|
1603
|
+
cwd: repoPath,
|
|
1604
|
+
label: "git rev-parse --show-toplevel"
|
|
1605
|
+
});
|
|
1606
|
+
return path3.resolve(result.stdout.trim()) === path3.resolve(repoPath);
|
|
1607
|
+
} catch {
|
|
1608
|
+
return false;
|
|
1325
1609
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1610
|
+
}
|
|
1611
|
+
async function ensureBaselineWorktree(options) {
|
|
1612
|
+
const paths = resolveSerenaPaths(options.repoRoot, options.dataDir);
|
|
1613
|
+
await mkdir2(paths.rootDir, { recursive: true });
|
|
1614
|
+
await mkdir2(paths.dataDir, { recursive: true });
|
|
1615
|
+
if (!await pathExists(paths.baselineWorktreePath)) {
|
|
1616
|
+
await mkdir2(path3.dirname(paths.baselineWorktreePath), { recursive: true });
|
|
1617
|
+
await execCapture(
|
|
1618
|
+
"git",
|
|
1619
|
+
["clone", options.repoRoot, paths.baselineWorktreePath],
|
|
1620
|
+
{
|
|
1621
|
+
cwd: options.repoRoot,
|
|
1622
|
+
label: "git clone baseline worktree"
|
|
1623
|
+
}
|
|
1332
1624
|
);
|
|
1625
|
+
return paths;
|
|
1333
1626
|
}
|
|
1334
|
-
if (!
|
|
1335
|
-
throw new
|
|
1336
|
-
|
|
1627
|
+
if (!await isGitRepoRoot(paths.baselineWorktreePath)) {
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
`Serena baseline worktree exists but is not a git repo: ${paths.baselineWorktreePath}`
|
|
1337
1630
|
);
|
|
1338
1631
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1632
|
+
return paths;
|
|
1633
|
+
}
|
|
1634
|
+
async function getSerenaBaselineStatus(options) {
|
|
1635
|
+
const remoteName = options.remoteName ?? "origin";
|
|
1636
|
+
const paths = resolveSerenaPaths(options.repoRoot, options.dataDir);
|
|
1637
|
+
const expectedRef = `${remoteName}/${options.baseBranch}`;
|
|
1638
|
+
if (!await pathExists(paths.baselineWorktreePath)) {
|
|
1639
|
+
return {
|
|
1640
|
+
exists: false,
|
|
1641
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1642
|
+
expectedRef,
|
|
1643
|
+
fresh: false
|
|
1644
|
+
};
|
|
1343
1645
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1646
|
+
if (!await isGitRepoRoot(paths.baselineWorktreePath)) {
|
|
1647
|
+
return {
|
|
1648
|
+
exists: false,
|
|
1649
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1650
|
+
expectedRef,
|
|
1651
|
+
fresh: false
|
|
1652
|
+
};
|
|
1349
1653
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1654
|
+
let currentCommit;
|
|
1655
|
+
try {
|
|
1656
|
+
const currentResult = await execCapture("git", ["rev-parse", "HEAD"], {
|
|
1657
|
+
cwd: paths.baselineWorktreePath,
|
|
1658
|
+
label: "git rev-parse HEAD"
|
|
1659
|
+
});
|
|
1660
|
+
currentCommit = currentResult.stdout.trim() || void 0;
|
|
1661
|
+
} catch {
|
|
1662
|
+
return {
|
|
1663
|
+
exists: true,
|
|
1664
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1665
|
+
expectedRef,
|
|
1666
|
+
fresh: false
|
|
1667
|
+
};
|
|
1354
1668
|
}
|
|
1355
|
-
let
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1669
|
+
let expectedCommit;
|
|
1670
|
+
try {
|
|
1671
|
+
const expectedResult = await execCapture(
|
|
1672
|
+
"git",
|
|
1673
|
+
["rev-parse", expectedRef],
|
|
1674
|
+
{
|
|
1675
|
+
cwd: paths.baselineWorktreePath,
|
|
1676
|
+
label: `git rev-parse ${expectedRef}`
|
|
1677
|
+
}
|
|
1361
1678
|
);
|
|
1362
|
-
|
|
1679
|
+
expectedCommit = expectedResult.stdout.trim() || void 0;
|
|
1680
|
+
} catch {
|
|
1681
|
+
return {
|
|
1682
|
+
exists: true,
|
|
1683
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1684
|
+
currentCommit,
|
|
1685
|
+
expectedRef,
|
|
1686
|
+
fresh: false
|
|
1687
|
+
};
|
|
1363
1688
|
}
|
|
1364
1689
|
return {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1690
|
+
exists: true,
|
|
1691
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1692
|
+
currentCommit,
|
|
1693
|
+
expectedRef,
|
|
1694
|
+
fresh: currentCommit === expectedCommit
|
|
1370
1695
|
};
|
|
1371
1696
|
}
|
|
1697
|
+
async function refreshSerenaBaseline(options) {
|
|
1698
|
+
const remoteName = options.remoteName ?? "origin";
|
|
1699
|
+
const paths = await ensureBaselineWorktree(options);
|
|
1700
|
+
await execCapture("git", ["fetch", remoteName, options.baseBranch], {
|
|
1701
|
+
cwd: paths.baselineWorktreePath,
|
|
1702
|
+
label: "git fetch baseline branch"
|
|
1703
|
+
});
|
|
1704
|
+
await execCapture(
|
|
1705
|
+
"git",
|
|
1706
|
+
["checkout", "--detach", `${remoteName}/${options.baseBranch}`],
|
|
1707
|
+
{
|
|
1708
|
+
cwd: paths.baselineWorktreePath,
|
|
1709
|
+
label: "git checkout detached baseline branch"
|
|
1710
|
+
}
|
|
1711
|
+
);
|
|
1712
|
+
return getSerenaBaselineStatus(options);
|
|
1713
|
+
}
|
|
1372
1714
|
|
|
1373
|
-
//
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1715
|
+
// serena/container.ts
|
|
1716
|
+
init_common();
|
|
1717
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1718
|
+
import path4 from "path";
|
|
1719
|
+
var DEFAULT_CONTAINER_NAME = "pourkit-serena-sidecar";
|
|
1720
|
+
var MCP_CONTAINER_PORT = 9121;
|
|
1721
|
+
var DASHBOARD_CONTAINER_PORT = 24282;
|
|
1722
|
+
var SERENA_DATA_MOUNT = "/workspaces/serena-data";
|
|
1723
|
+
function resolveSidecarUrls(options) {
|
|
1724
|
+
return {
|
|
1725
|
+
containerName: options.containerName ?? DEFAULT_CONTAINER_NAME,
|
|
1726
|
+
mcpUrl: options.mcpUrl ?? `http://localhost:${options.mcpPort}/mcp`,
|
|
1727
|
+
dashboardUrl: `http://localhost:${options.dashboardPort}`
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
async function inspectSidecarContainer(containerName) {
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await execCapture("docker", ["inspect", containerName]);
|
|
1733
|
+
const parsed = JSON.parse(result.stdout);
|
|
1734
|
+
return {
|
|
1735
|
+
exists: true,
|
|
1736
|
+
running: Boolean(parsed[0]?.State?.Running)
|
|
1737
|
+
};
|
|
1738
|
+
} catch {
|
|
1739
|
+
return {
|
|
1740
|
+
exists: false,
|
|
1741
|
+
running: false
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function readSidecarStatus(options) {
|
|
1746
|
+
const { containerName, mcpUrl, dashboardUrl } = resolveSidecarUrls(options);
|
|
1747
|
+
const container = await inspectSidecarContainer(containerName);
|
|
1748
|
+
return {
|
|
1749
|
+
running: container.running,
|
|
1750
|
+
mcpUrl,
|
|
1751
|
+
dashboardUrl,
|
|
1752
|
+
containerName
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
function buildStartArgs(options, containerName) {
|
|
1756
|
+
return [
|
|
1757
|
+
"run",
|
|
1758
|
+
"-d",
|
|
1759
|
+
"--name",
|
|
1760
|
+
containerName,
|
|
1761
|
+
"--restart",
|
|
1762
|
+
"unless-stopped",
|
|
1763
|
+
"-p",
|
|
1764
|
+
`${options.mcpPort}:${MCP_CONTAINER_PORT}`,
|
|
1765
|
+
"-p",
|
|
1766
|
+
`${options.dashboardPort}:${DASHBOARD_CONTAINER_PORT}`,
|
|
1767
|
+
"-v",
|
|
1768
|
+
`${options.baselineWorktreePath}:/workspaces/pourkit`,
|
|
1769
|
+
"-v",
|
|
1770
|
+
`${options.dataDir}:${SERENA_DATA_MOUNT}`,
|
|
1771
|
+
"-e",
|
|
1772
|
+
`SERENA_HOME=${SERENA_DATA_MOUNT}/config`,
|
|
1773
|
+
options.image,
|
|
1774
|
+
"serena",
|
|
1775
|
+
"start-mcp-server",
|
|
1776
|
+
"--transport",
|
|
1777
|
+
"streamable-http",
|
|
1778
|
+
"--port",
|
|
1779
|
+
String(MCP_CONTAINER_PORT),
|
|
1780
|
+
"--host",
|
|
1781
|
+
"0.0.0.0"
|
|
1782
|
+
];
|
|
1783
|
+
}
|
|
1784
|
+
async function getSerenaSidecarStatus(options) {
|
|
1785
|
+
return readSidecarStatus(options);
|
|
1786
|
+
}
|
|
1787
|
+
async function startSerenaSidecar(options) {
|
|
1788
|
+
const { containerName } = resolveSidecarUrls(options);
|
|
1789
|
+
const container = await inspectSidecarContainer(containerName);
|
|
1790
|
+
if (container.exists) {
|
|
1791
|
+
if (!container.running) {
|
|
1792
|
+
await execCapture("docker", ["start", containerName]);
|
|
1793
|
+
}
|
|
1794
|
+
return readSidecarStatus(options);
|
|
1795
|
+
}
|
|
1796
|
+
await execCapture("docker", buildStartArgs(options, containerName));
|
|
1797
|
+
return readSidecarStatus(options);
|
|
1798
|
+
}
|
|
1799
|
+
async function indexSerenaProject(options) {
|
|
1800
|
+
const { containerName } = resolveSidecarUrls(options);
|
|
1801
|
+
await execCapture("docker", [
|
|
1802
|
+
"exec",
|
|
1803
|
+
containerName,
|
|
1804
|
+
"serena",
|
|
1805
|
+
"project",
|
|
1806
|
+
"create",
|
|
1807
|
+
"--language",
|
|
1808
|
+
"typescript",
|
|
1809
|
+
"--index",
|
|
1810
|
+
"/workspaces/pourkit"
|
|
1811
|
+
]);
|
|
1812
|
+
}
|
|
1813
|
+
async function stopSerenaSidecar(options) {
|
|
1814
|
+
const { containerName } = resolveSidecarUrls(options);
|
|
1815
|
+
try {
|
|
1816
|
+
await execCapture("docker", ["stop", containerName]);
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
return readSidecarStatus(options);
|
|
1820
|
+
}
|
|
1821
|
+
async function prepareSerenaSidecarConfig(options) {
|
|
1822
|
+
const configDir = path4.join(options.dataDir, "config");
|
|
1823
|
+
await mkdir3(configDir, { recursive: true });
|
|
1824
|
+
}
|
|
1380
1825
|
|
|
1381
|
-
|
|
1826
|
+
// serena/preflight.ts
|
|
1827
|
+
var SERENA_MCP_PORT = 9121;
|
|
1828
|
+
var SERENA_DASHBOARD_PORT = 24282;
|
|
1829
|
+
var SERENA_IMAGE = "ghcr.io/oraios/serena:latest";
|
|
1830
|
+
function sidecarOptions(paths, mcpUrl) {
|
|
1831
|
+
return {
|
|
1832
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1833
|
+
dataDir: paths.dataDir,
|
|
1834
|
+
mcpPort: SERENA_MCP_PORT,
|
|
1835
|
+
dashboardPort: SERENA_DASHBOARD_PORT,
|
|
1836
|
+
image: SERENA_IMAGE,
|
|
1837
|
+
mcpUrl
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
async function canReachMcp(url) {
|
|
1841
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
1842
|
+
try {
|
|
1843
|
+
await fetch(url, { method: "GET", signal: AbortSignal.timeout(500) });
|
|
1844
|
+
return true;
|
|
1845
|
+
} catch {
|
|
1846
|
+
if (attempt < 9) {
|
|
1847
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
function formatError(error) {
|
|
1854
|
+
return error instanceof Error ? error.message : String(error);
|
|
1855
|
+
}
|
|
1856
|
+
async function prepareSerenaForTarget(options) {
|
|
1857
|
+
if (!options.enabled) {
|
|
1858
|
+
return { enabled: false };
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
const paths = await ensureBaselineWorktree({
|
|
1862
|
+
repoRoot: options.repoRoot,
|
|
1863
|
+
dataDir: options.dataDir
|
|
1864
|
+
});
|
|
1865
|
+
await prepareSerenaSidecarConfig({
|
|
1866
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
1867
|
+
dataDir: paths.dataDir
|
|
1868
|
+
});
|
|
1869
|
+
const status = options.autoStart ? await startSerenaSidecar(sidecarOptions(paths, options.mcpUrl)) : await getSerenaSidecarStatus(sidecarOptions(paths, options.mcpUrl));
|
|
1870
|
+
const mcpReachable = await canReachMcp(options.mcpUrl);
|
|
1871
|
+
if (!mcpReachable) {
|
|
1872
|
+
return {
|
|
1873
|
+
enabled: true,
|
|
1874
|
+
available: false,
|
|
1875
|
+
error: status.running ? `Serena MCP is not reachable at ${options.mcpUrl}` : `Serena sidecar is not running for target ${options.targetName}`
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
await refreshSerenaBaseline({
|
|
1879
|
+
repoRoot: options.repoRoot,
|
|
1880
|
+
dataDir: options.dataDir,
|
|
1881
|
+
baseBranch: options.baseBranch
|
|
1882
|
+
});
|
|
1883
|
+
return {
|
|
1884
|
+
enabled: true,
|
|
1885
|
+
available: true,
|
|
1886
|
+
mcpUrl: options.mcpUrl
|
|
1887
|
+
};
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
return {
|
|
1890
|
+
enabled: true,
|
|
1891
|
+
available: false,
|
|
1892
|
+
error: formatError(error)
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1382
1896
|
|
|
1383
|
-
|
|
1897
|
+
// failure-resolution/failure-resolution-agent.ts
|
|
1898
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1899
|
+
import { join as join6 } from "path";
|
|
1384
1900
|
|
|
1385
|
-
|
|
1901
|
+
// failure-resolution/recovery-policy.ts
|
|
1902
|
+
function isSecuritySensitiveFailure(failure) {
|
|
1903
|
+
return failure instanceof PublishedHistoryRisk || failure instanceof SafetyFailure;
|
|
1904
|
+
}
|
|
1905
|
+
async function evaluateRecoveryPolicy(params) {
|
|
1906
|
+
if (isSecuritySensitiveFailure(params.failure)) {
|
|
1907
|
+
return {
|
|
1908
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
1909
|
+
reason: "Security-sensitive failure \u2014 AI recovery bypassed"
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
if (params.failure instanceof ConfigFailure) {
|
|
1913
|
+
if (params.agentRecommendedDecision !== "FAIL_RUN" && params.agentRecommendedDecision !== "HANDOFF_TO_HUMAN") {
|
|
1914
|
+
return {
|
|
1915
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
1916
|
+
reason: `ConfigFailure \u2014 agent recommended ${params.agentRecommendedDecision} but config errors are not AI-repairable`
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const budget = recoveryBudgetForFailure(
|
|
1921
|
+
params.worktreePath,
|
|
1922
|
+
params.fingerprint,
|
|
1923
|
+
params.maxAttempts
|
|
1924
|
+
);
|
|
1925
|
+
if (budget.exhausted) {
|
|
1926
|
+
return {
|
|
1927
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
1928
|
+
reason: `Recovery budget exhausted (${budget.used}/${params.maxAttempts})`
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
if (!params.allowedDecisions.includes(params.agentRecommendedDecision)) {
|
|
1932
|
+
return {
|
|
1933
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
1934
|
+
reason: `Agent recommended ${params.agentRecommendedDecision} which is not allowed`
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
if (params.agentRecommendedDecision === "FAIL_RUN") {
|
|
1938
|
+
return { decision: "FAIL_RUN", reason: "Agent recommended FAIL_RUN" };
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
decision: params.agentRecommendedDecision,
|
|
1942
|
+
reason: "Agent recommendation accepted"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1386
1945
|
|
|
1387
|
-
|
|
1946
|
+
// failure-resolution/failure-resolution-agent.ts
|
|
1947
|
+
function constructFailureResolutionPacket(failure, context) {
|
|
1948
|
+
return {
|
|
1949
|
+
failureType: failure._tag,
|
|
1950
|
+
stageName: context.stageName,
|
|
1951
|
+
attemptNumber: context.attemptNumber,
|
|
1952
|
+
worktreePath: context.worktreePath,
|
|
1953
|
+
branchName: context.branchName,
|
|
1954
|
+
baseBranch: context.baseBranch,
|
|
1955
|
+
conflictedPaths: failure instanceof RebaseConflict ? failure.conflictedPaths : void 0,
|
|
1956
|
+
failureSummary: failure.message,
|
|
1957
|
+
maxAttempts: context.maxAttempts,
|
|
1958
|
+
allowedDecisions: context.allowedDecisions,
|
|
1959
|
+
artifactTarget: context.artifactTarget
|
|
1960
|
+
};
|
|
1388
1961
|
}
|
|
1389
|
-
async function
|
|
1962
|
+
async function runFailureResolutionAgent(options) {
|
|
1390
1963
|
const {
|
|
1391
1964
|
executionProvider,
|
|
1392
1965
|
config,
|
|
1393
1966
|
target,
|
|
1394
|
-
|
|
1395
|
-
|
|
1967
|
+
failure,
|
|
1968
|
+
packet,
|
|
1396
1969
|
worktreePath,
|
|
1397
1970
|
repoRoot: repoRoot2,
|
|
1398
|
-
conflictedPaths,
|
|
1399
|
-
attempt,
|
|
1400
1971
|
logger
|
|
1401
1972
|
} = options;
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1973
|
+
const frConfig = target.strategy.failureResolution;
|
|
1974
|
+
const artifactPath = packet.artifactTarget;
|
|
1975
|
+
const fullArtifactPath = join6(worktreePath, artifactPath);
|
|
1976
|
+
const fingerprint = computeFailureFingerprint(packet.stageName, failure._tag);
|
|
1977
|
+
const prompt = [
|
|
1978
|
+
`# Failure Resolution: ${packet.failureType}`,
|
|
1979
|
+
"",
|
|
1980
|
+
"## Failure Context",
|
|
1981
|
+
"",
|
|
1982
|
+
"```json",
|
|
1983
|
+
JSON.stringify(packet, null, 2),
|
|
1984
|
+
"```",
|
|
1985
|
+
"",
|
|
1986
|
+
"## Instructions",
|
|
1987
|
+
"",
|
|
1988
|
+
`Write your resolution to: ${artifactPath}`,
|
|
1989
|
+
"Include a ```json block with: recoveryDecision, summary, changedFiles, verificationSummary (optional), verificationCommands (optional), notes (optional).",
|
|
1990
|
+
"",
|
|
1991
|
+
"Allowed decisions: " + packet.allowedDecisions.join(", ")
|
|
1992
|
+
].join("\n");
|
|
1418
1993
|
const executionResult = await executionProvider.execute({
|
|
1419
|
-
stage: "
|
|
1420
|
-
agent:
|
|
1421
|
-
model:
|
|
1994
|
+
stage: "failureResolution",
|
|
1995
|
+
agent: frConfig.agent,
|
|
1996
|
+
model: frConfig.model,
|
|
1422
1997
|
prompt,
|
|
1423
1998
|
target,
|
|
1424
1999
|
repoRoot: repoRoot2,
|
|
1425
|
-
branchName,
|
|
2000
|
+
branchName: packet.branchName,
|
|
1426
2001
|
sandbox: config.sandbox,
|
|
1427
2002
|
autoApprove: true,
|
|
1428
2003
|
worktreePath,
|
|
1429
2004
|
artifactPath,
|
|
1430
|
-
artifacts: [
|
|
2005
|
+
artifacts: [],
|
|
1431
2006
|
logger
|
|
1432
2007
|
});
|
|
1433
2008
|
if (!executionResult.success) {
|
|
2009
|
+
await writeRecoveryAttempt(
|
|
2010
|
+
worktreePath,
|
|
2011
|
+
"failure",
|
|
2012
|
+
fingerprint,
|
|
2013
|
+
`Agent execution failed: ${executionResult.error}`,
|
|
2014
|
+
void 0,
|
|
2015
|
+
"HANDOFF_TO_HUMAN",
|
|
2016
|
+
packet.stageName
|
|
2017
|
+
);
|
|
1434
2018
|
return {
|
|
1435
|
-
status: "
|
|
1436
|
-
|
|
2019
|
+
status: "handoff",
|
|
2020
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2021
|
+
reason: `Agent execution failed: ${executionResult.error}`
|
|
1437
2022
|
};
|
|
1438
2023
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
2024
|
+
if (!existsSync4(fullArtifactPath)) {
|
|
2025
|
+
await writeRecoveryAttempt(
|
|
2026
|
+
worktreePath,
|
|
2027
|
+
"failure",
|
|
2028
|
+
fingerprint,
|
|
2029
|
+
"Agent did not write artifact",
|
|
2030
|
+
void 0,
|
|
2031
|
+
"HANDOFF_TO_HUMAN",
|
|
2032
|
+
packet.stageName
|
|
2033
|
+
);
|
|
1441
2034
|
return {
|
|
1442
|
-
status: "
|
|
1443
|
-
|
|
1444
|
-
|
|
2035
|
+
status: "handoff",
|
|
2036
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2037
|
+
reason: "Agent did not write artifact"
|
|
1445
2038
|
};
|
|
1446
2039
|
}
|
|
1447
|
-
let
|
|
2040
|
+
let artifact;
|
|
1448
2041
|
try {
|
|
1449
|
-
|
|
2042
|
+
const md = readFileSync4(fullArtifactPath, "utf-8");
|
|
2043
|
+
artifact = parseRecoveryArtifact(md, artifactPath);
|
|
1450
2044
|
} catch (error) {
|
|
2045
|
+
const reason = error instanceof Error ? error.message : "Failed to parse artifact";
|
|
2046
|
+
await writeRecoveryAttempt(
|
|
2047
|
+
worktreePath,
|
|
2048
|
+
"failure",
|
|
2049
|
+
fingerprint,
|
|
2050
|
+
reason,
|
|
2051
|
+
void 0,
|
|
2052
|
+
"HANDOFF_TO_HUMAN",
|
|
2053
|
+
packet.stageName
|
|
2054
|
+
);
|
|
2055
|
+
return { status: "handoff", decision: "HANDOFF_TO_HUMAN", reason };
|
|
2056
|
+
}
|
|
2057
|
+
const validation = validateRecoveryDecision(
|
|
2058
|
+
artifact,
|
|
2059
|
+
packet.allowedDecisions
|
|
2060
|
+
);
|
|
2061
|
+
if (!validation.valid) {
|
|
2062
|
+
await writeRecoveryAttempt(
|
|
2063
|
+
worktreePath,
|
|
2064
|
+
"failure",
|
|
2065
|
+
fingerprint,
|
|
2066
|
+
validation.reason,
|
|
2067
|
+
void 0,
|
|
2068
|
+
"HANDOFF_TO_HUMAN",
|
|
2069
|
+
packet.stageName
|
|
2070
|
+
);
|
|
1451
2071
|
return {
|
|
1452
|
-
status: "
|
|
1453
|
-
|
|
1454
|
-
|
|
2072
|
+
status: "handoff",
|
|
2073
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2074
|
+
reason: validation.reason
|
|
1455
2075
|
};
|
|
1456
2076
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2077
|
+
const policyResult = await evaluateRecoveryPolicy({
|
|
2078
|
+
failure,
|
|
2079
|
+
worktreePath,
|
|
2080
|
+
fingerprint,
|
|
2081
|
+
maxAttempts: packet.maxAttempts,
|
|
2082
|
+
agentRecommendedDecision: validation.decision,
|
|
2083
|
+
allowedDecisions: packet.allowedDecisions
|
|
2084
|
+
});
|
|
2085
|
+
await writeRecoveryAttempt(
|
|
2086
|
+
worktreePath,
|
|
2087
|
+
policyResult.decision === "HANDOFF_TO_HUMAN" ? "handoff" : policyResult.decision === "FAIL_RUN" ? "failure" : "success",
|
|
2088
|
+
fingerprint,
|
|
2089
|
+
policyResult.reason,
|
|
2090
|
+
artifactPath,
|
|
2091
|
+
policyResult.decision,
|
|
2092
|
+
packet.stageName
|
|
2093
|
+
);
|
|
2094
|
+
if (policyResult.decision === "HANDOFF_TO_HUMAN") {
|
|
2095
|
+
return {
|
|
2096
|
+
status: "handoff",
|
|
2097
|
+
decision: "HANDOFF_TO_HUMAN",
|
|
2098
|
+
reason: policyResult.reason
|
|
2099
|
+
};
|
|
1469
2100
|
}
|
|
1470
|
-
if (
|
|
2101
|
+
if (policyResult.decision === "FAIL_RUN") {
|
|
1471
2102
|
return {
|
|
1472
|
-
status: "
|
|
1473
|
-
|
|
1474
|
-
|
|
2103
|
+
status: "fail-run",
|
|
2104
|
+
decision: "FAIL_RUN",
|
|
2105
|
+
reason: policyResult.reason
|
|
1475
2106
|
};
|
|
1476
2107
|
}
|
|
1477
2108
|
return {
|
|
1478
|
-
status: "
|
|
1479
|
-
|
|
1480
|
-
|
|
2109
|
+
status: "recovered",
|
|
2110
|
+
decision: policyResult.decision,
|
|
2111
|
+
artifact
|
|
1481
2112
|
};
|
|
1482
2113
|
}
|
|
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 };
|
|
2114
|
+
async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary, artifactRef, decision, stageName = "baseRefresh") {
|
|
2115
|
+
writeAttemptLog(worktreePath, {
|
|
2116
|
+
attemptType: "recovery",
|
|
2117
|
+
fingerprint,
|
|
2118
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2119
|
+
stage: stageName,
|
|
2120
|
+
outcome,
|
|
2121
|
+
artifactRef,
|
|
2122
|
+
decision: decision ?? (outcome === "handoff" ? "HANDOFF_TO_HUMAN" : outcome === "success" ? "RETRY_STAGE" : void 0)
|
|
2123
|
+
});
|
|
1560
2124
|
}
|
|
1561
2125
|
|
|
1562
2126
|
// commands/pr-description-agent.ts
|
|
1563
|
-
import { existsSync as
|
|
1564
|
-
import { dirname as
|
|
2127
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2128
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1565
2129
|
|
|
1566
2130
|
// pr/pr-description.ts
|
|
1567
2131
|
var CONVENTIONAL_TITLE_PATTERN = /^(feat|fix|perf|refactor|docs|test|chore|ci|build)(\([^)]+\))?!?:\s+\S/;
|
|
@@ -1571,11 +2135,11 @@ var PrDescriptionProtocolError = class extends Error {
|
|
|
1571
2135
|
this.name = "PrDescriptionProtocolError";
|
|
1572
2136
|
}
|
|
1573
2137
|
};
|
|
1574
|
-
var
|
|
1575
|
-
function
|
|
2138
|
+
var SECTION_HEADING_PATTERN = /^## (PR Title|PR Body)\s*$/gm;
|
|
2139
|
+
function extractSections(output) {
|
|
1576
2140
|
const sections = [];
|
|
1577
2141
|
let match;
|
|
1578
|
-
const re = new RegExp(
|
|
2142
|
+
const re = new RegExp(SECTION_HEADING_PATTERN);
|
|
1579
2143
|
while ((match = re.exec(output)) !== null) {
|
|
1580
2144
|
const heading = match[1];
|
|
1581
2145
|
sections.push({
|
|
@@ -1586,7 +2150,7 @@ function extractSections2(output) {
|
|
|
1586
2150
|
}
|
|
1587
2151
|
return sections;
|
|
1588
2152
|
}
|
|
1589
|
-
function
|
|
2153
|
+
function contentAfter(output, sections, section) {
|
|
1590
2154
|
const index = sections.indexOf(section);
|
|
1591
2155
|
const nextSection = sections[index + 1];
|
|
1592
2156
|
const start = section.endIndex;
|
|
@@ -1594,7 +2158,7 @@ function contentAfter2(output, sections, section) {
|
|
|
1594
2158
|
return output.slice(start, end).trim();
|
|
1595
2159
|
}
|
|
1596
2160
|
function parsePrDescription(output) {
|
|
1597
|
-
const sections =
|
|
2161
|
+
const sections = extractSections(output);
|
|
1598
2162
|
const titleSections = sections.filter((s) => s.heading === "PR Title");
|
|
1599
2163
|
const bodySections = sections.filter((s) => s.heading === "PR Body");
|
|
1600
2164
|
if (titleSections.length === 0) {
|
|
@@ -1617,8 +2181,8 @@ function parsePrDescription(output) {
|
|
|
1617
2181
|
`Duplicate "## PR Body" sections found (${bodySections.length})`
|
|
1618
2182
|
);
|
|
1619
2183
|
}
|
|
1620
|
-
const title =
|
|
1621
|
-
const body =
|
|
2184
|
+
const title = contentAfter(output, sections, titleSections[0]);
|
|
2185
|
+
const body = contentAfter(output, sections, bodySections[0]);
|
|
1622
2186
|
if (title.length === 0) {
|
|
1623
2187
|
throw new PrDescriptionProtocolError('"## PR Title" section is empty');
|
|
1624
2188
|
}
|
|
@@ -1649,7 +2213,7 @@ function inferConventionalType(commitSummaries) {
|
|
|
1649
2213
|
|
|
1650
2214
|
// pr/pr-description-context.ts
|
|
1651
2215
|
init_common();
|
|
1652
|
-
import { join as
|
|
2216
|
+
import { join as join7 } from "path";
|
|
1653
2217
|
import { readFile } from "fs/promises";
|
|
1654
2218
|
async function collectFinalizerContext(options) {
|
|
1655
2219
|
const { targetBase, branchName, worktreePath, reviewArtifactPath, logger } = options;
|
|
@@ -1710,7 +2274,7 @@ async function readReviewArtifact(artifactPath) {
|
|
|
1710
2274
|
return content;
|
|
1711
2275
|
}
|
|
1712
2276
|
function buildFinalizerPrompt(context, promptTemplate) {
|
|
1713
|
-
const artifactPathInWorktree =
|
|
2277
|
+
const artifactPathInWorktree = join7(
|
|
1714
2278
|
".pourkit",
|
|
1715
2279
|
".tmp",
|
|
1716
2280
|
"finalizer",
|
|
@@ -1788,13 +2352,13 @@ async function runFinalizerAgent(options) {
|
|
|
1788
2352
|
finalizer.promptTemplate
|
|
1789
2353
|
);
|
|
1790
2354
|
const prompt = buildFinalizerPrompt(context, resolvedPrompt);
|
|
1791
|
-
const artifactPathInWorktree =
|
|
2355
|
+
const artifactPathInWorktree = join8(
|
|
1792
2356
|
".pourkit",
|
|
1793
2357
|
".tmp",
|
|
1794
2358
|
"finalizer",
|
|
1795
2359
|
"agent-output.md"
|
|
1796
2360
|
);
|
|
1797
|
-
const artifactPath =
|
|
2361
|
+
const artifactPath = join8(worktreePath, artifactPathInWorktree);
|
|
1798
2362
|
prepareArtifactPath(artifactPath);
|
|
1799
2363
|
let output = "";
|
|
1800
2364
|
let parsed;
|
|
@@ -1859,24 +2423,24 @@ async function runFinalizerAgent(options) {
|
|
|
1859
2423
|
}
|
|
1860
2424
|
function loadFinalizerPrompt(repoRoot2, promptTemplate) {
|
|
1861
2425
|
const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
|
|
1862
|
-
if (
|
|
1863
|
-
return
|
|
2426
|
+
if (existsSync5(promptPath)) {
|
|
2427
|
+
return readFileSync5(promptPath, "utf-8");
|
|
1864
2428
|
}
|
|
1865
2429
|
return promptTemplate;
|
|
1866
2430
|
}
|
|
1867
2431
|
function prepareArtifactPath(artifactPath) {
|
|
1868
|
-
|
|
1869
|
-
if (
|
|
2432
|
+
mkdirSync4(dirname3(artifactPath), { recursive: true });
|
|
2433
|
+
if (existsSync5(artifactPath)) {
|
|
1870
2434
|
rmSync(artifactPath);
|
|
1871
2435
|
}
|
|
1872
2436
|
}
|
|
1873
2437
|
function readAgentOutput(artifactPath) {
|
|
1874
|
-
if (!
|
|
2438
|
+
if (!existsSync5(artifactPath)) {
|
|
1875
2439
|
throw new Error(
|
|
1876
2440
|
`Finalizer agent did not produce output at ${artifactPath}`
|
|
1877
2441
|
);
|
|
1878
2442
|
}
|
|
1879
|
-
const output =
|
|
2443
|
+
const output = readFileSync5(artifactPath, "utf-8");
|
|
1880
2444
|
if (!output.trim()) {
|
|
1881
2445
|
throw new Error(`Finalizer agent produced empty output at ${artifactPath}`);
|
|
1882
2446
|
}
|
|
@@ -1884,9 +2448,9 @@ function readAgentOutput(artifactPath) {
|
|
|
1884
2448
|
}
|
|
1885
2449
|
async function persistGeneratedArtifact(worktreePath, output) {
|
|
1886
2450
|
try {
|
|
1887
|
-
const dir =
|
|
1888
|
-
|
|
1889
|
-
writeFileSync2(
|
|
2451
|
+
const dir = join8(worktreePath, ".pourkit", ".tmp", "finalizer");
|
|
2452
|
+
mkdirSync4(dir, { recursive: true });
|
|
2453
|
+
writeFileSync2(join8(dir, "generated.md"), output, "utf-8");
|
|
1890
2454
|
} catch {
|
|
1891
2455
|
}
|
|
1892
2456
|
}
|
|
@@ -2124,14 +2688,14 @@ async function runMergeCoordinator(options) {
|
|
|
2124
2688
|
|
|
2125
2689
|
// commands/review.ts
|
|
2126
2690
|
import {
|
|
2127
|
-
existsSync as
|
|
2128
|
-
mkdirSync as
|
|
2129
|
-
readFileSync as
|
|
2691
|
+
existsSync as existsSync6,
|
|
2692
|
+
mkdirSync as mkdirSync5,
|
|
2693
|
+
readFileSync as readFileSync6,
|
|
2130
2694
|
readdirSync,
|
|
2131
2695
|
rmSync as rmSync2,
|
|
2132
2696
|
writeFileSync as writeFileSync3
|
|
2133
2697
|
} from "fs";
|
|
2134
|
-
import { dirname as
|
|
2698
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
2135
2699
|
|
|
2136
2700
|
// pr/review-verdict.ts
|
|
2137
2701
|
var ReviewVerdictProtocolError = class extends Error {
|
|
@@ -2213,12 +2777,12 @@ function extractLatestFindingIds(reviewOutput, iteration) {
|
|
|
2213
2777
|
return ids;
|
|
2214
2778
|
}
|
|
2215
2779
|
function validateRefactorArtifact(artifactPath, findingIds) {
|
|
2216
|
-
if (!
|
|
2780
|
+
if (!existsSync6(artifactPath)) {
|
|
2217
2781
|
throw new RefactorArtifactValidationError(
|
|
2218
2782
|
`Refactor artifact missing at ${artifactPath}`
|
|
2219
2783
|
);
|
|
2220
2784
|
}
|
|
2221
|
-
const content =
|
|
2785
|
+
const content = readFileSync6(artifactPath, "utf-8");
|
|
2222
2786
|
if (!content.trim()) {
|
|
2223
2787
|
throw new RefactorArtifactValidationError("Refactor artifact is empty");
|
|
2224
2788
|
}
|
|
@@ -2363,13 +2927,13 @@ async function runReviewCommand(options) {
|
|
|
2363
2927
|
if (!reviewer) {
|
|
2364
2928
|
throw new Error("No reviewer config found");
|
|
2365
2929
|
}
|
|
2366
|
-
const artifactPathInWorktree =
|
|
2930
|
+
const artifactPathInWorktree = join9(
|
|
2367
2931
|
".pourkit",
|
|
2368
2932
|
".tmp",
|
|
2369
2933
|
"reviewers",
|
|
2370
2934
|
`iteration-${iteration ?? 1}.md`
|
|
2371
2935
|
);
|
|
2372
|
-
const artifactPath =
|
|
2936
|
+
const artifactPath = join9(worktreePath, artifactPathInWorktree);
|
|
2373
2937
|
prepareReviewArtifactPath(artifactPath);
|
|
2374
2938
|
const prompt = buildReviewerPrompt(
|
|
2375
2939
|
repoRoot2,
|
|
@@ -2476,8 +3040,8 @@ ${entry.trimEnd()}`).join("\n\n")}
|
|
|
2476
3040
|
`;
|
|
2477
3041
|
}
|
|
2478
3042
|
function renderPriorRefactorArtifacts(worktreePath, currentIteration) {
|
|
2479
|
-
const refactorsDir =
|
|
2480
|
-
if (!
|
|
3043
|
+
const refactorsDir = join9(worktreePath, ".pourkit", ".tmp", "refactors");
|
|
3044
|
+
if (!existsSync6(refactorsDir)) {
|
|
2481
3045
|
return "";
|
|
2482
3046
|
}
|
|
2483
3047
|
const files = readdirSync(refactorsDir);
|
|
@@ -2487,9 +3051,9 @@ function renderPriorRefactorArtifacts(worktreePath, currentIteration) {
|
|
|
2487
3051
|
if (match) {
|
|
2488
3052
|
const num = parseInt(match[1], 10);
|
|
2489
3053
|
if (num < currentIteration) {
|
|
2490
|
-
const filePath =
|
|
3054
|
+
const filePath = join9(refactorsDir, file);
|
|
2491
3055
|
try {
|
|
2492
|
-
const content =
|
|
3056
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
2493
3057
|
if (content.trim()) {
|
|
2494
3058
|
iterationFiles.push({ num, content });
|
|
2495
3059
|
}
|
|
@@ -2514,8 +3078,8 @@ ${iterationsBlocks}
|
|
|
2514
3078
|
`;
|
|
2515
3079
|
}
|
|
2516
3080
|
function renderPriorReviewerArtifacts(worktreePath, currentIteration) {
|
|
2517
|
-
const reviewersDir =
|
|
2518
|
-
if (!
|
|
3081
|
+
const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
3082
|
+
if (!existsSync6(reviewersDir)) {
|
|
2519
3083
|
return "";
|
|
2520
3084
|
}
|
|
2521
3085
|
const files = readdirSync(reviewersDir);
|
|
@@ -2525,9 +3089,9 @@ function renderPriorReviewerArtifacts(worktreePath, currentIteration) {
|
|
|
2525
3089
|
if (match) {
|
|
2526
3090
|
const num = parseInt(match[1], 10);
|
|
2527
3091
|
if (num < currentIteration) {
|
|
2528
|
-
const filePath =
|
|
3092
|
+
const filePath = join9(reviewersDir, file);
|
|
2529
3093
|
try {
|
|
2530
|
-
const content =
|
|
3094
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
2531
3095
|
if (content.trim()) {
|
|
2532
3096
|
iterationFiles.push({ num, content });
|
|
2533
3097
|
}
|
|
@@ -2556,7 +3120,7 @@ function loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock) {
|
|
|
2556
3120
|
repoRoot2,
|
|
2557
3121
|
promptTemplate
|
|
2558
3122
|
);
|
|
2559
|
-
const promptBody =
|
|
3123
|
+
const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
|
|
2560
3124
|
const hasCriteriaPlaceholder = promptBody.includes("{{REVIEW_CRITERIA}}");
|
|
2561
3125
|
return {
|
|
2562
3126
|
content: promptBody.replace(/\{\{REVIEW_CRITERIA\}\}/g, criteriaBlock),
|
|
@@ -2565,29 +3129,29 @@ function loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock) {
|
|
|
2565
3129
|
}
|
|
2566
3130
|
function renderReviewCriteria(repoRoot2, criteria) {
|
|
2567
3131
|
return criteria.map((criterion) => {
|
|
2568
|
-
const snippetPath =
|
|
3132
|
+
const snippetPath = join9(
|
|
2569
3133
|
repoRoot2,
|
|
2570
3134
|
".pourkit",
|
|
2571
3135
|
"prompts",
|
|
2572
3136
|
`reviewer-${criterion}.snippet.md`
|
|
2573
3137
|
);
|
|
2574
|
-
if (
|
|
2575
|
-
return
|
|
3138
|
+
if (existsSync6(snippetPath)) {
|
|
3139
|
+
return readFileSync6(snippetPath, "utf-8").trimEnd();
|
|
2576
3140
|
}
|
|
2577
3141
|
return `- ${criterion}`;
|
|
2578
3142
|
}).join("\n\n");
|
|
2579
3143
|
}
|
|
2580
3144
|
function prepareReviewArtifactPath(artifactPath) {
|
|
2581
|
-
|
|
2582
|
-
if (
|
|
3145
|
+
mkdirSync5(dirname4(artifactPath), { recursive: true });
|
|
3146
|
+
if (existsSync6(artifactPath)) {
|
|
2583
3147
|
rmSync2(artifactPath);
|
|
2584
3148
|
}
|
|
2585
3149
|
}
|
|
2586
3150
|
function recoverReviewOutputFromLog(logPath) {
|
|
2587
|
-
if (!
|
|
3151
|
+
if (!existsSync6(logPath)) {
|
|
2588
3152
|
return null;
|
|
2589
3153
|
}
|
|
2590
|
-
const logContent =
|
|
3154
|
+
const logContent = readFileSync6(logPath, "utf-8");
|
|
2591
3155
|
const startIndex = logContent.indexOf("## Findings");
|
|
2592
3156
|
if (startIndex === -1) {
|
|
2593
3157
|
return null;
|
|
@@ -2602,8 +3166,8 @@ function recoverReviewOutputFromLog(logPath) {
|
|
|
2602
3166
|
return recoveredOutput.length > 0 ? recoveredOutput : null;
|
|
2603
3167
|
}
|
|
2604
3168
|
function readReviewArtifact2(artifactPath, logPath) {
|
|
2605
|
-
if (
|
|
2606
|
-
const output =
|
|
3169
|
+
if (existsSync6(artifactPath)) {
|
|
3170
|
+
const output = readFileSync6(artifactPath, "utf-8");
|
|
2607
3171
|
if (output.trim()) {
|
|
2608
3172
|
return output;
|
|
2609
3173
|
}
|
|
@@ -2613,7 +3177,7 @@ function readReviewArtifact2(artifactPath, logPath) {
|
|
|
2613
3177
|
writeFileSync3(artifactPath, recoveredOutput, "utf-8");
|
|
2614
3178
|
return recoveredOutput;
|
|
2615
3179
|
}
|
|
2616
|
-
if (!
|
|
3180
|
+
if (!existsSync6(artifactPath)) {
|
|
2617
3181
|
throw new Error(`Reviewer did not produce output at ${artifactPath}`);
|
|
2618
3182
|
}
|
|
2619
3183
|
throw new Error(`Reviewer produced empty output at ${artifactPath}`);
|
|
@@ -2629,7 +3193,8 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2629
3193
|
repoRoot: repoRoot2,
|
|
2630
3194
|
logger,
|
|
2631
3195
|
startingLifetimeIteration = 0,
|
|
2632
|
-
humanHandoffResolved
|
|
3196
|
+
humanHandoffResolved,
|
|
3197
|
+
serena
|
|
2633
3198
|
} = options;
|
|
2634
3199
|
const strategy = target.strategy;
|
|
2635
3200
|
const reviewer = strategy.review.reviewer;
|
|
@@ -2644,7 +3209,7 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2644
3209
|
const passWithNotesRefactorAttempts = strategy.review.passWithNotesRefactorAttempts;
|
|
2645
3210
|
let resolvedStartingIteration = startingLifetimeIteration;
|
|
2646
3211
|
{
|
|
2647
|
-
const reviewersDir =
|
|
3212
|
+
const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
2648
3213
|
try {
|
|
2649
3214
|
const files = readdirSync(reviewersDir);
|
|
2650
3215
|
let maxExistingIteration = 0;
|
|
@@ -2752,7 +3317,7 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2752
3317
|
}
|
|
2753
3318
|
if (reviewResult.verdict === "NEEDS_REFACTOR" || reviewResult.verdict === "PASS_WITH_NOTES" || reviewResult.verdict === "FAIL") {
|
|
2754
3319
|
logger.step("info", "Running refactor agent");
|
|
2755
|
-
const refactorArtifactPathInWorktree =
|
|
3320
|
+
const refactorArtifactPathInWorktree = join9(
|
|
2756
3321
|
".pourkit",
|
|
2757
3322
|
".tmp",
|
|
2758
3323
|
"refactors",
|
|
@@ -2786,6 +3351,7 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2786
3351
|
sections: STAGE_SECTIONS.refactor
|
|
2787
3352
|
})
|
|
2788
3353
|
],
|
|
3354
|
+
...serena ? { serena } : {},
|
|
2789
3355
|
logger
|
|
2790
3356
|
});
|
|
2791
3357
|
if (!refactorResult.success) {
|
|
@@ -2808,7 +3374,7 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2808
3374
|
reviewResult.output,
|
|
2809
3375
|
lifetimeIteration
|
|
2810
3376
|
);
|
|
2811
|
-
const refactorArtifactPath =
|
|
3377
|
+
const refactorArtifactPath = join9(
|
|
2812
3378
|
worktreePath,
|
|
2813
3379
|
refactorArtifactPathInWorktree
|
|
2814
3380
|
);
|
|
@@ -2858,9 +3424,9 @@ async function runReviewWithRefactorLoop(options) {
|
|
|
2858
3424
|
}
|
|
2859
3425
|
async function writeArtifact(worktreePath, filename, output) {
|
|
2860
3426
|
try {
|
|
2861
|
-
const dir =
|
|
2862
|
-
|
|
2863
|
-
writeFileSync3(
|
|
3427
|
+
const dir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
3428
|
+
mkdirSync5(dir, { recursive: true });
|
|
3429
|
+
writeFileSync3(join9(dir, filename), output, "utf-8");
|
|
2864
3430
|
} catch {
|
|
2865
3431
|
}
|
|
2866
3432
|
}
|
|
@@ -2872,7 +3438,7 @@ function buildRefactorPrompt(repoRoot2, promptTemplate, latestReview, artifactPa
|
|
|
2872
3438
|
repoRoot2,
|
|
2873
3439
|
promptTemplate
|
|
2874
3440
|
);
|
|
2875
|
-
const promptBody =
|
|
3441
|
+
const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
|
|
2876
3442
|
return appendProtectedWorkGuidance(`${promptBody}
|
|
2877
3443
|
|
|
2878
3444
|
## Shared Run Context
|
|
@@ -3050,6 +3616,16 @@ function checkIssueGates(issue, config, force) {
|
|
|
3050
3616
|
}
|
|
3051
3617
|
return { allowed: true, gates };
|
|
3052
3618
|
}
|
|
3619
|
+
function resolveSerenaRuntimeConfig(config, target) {
|
|
3620
|
+
return {
|
|
3621
|
+
enabled: target.serena?.enabled ?? config.serena.enabled,
|
|
3622
|
+
required: target.serena?.required ?? config.serena.required,
|
|
3623
|
+
autoStart: config.serena.autoStart,
|
|
3624
|
+
dataDir: config.serena.dataDir,
|
|
3625
|
+
mcpUrl: config.serena.mcpUrl,
|
|
3626
|
+
sandboxMcpUrl: config.serena.sandboxMcpUrl
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3053
3629
|
async function startIssueRun(options) {
|
|
3054
3630
|
const {
|
|
3055
3631
|
issueNumber,
|
|
@@ -3070,6 +3646,35 @@ async function startIssueRun(options) {
|
|
|
3070
3646
|
const target = resolveTarget(config, targetName);
|
|
3071
3647
|
const branchName = renderBranchName(target.branchTemplate, issue);
|
|
3072
3648
|
const strategy = target.strategy;
|
|
3649
|
+
const serenaRuntimeConfig = resolveSerenaRuntimeConfig(config, target);
|
|
3650
|
+
const shouldPrepareSerena = serenaRuntimeConfig.enabled || serenaRuntimeConfig.required;
|
|
3651
|
+
let serenaExecutionContext;
|
|
3652
|
+
if (shouldPrepareSerena) {
|
|
3653
|
+
const serenaPreflight = await prepareSerenaForTarget({
|
|
3654
|
+
repoRoot: ROOT,
|
|
3655
|
+
targetName: target.name,
|
|
3656
|
+
baseBranch: target.baseBranch,
|
|
3657
|
+
dataDir: serenaRuntimeConfig.dataDir,
|
|
3658
|
+
mcpUrl: serenaRuntimeConfig.mcpUrl,
|
|
3659
|
+
enabled: shouldPrepareSerena,
|
|
3660
|
+
required: serenaRuntimeConfig.required,
|
|
3661
|
+
autoStart: serenaRuntimeConfig.autoStart,
|
|
3662
|
+
logger
|
|
3663
|
+
});
|
|
3664
|
+
if (serenaPreflight.enabled && serenaPreflight.available) {
|
|
3665
|
+
serenaExecutionContext = {
|
|
3666
|
+
available: true,
|
|
3667
|
+
sandboxMcpUrl: serenaRuntimeConfig.sandboxMcpUrl
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
if (serenaPreflight.enabled && !serenaPreflight.available) {
|
|
3671
|
+
const message = `Serena preflight unavailable for target ${target.name}: ${serenaPreflight.error}`;
|
|
3672
|
+
if (serenaRuntimeConfig.required) {
|
|
3673
|
+
throw new Error(message);
|
|
3674
|
+
}
|
|
3675
|
+
logger.step("warn", message);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3073
3678
|
if (options.resetWorktree) {
|
|
3074
3679
|
const existingPr = await prProvider.getPr(branchName);
|
|
3075
3680
|
if (existingPr && existingPr.state === "OPEN") {
|
|
@@ -3093,7 +3698,7 @@ async function startIssueRun(options) {
|
|
|
3093
3698
|
const worktreeState = resolution.worktreePath ? readWorktreeRunState(resolution.worktreePath) : null;
|
|
3094
3699
|
if (resolution.mode !== "new") {
|
|
3095
3700
|
const existingPr = await prProvider.getPr(branchName);
|
|
3096
|
-
const
|
|
3701
|
+
const exit = await runBaseRefreshAttempt({
|
|
3097
3702
|
worktreePath: resolution.worktreePath,
|
|
3098
3703
|
baseBranch: target.baseBranch,
|
|
3099
3704
|
localGitBaseRef: resolution.baseRef,
|
|
@@ -3101,113 +3706,85 @@ async function startIssueRun(options) {
|
|
|
3101
3706
|
prNumber: existingPr?.number,
|
|
3102
3707
|
prState: existingPr?.state
|
|
3103
3708
|
});
|
|
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
|
-
}
|
|
3709
|
+
if (Exit2.isSuccess(exit)) {
|
|
3710
|
+
const refreshResult = exit.value;
|
|
3711
|
+
if (refreshResult.status === "refreshed") {
|
|
3712
|
+
if (worktreeState?.completedStages.builder) {
|
|
3713
|
+
const invalidatedState = invalidateAfterBaseRefresh(worktreeState);
|
|
3714
|
+
writeWorktreeRunState(resolution.worktreePath, invalidatedState);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
} else {
|
|
3718
|
+
const cause = exit.cause;
|
|
3719
|
+
if (cause._tag === "Fail") {
|
|
3720
|
+
const failure = cause.error;
|
|
3721
|
+
if (failure instanceof RebaseConflict) {
|
|
3722
|
+
if (strategy.failureResolution && resolution.worktreePath) {
|
|
3723
|
+
await handleRebaseConflict(failure, {
|
|
3724
|
+
worktreePath: resolution.worktreePath,
|
|
3725
|
+
branchName,
|
|
3726
|
+
target,
|
|
3727
|
+
config,
|
|
3728
|
+
issueNumber,
|
|
3729
|
+
issueProvider,
|
|
3730
|
+
executionProvider,
|
|
3731
|
+
repoRoot: ROOT,
|
|
3732
|
+
worktreeState,
|
|
3733
|
+
logger
|
|
3146
3734
|
});
|
|
3147
3735
|
} else {
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3736
|
+
if (resolution.worktreePath) {
|
|
3737
|
+
if (worktreeState) {
|
|
3738
|
+
updateWorktreeRunState(resolution.worktreePath, {
|
|
3739
|
+
lastFailure: {
|
|
3740
|
+
stage: "baseRefresh",
|
|
3741
|
+
message: `Base refresh conflict detected. Handing off to human: ${failure.message}`
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
} else {
|
|
3745
|
+
writeWorktreeRunState(resolution.worktreePath, {
|
|
3746
|
+
issueNumber,
|
|
3747
|
+
targetName: target.name,
|
|
3748
|
+
branchName,
|
|
3749
|
+
baseBranch: target.baseBranch,
|
|
3750
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3751
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3752
|
+
completedStages: {},
|
|
3753
|
+
review: { lifetimeIterations: 0 },
|
|
3754
|
+
lastFailure: {
|
|
3755
|
+
stage: "baseRefresh",
|
|
3756
|
+
message: `Base refresh conflict detected. Handing off to human: ${failure.message}`
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3160
3759
|
}
|
|
3161
|
-
}
|
|
3760
|
+
}
|
|
3761
|
+
await transitionIssueToFailureState(
|
|
3762
|
+
issueProvider,
|
|
3763
|
+
issueNumber,
|
|
3764
|
+
config,
|
|
3765
|
+
`Base refresh conflicted: ${failure.message}. Worktree preserved at ${resolution.worktreePath}.`,
|
|
3766
|
+
logger
|
|
3767
|
+
);
|
|
3768
|
+
throw new Error(`Base refresh conflicted: ${failure.message}`);
|
|
3162
3769
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3770
|
+
} else if (failure instanceof PublishedHistoryRisk) {
|
|
3771
|
+
await handlePublishedHistoryRisk(failure, {
|
|
3772
|
+
worktreePath: resolution.worktreePath,
|
|
3773
|
+
branchName,
|
|
3774
|
+
target,
|
|
3166
3775
|
config,
|
|
3167
|
-
|
|
3776
|
+
issueNumber,
|
|
3777
|
+
issueProvider,
|
|
3778
|
+
worktreeState,
|
|
3168
3779
|
logger
|
|
3169
|
-
);
|
|
3170
|
-
|
|
3780
|
+
});
|
|
3781
|
+
} else {
|
|
3782
|
+
throw new Error(`Base refresh failed: ${failure.message}`);
|
|
3171
3783
|
}
|
|
3172
3784
|
} 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}`);
|
|
3785
|
+
const defectMessage = cause._tag === "Die" ? `Base refresh failed with unexpected error: ${cause.defect}` : `Base refresh failed with unhandled cause: ${cause._tag}`;
|
|
3786
|
+
throw new Error(defectMessage);
|
|
3206
3787
|
}
|
|
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
3788
|
}
|
|
3212
3789
|
}
|
|
3213
3790
|
const runContextArtifact = buildRunContextArtifact({
|
|
@@ -3232,6 +3809,7 @@ async function startIssueRun(options) {
|
|
|
3232
3809
|
branchName,
|
|
3233
3810
|
...resolution.mode === "new" ? { baseRef: resolution.baseRef } : {},
|
|
3234
3811
|
sandbox: config.sandbox,
|
|
3812
|
+
...serenaExecutionContext ? { serena: serenaExecutionContext } : {},
|
|
3235
3813
|
autoApprove: true,
|
|
3236
3814
|
timeoutMs: EXECUTION_TIMEOUT_MS,
|
|
3237
3815
|
...resolution.worktreePath ? { worktreePath: resolution.worktreePath } : {},
|
|
@@ -3274,7 +3852,8 @@ async function startIssueRun(options) {
|
|
|
3274
3852
|
target,
|
|
3275
3853
|
branchName,
|
|
3276
3854
|
worktreeState: finalWorktreeState,
|
|
3277
|
-
executionResult
|
|
3855
|
+
executionResult,
|
|
3856
|
+
...serenaExecutionContext ? { serena: serenaExecutionContext } : {}
|
|
3278
3857
|
};
|
|
3279
3858
|
}
|
|
3280
3859
|
async function advanceIssueRunReview(options) {
|
|
@@ -3353,12 +3932,12 @@ async function completeIssueRun(options) {
|
|
|
3353
3932
|
prTitle = finalizerFromState.title;
|
|
3354
3933
|
prBody = finalizerFromState.body;
|
|
3355
3934
|
} else if (finalizerFromState.artifactPath) {
|
|
3356
|
-
if (!
|
|
3935
|
+
if (!existsSync7(finalizerFromState.artifactPath)) {
|
|
3357
3936
|
throw new Error(
|
|
3358
3937
|
`Finalizer artifact missing at ${finalizerFromState.artifactPath}`
|
|
3359
3938
|
);
|
|
3360
3939
|
}
|
|
3361
|
-
const artifactContent =
|
|
3940
|
+
const artifactContent = readFileSync7(
|
|
3362
3941
|
finalizerFromState.artifactPath,
|
|
3363
3942
|
"utf-8"
|
|
3364
3943
|
);
|
|
@@ -3445,6 +4024,13 @@ async function completeIssueRun(options) {
|
|
|
3445
4024
|
if (prFromState && prFromState.state === "OPEN") {
|
|
3446
4025
|
pr = prFromState;
|
|
3447
4026
|
} else {
|
|
4027
|
+
await guardFinalPublishContent({
|
|
4028
|
+
worktreePath: executionResult.worktreePath,
|
|
4029
|
+
baseRef: `origin/${target.baseBranch}`,
|
|
4030
|
+
title: prTitle,
|
|
4031
|
+
body: finalBody,
|
|
4032
|
+
logger
|
|
4033
|
+
});
|
|
3448
4034
|
await execCapture("git", ["push", "-u", "origin", branchName], {
|
|
3449
4035
|
cwd: executionResult.worktreePath,
|
|
3450
4036
|
logger,
|
|
@@ -3644,12 +4230,106 @@ async function finalizeWorktreeCommit(options) {
|
|
|
3644
4230
|
logger,
|
|
3645
4231
|
label: "git add"
|
|
3646
4232
|
});
|
|
4233
|
+
await guardFinalCommitContent({
|
|
4234
|
+
worktreePath,
|
|
4235
|
+
title,
|
|
4236
|
+
body,
|
|
4237
|
+
logger
|
|
4238
|
+
});
|
|
3647
4239
|
await execCapture("git", ["commit", "--no-verify", "-m", title, "-m", body], {
|
|
3648
4240
|
cwd: worktreePath,
|
|
3649
4241
|
logger,
|
|
3650
4242
|
label: "git commit"
|
|
3651
4243
|
});
|
|
3652
4244
|
}
|
|
4245
|
+
async function guardFinalCommitContent(options) {
|
|
4246
|
+
const { worktreePath, title, body, logger } = options;
|
|
4247
|
+
const stagedDiff = await execCapture("git", ["diff", "--cached"], {
|
|
4248
|
+
cwd: worktreePath,
|
|
4249
|
+
logger,
|
|
4250
|
+
label: "git diff --cached secret guard"
|
|
4251
|
+
});
|
|
4252
|
+
assertNoSecretLikeContent([
|
|
4253
|
+
{ source: "staged diff", content: stagedDiff.stdout },
|
|
4254
|
+
{ source: "commit title", content: title },
|
|
4255
|
+
{ source: "commit body", content: body }
|
|
4256
|
+
]);
|
|
4257
|
+
}
|
|
4258
|
+
async function guardFinalPublishContent(options) {
|
|
4259
|
+
const { worktreePath, baseRef, title, body, logger } = options;
|
|
4260
|
+
const finalDiff = await execCapture("git", ["diff", `${baseRef}...HEAD`], {
|
|
4261
|
+
cwd: worktreePath,
|
|
4262
|
+
logger,
|
|
4263
|
+
label: "git diff final secret guard"
|
|
4264
|
+
});
|
|
4265
|
+
assertNoSecretLikeContent([
|
|
4266
|
+
{ source: "final diff", content: finalDiff.stdout },
|
|
4267
|
+
{ source: "PR title", content: title },
|
|
4268
|
+
{ source: "PR body", content: body }
|
|
4269
|
+
]);
|
|
4270
|
+
}
|
|
4271
|
+
function assertNoSecretLikeContent(inputs) {
|
|
4272
|
+
const findings = inputs.flatMap(
|
|
4273
|
+
({ source, content }) => findSecretLikeContent(source, content ?? "")
|
|
4274
|
+
);
|
|
4275
|
+
if (findings.length === 0) {
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
const summary = findings.slice(0, 5).map((finding) => `${finding.source}: ${finding.kind}`).join("; ");
|
|
4279
|
+
throw new Error(
|
|
4280
|
+
`Secret-like content detected before finalization (${summary}). Redact it before committing, pushing, or creating a PR.`
|
|
4281
|
+
);
|
|
4282
|
+
}
|
|
4283
|
+
function findSecretLikeContent(source, content) {
|
|
4284
|
+
const findings = [];
|
|
4285
|
+
const patterns = [
|
|
4286
|
+
{
|
|
4287
|
+
kind: "private key block",
|
|
4288
|
+
regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/gi
|
|
4289
|
+
},
|
|
4290
|
+
{
|
|
4291
|
+
kind: "GitHub token",
|
|
4292
|
+
regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g
|
|
4293
|
+
},
|
|
4294
|
+
{
|
|
4295
|
+
kind: "GitHub fine-grained token",
|
|
4296
|
+
regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g
|
|
4297
|
+
},
|
|
4298
|
+
{ kind: "npm token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g },
|
|
4299
|
+
{ kind: "OpenAI key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
4300
|
+
{ kind: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
4301
|
+
{
|
|
4302
|
+
kind: "authorization bearer token",
|
|
4303
|
+
regex: /\bAuthorization\s*:\s*Bearer\s+[^\s`'\"]{12,}/gi
|
|
4304
|
+
},
|
|
4305
|
+
{
|
|
4306
|
+
kind: "secret assignment",
|
|
4307
|
+
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
|
|
4308
|
+
}
|
|
4309
|
+
];
|
|
4310
|
+
for (const { kind, regex } of patterns) {
|
|
4311
|
+
for (const match of content.matchAll(regex)) {
|
|
4312
|
+
if (isAllowedSecretPlaceholder(match[0])) {
|
|
4313
|
+
continue;
|
|
4314
|
+
}
|
|
4315
|
+
findings.push({ source, kind });
|
|
4316
|
+
break;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
return findings;
|
|
4320
|
+
}
|
|
4321
|
+
function isAllowedSecretPlaceholder(value) {
|
|
4322
|
+
const normalized = value.toLowerCase();
|
|
4323
|
+
return [
|
|
4324
|
+
"<redacted>",
|
|
4325
|
+
"<example",
|
|
4326
|
+
"example-",
|
|
4327
|
+
"dummy-",
|
|
4328
|
+
"test-",
|
|
4329
|
+
"placeholder",
|
|
4330
|
+
"xxxxx"
|
|
4331
|
+
].some((allowed) => normalized.includes(allowed));
|
|
4332
|
+
}
|
|
3653
4333
|
async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
|
|
3654
4334
|
const remoteBase = parseRemoteBaseRef(baseRef);
|
|
3655
4335
|
if (!remoteBase) {
|
|
@@ -3865,7 +4545,7 @@ async function resolveIssueWorktree(root, branchName, baseBranch, logger) {
|
|
|
3865
4545
|
return { mode: "new", branchName, baseRef };
|
|
3866
4546
|
}
|
|
3867
4547
|
function issueWorktreePath(root, branchName) {
|
|
3868
|
-
return
|
|
4548
|
+
return join10(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
|
|
3869
4549
|
}
|
|
3870
4550
|
function resolveRegisteredIssueWorktreePath(worktreeListPorcelain, root, branchName) {
|
|
3871
4551
|
const branchWorktreePath = parseWorktreeListPorcelain(
|
|
@@ -3893,12 +4573,251 @@ async function syncTargetBranch(root, baseBranch, logger) {
|
|
|
3893
4573
|
}
|
|
3894
4574
|
function loadBuilderPrompt(repoRoot2, promptTemplate) {
|
|
3895
4575
|
const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
|
|
3896
|
-
const promptBody =
|
|
4576
|
+
const promptBody = existsSync7(promptPath) ? readFileSync7(promptPath, "utf-8") : promptTemplate;
|
|
3897
4577
|
return appendProtectedWorkGuidance(`${promptBody}
|
|
3898
4578
|
|
|
3899
4579
|
## Shared Run Context
|
|
3900
4580
|
|
|
3901
|
-
Read the selected issue requirements, comments, branch context,
|
|
4581
|
+
Read the selected issue requirements, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
|
|
4582
|
+
}
|
|
4583
|
+
async function handleRebaseConflict(failure, context) {
|
|
4584
|
+
const frConfig = context.target.strategy.failureResolution;
|
|
4585
|
+
if (!frConfig) {
|
|
4586
|
+
await transitionIssueToFailureState(
|
|
4587
|
+
context.issueProvider,
|
|
4588
|
+
context.issueNumber,
|
|
4589
|
+
context.config,
|
|
4590
|
+
"No failureResolution configured",
|
|
4591
|
+
context.logger
|
|
4592
|
+
);
|
|
4593
|
+
throw new Error("Base refresh conflicted: no failureResolution configured");
|
|
4594
|
+
}
|
|
4595
|
+
const maxAttempts = frConfig.failureLimits?.RebaseConflict ?? frConfig.maxAttemptsPerFailure;
|
|
4596
|
+
let attemptNumber = 0;
|
|
4597
|
+
let currentFailure = failure;
|
|
4598
|
+
while (attemptNumber < maxAttempts) {
|
|
4599
|
+
attemptNumber++;
|
|
4600
|
+
const packet = constructFailureResolutionPacket(currentFailure, {
|
|
4601
|
+
stageName: "baseRefresh",
|
|
4602
|
+
attemptNumber,
|
|
4603
|
+
worktreePath: context.worktreePath,
|
|
4604
|
+
branchName: context.branchName,
|
|
4605
|
+
baseBranch: context.target.baseBranch,
|
|
4606
|
+
maxAttempts,
|
|
4607
|
+
allowedDecisions: ["RETRY_STAGE", "HANDOFF_TO_HUMAN", "FAIL_RUN"],
|
|
4608
|
+
artifactTarget: `.pourkit/.tmp/failure-resolution/attempt-${attemptNumber}.md`
|
|
4609
|
+
});
|
|
4610
|
+
const result = await runFailureResolutionAgent({
|
|
4611
|
+
executionProvider: context.executionProvider,
|
|
4612
|
+
config: context.config,
|
|
4613
|
+
target: context.target,
|
|
4614
|
+
failure: currentFailure,
|
|
4615
|
+
packet,
|
|
4616
|
+
worktreePath: context.worktreePath,
|
|
4617
|
+
repoRoot: context.repoRoot,
|
|
4618
|
+
logger: context.logger
|
|
4619
|
+
});
|
|
4620
|
+
if (result.status === "handoff") {
|
|
4621
|
+
if (context.worktreeState) {
|
|
4622
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4623
|
+
lastFailure: { stage: "baseRefresh", message: result.reason }
|
|
4624
|
+
});
|
|
4625
|
+
} else {
|
|
4626
|
+
writeWorktreeRunState(context.worktreePath, {
|
|
4627
|
+
issueNumber: context.issueNumber,
|
|
4628
|
+
targetName: context.target.name,
|
|
4629
|
+
branchName: context.branchName,
|
|
4630
|
+
baseBranch: context.target.baseBranch,
|
|
4631
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4632
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4633
|
+
completedStages: {},
|
|
4634
|
+
review: { lifetimeIterations: 0 },
|
|
4635
|
+
lastFailure: { stage: "baseRefresh", message: result.reason }
|
|
4636
|
+
});
|
|
4637
|
+
}
|
|
4638
|
+
await transitionIssueToFailureState(
|
|
4639
|
+
context.issueProvider,
|
|
4640
|
+
context.issueNumber,
|
|
4641
|
+
context.config,
|
|
4642
|
+
result.reason,
|
|
4643
|
+
context.logger
|
|
4644
|
+
);
|
|
4645
|
+
throw new Error(result.reason);
|
|
4646
|
+
}
|
|
4647
|
+
if (result.status === "fail-run") {
|
|
4648
|
+
if (context.worktreeState) {
|
|
4649
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4650
|
+
lastFailure: { stage: "baseRefresh", message: result.reason }
|
|
4651
|
+
});
|
|
4652
|
+
} else {
|
|
4653
|
+
writeWorktreeRunState(context.worktreePath, {
|
|
4654
|
+
issueNumber: context.issueNumber,
|
|
4655
|
+
targetName: context.target.name,
|
|
4656
|
+
branchName: context.branchName,
|
|
4657
|
+
baseBranch: context.target.baseBranch,
|
|
4658
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4659
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4660
|
+
completedStages: {},
|
|
4661
|
+
review: { lifetimeIterations: 0 },
|
|
4662
|
+
lastFailure: { stage: "baseRefresh", message: result.reason }
|
|
4663
|
+
});
|
|
4664
|
+
}
|
|
4665
|
+
throw new Error(result.reason);
|
|
4666
|
+
}
|
|
4667
|
+
const changedPaths = packet.conflictedPaths ?? currentFailure.conflictedPaths;
|
|
4668
|
+
const markersRemain = await hasUnresolvedConflictMarkers(
|
|
4669
|
+
context.worktreePath,
|
|
4670
|
+
changedPaths
|
|
4671
|
+
);
|
|
4672
|
+
if (markersRemain) {
|
|
4673
|
+
const message = "Conflict resolution agent resolved artifact but conflict markers remain in files";
|
|
4674
|
+
if (context.worktreeState) {
|
|
4675
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4676
|
+
lastFailure: { stage: "baseRefresh", message }
|
|
4677
|
+
});
|
|
4678
|
+
} else {
|
|
4679
|
+
writeWorktreeRunState(context.worktreePath, {
|
|
4680
|
+
issueNumber: context.issueNumber,
|
|
4681
|
+
targetName: context.target.name,
|
|
4682
|
+
branchName: context.branchName,
|
|
4683
|
+
baseBranch: context.target.baseBranch,
|
|
4684
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4685
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4686
|
+
completedStages: {},
|
|
4687
|
+
review: { lifetimeIterations: 0 },
|
|
4688
|
+
lastFailure: { stage: "baseRefresh", message }
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
await transitionIssueToFailureState(
|
|
4692
|
+
context.issueProvider,
|
|
4693
|
+
context.issueNumber,
|
|
4694
|
+
context.config,
|
|
4695
|
+
message,
|
|
4696
|
+
context.logger
|
|
4697
|
+
);
|
|
4698
|
+
throw new Error(message);
|
|
4699
|
+
}
|
|
4700
|
+
await execCapture("git", ["add", ...changedPaths], {
|
|
4701
|
+
cwd: context.worktreePath,
|
|
4702
|
+
logger: context.logger,
|
|
4703
|
+
label: "git add conflicted paths"
|
|
4704
|
+
});
|
|
4705
|
+
try {
|
|
4706
|
+
await execCapture("git", ["rebase", "--continue"], {
|
|
4707
|
+
cwd: context.worktreePath,
|
|
4708
|
+
env: { ...process.env, GIT_EDITOR: "true" },
|
|
4709
|
+
logger: context.logger,
|
|
4710
|
+
label: "git rebase --continue"
|
|
4711
|
+
});
|
|
4712
|
+
if (context.worktreeState?.completedStages.builder) {
|
|
4713
|
+
const invalidatedState = invalidateAfterBaseRefresh(
|
|
4714
|
+
context.worktreeState
|
|
4715
|
+
);
|
|
4716
|
+
writeWorktreeRunState(context.worktreePath, invalidatedState);
|
|
4717
|
+
}
|
|
4718
|
+
return;
|
|
4719
|
+
} catch {
|
|
4720
|
+
const statusResult = await execCapture("git", ["status", "--porcelain"], {
|
|
4721
|
+
cwd: context.worktreePath,
|
|
4722
|
+
logger: context.logger,
|
|
4723
|
+
label: "git status"
|
|
4724
|
+
});
|
|
4725
|
+
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);
|
|
4726
|
+
if (newConflictedPaths.length === 0) {
|
|
4727
|
+
const message = "git rebase --continue failed with no remaining conflicts";
|
|
4728
|
+
if (context.worktreeState) {
|
|
4729
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4730
|
+
lastFailure: { stage: "baseRefresh", message }
|
|
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: { stage: "baseRefresh", message }
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
await transitionIssueToFailureState(
|
|
4746
|
+
context.issueProvider,
|
|
4747
|
+
context.issueNumber,
|
|
4748
|
+
context.config,
|
|
4749
|
+
message,
|
|
4750
|
+
context.logger
|
|
4751
|
+
);
|
|
4752
|
+
throw new Error(message);
|
|
4753
|
+
}
|
|
4754
|
+
currentFailure = new RebaseConflict({
|
|
4755
|
+
conflictedPaths: newConflictedPaths,
|
|
4756
|
+
message: `Rebase conflict in: ${newConflictedPaths.join(", ")}`
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
if (context.worktreeState) {
|
|
4761
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4762
|
+
lastFailure: {
|
|
4763
|
+
stage: "baseRefresh",
|
|
4764
|
+
message: `Base refresh recovery exhausted after ${maxAttempts} attempts`
|
|
4765
|
+
}
|
|
4766
|
+
});
|
|
4767
|
+
} else {
|
|
4768
|
+
writeWorktreeRunState(context.worktreePath, {
|
|
4769
|
+
issueNumber: context.issueNumber,
|
|
4770
|
+
targetName: context.target.name,
|
|
4771
|
+
branchName: context.branchName,
|
|
4772
|
+
baseBranch: context.target.baseBranch,
|
|
4773
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4774
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4775
|
+
completedStages: {},
|
|
4776
|
+
review: { lifetimeIterations: 0 },
|
|
4777
|
+
lastFailure: {
|
|
4778
|
+
stage: "baseRefresh",
|
|
4779
|
+
message: `Base refresh recovery exhausted after ${maxAttempts} attempts`
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
}
|
|
4783
|
+
await transitionIssueToFailureState(
|
|
4784
|
+
context.issueProvider,
|
|
4785
|
+
context.issueNumber,
|
|
4786
|
+
context.config,
|
|
4787
|
+
`Base refresh recovery exhausted after ${maxAttempts} attempts`,
|
|
4788
|
+
context.logger
|
|
4789
|
+
);
|
|
4790
|
+
throw new Error(
|
|
4791
|
+
`Base refresh recovery exhausted after ${maxAttempts} attempts`
|
|
4792
|
+
);
|
|
4793
|
+
}
|
|
4794
|
+
async function handlePublishedHistoryRisk(failure, context) {
|
|
4795
|
+
const message = `Cannot auto-refresh published history: PR #${failure.prNumber} (${failure.prState}) exists for branch ${context.branchName}`;
|
|
4796
|
+
if (context.worktreeState) {
|
|
4797
|
+
updateWorktreeRunState(context.worktreePath, {
|
|
4798
|
+
lastFailure: { stage: "baseRefresh", message }
|
|
4799
|
+
});
|
|
4800
|
+
} else {
|
|
4801
|
+
writeWorktreeRunState(context.worktreePath, {
|
|
4802
|
+
issueNumber: context.issueNumber,
|
|
4803
|
+
targetName: context.target.name,
|
|
4804
|
+
branchName: context.branchName,
|
|
4805
|
+
baseBranch: context.target.baseBranch,
|
|
4806
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4807
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4808
|
+
completedStages: {},
|
|
4809
|
+
review: { lifetimeIterations: 0 },
|
|
4810
|
+
lastFailure: { stage: "baseRefresh", message }
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
await transitionIssueToFailureState(
|
|
4814
|
+
context.issueProvider,
|
|
4815
|
+
context.issueNumber,
|
|
4816
|
+
context.config,
|
|
4817
|
+
message,
|
|
4818
|
+
context.logger
|
|
4819
|
+
);
|
|
4820
|
+
throw new Error(message);
|
|
3902
4821
|
}
|
|
3903
4822
|
|
|
3904
4823
|
// commands/issue.ts
|
|
@@ -3941,7 +4860,8 @@ async function runIssueCommand(options) {
|
|
|
3941
4860
|
repoRoot: ROOT,
|
|
3942
4861
|
logger,
|
|
3943
4862
|
startingLifetimeIteration: lifetimeIterationsFromState,
|
|
3944
|
-
humanHandoffResolved
|
|
4863
|
+
humanHandoffResolved,
|
|
4864
|
+
serena: startResult.serena
|
|
3945
4865
|
});
|
|
3946
4866
|
if (reviewResult.exhaustedMaxIterations) {
|
|
3947
4867
|
throw new Error(
|
|
@@ -3986,6 +4906,95 @@ async function runIssueCommand(options) {
|
|
|
3986
4906
|
}
|
|
3987
4907
|
}
|
|
3988
4908
|
|
|
4909
|
+
// commands/issue-create.ts
|
|
4910
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
4911
|
+
function isFlag(value) {
|
|
4912
|
+
return value.startsWith("--");
|
|
4913
|
+
}
|
|
4914
|
+
function requireFlagValue(flag, value) {
|
|
4915
|
+
if (!value || isFlag(value)) {
|
|
4916
|
+
throw new Error(`${flag} requires a value`);
|
|
4917
|
+
}
|
|
4918
|
+
return value;
|
|
4919
|
+
}
|
|
4920
|
+
function parseIssueCreateArgs(args) {
|
|
4921
|
+
let title;
|
|
4922
|
+
let body;
|
|
4923
|
+
let bodyFile;
|
|
4924
|
+
const labels = [];
|
|
4925
|
+
const assignees = [];
|
|
4926
|
+
const remaining = [];
|
|
4927
|
+
let i = 0;
|
|
4928
|
+
while (i < args.length) {
|
|
4929
|
+
const arg = args[i];
|
|
4930
|
+
if (arg === "--title") {
|
|
4931
|
+
title = requireFlagValue("--title", args[i + 1]);
|
|
4932
|
+
i += 2;
|
|
4933
|
+
} else if (arg === "--body") {
|
|
4934
|
+
body = requireFlagValue("--body", args[i + 1]);
|
|
4935
|
+
i += 2;
|
|
4936
|
+
} else if (arg === "--body-file") {
|
|
4937
|
+
bodyFile = requireFlagValue("--body-file", args[i + 1]);
|
|
4938
|
+
i += 2;
|
|
4939
|
+
} else if (arg === "--label") {
|
|
4940
|
+
const label = requireFlagValue("--label", args[i + 1]);
|
|
4941
|
+
labels.push(label);
|
|
4942
|
+
i += 2;
|
|
4943
|
+
} else if (arg === "--assignee") {
|
|
4944
|
+
const assignee = requireFlagValue("--assignee", args[i + 1]);
|
|
4945
|
+
assignees.push(assignee);
|
|
4946
|
+
i += 2;
|
|
4947
|
+
} else {
|
|
4948
|
+
remaining.push(arg);
|
|
4949
|
+
i++;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
return {
|
|
4953
|
+
options: {
|
|
4954
|
+
title,
|
|
4955
|
+
body,
|
|
4956
|
+
bodyFile,
|
|
4957
|
+
labels,
|
|
4958
|
+
assignees
|
|
4959
|
+
},
|
|
4960
|
+
remaining
|
|
4961
|
+
};
|
|
4962
|
+
}
|
|
4963
|
+
function validateIssueCreateOptions(options) {
|
|
4964
|
+
const errors = [];
|
|
4965
|
+
if (!options.title) {
|
|
4966
|
+
errors.push("--title is required");
|
|
4967
|
+
}
|
|
4968
|
+
if (options.body && options.bodyFile) {
|
|
4969
|
+
errors.push("--body and --body-file cannot be used together");
|
|
4970
|
+
}
|
|
4971
|
+
if (errors.length > 0) {
|
|
4972
|
+
throw new Error(errors.join("; "));
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
async function runIssueCreateCommand(args, issueProvider, logger) {
|
|
4976
|
+
const { options, remaining } = parseIssueCreateArgs(args);
|
|
4977
|
+
if (remaining.length > 0) {
|
|
4978
|
+
throw new Error(`Unsupported arguments: ${remaining.join(" ")}`);
|
|
4979
|
+
}
|
|
4980
|
+
validateIssueCreateOptions(options);
|
|
4981
|
+
const renderedBody = options.bodyFile ? await readFile3(options.bodyFile, "utf-8") : options.body ?? "";
|
|
4982
|
+
const createOptions = {
|
|
4983
|
+
title: options.title,
|
|
4984
|
+
body: renderedBody,
|
|
4985
|
+
labels: options.labels.length > 0 ? options.labels : void 0,
|
|
4986
|
+
assignees: options.assignees.length > 0 ? options.assignees : void 0
|
|
4987
|
+
};
|
|
4988
|
+
const created = await issueProvider.createIssue(createOptions);
|
|
4989
|
+
return {
|
|
4990
|
+
options,
|
|
4991
|
+
renderedBody,
|
|
4992
|
+
issueNumber: created.number,
|
|
4993
|
+
issueUrl: created.url,
|
|
4994
|
+
issueTitle: created.title
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
|
|
3989
4998
|
// commands/queue.ts
|
|
3990
4999
|
init_common();
|
|
3991
5000
|
|
|
@@ -4423,11 +5432,11 @@ async function runPrWorkflow(options) {
|
|
|
4423
5432
|
}
|
|
4424
5433
|
|
|
4425
5434
|
// commands/pr-create.ts
|
|
4426
|
-
function
|
|
5435
|
+
function isFlag2(value) {
|
|
4427
5436
|
return value.startsWith("--");
|
|
4428
5437
|
}
|
|
4429
|
-
function
|
|
4430
|
-
if (!value ||
|
|
5438
|
+
function requireFlagValue2(flag, value) {
|
|
5439
|
+
if (!value || isFlag2(value)) {
|
|
4431
5440
|
throw new Error(`${flag} requires a value`);
|
|
4432
5441
|
}
|
|
4433
5442
|
return value;
|
|
@@ -4445,25 +5454,25 @@ function parsePrCreateArgs(args) {
|
|
|
4445
5454
|
while (i < args.length) {
|
|
4446
5455
|
const arg = args[i];
|
|
4447
5456
|
if (arg === "--target") {
|
|
4448
|
-
target =
|
|
5457
|
+
target = requireFlagValue2("--target", args[i + 1]);
|
|
4449
5458
|
i += 2;
|
|
4450
5459
|
} else if (arg === "--title") {
|
|
4451
|
-
title =
|
|
5460
|
+
title = requireFlagValue2("--title", args[i + 1]);
|
|
4452
5461
|
i += 2;
|
|
4453
5462
|
} else if (arg === "--base") {
|
|
4454
|
-
base =
|
|
5463
|
+
base = requireFlagValue2("--base", args[i + 1]);
|
|
4455
5464
|
i += 2;
|
|
4456
5465
|
} else if (arg === "--head") {
|
|
4457
|
-
head =
|
|
5466
|
+
head = requireFlagValue2("--head", args[i + 1]);
|
|
4458
5467
|
i += 2;
|
|
4459
5468
|
} else if (arg === "--body") {
|
|
4460
|
-
body =
|
|
5469
|
+
body = requireFlagValue2("--body", args[i + 1]);
|
|
4461
5470
|
i += 2;
|
|
4462
5471
|
} else if (arg === "--body-file") {
|
|
4463
|
-
bodyFile =
|
|
5472
|
+
bodyFile = requireFlagValue2("--body-file", args[i + 1]);
|
|
4464
5473
|
i += 2;
|
|
4465
5474
|
} else if (arg === "--issue") {
|
|
4466
|
-
const raw =
|
|
5475
|
+
const raw = requireFlagValue2("--issue", args[i + 1]);
|
|
4467
5476
|
if (!/^\d+$/.test(raw)) {
|
|
4468
5477
|
throw new Error(`Invalid issue number: ${raw}`);
|
|
4469
5478
|
}
|
|
@@ -4561,17 +5570,17 @@ async function runPrCreateCommand(args, logger, prProvider, config, repoRoot2) {
|
|
|
4561
5570
|
|
|
4562
5571
|
// commands/pr-merge.ts
|
|
4563
5572
|
init_common();
|
|
4564
|
-
function
|
|
5573
|
+
function isFlag3(value) {
|
|
4565
5574
|
return value.startsWith("--");
|
|
4566
5575
|
}
|
|
4567
|
-
function
|
|
4568
|
-
if (!value ||
|
|
5576
|
+
function requireFlagValue3(flag, value) {
|
|
5577
|
+
if (!value || isFlag3(value)) {
|
|
4569
5578
|
throw new Error(`${flag} requires a value`);
|
|
4570
5579
|
}
|
|
4571
5580
|
return value;
|
|
4572
5581
|
}
|
|
4573
5582
|
function parsePrNumber(raw) {
|
|
4574
|
-
if (!raw ||
|
|
5583
|
+
if (!raw || isFlag3(raw) || !/^\d+$/.test(raw)) {
|
|
4575
5584
|
throw new Error("PR number is required");
|
|
4576
5585
|
}
|
|
4577
5586
|
const prNumber = parseInt(raw, 10);
|
|
@@ -4591,10 +5600,10 @@ function parsePrMergeArgs(args) {
|
|
|
4591
5600
|
while (i < args.length) {
|
|
4592
5601
|
const arg = args[i];
|
|
4593
5602
|
if (arg === "--target") {
|
|
4594
|
-
target =
|
|
5603
|
+
target = requireFlagValue3("--target", args[i + 1]);
|
|
4595
5604
|
i += 2;
|
|
4596
5605
|
} else if (arg === "--method") {
|
|
4597
|
-
const raw =
|
|
5606
|
+
const raw = requireFlagValue3("--method", args[i + 1]);
|
|
4598
5607
|
if (raw !== "merge" && raw !== "squash" && raw !== "rebase") {
|
|
4599
5608
|
throw new Error(`Invalid merge method: ${raw}`);
|
|
4600
5609
|
}
|
|
@@ -4707,17 +5716,17 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
|
|
|
4707
5716
|
}
|
|
4708
5717
|
|
|
4709
5718
|
// commands/init.ts
|
|
4710
|
-
import { existsSync as
|
|
5719
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
4711
5720
|
import {
|
|
4712
5721
|
copyFile,
|
|
4713
|
-
mkdir as
|
|
4714
|
-
readFile as
|
|
5722
|
+
mkdir as mkdir4,
|
|
5723
|
+
readFile as readFile4,
|
|
4715
5724
|
readdir,
|
|
4716
5725
|
rename,
|
|
4717
5726
|
writeFile
|
|
4718
5727
|
} from "fs/promises";
|
|
4719
5728
|
import { createHash, randomUUID } from "crypto";
|
|
4720
|
-
import
|
|
5729
|
+
import path5 from "path";
|
|
4721
5730
|
import { execFile as execFile2 } from "child_process";
|
|
4722
5731
|
import { promisify as promisify2 } from "util";
|
|
4723
5732
|
import { confirm, isCancel, log, select, text } from "@clack/prompts";
|
|
@@ -4942,7 +5951,7 @@ function generateConfigTemplate(options) {
|
|
|
4942
5951
|
labels: maybeLabels
|
|
4943
5952
|
} = options;
|
|
4944
5953
|
const labels = maybeLabels ?? DEFAULT_RUNNER_LABELS;
|
|
4945
|
-
const relPath =
|
|
5954
|
+
const relPath = path5.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
|
|
4946
5955
|
const importPath = relPath || ".";
|
|
4947
5956
|
const setupCommand = `${packageManager} install`;
|
|
4948
5957
|
let setupSection;
|
|
@@ -5190,9 +6199,9 @@ _Avoid_: Alternative terms
|
|
|
5190
6199
|
`;
|
|
5191
6200
|
}
|
|
5192
6201
|
async function generateManagedAgentInstructions(options) {
|
|
5193
|
-
const sourcePath =
|
|
6202
|
+
const sourcePath = path5.join(options.sourceRoot, "AGENTS.md");
|
|
5194
6203
|
try {
|
|
5195
|
-
return await
|
|
6204
|
+
return await readFile4(sourcePath, "utf-8");
|
|
5196
6205
|
} catch {
|
|
5197
6206
|
return `## Agent Skills
|
|
5198
6207
|
|
|
@@ -5220,7 +6229,7 @@ async function walkDir(dir) {
|
|
|
5220
6229
|
const files = [];
|
|
5221
6230
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
5222
6231
|
for (const entry of entries) {
|
|
5223
|
-
const full =
|
|
6232
|
+
const full = path5.join(dir, entry.name);
|
|
5224
6233
|
if (entry.isDirectory()) {
|
|
5225
6234
|
files.push(...await walkDir(full));
|
|
5226
6235
|
} else {
|
|
@@ -5230,11 +6239,11 @@ async function walkDir(dir) {
|
|
|
5230
6239
|
return files;
|
|
5231
6240
|
}
|
|
5232
6241
|
async function computeFileChecksum(filePath) {
|
|
5233
|
-
const content = await
|
|
6242
|
+
const content = await readFile4(filePath);
|
|
5234
6243
|
return createHash("sha256").update(content).digest("hex");
|
|
5235
6244
|
}
|
|
5236
6245
|
function lockfileExists(root, name) {
|
|
5237
|
-
return
|
|
6246
|
+
return existsSync8(path5.join(root, name));
|
|
5238
6247
|
}
|
|
5239
6248
|
function detectPackageManager(root) {
|
|
5240
6249
|
if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
|
|
@@ -5277,8 +6286,8 @@ async function discoverLocalSource(sourcePath) {
|
|
|
5277
6286
|
}
|
|
5278
6287
|
async function discoverReadme(root) {
|
|
5279
6288
|
for (const name of ["README.md", "readme.md"]) {
|
|
5280
|
-
const p =
|
|
5281
|
-
if (
|
|
6289
|
+
const p = path5.join(root, name);
|
|
6290
|
+
if (existsSync8(p)) {
|
|
5282
6291
|
return p;
|
|
5283
6292
|
}
|
|
5284
6293
|
}
|
|
@@ -5287,25 +6296,25 @@ async function discoverReadme(root) {
|
|
|
5287
6296
|
async function discoverAgentFiles(root) {
|
|
5288
6297
|
const files = [];
|
|
5289
6298
|
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
5290
|
-
const p =
|
|
5291
|
-
if (
|
|
6299
|
+
const p = path5.join(root, name);
|
|
6300
|
+
if (existsSync8(p)) {
|
|
5292
6301
|
files.push(p);
|
|
5293
6302
|
}
|
|
5294
6303
|
}
|
|
5295
6304
|
return files;
|
|
5296
6305
|
}
|
|
5297
6306
|
async function discoverMerlleState(root) {
|
|
5298
|
-
const p =
|
|
5299
|
-
return
|
|
6307
|
+
const p = path5.join(root, ".pourkit", "state.json");
|
|
6308
|
+
return existsSync8(p) ? p : null;
|
|
5300
6309
|
}
|
|
5301
6310
|
async function discoverAgentSkills(root) {
|
|
5302
6311
|
const dirs = [
|
|
5303
|
-
|
|
5304
|
-
|
|
6312
|
+
path5.join(root, ".agents", "skills"),
|
|
6313
|
+
path5.join(root, ".opencode", "skills")
|
|
5305
6314
|
];
|
|
5306
6315
|
const found = [];
|
|
5307
6316
|
for (const d of dirs) {
|
|
5308
|
-
if (
|
|
6317
|
+
if (existsSync8(d)) {
|
|
5309
6318
|
found.push(d);
|
|
5310
6319
|
}
|
|
5311
6320
|
}
|
|
@@ -5314,17 +6323,17 @@ async function discoverAgentSkills(root) {
|
|
|
5314
6323
|
async function discoverRootDomainDocs(root) {
|
|
5315
6324
|
const docs = [];
|
|
5316
6325
|
for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
|
|
5317
|
-
const p =
|
|
5318
|
-
if (
|
|
6326
|
+
const p = path5.join(root, name);
|
|
6327
|
+
if (existsSync8(p)) {
|
|
5319
6328
|
docs.push(p);
|
|
5320
6329
|
}
|
|
5321
6330
|
}
|
|
5322
|
-
const adrDir =
|
|
5323
|
-
if (
|
|
6331
|
+
const adrDir = path5.join(root, "docs", "adr");
|
|
6332
|
+
if (existsSync8(adrDir)) {
|
|
5324
6333
|
const entries = await readdir(adrDir, { withFileTypes: true });
|
|
5325
6334
|
for (const entry of entries) {
|
|
5326
6335
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5327
|
-
docs.push(
|
|
6336
|
+
docs.push(path5.join(adrDir, entry.name));
|
|
5328
6337
|
}
|
|
5329
6338
|
}
|
|
5330
6339
|
}
|
|
@@ -5412,7 +6421,7 @@ async function planInit(options) {
|
|
|
5412
6421
|
for (const f of agentFiles) {
|
|
5413
6422
|
if (sourceRoot) {
|
|
5414
6423
|
const agentFileMode = options.conflictPolicy?.agentFile ?? "both";
|
|
5415
|
-
const basename =
|
|
6424
|
+
const basename = path5.basename(f);
|
|
5416
6425
|
if (agentFileMode === "skip" || agentFileMode === "agents" && basename !== "AGENTS.md" || agentFileMode === "claude" && basename !== "CLAUDE.md") {
|
|
5417
6426
|
operations.push({
|
|
5418
6427
|
kind: "skip",
|
|
@@ -5438,7 +6447,7 @@ async function planInit(options) {
|
|
|
5438
6447
|
kind: "skip",
|
|
5439
6448
|
path: f,
|
|
5440
6449
|
ownership: "project-owned",
|
|
5441
|
-
reason: `Existing agent file: ${
|
|
6450
|
+
reason: `Existing agent file: ${path5.basename(f)}`,
|
|
5442
6451
|
requiresConfirmation: false,
|
|
5443
6452
|
destructive: false
|
|
5444
6453
|
});
|
|
@@ -5461,15 +6470,15 @@ async function planInit(options) {
|
|
|
5461
6470
|
if (s.includes(".opencode") && options.legacySkills) {
|
|
5462
6471
|
const skillFiles = await walkDir(s);
|
|
5463
6472
|
for (const file of skillFiles) {
|
|
5464
|
-
const relPath =
|
|
5465
|
-
const destPath =
|
|
5466
|
-
if (!
|
|
6473
|
+
const relPath = path5.relative(s, file);
|
|
6474
|
+
const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
|
|
6475
|
+
if (!existsSync8(destPath)) {
|
|
5467
6476
|
operations.push({
|
|
5468
6477
|
kind: "copy",
|
|
5469
6478
|
sourcePath: file,
|
|
5470
6479
|
path: destPath,
|
|
5471
6480
|
ownership: "project-owned",
|
|
5472
|
-
reason: `Migrate legacy skill: ${
|
|
6481
|
+
reason: `Migrate legacy skill: ${path5.join(".opencode/skills", relPath)}`,
|
|
5473
6482
|
requiresConfirmation: false,
|
|
5474
6483
|
destructive: false
|
|
5475
6484
|
});
|
|
@@ -5526,7 +6535,7 @@ async function planInit(options) {
|
|
|
5526
6535
|
});
|
|
5527
6536
|
}
|
|
5528
6537
|
if (sourceRoot) {
|
|
5529
|
-
if (!
|
|
6538
|
+
if (!existsSync8(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
|
|
5530
6539
|
warnings.push(
|
|
5531
6540
|
`--from-local path does not exist or is not a directory: ${sourceRoot}`
|
|
5532
6541
|
);
|
|
@@ -5587,17 +6596,17 @@ async function planInit(options) {
|
|
|
5587
6596
|
continue;
|
|
5588
6597
|
}
|
|
5589
6598
|
const targetDirName = ".agents/skills";
|
|
5590
|
-
const targetPath =
|
|
6599
|
+
const targetPath = path5.join(targetRoot, targetDirName);
|
|
5591
6600
|
const skillFiles = await walkDir(s);
|
|
5592
6601
|
for (const file of skillFiles) {
|
|
5593
|
-
const relPath =
|
|
5594
|
-
const destPath =
|
|
6602
|
+
const relPath = path5.relative(s, file);
|
|
6603
|
+
const destPath = path5.join(targetPath, relPath);
|
|
5595
6604
|
if (plannedSkillDests.has(destPath)) {
|
|
5596
6605
|
operations.push({
|
|
5597
6606
|
kind: "skip",
|
|
5598
6607
|
path: destPath,
|
|
5599
6608
|
ownership: "project-owned",
|
|
5600
|
-
reason: `Skill destination conflict, skipping source copy: ${
|
|
6609
|
+
reason: `Skill destination conflict, skipping source copy: ${path5.join(targetDirName, relPath)}`,
|
|
5601
6610
|
requiresConfirmation: false,
|
|
5602
6611
|
destructive: false,
|
|
5603
6612
|
conflict: "destination already planned"
|
|
@@ -5610,7 +6619,7 @@ async function planInit(options) {
|
|
|
5610
6619
|
sourcePath: file,
|
|
5611
6620
|
path: destPath,
|
|
5612
6621
|
ownership: "copied-customizable",
|
|
5613
|
-
reason: `Copy skill: ${
|
|
6622
|
+
reason: `Copy skill: ${path5.join(targetDirName, relPath)}`,
|
|
5614
6623
|
requiresConfirmation: false,
|
|
5615
6624
|
destructive: false,
|
|
5616
6625
|
checksum
|
|
@@ -5622,7 +6631,7 @@ async function planInit(options) {
|
|
|
5622
6631
|
operations.push({
|
|
5623
6632
|
kind: "copy",
|
|
5624
6633
|
sourcePath: srcReadme,
|
|
5625
|
-
path:
|
|
6634
|
+
path: path5.join(targetRoot, "README.md"),
|
|
5626
6635
|
ownership: "project-owned",
|
|
5627
6636
|
reason: "Copy README.md from source",
|
|
5628
6637
|
requiresConfirmation: false,
|
|
@@ -5633,8 +6642,8 @@ async function planInit(options) {
|
|
|
5633
6642
|
const rootDocs = await discoverRootDomainDocs(targetRoot);
|
|
5634
6643
|
const merleDestPaths = /* @__PURE__ */ new Set();
|
|
5635
6644
|
for (const docPath of rootDocs) {
|
|
5636
|
-
const relPath =
|
|
5637
|
-
const destPath =
|
|
6645
|
+
const relPath = path5.relative(targetRoot, docPath);
|
|
6646
|
+
const destPath = path5.join(targetRoot, ".pourkit", relPath);
|
|
5638
6647
|
merleDestPaths.add(destPath);
|
|
5639
6648
|
if (docsMigration === "skip") {
|
|
5640
6649
|
operations.push({
|
|
@@ -5645,7 +6654,7 @@ async function planInit(options) {
|
|
|
5645
6654
|
requiresConfirmation: false,
|
|
5646
6655
|
destructive: false
|
|
5647
6656
|
});
|
|
5648
|
-
} else if (
|
|
6657
|
+
} else if (existsSync8(destPath)) {
|
|
5649
6658
|
operations.push({
|
|
5650
6659
|
kind: "skip",
|
|
5651
6660
|
path: destPath,
|
|
@@ -5680,8 +6689,8 @@ async function planInit(options) {
|
|
|
5680
6689
|
let packageScripts = {};
|
|
5681
6690
|
let hasPackageJson = true;
|
|
5682
6691
|
try {
|
|
5683
|
-
const pkgContent = await
|
|
5684
|
-
|
|
6692
|
+
const pkgContent = await readFile4(
|
|
6693
|
+
path5.join(targetRoot, "package.json"),
|
|
5685
6694
|
"utf-8"
|
|
5686
6695
|
);
|
|
5687
6696
|
const pkg = JSON.parse(pkgContent);
|
|
@@ -5702,8 +6711,8 @@ async function planInit(options) {
|
|
|
5702
6711
|
} catch {
|
|
5703
6712
|
}
|
|
5704
6713
|
}
|
|
5705
|
-
const contextPath =
|
|
5706
|
-
if (!
|
|
6714
|
+
const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
|
|
6715
|
+
if (!existsSync8(contextPath) && !merleDestPaths.has(contextPath)) {
|
|
5707
6716
|
operations.push({
|
|
5708
6717
|
kind: "create",
|
|
5709
6718
|
path: contextPath,
|
|
@@ -5714,14 +6723,14 @@ async function planInit(options) {
|
|
|
5714
6723
|
content: generateContextScaffold()
|
|
5715
6724
|
});
|
|
5716
6725
|
}
|
|
5717
|
-
const adrGitkeep =
|
|
6726
|
+
const adrGitkeep = path5.join(
|
|
5718
6727
|
targetRoot,
|
|
5719
6728
|
".pourkit",
|
|
5720
6729
|
"docs",
|
|
5721
6730
|
"adr",
|
|
5722
6731
|
".gitkeep"
|
|
5723
6732
|
);
|
|
5724
|
-
if (!
|
|
6733
|
+
if (!existsSync8(adrGitkeep)) {
|
|
5725
6734
|
operations.push({
|
|
5726
6735
|
kind: "create",
|
|
5727
6736
|
path: adrGitkeep,
|
|
@@ -5731,16 +6740,16 @@ async function planInit(options) {
|
|
|
5731
6740
|
destructive: false
|
|
5732
6741
|
});
|
|
5733
6742
|
}
|
|
5734
|
-
const srcDocAgents =
|
|
5735
|
-
const tgtDocAgents =
|
|
5736
|
-
if (
|
|
6743
|
+
const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
|
|
6744
|
+
const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
|
|
6745
|
+
if (existsSync8(srcDocAgents) && !existsSync8(tgtDocAgents)) {
|
|
5737
6746
|
const docFiles = await walkDir(srcDocAgents);
|
|
5738
6747
|
for (const file of docFiles) {
|
|
5739
|
-
const relPath =
|
|
6748
|
+
const relPath = path5.relative(srcDocAgents, file);
|
|
5740
6749
|
if (relPath === "triage-labels.md") {
|
|
5741
6750
|
operations.push({
|
|
5742
6751
|
kind: "create",
|
|
5743
|
-
path:
|
|
6752
|
+
path: path5.join(tgtDocAgents, relPath),
|
|
5744
6753
|
ownership: "managed",
|
|
5745
6754
|
reason: "Init triage labels doc",
|
|
5746
6755
|
requiresConfirmation: false,
|
|
@@ -5755,7 +6764,7 @@ async function planInit(options) {
|
|
|
5755
6764
|
operations.push({
|
|
5756
6765
|
kind: "copy",
|
|
5757
6766
|
sourcePath: file,
|
|
5758
|
-
path:
|
|
6767
|
+
path: path5.join(tgtDocAgents, relPath),
|
|
5759
6768
|
ownership: "managed",
|
|
5760
6769
|
reason: `Copy agent doc: ${relPath}`,
|
|
5761
6770
|
requiresConfirmation: false,
|
|
@@ -5764,17 +6773,17 @@ async function planInit(options) {
|
|
|
5764
6773
|
});
|
|
5765
6774
|
}
|
|
5766
6775
|
}
|
|
5767
|
-
const srcPrompts =
|
|
5768
|
-
const tgtPrompts =
|
|
5769
|
-
if (
|
|
6776
|
+
const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
|
|
6777
|
+
const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
|
|
6778
|
+
if (existsSync8(srcPrompts) && !existsSync8(tgtPrompts)) {
|
|
5770
6779
|
const promptFiles = await walkDir(srcPrompts);
|
|
5771
6780
|
for (const file of promptFiles) {
|
|
5772
|
-
const relPath =
|
|
6781
|
+
const relPath = path5.relative(srcPrompts, file);
|
|
5773
6782
|
const checksum = await computeFileChecksum(file);
|
|
5774
6783
|
operations.push({
|
|
5775
6784
|
kind: "copy",
|
|
5776
6785
|
sourcePath: file,
|
|
5777
|
-
path:
|
|
6786
|
+
path: path5.join(tgtPrompts, relPath),
|
|
5778
6787
|
ownership: "managed",
|
|
5779
6788
|
reason: `Copy prompt: ${relPath}`,
|
|
5780
6789
|
requiresConfirmation: false,
|
|
@@ -5783,17 +6792,17 @@ async function planInit(options) {
|
|
|
5783
6792
|
});
|
|
5784
6793
|
}
|
|
5785
6794
|
}
|
|
5786
|
-
const srcSandboxDockerfile =
|
|
6795
|
+
const srcSandboxDockerfile = path5.join(
|
|
5787
6796
|
sourceRoot,
|
|
5788
6797
|
".sandcastle",
|
|
5789
6798
|
"Dockerfile"
|
|
5790
6799
|
);
|
|
5791
|
-
const tgtSandboxDockerfile =
|
|
6800
|
+
const tgtSandboxDockerfile = path5.join(
|
|
5792
6801
|
targetRoot,
|
|
5793
6802
|
".sandcastle",
|
|
5794
6803
|
"Dockerfile"
|
|
5795
6804
|
);
|
|
5796
|
-
if (
|
|
6805
|
+
if (existsSync8(tgtSandboxDockerfile)) {
|
|
5797
6806
|
operations.push({
|
|
5798
6807
|
kind: "skip",
|
|
5799
6808
|
path: tgtSandboxDockerfile,
|
|
@@ -5802,7 +6811,7 @@ async function planInit(options) {
|
|
|
5802
6811
|
requiresConfirmation: false,
|
|
5803
6812
|
destructive: false
|
|
5804
6813
|
});
|
|
5805
|
-
} else if (
|
|
6814
|
+
} else if (existsSync8(srcSandboxDockerfile)) {
|
|
5806
6815
|
const checksum = await computeFileChecksum(srcSandboxDockerfile);
|
|
5807
6816
|
operations.push({
|
|
5808
6817
|
kind: "copy",
|
|
@@ -5815,8 +6824,8 @@ async function planInit(options) {
|
|
|
5815
6824
|
checksum
|
|
5816
6825
|
});
|
|
5817
6826
|
}
|
|
5818
|
-
const configTsPath =
|
|
5819
|
-
if (!
|
|
6827
|
+
const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
|
|
6828
|
+
if (!existsSync8(configTsPath)) {
|
|
5820
6829
|
const verifyCommands = inferVerificationCommands(
|
|
5821
6830
|
packageScripts,
|
|
5822
6831
|
pm || "npm"
|
|
@@ -5853,10 +6862,10 @@ async function planInit(options) {
|
|
|
5853
6862
|
const hasExistingAgents = operations.some(
|
|
5854
6863
|
(op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
|
|
5855
6864
|
);
|
|
5856
|
-
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !
|
|
6865
|
+
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync8(path5.join(targetRoot, "AGENTS.md"))) {
|
|
5857
6866
|
operations.push({
|
|
5858
6867
|
kind: "create",
|
|
5859
|
-
path:
|
|
6868
|
+
path: path5.join(targetRoot, "AGENTS.md"),
|
|
5860
6869
|
ownership: "managed",
|
|
5861
6870
|
reason: "Init AGENTS.md with Pourkit managed block",
|
|
5862
6871
|
requiresConfirmation: false,
|
|
@@ -5869,10 +6878,10 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
|
|
|
5869
6878
|
const hasExistingClaude = operations.some(
|
|
5870
6879
|
(op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
|
|
5871
6880
|
);
|
|
5872
|
-
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !
|
|
6881
|
+
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync8(path5.join(targetRoot, "CLAUDE.md"))) {
|
|
5873
6882
|
operations.push({
|
|
5874
6883
|
kind: "create",
|
|
5875
|
-
path:
|
|
6884
|
+
path: path5.join(targetRoot, "CLAUDE.md"),
|
|
5876
6885
|
ownership: "managed",
|
|
5877
6886
|
reason: "Init CLAUDE.md with Pourkit managed block",
|
|
5878
6887
|
requiresConfirmation: false,
|
|
@@ -5882,9 +6891,9 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
|
|
|
5882
6891
|
`
|
|
5883
6892
|
});
|
|
5884
6893
|
}
|
|
5885
|
-
const gitignoreTarget =
|
|
6894
|
+
const gitignoreTarget = path5.join(targetRoot, ".gitignore");
|
|
5886
6895
|
const gitignoreContent = generateGitignoreBlock();
|
|
5887
|
-
if (!
|
|
6896
|
+
if (!existsSync8(gitignoreTarget)) {
|
|
5888
6897
|
operations.push({
|
|
5889
6898
|
kind: "create",
|
|
5890
6899
|
path: gitignoreTarget,
|
|
@@ -5907,8 +6916,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
|
|
|
5907
6916
|
content: gitignoreContent
|
|
5908
6917
|
});
|
|
5909
6918
|
}
|
|
5910
|
-
const openCodePath =
|
|
5911
|
-
if (!
|
|
6919
|
+
const openCodePath = path5.join(targetRoot, "opencode.json");
|
|
6920
|
+
if (!existsSync8(openCodePath)) {
|
|
5912
6921
|
operations.push({
|
|
5913
6922
|
kind: "create",
|
|
5914
6923
|
path: openCodePath,
|
|
@@ -5920,7 +6929,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
|
|
|
5920
6929
|
});
|
|
5921
6930
|
} else {
|
|
5922
6931
|
try {
|
|
5923
|
-
const existingContent = await
|
|
6932
|
+
const existingContent = await readFile4(openCodePath, "utf-8");
|
|
5924
6933
|
const existingConfig = JSON.parse(existingContent);
|
|
5925
6934
|
if (typeof existingConfig !== "object" || existingConfig === null || Array.isArray(existingConfig)) {
|
|
5926
6935
|
warnings.push(
|
|
@@ -5964,8 +6973,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
|
|
|
5964
6973
|
});
|
|
5965
6974
|
}
|
|
5966
6975
|
}
|
|
5967
|
-
const manifestPath =
|
|
5968
|
-
if (
|
|
6976
|
+
const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
|
|
6977
|
+
if (existsSync8(manifestPath)) {
|
|
5969
6978
|
operations.push({
|
|
5970
6979
|
kind: "skip",
|
|
5971
6980
|
path: manifestPath,
|
|
@@ -6284,13 +7293,13 @@ async function updateManagedBlock(filePath, content) {
|
|
|
6284
7293
|
const blockContent = `${MANAGED_BLOCK_BEGIN}
|
|
6285
7294
|
${content}${MANAGED_BLOCK_END}
|
|
6286
7295
|
`;
|
|
6287
|
-
if (!
|
|
6288
|
-
const dir =
|
|
6289
|
-
await
|
|
7296
|
+
if (!existsSync8(filePath)) {
|
|
7297
|
+
const dir = path5.dirname(filePath);
|
|
7298
|
+
await mkdir4(dir, { recursive: true });
|
|
6290
7299
|
await writeFileAtomic(filePath, blockContent);
|
|
6291
7300
|
return;
|
|
6292
7301
|
}
|
|
6293
|
-
const existing = await
|
|
7302
|
+
const existing = await readFile4(filePath, "utf-8");
|
|
6294
7303
|
const beginIdx = existing.indexOf(MANAGED_BLOCK_BEGIN);
|
|
6295
7304
|
const endIdx = existing.indexOf(MANAGED_BLOCK_END);
|
|
6296
7305
|
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
@@ -6303,17 +7312,17 @@ ${content}${MANAGED_BLOCK_END}
|
|
|
6303
7312
|
}
|
|
6304
7313
|
}
|
|
6305
7314
|
async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
|
|
6306
|
-
const manifestDir =
|
|
6307
|
-
const manifestPath =
|
|
7315
|
+
const manifestDir = path5.join(plan.targetRoot, ".pourkit");
|
|
7316
|
+
const manifestPath = path5.join(manifestDir, "manifest.json");
|
|
6308
7317
|
const assets = {};
|
|
6309
7318
|
for (const op of plan.operations) {
|
|
6310
7319
|
if (!op.path) continue;
|
|
6311
7320
|
if (op.kind !== "create" && op.kind !== "copy" && op.kind !== "update" && op.kind !== "move")
|
|
6312
7321
|
continue;
|
|
6313
7322
|
if (op.requiresConfirmation) continue;
|
|
6314
|
-
const relPath =
|
|
7323
|
+
const relPath = path5.relative(plan.targetRoot, op.path);
|
|
6315
7324
|
if (relPath === ".pourkit/manifest.json") continue;
|
|
6316
|
-
if (
|
|
7325
|
+
if (existsSync8(op.path)) {
|
|
6317
7326
|
const sha256 = await computeFileChecksum(op.path);
|
|
6318
7327
|
assets[relPath] = {
|
|
6319
7328
|
ownership: op.ownership || "managed",
|
|
@@ -6337,7 +7346,7 @@ async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
|
|
|
6337
7346
|
packageManager,
|
|
6338
7347
|
assets
|
|
6339
7348
|
};
|
|
6340
|
-
await
|
|
7349
|
+
await mkdir4(manifestDir, { recursive: true });
|
|
6341
7350
|
await writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
6342
7351
|
return manifest;
|
|
6343
7352
|
}
|
|
@@ -6358,12 +7367,12 @@ async function applyInitPlan(plan, options) {
|
|
|
6358
7367
|
skipped++;
|
|
6359
7368
|
continue;
|
|
6360
7369
|
}
|
|
6361
|
-
if (
|
|
7370
|
+
if (existsSync8(op.path) && !op.destructive) {
|
|
6362
7371
|
skipped++;
|
|
6363
7372
|
continue;
|
|
6364
7373
|
}
|
|
6365
|
-
const dir =
|
|
6366
|
-
await
|
|
7374
|
+
const dir = path5.dirname(op.path);
|
|
7375
|
+
await mkdir4(dir, { recursive: true });
|
|
6367
7376
|
await writeFileAtomic(op.path, op.content ?? "");
|
|
6368
7377
|
applied++;
|
|
6369
7378
|
break;
|
|
@@ -6373,7 +7382,7 @@ async function applyInitPlan(plan, options) {
|
|
|
6373
7382
|
skipped++;
|
|
6374
7383
|
continue;
|
|
6375
7384
|
}
|
|
6376
|
-
if (
|
|
7385
|
+
if (existsSync8(op.path)) {
|
|
6377
7386
|
skipped++;
|
|
6378
7387
|
continue;
|
|
6379
7388
|
}
|
|
@@ -6381,8 +7390,8 @@ async function applyInitPlan(plan, options) {
|
|
|
6381
7390
|
if (srcStat.isDirectory()) {
|
|
6382
7391
|
skipped++;
|
|
6383
7392
|
} else {
|
|
6384
|
-
const dir =
|
|
6385
|
-
await
|
|
7393
|
+
const dir = path5.dirname(op.path);
|
|
7394
|
+
await mkdir4(dir, { recursive: true });
|
|
6386
7395
|
await copyFile(op.sourcePath, op.path);
|
|
6387
7396
|
applied++;
|
|
6388
7397
|
}
|
|
@@ -6402,12 +7411,12 @@ async function applyInitPlan(plan, options) {
|
|
|
6402
7411
|
skipped++;
|
|
6403
7412
|
continue;
|
|
6404
7413
|
}
|
|
6405
|
-
if (
|
|
7414
|
+
if (existsSync8(op.path)) {
|
|
6406
7415
|
skipped++;
|
|
6407
7416
|
continue;
|
|
6408
7417
|
}
|
|
6409
|
-
const dir =
|
|
6410
|
-
await
|
|
7418
|
+
const dir = path5.dirname(op.path);
|
|
7419
|
+
await mkdir4(dir, { recursive: true });
|
|
6411
7420
|
await rename(op.sourcePath, op.path);
|
|
6412
7421
|
applied++;
|
|
6413
7422
|
break;
|
|
@@ -6537,13 +7546,13 @@ async function applyInitFromSource(options) {
|
|
|
6537
7546
|
let manifestWritten = false;
|
|
6538
7547
|
if (result.errors.length === 0) {
|
|
6539
7548
|
const manifestSkipped = plan.operations.some(
|
|
6540
|
-
(op) => op.kind === "skip" && op.path ===
|
|
7549
|
+
(op) => op.kind === "skip" && op.path === path5.join(targetRoot, ".pourkit", "manifest.json")
|
|
6541
7550
|
);
|
|
6542
7551
|
if (!manifestSkipped) {
|
|
6543
7552
|
const agentFiles = [];
|
|
6544
7553
|
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
6545
|
-
if (
|
|
6546
|
-
agentFiles.push(
|
|
7554
|
+
if (existsSync8(path5.join(targetRoot, name))) {
|
|
7555
|
+
agentFiles.push(path5.join(targetRoot, name));
|
|
6547
7556
|
}
|
|
6548
7557
|
}
|
|
6549
7558
|
const pm = detectPackageManager(targetRoot);
|
|
@@ -6718,6 +7727,121 @@ Init applied: ${result.applied} operations applied, ${result.skipped} skipped.`
|
|
|
6718
7727
|
}
|
|
6719
7728
|
}
|
|
6720
7729
|
|
|
7730
|
+
// commands/serena.ts
|
|
7731
|
+
init_common();
|
|
7732
|
+
var SERENA_MCP_PORT2 = 9121;
|
|
7733
|
+
var SERENA_DASHBOARD_PORT2 = 24282;
|
|
7734
|
+
var SERENA_IMAGE2 = "ghcr.io/oraios/serena:latest";
|
|
7735
|
+
async function resolveSerenaCommandContext(options) {
|
|
7736
|
+
const repoRootPath = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7737
|
+
const config = await loadRepoConfig(repoRootPath);
|
|
7738
|
+
const target = resolveTarget(config, options.target);
|
|
7739
|
+
return {
|
|
7740
|
+
repoRootPath,
|
|
7741
|
+
config,
|
|
7742
|
+
target
|
|
7743
|
+
};
|
|
7744
|
+
}
|
|
7745
|
+
async function resolveSerenaLifecycleContext(options) {
|
|
7746
|
+
const repoRootPath = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7747
|
+
const config = await loadRepoConfig(repoRootPath);
|
|
7748
|
+
return {
|
|
7749
|
+
repoRootPath,
|
|
7750
|
+
config,
|
|
7751
|
+
paths: resolveSerenaPaths(repoRootPath, config.serena.dataDir)
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
function buildSerenaSidecarOptions(paths, mcpUrl) {
|
|
7755
|
+
return {
|
|
7756
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
7757
|
+
dataDir: paths.dataDir,
|
|
7758
|
+
mcpPort: SERENA_MCP_PORT2,
|
|
7759
|
+
dashboardPort: SERENA_DASHBOARD_PORT2,
|
|
7760
|
+
image: SERENA_IMAGE2,
|
|
7761
|
+
mcpUrl
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7764
|
+
function logSerenaSidecarStatus(heading, status, baselineFreshness) {
|
|
7765
|
+
console.log(`${heading}:`);
|
|
7766
|
+
console.log(` running: ${status.running ? "yes" : "no"}`);
|
|
7767
|
+
console.log(` mcpUrl: ${status.mcpUrl}`);
|
|
7768
|
+
console.log(` dashboardUrl: ${status.dashboardUrl}`);
|
|
7769
|
+
console.log(` containerName: ${status.containerName}`);
|
|
7770
|
+
if (baselineFreshness) {
|
|
7771
|
+
console.log(` Baseline freshness: ${baselineFreshness}`);
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
async function runSerenaInitCommand(options) {
|
|
7775
|
+
const { repoRootPath, config, target } = await resolveSerenaCommandContext(options);
|
|
7776
|
+
const paths = await ensureBaselineWorktree({
|
|
7777
|
+
repoRoot: repoRootPath,
|
|
7778
|
+
dataDir: config.serena.dataDir
|
|
7779
|
+
});
|
|
7780
|
+
await refreshSerenaBaseline({
|
|
7781
|
+
repoRoot: repoRootPath,
|
|
7782
|
+
dataDir: config.serena.dataDir,
|
|
7783
|
+
baseBranch: target.baseBranch
|
|
7784
|
+
});
|
|
7785
|
+
await prepareSerenaSidecarConfig({
|
|
7786
|
+
baselineWorktreePath: paths.baselineWorktreePath,
|
|
7787
|
+
dataDir: paths.dataDir
|
|
7788
|
+
});
|
|
7789
|
+
const sidecarOptions2 = buildSerenaSidecarOptions(paths, config.serena.mcpUrl);
|
|
7790
|
+
await startSerenaSidecar(sidecarOptions2);
|
|
7791
|
+
await indexSerenaProject(sidecarOptions2);
|
|
7792
|
+
}
|
|
7793
|
+
async function runSerenaRefreshCommand(options) {
|
|
7794
|
+
const { repoRootPath, config, target } = await resolveSerenaCommandContext(options);
|
|
7795
|
+
await refreshSerenaBaseline({
|
|
7796
|
+
repoRoot: repoRootPath,
|
|
7797
|
+
dataDir: config.serena.dataDir,
|
|
7798
|
+
baseBranch: target.baseBranch
|
|
7799
|
+
});
|
|
7800
|
+
}
|
|
7801
|
+
async function runSerenaStartCommand(options) {
|
|
7802
|
+
const { repoRootPath, config } = await resolveSerenaLifecycleContext(options);
|
|
7803
|
+
const ensuredPaths = await ensureBaselineWorktree({
|
|
7804
|
+
repoRoot: repoRootPath,
|
|
7805
|
+
dataDir: config.serena.dataDir
|
|
7806
|
+
});
|
|
7807
|
+
await prepareSerenaSidecarConfig({
|
|
7808
|
+
baselineWorktreePath: ensuredPaths.baselineWorktreePath,
|
|
7809
|
+
dataDir: ensuredPaths.dataDir
|
|
7810
|
+
});
|
|
7811
|
+
const status = await startSerenaSidecar(
|
|
7812
|
+
buildSerenaSidecarOptions(ensuredPaths, config.serena.mcpUrl)
|
|
7813
|
+
);
|
|
7814
|
+
logSerenaSidecarStatus("Serena sidecar started", status);
|
|
7815
|
+
}
|
|
7816
|
+
async function runSerenaStopCommand(options) {
|
|
7817
|
+
const { config, paths } = await resolveSerenaLifecycleContext(options);
|
|
7818
|
+
const status = await stopSerenaSidecar(
|
|
7819
|
+
buildSerenaSidecarOptions(paths, config.serena.mcpUrl)
|
|
7820
|
+
);
|
|
7821
|
+
logSerenaSidecarStatus("Serena sidecar stopped", status);
|
|
7822
|
+
}
|
|
7823
|
+
async function runSerenaStatusCommand(options) {
|
|
7824
|
+
const { repoRootPath, config, paths } = await resolveSerenaLifecycleContext(options);
|
|
7825
|
+
const status = await getSerenaSidecarStatus(
|
|
7826
|
+
buildSerenaSidecarOptions(paths, config.serena.mcpUrl)
|
|
7827
|
+
);
|
|
7828
|
+
if (options.target) {
|
|
7829
|
+
const target = resolveTarget(config, options.target);
|
|
7830
|
+
const baseline = await getSerenaBaselineStatus({
|
|
7831
|
+
repoRoot: repoRootPath,
|
|
7832
|
+
dataDir: config.serena.dataDir,
|
|
7833
|
+
baseBranch: target.baseBranch
|
|
7834
|
+
});
|
|
7835
|
+
logSerenaSidecarStatus(
|
|
7836
|
+
"Serena sidecar status",
|
|
7837
|
+
status,
|
|
7838
|
+
baseline.fresh ? "fresh" : "stale"
|
|
7839
|
+
);
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7842
|
+
logSerenaSidecarStatus("Serena sidecar status", status);
|
|
7843
|
+
}
|
|
7844
|
+
|
|
6721
7845
|
// providers/github-provider.ts
|
|
6722
7846
|
var GitHubIssueProvider = class {
|
|
6723
7847
|
client;
|
|
@@ -6730,6 +7854,17 @@ var GitHubIssueProvider = class {
|
|
|
6730
7854
|
this.blockedLabel = options?.blockedLabel ?? "blocked";
|
|
6731
7855
|
this.issueListLimit = options?.issueListLimit ?? 50;
|
|
6732
7856
|
}
|
|
7857
|
+
async createIssue(options) {
|
|
7858
|
+
const { data } = await this.client.octokit.rest.issues.create({
|
|
7859
|
+
owner: this.client.owner,
|
|
7860
|
+
repo: this.client.repo,
|
|
7861
|
+
title: options.title,
|
|
7862
|
+
body: options.body ?? "",
|
|
7863
|
+
labels: options.labels,
|
|
7864
|
+
assignees: options.assignees
|
|
7865
|
+
});
|
|
7866
|
+
return { number: data.number, url: data.html_url, title: data.title };
|
|
7867
|
+
}
|
|
6733
7868
|
async fetchIssue(number) {
|
|
6734
7869
|
const { data } = await this.client.octokit.rest.issues.get({
|
|
6735
7870
|
owner: this.client.owner,
|
|
@@ -7234,47 +8369,47 @@ function formatChecks2(checks) {
|
|
|
7234
8369
|
init_common();
|
|
7235
8370
|
|
|
7236
8371
|
// execution/sandcastle-execution.ts
|
|
7237
|
-
import { mkdirSync as
|
|
7238
|
-
import { join as
|
|
8372
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
8373
|
+
import { join as join12 } from "path";
|
|
7239
8374
|
import { createWorktree, opencode } from "@ai-hero/sandcastle";
|
|
7240
8375
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
7241
8376
|
|
|
7242
8377
|
// execution/execution-provider.ts
|
|
7243
8378
|
init_common();
|
|
7244
8379
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
7245
|
-
import { dirname as
|
|
8380
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
7246
8381
|
async function writeExecutionArtifacts(worktreePath, artifacts) {
|
|
7247
8382
|
for (const artifact of artifacts) {
|
|
7248
|
-
const filePath =
|
|
7249
|
-
await ensureDir(
|
|
8383
|
+
const filePath = join11(worktreePath, artifact.path);
|
|
8384
|
+
await ensureDir(dirname5(filePath));
|
|
7250
8385
|
await writeFile2(filePath, artifact.content, "utf-8");
|
|
7251
8386
|
}
|
|
7252
8387
|
}
|
|
7253
8388
|
|
|
7254
8389
|
// execution/sandbox-image-build.ts
|
|
7255
8390
|
init_common();
|
|
7256
|
-
import
|
|
8391
|
+
import path7 from "path";
|
|
7257
8392
|
|
|
7258
8393
|
// execution/sandbox-image.ts
|
|
7259
8394
|
import { createHash as createHash2 } from "crypto";
|
|
7260
|
-
import { existsSync as
|
|
7261
|
-
import
|
|
8395
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
8396
|
+
import path6 from "path";
|
|
7262
8397
|
function sandboxImageName(repoRoot2) {
|
|
7263
|
-
const dirName =
|
|
8398
|
+
const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
|
|
7264
8399
|
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
7265
8400
|
const baseName = sanitized || "local";
|
|
7266
|
-
const dockerfilePath =
|
|
7267
|
-
if (!
|
|
8401
|
+
const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
|
|
8402
|
+
if (!existsSync9(dockerfilePath)) {
|
|
7268
8403
|
return `sandcastle:${baseName}`;
|
|
7269
8404
|
}
|
|
7270
|
-
const fingerprint = createHash2("sha256").update(
|
|
8405
|
+
const fingerprint = createHash2("sha256").update(readFileSync8(dockerfilePath)).digest("hex").slice(0, 8);
|
|
7271
8406
|
return `sandcastle:${baseName}-${fingerprint}`;
|
|
7272
8407
|
}
|
|
7273
8408
|
|
|
7274
8409
|
// execution/sandbox-image-build.ts
|
|
7275
8410
|
async function ensureSandboxImageBuilt(repoRoot2, options) {
|
|
7276
8411
|
const imageName = sandboxImageName(repoRoot2);
|
|
7277
|
-
const dockerfilePath =
|
|
8412
|
+
const dockerfilePath = path7.join(repoRoot2, ".sandcastle", "Dockerfile");
|
|
7278
8413
|
if (!options?.force) {
|
|
7279
8414
|
try {
|
|
7280
8415
|
await execCapture("docker", ["image", "inspect", imageName]);
|
|
@@ -7312,6 +8447,25 @@ function buildSandboxOptions(repoRoot2, sandbox) {
|
|
|
7312
8447
|
};
|
|
7313
8448
|
}
|
|
7314
8449
|
|
|
8450
|
+
// execution/opencode-config.ts
|
|
8451
|
+
function isSerenaEligibleStage(stage) {
|
|
8452
|
+
return stage === "builder" || stage === "refactor";
|
|
8453
|
+
}
|
|
8454
|
+
function buildSerenaOpenCodeConfig(stage, serena) {
|
|
8455
|
+
if (!serena?.available || !isSerenaEligibleStage(stage)) {
|
|
8456
|
+
return void 0;
|
|
8457
|
+
}
|
|
8458
|
+
return {
|
|
8459
|
+
mcp: {
|
|
8460
|
+
serena: {
|
|
8461
|
+
type: "remote",
|
|
8462
|
+
url: serena.sandboxMcpUrl,
|
|
8463
|
+
enabled: true
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
};
|
|
8467
|
+
}
|
|
8468
|
+
|
|
7315
8469
|
// execution/sandcastle-execution.ts
|
|
7316
8470
|
var SandcastleExecutionProvider = class {
|
|
7317
8471
|
async createSession() {
|
|
@@ -7364,6 +8518,10 @@ var SandcastleExecutionSession = class {
|
|
|
7364
8518
|
if (autoApprove) {
|
|
7365
8519
|
env.OPENCODE_AUTO_APPROVE = "true";
|
|
7366
8520
|
}
|
|
8521
|
+
const serenaConfig = buildSerenaOpenCodeConfig(stage, options.serena);
|
|
8522
|
+
if (serenaConfig) {
|
|
8523
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(serenaConfig);
|
|
8524
|
+
}
|
|
7367
8525
|
const logPath = `${repoRoot2}/.pourkit/logs/${sanitizeBranch(branchName)}-${Date.now()}.log`;
|
|
7368
8526
|
await ensureSandboxImageBuilt(repoRoot2, { force: sandbox.forceRebuild });
|
|
7369
8527
|
try {
|
|
@@ -7482,12 +8640,12 @@ function sanitizeBranch(branchName) {
|
|
|
7482
8640
|
return branchName.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
7483
8641
|
}
|
|
7484
8642
|
function savePromptToFile(repoRoot2, stage, iteration, prompt) {
|
|
7485
|
-
const promptsDir =
|
|
7486
|
-
|
|
8643
|
+
const promptsDir = join12(repoRoot2, ".pourkit", ".tmp", "prompts");
|
|
8644
|
+
mkdirSync6(promptsDir, { recursive: true });
|
|
7487
8645
|
const timestamp2 = Date.now();
|
|
7488
8646
|
const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
|
|
7489
8647
|
const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
|
|
7490
|
-
const filePath =
|
|
8648
|
+
const filePath = join12(promptsDir, filename);
|
|
7491
8649
|
writeFileSync4(filePath, prompt, "utf-8");
|
|
7492
8650
|
}
|
|
7493
8651
|
|
|
@@ -7553,19 +8711,91 @@ async function handleError(logger, error) {
|
|
|
7553
8711
|
function createCliProgram(version) {
|
|
7554
8712
|
const program = new Command();
|
|
7555
8713
|
program.name("pourkit").version(version).exitOverride().description("AI-driven issue-to-PR workflow for GitHub repositories.");
|
|
7556
|
-
|
|
8714
|
+
const issueCommand = program.command("issue");
|
|
8715
|
+
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(
|
|
8716
|
+
"--label <label>",
|
|
8717
|
+
"label to apply (repeatable)",
|
|
8718
|
+
(value, previous) => {
|
|
8719
|
+
previous.push(value);
|
|
8720
|
+
return previous;
|
|
8721
|
+
},
|
|
8722
|
+
[]
|
|
8723
|
+
).option(
|
|
8724
|
+
"--assignee <login>",
|
|
8725
|
+
"assignee login (repeatable)",
|
|
8726
|
+
(value, previous) => {
|
|
8727
|
+
previous.push(value);
|
|
8728
|
+
return previous;
|
|
8729
|
+
},
|
|
8730
|
+
[]
|
|
8731
|
+
).option("--cwd <path>", "target repository directory").action(
|
|
8732
|
+
async (options) => {
|
|
8733
|
+
const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
8734
|
+
const logPath = path8.join(
|
|
8735
|
+
targetRepoRoot,
|
|
8736
|
+
".pourkit",
|
|
8737
|
+
"logs",
|
|
8738
|
+
"issue-create.log"
|
|
8739
|
+
);
|
|
8740
|
+
const logger = createLogger("pourkit", logPath);
|
|
8741
|
+
const client = await requireGitHubClient({ cwd: targetRepoRoot });
|
|
8742
|
+
const issueProvider = new GitHubIssueProvider(client);
|
|
8743
|
+
try {
|
|
8744
|
+
const args = ["--title", options.title];
|
|
8745
|
+
if (options.body) {
|
|
8746
|
+
args.push("--body", options.body);
|
|
8747
|
+
}
|
|
8748
|
+
if (options.bodyFile) {
|
|
8749
|
+
const bodyFileArg = options.cwd && !path8.isAbsolute(options.bodyFile) ? path8.resolve(options.cwd, options.bodyFile) : options.bodyFile;
|
|
8750
|
+
args.push("--body-file", bodyFileArg);
|
|
8751
|
+
}
|
|
8752
|
+
for (const label of options.label) {
|
|
8753
|
+
args.push("--label", label);
|
|
8754
|
+
}
|
|
8755
|
+
for (const assignee of options.assignee) {
|
|
8756
|
+
args.push("--assignee", assignee);
|
|
8757
|
+
}
|
|
8758
|
+
const result = await runIssueCreateCommand(
|
|
8759
|
+
args,
|
|
8760
|
+
issueProvider,
|
|
8761
|
+
logger
|
|
8762
|
+
);
|
|
8763
|
+
logger.raw("Issue created successfully:");
|
|
8764
|
+
logger.raw(` Issue Number: #${result.issueNumber}`);
|
|
8765
|
+
logger.raw(` Issue Title: ${result.issueTitle}`);
|
|
8766
|
+
logger.raw(` Issue URL: ${result.issueUrl}`);
|
|
8767
|
+
await logger.close();
|
|
8768
|
+
} catch (error) {
|
|
8769
|
+
await handleError(logger, error);
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
);
|
|
8773
|
+
issueCommand.argument("[value]", "issue number or subcommand").option("--target <name>", "target name").option("--force", "bypass issue gates").option(
|
|
7557
8774
|
"--reset-worktree",
|
|
7558
8775
|
"delete local issue worktree and branch before starting"
|
|
7559
8776
|
).option("--cwd <path>", "target repository directory").action(
|
|
7560
|
-
async (
|
|
8777
|
+
async (value, options) => {
|
|
8778
|
+
if (!value) {
|
|
8779
|
+
console.error(
|
|
8780
|
+
"Invalid command. Use 'issue <number>' or 'issue create --title ...'"
|
|
8781
|
+
);
|
|
8782
|
+
process.exit(1);
|
|
8783
|
+
}
|
|
8784
|
+
const issueNumberRaw = value;
|
|
7561
8785
|
const issueNumber = Number.parseInt(issueNumberRaw, 10);
|
|
7562
8786
|
if (Number.isNaN(issueNumber)) {
|
|
7563
8787
|
console.error(`Invalid issue number: ${issueNumberRaw}`);
|
|
7564
8788
|
process.exit(1);
|
|
7565
8789
|
}
|
|
8790
|
+
if (!options.target) {
|
|
8791
|
+
console.error(
|
|
8792
|
+
"error: required option '--target <name>' not specified"
|
|
8793
|
+
);
|
|
8794
|
+
process.exit(1);
|
|
8795
|
+
}
|
|
7566
8796
|
const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7567
8797
|
const config = await loadRepoConfig(targetRepoRoot);
|
|
7568
|
-
const logPath =
|
|
8798
|
+
const logPath = path8.join(
|
|
7569
8799
|
targetRepoRoot,
|
|
7570
8800
|
".pourkit",
|
|
7571
8801
|
"logs",
|
|
@@ -7624,7 +8854,7 @@ function createCliProgram(version) {
|
|
|
7624
8854
|
const prdRef = options.prd ? normalizePrdRef(options.prd) : void 0;
|
|
7625
8855
|
const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7626
8856
|
const config = await loadRepoConfig(targetRepoRoot);
|
|
7627
|
-
const logPath =
|
|
8857
|
+
const logPath = path8.join(
|
|
7628
8858
|
targetRepoRoot,
|
|
7629
8859
|
".pourkit",
|
|
7630
8860
|
"logs",
|
|
@@ -7721,6 +8951,35 @@ function createCliProgram(version) {
|
|
|
7721
8951
|
await runInitCommand(initOptions);
|
|
7722
8952
|
}
|
|
7723
8953
|
);
|
|
8954
|
+
const serena = program.command("serena").description("Serena baseline and sidecar commands");
|
|
8955
|
+
serena.command("init").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
|
|
8956
|
+
await runSerenaInitCommand({
|
|
8957
|
+
target: options.target,
|
|
8958
|
+
cwd: options.cwd
|
|
8959
|
+
});
|
|
8960
|
+
});
|
|
8961
|
+
serena.command("refresh").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
|
|
8962
|
+
await runSerenaRefreshCommand({
|
|
8963
|
+
target: options.target,
|
|
8964
|
+
cwd: options.cwd
|
|
8965
|
+
});
|
|
8966
|
+
});
|
|
8967
|
+
serena.command("start").option("--cwd <path>", "target repository directory").action(async (options) => {
|
|
8968
|
+
await runSerenaStartCommand({
|
|
8969
|
+
cwd: options.cwd
|
|
8970
|
+
});
|
|
8971
|
+
});
|
|
8972
|
+
serena.command("stop").option("--cwd <path>", "target repository directory").action(async (options) => {
|
|
8973
|
+
await runSerenaStopCommand({
|
|
8974
|
+
cwd: options.cwd
|
|
8975
|
+
});
|
|
8976
|
+
});
|
|
8977
|
+
serena.command("status").option("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
|
|
8978
|
+
await runSerenaStatusCommand({
|
|
8979
|
+
target: options.target,
|
|
8980
|
+
cwd: options.cwd
|
|
8981
|
+
});
|
|
8982
|
+
});
|
|
7724
8983
|
const pr = program.command("pr").description("Pull request commands");
|
|
7725
8984
|
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
8985
|
"--issue <number>",
|
|
@@ -7734,7 +8993,7 @@ function createCliProgram(version) {
|
|
|
7734
8993
|
async (options) => {
|
|
7735
8994
|
const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7736
8995
|
const config = await loadRepoConfig(targetRepoRoot);
|
|
7737
|
-
const logPath =
|
|
8996
|
+
const logPath = path8.join(
|
|
7738
8997
|
targetRepoRoot,
|
|
7739
8998
|
".pourkit",
|
|
7740
8999
|
"logs",
|
|
@@ -7780,7 +9039,7 @@ function createCliProgram(version) {
|
|
|
7780
9039
|
async (prNumber, options) => {
|
|
7781
9040
|
const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
|
|
7782
9041
|
const config = await loadRepoConfig(targetRepoRoot);
|
|
7783
|
-
const logPath =
|
|
9042
|
+
const logPath = path8.join(
|
|
7784
9043
|
targetRepoRoot,
|
|
7785
9044
|
".pourkit",
|
|
7786
9045
|
"logs",
|
|
@@ -7817,11 +9076,11 @@ function createCliProgram(version) {
|
|
|
7817
9076
|
return program;
|
|
7818
9077
|
}
|
|
7819
9078
|
async function resolveCliVersion() {
|
|
7820
|
-
if (isPackageVersion("0.0.0-next-
|
|
7821
|
-
return "0.0.0-next-
|
|
9079
|
+
if (isPackageVersion("0.0.0-next-20260531213458")) {
|
|
9080
|
+
return "0.0.0-next-20260531213458";
|
|
7822
9081
|
}
|
|
7823
|
-
if (isReleaseVersion("0.0.0-next-
|
|
7824
|
-
return "0.0.0-next-
|
|
9082
|
+
if (isReleaseVersion("0.0.0-next-20260531213458")) {
|
|
9083
|
+
return "0.0.0-next-20260531213458";
|
|
7825
9084
|
}
|
|
7826
9085
|
try {
|
|
7827
9086
|
const root = repoRoot();
|