ralphctl 0.4.2 → 0.4.4
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/README.md +13 -11
- package/dist/{add-CIM72NE3.mjs → add-DVPVHENV.mjs} +7 -7
- package/dist/{add-GX7P7XTT.mjs → add-YVXM34RP.mjs} +6 -5
- package/dist/{chunk-GL7MKLLS.mjs → chunk-ACRMBVEE.mjs} +458 -181
- package/dist/{chunk-NUYQK5MN.mjs → chunk-BSB4EDGR.mjs} +2 -2
- package/dist/{chunk-YCDUVPRT.mjs → chunk-CBMFRQ4Y.mjs} +5 -73
- package/dist/{chunk-3QBEBKMZ.mjs → chunk-FNAAA32W.mjs} +7 -7
- package/dist/{chunk-JOQO4HMM.mjs → chunk-GQ2WFKBN.mjs} +11 -11
- package/dist/{chunk-TKPTT2UG.mjs → chunk-OFILN7QL.mjs} +798 -1023
- package/dist/{chunk-7JLZQICD.mjs → chunk-OGEXYSFS.mjs} +7 -7
- package/dist/{chunk-D2YGPLIV.mjs → chunk-PYZEQ2VK.mjs} +214 -9
- package/dist/{chunk-57UWLHRH.mjs → chunk-VAZ3LJBI.mjs} +12 -1
- package/dist/{chunk-CTP2A436.mjs → chunk-WDMLPXOD.mjs} +11 -4
- package/dist/{chunk-FKMKOWLA.mjs → chunk-XN2UIHBY.mjs} +84 -3
- package/dist/chunk-ZLWSPLWI.mjs +1117 -0
- package/dist/cli.mjs +72 -21
- package/dist/create-Z635FQKO.mjs +15 -0
- package/dist/{handle-BBAZJ44Y.mjs → handle-23EFF3BE.mjs} +1 -1
- package/dist/{mount-ISHZM36X.mjs → mount-VEV3TESX.mjs} +1702 -1202
- package/dist/{project-2IE7VWDB.mjs → project-DQHF4ISP.mjs} +3 -3
- package/dist/prompts/check-script-discover.md +69 -0
- package/dist/prompts/repo-onboard.md +111 -0
- package/dist/prompts/sprint-feedback.md +4 -0
- package/dist/prompts/task-evaluation.md +44 -2
- package/dist/prompts/task-execution.md +5 -0
- package/dist/{resolver-EOE5WUMV.mjs → resolver-OVPYVW6Q.mjs} +4 -4
- package/dist/{sprint-OGOFEJJH.mjs → sprint-4E26AB5F.mjs} +4 -4
- package/dist/start-2WH4BTDB.mjs +19 -0
- package/package.json +6 -6
- package/dist/create-7WFSCMP4.mjs +0 -15
- package/dist/start-76JKJQIH.mjs +0 -17
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ProviderAiSessionAdapter,
|
|
4
|
+
SignalParser,
|
|
5
|
+
buildAutoPrompt,
|
|
6
|
+
buildEvaluatorPrompt,
|
|
7
|
+
buildIdeateAutoPrompt,
|
|
8
|
+
buildIdeatePrompt,
|
|
9
|
+
buildInteractivePrompt,
|
|
10
|
+
buildRepoOnboardPrompt,
|
|
11
|
+
buildSprintFeedbackPrompt,
|
|
12
|
+
buildTaskExecutionPrompt,
|
|
13
|
+
buildTicketRefinePrompt,
|
|
14
|
+
getActiveProvider,
|
|
15
|
+
spawnInteractive
|
|
16
|
+
} from "./chunk-ZLWSPLWI.mjs";
|
|
2
17
|
import {
|
|
3
18
|
fetchIssueFromUrl,
|
|
4
19
|
formatIssueContext,
|
|
5
20
|
formatTicketDisplay,
|
|
6
21
|
truncate
|
|
7
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-GQ2WFKBN.mjs";
|
|
8
23
|
import {
|
|
9
24
|
EXIT_ERROR,
|
|
10
|
-
EXIT_INTERRUPTED,
|
|
11
25
|
EXIT_NO_TASKS,
|
|
12
26
|
exitWithCode
|
|
13
27
|
} from "./chunk-CFUVE2BP.mjs";
|
|
@@ -15,17 +29,18 @@ import {
|
|
|
15
29
|
getPrompt,
|
|
16
30
|
getSharedDeps
|
|
17
31
|
} from "./chunk-747KW2RW.mjs";
|
|
32
|
+
import {
|
|
33
|
+
updateProject
|
|
34
|
+
} from "./chunk-BSB4EDGR.mjs";
|
|
18
35
|
import {
|
|
19
36
|
assertSprintStatus,
|
|
20
37
|
closeSprint,
|
|
21
|
-
getAiProvider,
|
|
22
38
|
getSprint,
|
|
23
39
|
resolveSprintId,
|
|
24
|
-
setAiProvider,
|
|
25
40
|
withFileLock
|
|
26
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-CBMFRQ4Y.mjs";
|
|
27
42
|
import {
|
|
28
|
-
|
|
43
|
+
isTTY,
|
|
29
44
|
log,
|
|
30
45
|
printHeader,
|
|
31
46
|
renderTable,
|
|
@@ -35,13 +50,14 @@ import {
|
|
|
35
50
|
showSuccess,
|
|
36
51
|
showWarning,
|
|
37
52
|
terminalBell
|
|
38
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-XN2UIHBY.mjs";
|
|
39
54
|
import {
|
|
40
55
|
ensureError,
|
|
41
56
|
unwrapOrThrow,
|
|
42
57
|
wrapAsync
|
|
43
58
|
} from "./chunk-IWXBJD2D.mjs";
|
|
44
59
|
import {
|
|
60
|
+
CURRENT_ONBOARDING_VERSION,
|
|
45
61
|
IdeateOutputSchema,
|
|
46
62
|
ImportTasksSchema,
|
|
47
63
|
RefinedRequirementsSchema,
|
|
@@ -53,12 +69,11 @@ import {
|
|
|
53
69
|
getTasksFilePath,
|
|
54
70
|
readValidatedJson,
|
|
55
71
|
writeValidatedJson
|
|
56
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-WDMLPXOD.mjs";
|
|
57
73
|
import {
|
|
58
74
|
BranchPreflightError,
|
|
59
75
|
DependencyCycleError,
|
|
60
76
|
DomainError,
|
|
61
|
-
IOError,
|
|
62
77
|
ParseError,
|
|
63
78
|
ProjectNotFoundError,
|
|
64
79
|
SpawnError,
|
|
@@ -67,7 +82,7 @@ import {
|
|
|
67
82
|
StepError,
|
|
68
83
|
StorageError,
|
|
69
84
|
TaskNotFoundError
|
|
70
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-VAZ3LJBI.mjs";
|
|
71
86
|
|
|
72
87
|
// src/integration/persistence/task.ts
|
|
73
88
|
async function getTasks(sprintId) {
|
|
@@ -372,7 +387,9 @@ function unwrapError(result) {
|
|
|
372
387
|
async function executePipeline(pipeline2, initialContext) {
|
|
373
388
|
let ctx = { ...initialContext };
|
|
374
389
|
const stepResults = [];
|
|
390
|
+
let stepsRun = 0;
|
|
375
391
|
for (const step2 of pipeline2.steps) {
|
|
392
|
+
if (stepsRun > 0 && ctx.abortSignal?.aborted) break;
|
|
376
393
|
const startTime = Date.now();
|
|
377
394
|
try {
|
|
378
395
|
if (step2.hooks?.pre) {
|
|
@@ -425,6 +442,7 @@ async function executePipeline(pipeline2, initialContext) {
|
|
|
425
442
|
status: "success",
|
|
426
443
|
durationMs: Date.now() - startTime
|
|
427
444
|
});
|
|
445
|
+
stepsRun++;
|
|
428
446
|
} catch (err) {
|
|
429
447
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
430
448
|
stepResults.push({
|
|
@@ -1359,14 +1377,16 @@ async function runScheduler(opts, ctx) {
|
|
|
1359
1377
|
const itemKey = opts.itemKey ?? DEFAULT_ITEM_KEY;
|
|
1360
1378
|
const maxConcurrency = opts.strategy.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY;
|
|
1361
1379
|
const mutexKeyFn = opts.strategy.mutexKey ?? identityMutexKey;
|
|
1362
|
-
const
|
|
1380
|
+
const abortSignal = ctx.abortSignal;
|
|
1381
|
+
const services = opts.createServices?.({ abortSignal }) ?? defaultServices();
|
|
1363
1382
|
const state = {
|
|
1364
1383
|
stats: {
|
|
1365
1384
|
completed: 0,
|
|
1366
1385
|
failed: 0,
|
|
1367
1386
|
requeued: 0,
|
|
1368
1387
|
inFlight: 0,
|
|
1369
|
-
pausedRepos: /* @__PURE__ */ new Set()
|
|
1388
|
+
pausedRepos: /* @__PURE__ */ new Set(),
|
|
1389
|
+
cancelled: false
|
|
1370
1390
|
},
|
|
1371
1391
|
retryNowQueue: [],
|
|
1372
1392
|
requeueQueue: [],
|
|
@@ -1380,6 +1400,10 @@ async function runScheduler(opts, ctx) {
|
|
|
1380
1400
|
let concurrencyLimit = -1;
|
|
1381
1401
|
try {
|
|
1382
1402
|
for (; ; ) {
|
|
1403
|
+
if (abortSignal?.aborted) {
|
|
1404
|
+
state.stats.cancelled = true;
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1383
1407
|
if (state.stopRequested) break;
|
|
1384
1408
|
if (state.terminalError) break;
|
|
1385
1409
|
if (opts.strategy.stopWhen?.(state.stats)) break;
|
|
@@ -1419,6 +1443,11 @@ async function runScheduler(opts, ctx) {
|
|
|
1419
1443
|
}
|
|
1420
1444
|
continue;
|
|
1421
1445
|
}
|
|
1446
|
+
if (abortSignal?.aborted) {
|
|
1447
|
+
state.stats.cancelled = true;
|
|
1448
|
+
opts.policies.onSettle?.(item, "failed");
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1422
1451
|
const attempt = (state.attempts.get(item) ?? 0) + 1;
|
|
1423
1452
|
state.attempts.set(item, attempt);
|
|
1424
1453
|
const action = opts.policies.retryPolicy(item, error, attempt);
|
|
@@ -1427,6 +1456,9 @@ async function runScheduler(opts, ctx) {
|
|
|
1427
1456
|
if (state.terminalError && state.shouldDrainOnFail && state.inFlight.size > 0) {
|
|
1428
1457
|
await Promise.allSettled(state.inFlight.values());
|
|
1429
1458
|
}
|
|
1459
|
+
if (state.stats.cancelled && state.inFlight.size > 0) {
|
|
1460
|
+
await Promise.allSettled(state.inFlight.values());
|
|
1461
|
+
}
|
|
1430
1462
|
} finally {
|
|
1431
1463
|
if (opts.disposeServices) {
|
|
1432
1464
|
opts.disposeServices(services);
|
|
@@ -1807,7 +1839,8 @@ ${instructions}`;
|
|
|
1807
1839
|
args,
|
|
1808
1840
|
env: this.aiSession.getSpawnEnv(),
|
|
1809
1841
|
maxRetries: options?.maxRetries,
|
|
1810
|
-
resumeSessionId: options?.resumeSessionId
|
|
1842
|
+
resumeSessionId: options?.resumeSessionId,
|
|
1843
|
+
abortSignal: options?.abortSignal
|
|
1811
1844
|
});
|
|
1812
1845
|
spinner.succeed(`${this.aiSession.getProviderDisplayName()} completed: ${task.name}`);
|
|
1813
1846
|
const ctx = { sprintId: sprint.id, taskId: task.id, projectPath: repoPath };
|
|
@@ -1944,7 +1977,8 @@ ${instructions}`;
|
|
|
1944
1977
|
cwd: repoPath,
|
|
1945
1978
|
args: ["--add-dir", sprintDir],
|
|
1946
1979
|
env: this.aiSession.getSpawnEnv(),
|
|
1947
|
-
maxTurns: options?.maxTurns
|
|
1980
|
+
maxTurns: options?.maxTurns,
|
|
1981
|
+
abortSignal: options?.abortSignal
|
|
1948
1982
|
});
|
|
1949
1983
|
spinner.succeed(`${this.aiSession.getProviderDisplayName()} completed: ${syntheticTask.name}`);
|
|
1950
1984
|
const ctx = { sprintId: sprint.id, taskId: syntheticTask.id, projectPath: repoPath };
|
|
@@ -2139,6 +2173,10 @@ ${repoPath}`);
|
|
|
2139
2173
|
case "note":
|
|
2140
2174
|
await this.signalHandler.handleNote(signal, ctx);
|
|
2141
2175
|
break;
|
|
2176
|
+
case "check-script-discovery":
|
|
2177
|
+
break;
|
|
2178
|
+
case "agents-md-proposal":
|
|
2179
|
+
break;
|
|
2142
2180
|
default: {
|
|
2143
2181
|
const _exhaustive = signal;
|
|
2144
2182
|
void _exhaustive;
|
|
@@ -2286,7 +2324,8 @@ function executeTask(deps) {
|
|
|
2286
2324
|
const result = await deps.useCase.executeOneTask(task, sprint, {
|
|
2287
2325
|
...deps.options,
|
|
2288
2326
|
...resumeSessionId ? { resumeSessionId } : {},
|
|
2289
|
-
...ctx.contractPath ? { contractPath: ctx.contractPath } : {}
|
|
2327
|
+
...ctx.contractPath ? { contractPath: ctx.contractPath } : {},
|
|
2328
|
+
...ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}
|
|
2290
2329
|
});
|
|
2291
2330
|
if (!result.success) {
|
|
2292
2331
|
return Result.error(new ParseError(`Task not completed: ${result.blocked ?? "Unknown reason"}`));
|
|
@@ -2537,7 +2576,8 @@ var EvaluateTaskUseCase = class {
|
|
|
2537
2576
|
result = await this.aiSession.spawnWithRetry(prompt, {
|
|
2538
2577
|
cwd: repoPath,
|
|
2539
2578
|
args,
|
|
2540
|
-
env: this.aiSession.getSpawnEnv()
|
|
2579
|
+
env: this.aiSession.getSpawnEnv(),
|
|
2580
|
+
abortSignal: options?.abortSignal
|
|
2541
2581
|
});
|
|
2542
2582
|
} catch (err) {
|
|
2543
2583
|
this.logger.warning(
|
|
@@ -2595,7 +2635,8 @@ var EvaluateTaskUseCase = class {
|
|
|
2595
2635
|
cwd: repoPath,
|
|
2596
2636
|
args: ["--add-dir", sprintDir],
|
|
2597
2637
|
env: this.aiSession.getSpawnEnv(),
|
|
2598
|
-
maxTurns: options?.maxTurns
|
|
2638
|
+
maxTurns: options?.maxTurns,
|
|
2639
|
+
abortSignal: options?.abortSignal
|
|
2599
2640
|
});
|
|
2600
2641
|
spinner.succeed(`Fix attempt completed: ${task.name}`);
|
|
2601
2642
|
const signals = this.parser.parseExecutionSignals(result.output);
|
|
@@ -2714,7 +2755,8 @@ function runEvaluatorLoopStep(useCase, options) {
|
|
|
2714
2755
|
const result = await useCase.execute(ctx.sprintId, ctx.taskId, {
|
|
2715
2756
|
iterations: options.iterations,
|
|
2716
2757
|
maxTurns: options.maxTurns,
|
|
2717
|
-
fallbackModel: ctx.generatorModel ?? void 0
|
|
2758
|
+
fallbackModel: ctx.generatorModel ?? void 0,
|
|
2759
|
+
abortSignal: ctx.abortSignal ?? options.abortSignal
|
|
2718
2760
|
});
|
|
2719
2761
|
if (!result.ok) {
|
|
2720
2762
|
return Result.error(result.error);
|
|
@@ -2763,13 +2805,15 @@ function evaluateTask(deps) {
|
|
|
2763
2805
|
},
|
|
2764
2806
|
{
|
|
2765
2807
|
iterations: evalCfg.iterations,
|
|
2766
|
-
maxTurns: deps.options.maxTurns
|
|
2808
|
+
maxTurns: deps.options.maxTurns,
|
|
2809
|
+
abortSignal: ctx.abortSignal
|
|
2767
2810
|
}
|
|
2768
2811
|
);
|
|
2769
2812
|
const innerCtx = {
|
|
2770
2813
|
sprintId: ctx.sprint.id,
|
|
2771
2814
|
taskId: ctx.task.id,
|
|
2772
|
-
generatorModel: ctx.generatorModel ?? null
|
|
2815
|
+
generatorModel: ctx.generatorModel ?? null,
|
|
2816
|
+
abortSignal: ctx.abortSignal
|
|
2773
2817
|
};
|
|
2774
2818
|
let stepNames = [];
|
|
2775
2819
|
try {
|
|
@@ -2915,11 +2959,59 @@ function withStepTrace(signalBus) {
|
|
|
2915
2959
|
});
|
|
2916
2960
|
}
|
|
2917
2961
|
|
|
2962
|
+
// src/business/pipelines/execute/resolve-dirty-tree.ts
|
|
2963
|
+
async function resolveDirtyTree(deps) {
|
|
2964
|
+
const { repoPath, options, prompt, isTTY: isTTY2, logger, external } = deps;
|
|
2965
|
+
let dirty;
|
|
2966
|
+
try {
|
|
2967
|
+
dirty = external.hasUncommittedChanges(repoPath);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
if (!dirty) return;
|
|
2972
|
+
if (options.resetOnResume) {
|
|
2973
|
+
logger.warning(`Resetting working tree to HEAD in ${repoPath}...`);
|
|
2974
|
+
external.hardResetWorkingTree(repoPath);
|
|
2975
|
+
logger.success(`Working tree reset in ${repoPath}`);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (options.resumeDirty) {
|
|
2979
|
+
logger.info(`Resuming with existing changes in ${repoPath}`);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (isTTY2) {
|
|
2983
|
+
const keepDirty = await prompt.confirm({
|
|
2984
|
+
message: `Repository at ${repoPath} has uncommitted changes. Resume with existing changes?`,
|
|
2985
|
+
default: true
|
|
2986
|
+
});
|
|
2987
|
+
if (keepDirty) {
|
|
2988
|
+
logger.info(`Resuming with existing changes in ${repoPath}`);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const doReset = await prompt.confirm({
|
|
2992
|
+
message: "Reset to latest commit and resume?",
|
|
2993
|
+
default: false
|
|
2994
|
+
});
|
|
2995
|
+
if (doReset) {
|
|
2996
|
+
logger.warning(`Resetting working tree to HEAD in ${repoPath}...`);
|
|
2997
|
+
external.hardResetWorkingTree(repoPath);
|
|
2998
|
+
logger.success(`Working tree reset in ${repoPath}`);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
throw new StorageError("Aborted: commit, stash, or discard changes before resuming.");
|
|
3002
|
+
}
|
|
3003
|
+
throw new StorageError(
|
|
3004
|
+
`Repository at ${repoPath} has uncommitted changes. Commit or stash them before starting.
|
|
3005
|
+
Hint: pass --resume-dirty to resume with the changes intact, or --reset-on-resume to discard them.`
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2918
3009
|
// src/business/pipelines/execute.ts
|
|
2919
3010
|
var EXIT_SUCCESS = 0;
|
|
2920
3011
|
var EXIT_ERROR2 = 1;
|
|
2921
3012
|
var EXIT_NO_TASKS2 = 2;
|
|
2922
3013
|
var EXIT_ALL_BLOCKED = 3;
|
|
3014
|
+
var EXIT_INTERRUPTED = 130;
|
|
2923
3015
|
var MAX_CONCURRENCY = 10;
|
|
2924
3016
|
var MAX_BRANCH_RETRIES = 3;
|
|
2925
3017
|
function checkPreconditionsStep(persistence, ui, logger, options) {
|
|
@@ -3093,7 +3185,7 @@ function prepareTasksStep(persistence) {
|
|
|
3093
3185
|
}
|
|
3094
3186
|
});
|
|
3095
3187
|
}
|
|
3096
|
-
function ensureBranchesStep(external, persistence, logger) {
|
|
3188
|
+
function ensureBranchesStep(external, persistence, logger, prompt, isTTY2, options) {
|
|
3097
3189
|
return step("ensure-branches", async (ctx) => {
|
|
3098
3190
|
if (ctx.proceedAfterPrecondition === false || ctx.tasksEmpty) {
|
|
3099
3191
|
const empty = {};
|
|
@@ -3128,17 +3220,14 @@ function ensureBranchesStep(external, persistence, logger) {
|
|
|
3128
3220
|
}
|
|
3129
3221
|
try {
|
|
3130
3222
|
for (const projectPath of uniquePaths) {
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
} catch (err) {
|
|
3140
|
-
if (err instanceof StorageError) return Result.error(err);
|
|
3141
|
-
}
|
|
3223
|
+
await resolveDirtyTree({
|
|
3224
|
+
repoPath: projectPath,
|
|
3225
|
+
options,
|
|
3226
|
+
prompt,
|
|
3227
|
+
isTTY: isTTY2(),
|
|
3228
|
+
logger,
|
|
3229
|
+
external
|
|
3230
|
+
});
|
|
3142
3231
|
}
|
|
3143
3232
|
for (const projectPath of uniquePaths) {
|
|
3144
3233
|
const currentBranch = external.getCurrentBranch(projectPath);
|
|
@@ -3210,6 +3299,7 @@ function executeTasksStep(deps, options) {
|
|
|
3210
3299
|
);
|
|
3211
3300
|
const taskSessionIds = /* @__PURE__ */ new Map();
|
|
3212
3301
|
const failedRepos = /* @__PURE__ */ new Set();
|
|
3302
|
+
const launchedTaskIds = /* @__PURE__ */ new Set();
|
|
3213
3303
|
let firstBlockedReason = null;
|
|
3214
3304
|
const forceSequential = options.session === true || options.step === true;
|
|
3215
3305
|
const uniqueRepoIds = new Set(allTasks.map((t) => t.repoId));
|
|
@@ -3342,6 +3432,7 @@ function executeTasksStep(deps, options) {
|
|
|
3342
3432
|
deps.signalBus.emit({ type: "rate-limit-resumed", timestamp: /* @__PURE__ */ new Date() });
|
|
3343
3433
|
},
|
|
3344
3434
|
onLaunch: (task) => {
|
|
3435
|
+
launchedTaskIds.add(task.id);
|
|
3345
3436
|
const resumeId = taskSessionIds.get(task.id);
|
|
3346
3437
|
const action = resumeId ? "Resuming" : "Starting";
|
|
3347
3438
|
deps.logger.info(`--- ${action} task ${String(task.order)}: ${task.name} ---`);
|
|
@@ -3368,9 +3459,36 @@ function executeTasksStep(deps, options) {
|
|
|
3368
3459
|
failed: 0,
|
|
3369
3460
|
requeued: 0,
|
|
3370
3461
|
inFlight: 0,
|
|
3371
|
-
pausedRepos: /* @__PURE__ */ new Set()
|
|
3462
|
+
pausedRepos: /* @__PURE__ */ new Set(),
|
|
3463
|
+
cancelled: false
|
|
3372
3464
|
};
|
|
3373
3465
|
const stats = schedResult.ok ? schedResult.value.schedulerStats ?? emptyStats : emptyStats;
|
|
3466
|
+
if (stats.cancelled && launchedTaskIds.size > 0) {
|
|
3467
|
+
try {
|
|
3468
|
+
const currentTasks = await deps.persistence.getTasks(sprint.id);
|
|
3469
|
+
const toCancel = currentTasks.filter((t) => launchedTaskIds.has(t.id) && t.status === "in_progress");
|
|
3470
|
+
if (toCancel.length > 0) {
|
|
3471
|
+
const updated = currentTasks.map(
|
|
3472
|
+
(t) => launchedTaskIds.has(t.id) && t.status === "in_progress" ? { ...t, status: "cancelled" } : t
|
|
3473
|
+
);
|
|
3474
|
+
await deps.persistence.saveTasks(updated, sprint.id);
|
|
3475
|
+
for (const t of toCancel) {
|
|
3476
|
+
deps.signalBus.emit({
|
|
3477
|
+
type: "task-finished",
|
|
3478
|
+
sprintId: sprint.id,
|
|
3479
|
+
taskId: t.id,
|
|
3480
|
+
status: "cancelled",
|
|
3481
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
deps.logger.warning(`Cancelled ${String(toCancel.length)} in-progress task(s).`);
|
|
3485
|
+
}
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
deps.logger.warning(
|
|
3488
|
+
`Failed to flip in-progress tasks to cancelled: ${err instanceof Error ? err.message : String(err)}`
|
|
3489
|
+
);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3374
3492
|
const summary = await buildExecutionSummary({
|
|
3375
3493
|
persistence: deps.persistence,
|
|
3376
3494
|
sprintId: sprint.id,
|
|
@@ -3424,6 +3542,15 @@ async function buildExecutionSummary(args) {
|
|
|
3424
3542
|
const remaining = await persistence.getRemainingTasks(sprintId);
|
|
3425
3543
|
const currentTasks = await persistence.getTasks(sprintId);
|
|
3426
3544
|
const blocked = remaining.filter((t) => isBlocked(t, currentTasks));
|
|
3545
|
+
if (stats.cancelled) {
|
|
3546
|
+
return {
|
|
3547
|
+
completed: stats.completed,
|
|
3548
|
+
remaining: remaining.length,
|
|
3549
|
+
blocked: blocked.length,
|
|
3550
|
+
stopReason: "cancelled",
|
|
3551
|
+
exitCode: EXIT_INTERRUPTED
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3427
3554
|
if (failedRepos.size > 0) {
|
|
3428
3555
|
logger.warning(`Repos with failed checks: ${[...failedRepos].join(", ")}`);
|
|
3429
3556
|
}
|
|
@@ -3544,850 +3671,322 @@ function createExecuteSprintPipeline(deps, options = {}) {
|
|
|
3544
3671
|
autoActivateStep(deps.persistence),
|
|
3545
3672
|
assertActiveStep(),
|
|
3546
3673
|
prepareTasksStep(deps.persistence),
|
|
3547
|
-
ensureBranchesStep(deps.external, deps.persistence, deps.logger),
|
|
3674
|
+
ensureBranchesStep(deps.external, deps.persistence, deps.logger, deps.prompt, deps.isTTY, options),
|
|
3548
3675
|
sprintStartCheckStep(deps.external, deps.persistence, deps.logger, options),
|
|
3549
3676
|
executeTasksStep(deps, options),
|
|
3550
3677
|
feedbackLoopStep(deps, options)
|
|
3551
3678
|
]);
|
|
3552
3679
|
}
|
|
3553
3680
|
|
|
3554
|
-
// src/
|
|
3555
|
-
|
|
3681
|
+
// src/business/pipelines/steps/validate-agents-md.ts
|
|
3682
|
+
function validateAgentsMdStep(adapter) {
|
|
3683
|
+
return step("validate-agents-md", (ctx) => {
|
|
3684
|
+
const draft = ctx.agentsMdDraft;
|
|
3685
|
+
if (!draft || draft.trim().length === 0) {
|
|
3686
|
+
return Result.error(new ParseError("Project context file draft is empty \u2014 AI discovery produced no content."));
|
|
3687
|
+
}
|
|
3688
|
+
const { violations } = adapter.lintAgentsMd(draft);
|
|
3689
|
+
const partial = { agentsMdViolations: violations };
|
|
3690
|
+
return Result.ok(partial);
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3556
3693
|
|
|
3557
|
-
// src/
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
sigintHandler = null;
|
|
3574
|
-
sigtermHandler = null;
|
|
3575
|
-
constructor() {
|
|
3576
|
-
}
|
|
3577
|
-
/**
|
|
3578
|
-
* Get the singleton instance.
|
|
3579
|
-
*/
|
|
3580
|
-
static getInstance() {
|
|
3581
|
-
_ProcessManager.instance ??= new _ProcessManager();
|
|
3582
|
-
return _ProcessManager.instance;
|
|
3583
|
-
}
|
|
3584
|
-
/**
|
|
3585
|
-
* Reset the singleton for testing.
|
|
3586
|
-
* @internal
|
|
3587
|
-
*/
|
|
3588
|
-
static resetForTesting() {
|
|
3589
|
-
if (_ProcessManager.instance) {
|
|
3590
|
-
_ProcessManager.instance.dispose();
|
|
3591
|
-
_ProcessManager.instance = null;
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
/**
|
|
3595
|
-
* Register a child process for tracking.
|
|
3596
|
-
* Automatically installs signal handlers on first registration.
|
|
3597
|
-
* Throws an error if called during shutdown.
|
|
3598
|
-
*
|
|
3599
|
-
* @throws Error if called during shutdown
|
|
3600
|
-
*/
|
|
3601
|
-
registerChild(child) {
|
|
3602
|
-
if (this.exiting) {
|
|
3603
|
-
throw new Error("Cannot register child process during shutdown");
|
|
3604
|
-
}
|
|
3605
|
-
this.children.add(child);
|
|
3606
|
-
child.once("close", () => {
|
|
3607
|
-
this.children.delete(child);
|
|
3608
|
-
});
|
|
3609
|
-
if (!this.handlersInstalled) {
|
|
3610
|
-
this.installSignalHandlers();
|
|
3611
|
-
this.handlersInstalled = true;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
/**
|
|
3615
|
-
* Eagerly install signal handlers without requiring a child registration.
|
|
3616
|
-
* Call this at the top of execution loops so Ctrl+C works even before
|
|
3617
|
-
* the first AI process is spawned (e.g. while the spinner is visible).
|
|
3618
|
-
* Idempotent — safe to call multiple times.
|
|
3619
|
-
*/
|
|
3620
|
-
ensureHandlers() {
|
|
3621
|
-
if (!this.handlersInstalled) {
|
|
3622
|
-
this.installSignalHandlers();
|
|
3623
|
-
this.handlersInstalled = true;
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
/**
|
|
3627
|
-
* Check if a shutdown is in progress.
|
|
3628
|
-
* Used by execution loops to break immediately on Ctrl+C.
|
|
3629
|
-
*/
|
|
3630
|
-
isShuttingDown() {
|
|
3631
|
-
return this.exiting;
|
|
3632
|
-
}
|
|
3633
|
-
/**
|
|
3634
|
-
* Manually unregister a child process.
|
|
3635
|
-
* Normally not needed - children auto-unregister via event listeners.
|
|
3636
|
-
*/
|
|
3637
|
-
unregisterChild(child) {
|
|
3638
|
-
this.children.delete(child);
|
|
3639
|
-
}
|
|
3640
|
-
/**
|
|
3641
|
-
* Register a cleanup callback (for spinners, temp files, etc.).
|
|
3642
|
-
* Returns a deregister function.
|
|
3643
|
-
*/
|
|
3644
|
-
registerCleanup(callback) {
|
|
3645
|
-
this.cleanupCallbacks.add(callback);
|
|
3646
|
-
return () => {
|
|
3647
|
-
this.cleanupCallbacks.delete(callback);
|
|
3648
|
-
};
|
|
3649
|
-
}
|
|
3650
|
-
/**
|
|
3651
|
-
* Kill all tracked child processes with the given signal.
|
|
3652
|
-
* Catches errors (ESRCH = already dead, EPERM = permission denied).
|
|
3653
|
-
*/
|
|
3654
|
-
killAll(signal) {
|
|
3655
|
-
for (const child of this.children) {
|
|
3656
|
-
try {
|
|
3657
|
-
child.kill(signal);
|
|
3658
|
-
} catch (err) {
|
|
3659
|
-
const error = err;
|
|
3660
|
-
if (error.code === "ESRCH") {
|
|
3661
|
-
this.children.delete(child);
|
|
3662
|
-
} else if (error.code === "EPERM") {
|
|
3663
|
-
log.warn(`Permission denied killing process ${String(child.pid)}`);
|
|
3664
|
-
} else {
|
|
3665
|
-
log.error(`Error killing process ${String(child.pid)}: ${error.message}`);
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
/**
|
|
3671
|
-
* Graceful shutdown sequence:
|
|
3672
|
-
* 1. Run all cleanup callbacks (stop spinners)
|
|
3673
|
-
* 2. Send SIGINT to all children (what AI CLI processes expect)
|
|
3674
|
-
* 3. Wait up to 5 seconds for children to exit
|
|
3675
|
-
* 4. Send SIGKILL to any remaining children (force)
|
|
3676
|
-
* 5. Exit with code 130 (SIGINT) or 1 (force-quit)
|
|
3677
|
-
*
|
|
3678
|
-
* Double Ctrl+C: immediate SIGKILL + exit(1)
|
|
3679
|
-
*/
|
|
3680
|
-
async shutdown(signal) {
|
|
3681
|
-
if (signal === "SIGINT" && this.firstSigintAt) {
|
|
3682
|
-
const now = Date.now();
|
|
3683
|
-
if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
|
|
3684
|
-
log.warn("\n\nForce quit (double signal) \u2014 killing all processes immediately...");
|
|
3685
|
-
this.killAll("SIGKILL");
|
|
3686
|
-
process.exit(1);
|
|
3687
|
-
return;
|
|
3694
|
+
// src/business/pipelines/onboard.ts
|
|
3695
|
+
function providerInstructionsFileName(provider) {
|
|
3696
|
+
if (provider === "claude") return "CLAUDE.md";
|
|
3697
|
+
return ".github/copilot-instructions.md";
|
|
3698
|
+
}
|
|
3699
|
+
function loadProjectStep(deps) {
|
|
3700
|
+
return step("load-project", async (ctx) => {
|
|
3701
|
+
try {
|
|
3702
|
+
const project = await deps.persistence.getProject(ctx.projectName);
|
|
3703
|
+
const config = await deps.persistence.getConfig();
|
|
3704
|
+
if (!config.aiProvider) {
|
|
3705
|
+
return Result.error(
|
|
3706
|
+
new ParseError(
|
|
3707
|
+
"No AI provider configured \u2014 run `ralphctl config set provider <claude|copilot>` before onboarding."
|
|
3708
|
+
)
|
|
3709
|
+
);
|
|
3688
3710
|
}
|
|
3711
|
+
const partial = { project, provider: config.aiProvider };
|
|
3712
|
+
return Result.ok(partial);
|
|
3713
|
+
} catch (err) {
|
|
3714
|
+
if (err instanceof ProjectNotFoundError) return Result.error(err);
|
|
3715
|
+
return Result.error(new ParseError(err instanceof Error ? err.message : String(err)));
|
|
3689
3716
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
log.error(`Error in cleanup callback: ${err instanceof Error ? err.message : String(err)}`);
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
function selectRepoStep(deps, options) {
|
|
3720
|
+
return step("select-repo", async (ctx) => {
|
|
3721
|
+
const project = ctx.project;
|
|
3722
|
+
if (!project) return Result.error(new ParseError("Project not loaded."));
|
|
3723
|
+
const repos = project.repositories;
|
|
3724
|
+
if (repos.length === 0) return Result.error(new ParseError("Project has no repositories."));
|
|
3725
|
+
if (options.repo) {
|
|
3726
|
+
const match = repos.find((r) => r.name === options.repo);
|
|
3727
|
+
if (!match) {
|
|
3728
|
+
return Result.error(new ParseError(`No repository named "${options.repo}" in project "${project.name}".`));
|
|
3703
3729
|
}
|
|
3730
|
+
return Result.ok({ repo: match });
|
|
3704
3731
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3732
|
+
if (repos.length === 1) {
|
|
3733
|
+
const only = repos[0];
|
|
3734
|
+
if (!only) return Result.error(new ParseError("Project has no repositories."));
|
|
3735
|
+
return Result.ok({ repo: only });
|
|
3710
3736
|
}
|
|
3711
|
-
if (
|
|
3712
|
-
|
|
3713
|
-
|
|
3737
|
+
if (options.auto) {
|
|
3738
|
+
const first = repos[0];
|
|
3739
|
+
if (!first) return Result.error(new ParseError("Project has no repositories."));
|
|
3740
|
+
return Result.ok({ repo: first });
|
|
3714
3741
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3742
|
+
const choice = await deps.prompt.select({
|
|
3743
|
+
message: `Select a repository to onboard in "${project.name}":`,
|
|
3744
|
+
choices: repos.map((r) => ({ label: `${r.name} \u2014 ${r.path}`, value: r.id }))
|
|
3745
|
+
});
|
|
3746
|
+
const selected = repos.find((r) => r.id === choice);
|
|
3747
|
+
if (!selected) return Result.error(new ParseError("Invalid repository selection."));
|
|
3748
|
+
return Result.ok({ repo: selected });
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
function repoPreflightStep(deps) {
|
|
3752
|
+
return step("repo-preflight", (ctx) => {
|
|
3753
|
+
const repo = ctx.repo;
|
|
3754
|
+
const provider = ctx.provider;
|
|
3755
|
+
if (!repo) return Result.error(new ParseError("Repository not resolved."));
|
|
3756
|
+
if (!provider) return Result.error(new ParseError("AI provider not resolved."));
|
|
3757
|
+
const validation = deps.adapter.validateRepoPath(repo.path);
|
|
3758
|
+
if (!validation.exists) {
|
|
3759
|
+
return Result.error(new ParseError(`Repository path does not exist or is not a directory: ${repo.path}`));
|
|
3760
|
+
}
|
|
3761
|
+
if (!validation.isGitRepo) {
|
|
3762
|
+
return Result.error(new ParseError(`Repository is not a git repository: ${repo.path}`));
|
|
3763
|
+
}
|
|
3764
|
+
const existing = deps.adapter.readExistingInstructions(repo.path, provider);
|
|
3765
|
+
let mode;
|
|
3766
|
+
if (existing.content === null) {
|
|
3767
|
+
mode = "bootstrap";
|
|
3768
|
+
} else if (repo.onboardingVersion != null) {
|
|
3769
|
+
mode = "update";
|
|
3770
|
+
} else {
|
|
3771
|
+
mode = "adopt";
|
|
3729
3772
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
this.handlersInstalled = false;
|
|
3734
|
-
this.firstSigintAt = null;
|
|
3735
|
-
}
|
|
3736
|
-
/**
|
|
3737
|
-
* Install signal handlers for SIGINT and SIGTERM.
|
|
3738
|
-
* Uses process.on() (persistent) not process.once() (one-shot).
|
|
3739
|
-
* Stores handler references so dispose() can remove them.
|
|
3740
|
-
*/
|
|
3741
|
-
installSignalHandlers() {
|
|
3742
|
-
this.sigintHandler = () => {
|
|
3743
|
-
void this.shutdown("SIGINT");
|
|
3744
|
-
};
|
|
3745
|
-
this.sigtermHandler = () => {
|
|
3746
|
-
void this.shutdown("SIGTERM");
|
|
3747
|
-
};
|
|
3748
|
-
process.on("SIGINT", this.sigintHandler);
|
|
3749
|
-
process.on("SIGTERM", this.sigtermHandler);
|
|
3750
|
-
}
|
|
3751
|
-
};
|
|
3752
|
-
var processLifecycleAdapter = {
|
|
3753
|
-
ensureHandlers: () => {
|
|
3754
|
-
ProcessManager.getInstance().ensureHandlers();
|
|
3755
|
-
},
|
|
3756
|
-
isShuttingDown: () => ProcessManager.getInstance().isShuttingDown()
|
|
3757
|
-
};
|
|
3758
|
-
|
|
3759
|
-
// src/integration/ai/providers/claude.ts
|
|
3760
|
-
import { Result as Result2 } from "typescript-result";
|
|
3761
|
-
var claudeAdapter = {
|
|
3762
|
-
name: "claude",
|
|
3763
|
-
displayName: "Claude",
|
|
3764
|
-
binary: "claude",
|
|
3765
|
-
baseArgs: ["--permission-mode", "acceptEdits", "--effort", "xhigh"],
|
|
3766
|
-
experimental: false,
|
|
3767
|
-
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
3768
|
-
return [...this.baseArgs, ...extraArgs, "--", prompt];
|
|
3769
|
-
},
|
|
3770
|
-
buildHeadlessArgs(extraArgs = []) {
|
|
3771
|
-
return ["-p", "--output-format", "json", ...this.baseArgs, ...extraArgs];
|
|
3772
|
-
},
|
|
3773
|
-
parseJsonOutput(stdout) {
|
|
3774
|
-
const jsonResult = Result2.try(() => JSON.parse(stdout));
|
|
3775
|
-
if (!jsonResult.ok) {
|
|
3776
|
-
return { result: stdout, sessionId: null, model: null };
|
|
3777
|
-
}
|
|
3778
|
-
const parsed = jsonResult.value;
|
|
3779
|
-
return {
|
|
3780
|
-
result: parsed.result ?? stdout,
|
|
3781
|
-
sessionId: parsed.session_id ?? null,
|
|
3782
|
-
model: parsed.model ?? null
|
|
3773
|
+
const partial = {
|
|
3774
|
+
mode,
|
|
3775
|
+
existingAgentsMd: existing.content
|
|
3783
3776
|
};
|
|
3784
|
-
|
|
3785
|
-
buildResumeArgs(sessionId) {
|
|
3786
|
-
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
3787
|
-
throw new Error("Invalid session ID format");
|
|
3788
|
-
}
|
|
3789
|
-
return ["--resume", sessionId];
|
|
3790
|
-
},
|
|
3791
|
-
detectRateLimit(stderr) {
|
|
3792
|
-
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
3793
|
-
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
3794
|
-
if (!isRateLimited) {
|
|
3795
|
-
return { rateLimited: false, retryAfterMs: null };
|
|
3796
|
-
}
|
|
3797
|
-
const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
|
|
3798
|
-
const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
|
|
3799
|
-
return { rateLimited: true, retryAfterMs };
|
|
3800
|
-
},
|
|
3801
|
-
getSpawnEnv() {
|
|
3802
|
-
return { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" };
|
|
3803
|
-
}
|
|
3804
|
-
};
|
|
3805
|
-
|
|
3806
|
-
// src/integration/ai/providers/copilot.ts
|
|
3807
|
-
import { lstat, readdir, unlink } from "fs/promises";
|
|
3808
|
-
import { join as join2 } from "path";
|
|
3809
|
-
import { Result as Result3 } from "typescript-result";
|
|
3810
|
-
var copilotAdapter = {
|
|
3811
|
-
name: "copilot",
|
|
3812
|
-
displayName: "Copilot",
|
|
3813
|
-
binary: "copilot",
|
|
3814
|
-
experimental: true,
|
|
3815
|
-
baseArgs: ["--allow-all-tools"],
|
|
3816
|
-
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
3817
|
-
return [...this.baseArgs, ...extraArgs, "-i", prompt];
|
|
3818
|
-
},
|
|
3819
|
-
buildHeadlessArgs(extraArgs = []) {
|
|
3820
|
-
return ["-p", "--output-format", "json", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
|
|
3821
|
-
},
|
|
3822
|
-
parseJsonOutput(stdout) {
|
|
3823
|
-
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
3824
|
-
if (lines.length === 0) {
|
|
3825
|
-
return { result: "", sessionId: null, model: null };
|
|
3826
|
-
}
|
|
3827
|
-
const lastLine = lines.at(-1) ?? "";
|
|
3828
|
-
const jsonResult = Result3.try(() => JSON.parse(lastLine));
|
|
3829
|
-
if (jsonResult.ok) {
|
|
3830
|
-
const parsed = jsonResult.value;
|
|
3831
|
-
return {
|
|
3832
|
-
result: parsed.result ?? parsed.result_text ?? lastLine,
|
|
3833
|
-
sessionId: parsed.session_id ?? null,
|
|
3834
|
-
model: null
|
|
3835
|
-
};
|
|
3836
|
-
}
|
|
3837
|
-
return { result: stdout.trim(), sessionId: null, model: null };
|
|
3838
|
-
},
|
|
3839
|
-
buildResumeArgs(sessionId) {
|
|
3840
|
-
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
3841
|
-
throw new Error("Invalid session ID format");
|
|
3842
|
-
}
|
|
3843
|
-
return [`--resume=${sessionId}`];
|
|
3844
|
-
},
|
|
3845
|
-
async extractSessionId(cwd) {
|
|
3846
|
-
const filesResult = await wrapAsync(
|
|
3847
|
-
() => readdir(cwd),
|
|
3848
|
-
(err) => new IOError(`Failed to read directory: ${cwd}`, err instanceof Error ? err : void 0)
|
|
3849
|
-
);
|
|
3850
|
-
if (!filesResult.ok) return null;
|
|
3851
|
-
const files = filesResult.value;
|
|
3852
|
-
const shareFile = files.find((f) => /^copilot-session-[a-zA-Z0-9_][a-zA-Z0-9_-]*\.md$/.test(f));
|
|
3853
|
-
if (!shareFile) return null;
|
|
3854
|
-
const match = /^copilot-session-([a-zA-Z0-9_][a-zA-Z0-9_-]{0,127})\.md$/.exec(shareFile);
|
|
3855
|
-
if (!match?.[1]) return null;
|
|
3856
|
-
const filePath = join2(cwd, shareFile);
|
|
3857
|
-
const stat = await lstat(filePath).catch(() => null);
|
|
3858
|
-
if (stat?.isFile()) {
|
|
3859
|
-
await unlink(filePath).catch(() => {
|
|
3860
|
-
});
|
|
3861
|
-
}
|
|
3862
|
-
return match[1];
|
|
3863
|
-
},
|
|
3864
|
-
detectRateLimit(stderr) {
|
|
3865
|
-
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
3866
|
-
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
3867
|
-
if (!isRateLimited) {
|
|
3868
|
-
return { rateLimited: false, retryAfterMs: null };
|
|
3869
|
-
}
|
|
3870
|
-
const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
|
|
3871
|
-
const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
|
|
3872
|
-
return { rateLimited: true, retryAfterMs };
|
|
3873
|
-
},
|
|
3874
|
-
getSpawnEnv() {
|
|
3875
|
-
return {};
|
|
3876
|
-
}
|
|
3877
|
-
};
|
|
3878
|
-
|
|
3879
|
-
// src/integration/external/provider.ts
|
|
3880
|
-
async function resolveProvider() {
|
|
3881
|
-
const stored = await getAiProvider();
|
|
3882
|
-
if (stored) return stored;
|
|
3883
|
-
const choice = await getPrompt().select({
|
|
3884
|
-
message: `${emoji.donut} Which AI buddy should help with my homework?`,
|
|
3885
|
-
choices: [
|
|
3886
|
-
{ label: "Claude Code", value: "claude" },
|
|
3887
|
-
{ label: "GitHub Copilot", value: "copilot" }
|
|
3888
|
-
]
|
|
3777
|
+
return Result.ok(partial);
|
|
3889
3778
|
});
|
|
3890
|
-
await setAiProvider(choice);
|
|
3891
|
-
return choice;
|
|
3892
|
-
}
|
|
3893
|
-
function providerDisplayName(provider) {
|
|
3894
|
-
return provider === "claude" ? "Claude" : "Copilot";
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
// src/integration/ai/providers/registry.ts
|
|
3898
|
-
function getProvider(provider) {
|
|
3899
|
-
switch (provider) {
|
|
3900
|
-
case "claude":
|
|
3901
|
-
return claudeAdapter;
|
|
3902
|
-
case "copilot":
|
|
3903
|
-
return copilotAdapter;
|
|
3904
|
-
}
|
|
3905
|
-
}
|
|
3906
|
-
async function getActiveProvider() {
|
|
3907
|
-
const provider = await resolveProvider();
|
|
3908
|
-
return getProvider(provider);
|
|
3909
3779
|
}
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3780
|
+
function aiInventoryStep(deps) {
|
|
3781
|
+
return step("ai-inventory", async (ctx) => {
|
|
3782
|
+
const repo = ctx.repo;
|
|
3783
|
+
const mode = ctx.mode;
|
|
3784
|
+
const provider = ctx.provider;
|
|
3785
|
+
if (!repo || !mode || !provider)
|
|
3786
|
+
return Result.error(new ParseError("Preflight did not populate repo/mode/provider."));
|
|
3787
|
+
deps.logger.info(`Asking AI to inventory ${repo.name}...`);
|
|
3788
|
+
let result;
|
|
3789
|
+
try {
|
|
3790
|
+
result = await deps.adapter.discoverAgentsMd({
|
|
3791
|
+
repoPath: repo.path,
|
|
3792
|
+
mode,
|
|
3793
|
+
existingAgentsMd: ctx.existingAgentsMd ?? null,
|
|
3794
|
+
projectType: deps.adapter.inferProjectType(repo.path),
|
|
3795
|
+
checkScriptSuggestion: repo.checkScript ?? "",
|
|
3796
|
+
fileName: providerInstructionsFileName(provider)
|
|
3797
|
+
});
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
return Result.error(new ParseError(`AI inventory failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
3800
|
+
}
|
|
3801
|
+
if (!result.agentsMd) {
|
|
3802
|
+
return Result.error(
|
|
3803
|
+
new ParseError("AI returned no project context file proposal \u2014 try again, or edit the file manually.")
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
const partial = {
|
|
3807
|
+
agentsMdDraft: result.agentsMd,
|
|
3808
|
+
checkScriptDraft: result.checkScript,
|
|
3809
|
+
changes: result.changes
|
|
3810
|
+
};
|
|
3811
|
+
return Result.ok(partial);
|
|
3920
3812
|
});
|
|
3921
|
-
if (result.error) {
|
|
3922
|
-
return { code: 1, error: `Failed to spawn ${provider.binary} CLI: ${result.error.message}` };
|
|
3923
|
-
}
|
|
3924
|
-
return { code: result.status ?? 1 };
|
|
3925
3813
|
}
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3814
|
+
function retryOnViolationStep(deps) {
|
|
3815
|
+
return step(
|
|
3816
|
+
"retry-agents-md-on-violation",
|
|
3817
|
+
async (ctx) => {
|
|
3818
|
+
const violations = ctx.agentsMdViolations ?? [];
|
|
3819
|
+
if (violations.length === 0) return Result.ok({});
|
|
3820
|
+
const repo = ctx.repo;
|
|
3821
|
+
const mode = ctx.mode;
|
|
3822
|
+
const provider = ctx.provider;
|
|
3823
|
+
const draft = ctx.agentsMdDraft;
|
|
3824
|
+
if (!repo || !mode || !provider || !draft) {
|
|
3825
|
+
return Result.error(new ParseError("Retry requires repo, mode, provider, and an existing draft."));
|
|
3937
3826
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3827
|
+
deps.logger.warn(
|
|
3828
|
+
`Project context file draft failed ${String(violations.length)} rule(s); asking AI for a fix...`
|
|
3829
|
+
);
|
|
3830
|
+
const violationSummary = violations.map((v) => `- [${v.rule}] ${v.message}`).join("\n");
|
|
3831
|
+
const feedbackContext = [
|
|
3832
|
+
ctx.existingAgentsMd ?? "",
|
|
3833
|
+
"",
|
|
3834
|
+
"---",
|
|
3835
|
+
"",
|
|
3836
|
+
"Your previous draft (below) violated these rules:",
|
|
3837
|
+
violationSummary,
|
|
3838
|
+
"",
|
|
3839
|
+
"Fix every violation and re-emit the full project context file plus check-script.",
|
|
3840
|
+
"",
|
|
3841
|
+
draft
|
|
3842
|
+
].join("\n");
|
|
3843
|
+
let retry;
|
|
3844
|
+
try {
|
|
3845
|
+
retry = await deps.adapter.discoverAgentsMd({
|
|
3846
|
+
repoPath: repo.path,
|
|
3847
|
+
mode,
|
|
3848
|
+
existingAgentsMd: feedbackContext,
|
|
3849
|
+
projectType: deps.adapter.inferProjectType(repo.path),
|
|
3850
|
+
checkScriptSuggestion: ctx.checkScriptDraft ?? repo.checkScript ?? "",
|
|
3851
|
+
fileName: providerInstructionsFileName(provider)
|
|
3852
|
+
});
|
|
3853
|
+
} catch (err) {
|
|
3854
|
+
return Result.error(new ParseError(`Retry failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
3957
3855
|
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
let rawStdout = "";
|
|
3962
|
-
let stderr = "";
|
|
3963
|
-
child.stdout.on("data", (data) => {
|
|
3964
|
-
if (rawStdout.length < MAX_STDOUT_SIZE) {
|
|
3965
|
-
rawStdout += data.toString();
|
|
3856
|
+
if (!retry.agentsMd) {
|
|
3857
|
+
deps.logger.warn("Retry produced no new proposal \u2014 keeping original draft.");
|
|
3858
|
+
return Result.ok({});
|
|
3966
3859
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, "", 1));
|
|
3994
|
-
});
|
|
3860
|
+
const { violations: retryViolations } = deps.adapter.lintAgentsMd(retry.agentsMd);
|
|
3861
|
+
const partial = {
|
|
3862
|
+
agentsMdDraft: retry.agentsMd,
|
|
3863
|
+
checkScriptDraft: retry.checkScript ?? ctx.checkScriptDraft,
|
|
3864
|
+
agentsMdViolations: retryViolations
|
|
3865
|
+
};
|
|
3866
|
+
return Result.ok(partial);
|
|
3867
|
+
}
|
|
3868
|
+
);
|
|
3869
|
+
}
|
|
3870
|
+
function checkDriftStep(deps) {
|
|
3871
|
+
return step("check-drift", (ctx) => {
|
|
3872
|
+
const draft = ctx.agentsMdDraft;
|
|
3873
|
+
const repo = ctx.repo;
|
|
3874
|
+
if (!draft || !repo) return Result.error(new ParseError("check-drift requires a draft and repo."));
|
|
3875
|
+
const warnings = deps.adapter.detectCommandDrift(draft, repo.path);
|
|
3876
|
+
const residual = ctx.agentsMdViolations ?? [];
|
|
3877
|
+
for (const v of residual) {
|
|
3878
|
+
warnings.push(`lint[${v.rule}]: ${v.message}`);
|
|
3879
|
+
}
|
|
3880
|
+
const alreadyCurrent = ctx.mode === "update" && warnings.length === 0 && (!ctx.changes || ctx.changes.trim().length === 0);
|
|
3881
|
+
const partial = {
|
|
3882
|
+
driftWarnings: warnings,
|
|
3883
|
+
alreadyCurrent
|
|
3884
|
+
};
|
|
3885
|
+
return Result.ok(partial);
|
|
3995
3886
|
});
|
|
3996
3887
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
|
-
return Math.floor(Math.random() * 1e3);
|
|
4006
|
-
}
|
|
4007
|
-
async function spawnWithRetry(options, retryOptions, provider) {
|
|
4008
|
-
const p = provider ?? await getActiveProvider();
|
|
4009
|
-
const maxRetries = retryOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
4010
|
-
const totalTimeoutMs = retryOptions?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
4011
|
-
const startTime = Date.now();
|
|
4012
|
-
let resumeSessionId = options.resumeSessionId;
|
|
4013
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4014
|
-
const elapsed = Date.now() - startTime;
|
|
4015
|
-
if (attempt > 0 && elapsed >= totalTimeoutMs) {
|
|
4016
|
-
throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, "", 1, resumeSessionId);
|
|
4017
|
-
}
|
|
4018
|
-
const r = await wrapAsync(async () => spawnHeadless({ ...options, resumeSessionId }, p), ensureError);
|
|
4019
|
-
if (r.ok) return r.value;
|
|
4020
|
-
const err = r.error;
|
|
4021
|
-
if (!(err instanceof SpawnError) || !err.rateLimited) {
|
|
4022
|
-
throw err;
|
|
4023
|
-
}
|
|
4024
|
-
if (err.sessionId) {
|
|
4025
|
-
resumeSessionId = err.sessionId;
|
|
4026
|
-
}
|
|
4027
|
-
if (attempt >= maxRetries) {
|
|
4028
|
-
throw err;
|
|
3888
|
+
function reviewAndConfirmStep(deps, options) {
|
|
3889
|
+
return step("review-and-confirm", async (ctx) => {
|
|
3890
|
+
if (ctx.alreadyCurrent || options.auto || options.dryRun) {
|
|
3891
|
+
const partial2 = {
|
|
3892
|
+
agentsMdFinal: ctx.agentsMdDraft,
|
|
3893
|
+
checkScriptFinal: ctx.checkScriptDraft ?? null
|
|
3894
|
+
};
|
|
3895
|
+
return Result.ok(partial2);
|
|
4029
3896
|
}
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
throw new Error("Max retries exceeded");
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
// src/integration/ui/tui/runtime/screen.ts
|
|
4038
|
-
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
4039
|
-
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
4040
|
-
var HIDE_CURSOR = "\x1B[?25l";
|
|
4041
|
-
var SHOW_CURSOR = "\x1B[?25h";
|
|
4042
|
-
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
4043
|
-
var altScreenActive = false;
|
|
4044
|
-
var safetyNetsInstalled = false;
|
|
4045
|
-
function writeRaw(seq) {
|
|
4046
|
-
if (process.stdout.isTTY) process.stdout.write(seq);
|
|
4047
|
-
}
|
|
4048
|
-
function restore() {
|
|
4049
|
-
if (!altScreenActive) return;
|
|
4050
|
-
altScreenActive = false;
|
|
4051
|
-
writeRaw(SHOW_CURSOR);
|
|
4052
|
-
writeRaw(LEAVE_ALT_SCREEN);
|
|
4053
|
-
}
|
|
4054
|
-
function installSafetyNets() {
|
|
4055
|
-
if (safetyNetsInstalled) return;
|
|
4056
|
-
safetyNetsInstalled = true;
|
|
4057
|
-
process.on("exit", restore);
|
|
4058
|
-
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
4059
|
-
process.on(sig, () => {
|
|
4060
|
-
restore();
|
|
4061
|
-
process.kill(process.pid, sig);
|
|
3897
|
+
const fileName = ctx.provider ? providerInstructionsFileName(ctx.provider) : "project context file";
|
|
3898
|
+
const edited = await deps.prompt.editor({
|
|
3899
|
+
message: `Review ${fileName} (save to accept, cancel to abort):`,
|
|
3900
|
+
default: ctx.agentsMdDraft ?? ""
|
|
4062
3901
|
});
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
3902
|
+
if (edited === null) {
|
|
3903
|
+
return Result.error(new ParseError("User cancelled project context file review."));
|
|
3904
|
+
}
|
|
3905
|
+
const checkEdited = await deps.prompt.input({
|
|
3906
|
+
message: "Check script (optional; empty skips):",
|
|
3907
|
+
default: ctx.checkScriptDraft ?? ""
|
|
4068
3908
|
});
|
|
3909
|
+
const finalCheck = checkEdited.trim() === "" ? null : checkEdited.trim();
|
|
3910
|
+
const partial = {
|
|
3911
|
+
agentsMdFinal: edited,
|
|
3912
|
+
checkScriptFinal: finalCheck
|
|
3913
|
+
};
|
|
3914
|
+
return Result.ok(partial);
|
|
4069
3915
|
});
|
|
4070
3916
|
}
|
|
4071
|
-
function
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
activeInstance = instance;
|
|
4088
|
-
return () => {
|
|
4089
|
-
if (activeInstance === instance) {
|
|
4090
|
-
activeInstance = null;
|
|
4091
|
-
}
|
|
4092
|
-
};
|
|
4093
|
-
}
|
|
4094
|
-
async function withSuspendedTui(cb) {
|
|
4095
|
-
const instance = activeInstance;
|
|
4096
|
-
if (instance === null) {
|
|
4097
|
-
return cb();
|
|
4098
|
-
}
|
|
4099
|
-
exitAltScreen();
|
|
4100
|
-
try {
|
|
4101
|
-
return await cb();
|
|
4102
|
-
} finally {
|
|
4103
|
-
enterAltScreen();
|
|
4104
|
-
instance.clear();
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
// src/integration/ai/session/session-adapter.ts
|
|
4109
|
-
var ProviderAiSessionAdapter = class {
|
|
4110
|
-
provider = null;
|
|
4111
|
-
/** Lazily resolve and cache the active provider. */
|
|
4112
|
-
async getProvider() {
|
|
4113
|
-
this.provider ??= await getActiveProvider();
|
|
4114
|
-
return this.provider;
|
|
4115
|
-
}
|
|
4116
|
-
/** Public eager resolver — required before the sync getters can be used safely. */
|
|
4117
|
-
async ensureReady() {
|
|
4118
|
-
await this.getProvider();
|
|
4119
|
-
}
|
|
4120
|
-
async spawnInteractive(prompt, options) {
|
|
4121
|
-
const provider = await this.getProvider();
|
|
4122
|
-
await withSuspendedTui(() => {
|
|
4123
|
-
const result = spawnInteractive(
|
|
4124
|
-
prompt,
|
|
4125
|
-
{
|
|
4126
|
-
cwd: options.cwd,
|
|
4127
|
-
args: options.args,
|
|
4128
|
-
env: options.env
|
|
4129
|
-
},
|
|
4130
|
-
provider
|
|
3917
|
+
function writeArtifactsStep(deps, options) {
|
|
3918
|
+
return step("write-artifacts", async (ctx) => {
|
|
3919
|
+
if (options.dryRun || ctx.alreadyCurrent) {
|
|
3920
|
+
deps.logger.info(options.dryRun ? "Dry run \u2014 skipping writes." : "Already up to date \u2014 skipping writes.");
|
|
3921
|
+
return Result.ok({});
|
|
3922
|
+
}
|
|
3923
|
+
const repo = ctx.repo;
|
|
3924
|
+
const project = ctx.project;
|
|
3925
|
+
const provider = ctx.provider;
|
|
3926
|
+
const content = ctx.agentsMdFinal;
|
|
3927
|
+
if (!repo || !project || !provider || !content) {
|
|
3928
|
+
return Result.error(new ParseError("write-artifacts requires repo, project, provider, and final content."));
|
|
3929
|
+
}
|
|
3930
|
+
if (ctx.mode === "adopt") {
|
|
3931
|
+
deps.logger.warn(
|
|
3932
|
+
"Adopt mode \u2014 existing project context file left untouched. Review the proposed additions and apply them manually."
|
|
4131
3933
|
);
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
const provider = await this.getProvider();
|
|
4139
|
-
const result = await spawnHeadless(
|
|
4140
|
-
{
|
|
4141
|
-
cwd: options.cwd,
|
|
4142
|
-
args: options.args,
|
|
4143
|
-
env: options.env,
|
|
4144
|
-
prompt,
|
|
4145
|
-
resumeSessionId: options.resumeSessionId
|
|
4146
|
-
},
|
|
4147
|
-
provider
|
|
4148
|
-
);
|
|
4149
|
-
return {
|
|
4150
|
-
output: result.stdout,
|
|
4151
|
-
sessionId: result.sessionId ?? void 0,
|
|
4152
|
-
model: result.model ?? void 0
|
|
4153
|
-
};
|
|
4154
|
-
}
|
|
4155
|
-
async spawnWithRetry(prompt, options) {
|
|
4156
|
-
const provider = await this.getProvider();
|
|
4157
|
-
const result = await spawnWithRetry(
|
|
4158
|
-
{
|
|
4159
|
-
cwd: options.cwd,
|
|
4160
|
-
args: options.args,
|
|
4161
|
-
env: options.env,
|
|
4162
|
-
prompt,
|
|
4163
|
-
resumeSessionId: options.resumeSessionId
|
|
4164
|
-
},
|
|
4165
|
-
{ maxRetries: options.maxRetries },
|
|
4166
|
-
provider
|
|
4167
|
-
);
|
|
4168
|
-
return {
|
|
4169
|
-
output: result.stdout,
|
|
4170
|
-
sessionId: result.sessionId ?? void 0,
|
|
4171
|
-
model: result.model ?? void 0
|
|
4172
|
-
};
|
|
4173
|
-
}
|
|
4174
|
-
async resumeSession(sessionId, prompt, options) {
|
|
4175
|
-
const provider = await this.getProvider();
|
|
4176
|
-
const result = await spawnWithRetry(
|
|
4177
|
-
{
|
|
4178
|
-
cwd: options.cwd,
|
|
4179
|
-
args: options.args,
|
|
4180
|
-
env: options.env,
|
|
4181
|
-
prompt,
|
|
4182
|
-
resumeSessionId: sessionId
|
|
4183
|
-
},
|
|
4184
|
-
void 0,
|
|
4185
|
-
provider
|
|
4186
|
-
);
|
|
4187
|
-
return {
|
|
4188
|
-
output: result.stdout,
|
|
4189
|
-
sessionId: result.sessionId ?? void 0,
|
|
4190
|
-
model: result.model ?? void 0
|
|
4191
|
-
};
|
|
4192
|
-
}
|
|
4193
|
-
getProviderName() {
|
|
4194
|
-
if (!this.provider) {
|
|
4195
|
-
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
4196
|
-
}
|
|
4197
|
-
return this.provider.name;
|
|
4198
|
-
}
|
|
4199
|
-
getProviderDisplayName() {
|
|
4200
|
-
if (!this.provider) {
|
|
4201
|
-
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
3934
|
+
return Result.ok({
|
|
3935
|
+
driftWarnings: [
|
|
3936
|
+
...ctx.driftWarnings ?? [],
|
|
3937
|
+
"adopt-mode: authored file preserved; proposed additions not written \u2014 apply manually."
|
|
3938
|
+
]
|
|
3939
|
+
});
|
|
4202
3940
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
3941
|
+
try {
|
|
3942
|
+
const written = deps.adapter.writeProviderInstructions(repo.path, content, provider);
|
|
3943
|
+
const updatedRepos = project.repositories.map((r) => {
|
|
3944
|
+
if (r.id !== repo.id) return r;
|
|
3945
|
+
const next = {
|
|
3946
|
+
...r,
|
|
3947
|
+
onboardingVersion: CURRENT_ONBOARDING_VERSION
|
|
3948
|
+
};
|
|
3949
|
+
const cs = ctx.checkScriptFinal;
|
|
3950
|
+
if (cs && cs.length > 0) {
|
|
3951
|
+
next.checkScript = cs;
|
|
3952
|
+
} else if (cs === null) {
|
|
3953
|
+
delete next.checkScript;
|
|
3954
|
+
}
|
|
3955
|
+
return next;
|
|
3956
|
+
});
|
|
3957
|
+
await deps.updateProjectRepos(project.name, updatedRepos);
|
|
3958
|
+
const partial = {
|
|
3959
|
+
writtenPath: written.path
|
|
3960
|
+
};
|
|
3961
|
+
return Result.ok(partial);
|
|
3962
|
+
} catch (err) {
|
|
3963
|
+
return Result.error(new ParseError(`Write failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
4208
3964
|
}
|
|
4209
|
-
return this.provider.getSpawnEnv();
|
|
4210
|
-
}
|
|
4211
|
-
};
|
|
4212
|
-
|
|
4213
|
-
// src/integration/ai/prompts/loader.ts
|
|
4214
|
-
import { existsSync, readFileSync } from "fs";
|
|
4215
|
-
import { dirname, join as join3 } from "path";
|
|
4216
|
-
import { fileURLToPath } from "url";
|
|
4217
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
4218
|
-
function getPromptDir() {
|
|
4219
|
-
const bundled = join3(__dirname, "prompts");
|
|
4220
|
-
if (existsSync(bundled)) return bundled;
|
|
4221
|
-
return __dirname;
|
|
4222
|
-
}
|
|
4223
|
-
var promptDir = getPromptDir();
|
|
4224
|
-
function loadTemplate(name) {
|
|
4225
|
-
return readFileSync(join3(promptDir, `${name}.md`), "utf-8");
|
|
4226
|
-
}
|
|
4227
|
-
function loadPartial(name) {
|
|
4228
|
-
return loadTemplate(name).replace(/\s+$/, "");
|
|
4229
|
-
}
|
|
4230
|
-
var UNREPLACED_TOKEN_RE = /\{\{[A-Z_]+\}\}/g;
|
|
4231
|
-
function composePrompt(template, substitutions) {
|
|
4232
|
-
let result = template;
|
|
4233
|
-
for (const [key, value] of Object.entries(substitutions)) {
|
|
4234
|
-
result = result.replaceAll(`{{${key}}}`, value);
|
|
4235
|
-
}
|
|
4236
|
-
const remaining = result.match(UNREPLACED_TOKEN_RE);
|
|
4237
|
-
if (remaining) {
|
|
4238
|
-
throw new Error(`composePrompt: unreplaced placeholders: ${[...new Set(remaining)].join(", ")}`);
|
|
4239
|
-
}
|
|
4240
|
-
return result;
|
|
4241
|
-
}
|
|
4242
|
-
var CHECK_GATE_EXAMPLE = "Run the project's check gate \u2014 all pass (omit this step when the project has no check script)";
|
|
4243
|
-
function buildPlanCommon(projectToolingSection) {
|
|
4244
|
-
return composePrompt(loadPartial("plan-common"), {
|
|
4245
|
-
PLAN_COMMON_EXAMPLES: loadPartial("plan-common-examples"),
|
|
4246
|
-
PROJECT_TOOLING: projectToolingSection,
|
|
4247
|
-
CHECK_GATE_EXAMPLE
|
|
4248
|
-
});
|
|
4249
|
-
}
|
|
4250
|
-
function buildPlannerBase(projectToolingSection) {
|
|
4251
|
-
return {
|
|
4252
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4253
|
-
COMMON: buildPlanCommon(projectToolingSection),
|
|
4254
|
-
VALIDATION: loadPartial("validation-checklist"),
|
|
4255
|
-
SIGNALS: loadPartial("signals-planning"),
|
|
4256
|
-
CHECK_GATE_EXAMPLE
|
|
4257
|
-
};
|
|
4258
|
-
}
|
|
4259
|
-
function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
|
|
4260
|
-
return composePrompt(loadTemplate("plan-interactive"), {
|
|
4261
|
-
...buildPlannerBase(projectToolingSection),
|
|
4262
|
-
CONTEXT: context,
|
|
4263
|
-
OUTPUT_FILE: outputFile,
|
|
4264
|
-
SCHEMA: schema
|
|
4265
|
-
});
|
|
4266
|
-
}
|
|
4267
|
-
function buildAutoPrompt(context, schema, projectToolingSection) {
|
|
4268
|
-
return composePrompt(loadTemplate("plan-auto"), {
|
|
4269
|
-
...buildPlannerBase(projectToolingSection),
|
|
4270
|
-
CONTEXT: context,
|
|
4271
|
-
SCHEMA: schema
|
|
4272
|
-
});
|
|
4273
|
-
}
|
|
4274
|
-
function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName, projectToolingSection = "") {
|
|
4275
|
-
let template = loadTemplate("task-execution");
|
|
4276
|
-
if (noCommit) {
|
|
4277
|
-
template = template.replace(/^[ \t]*\{\{COMMIT_STEP\}\}\n/m, "\n");
|
|
4278
|
-
template = template.replace(/^[ \t]*\{\{COMMIT_CONSTRAINT\}\}\n/m, "");
|
|
4279
|
-
}
|
|
4280
|
-
const commitStep = noCommit ? "" : " - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
|
|
4281
|
-
const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.";
|
|
4282
|
-
return composePrompt(template, {
|
|
4283
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4284
|
-
SIGNALS: loadPartial("signals-task"),
|
|
4285
|
-
PROGRESS_FILE: progressFilePath,
|
|
4286
|
-
COMMIT_STEP: commitStep,
|
|
4287
|
-
COMMIT_CONSTRAINT: commitConstraint,
|
|
4288
|
-
CONTEXT_FILE: contextFileName,
|
|
4289
|
-
PROJECT_TOOLING: projectToolingSection
|
|
4290
|
-
});
|
|
4291
|
-
}
|
|
4292
|
-
function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
|
|
4293
|
-
const template = loadTemplate("ticket-refine");
|
|
4294
|
-
const issueContextSection = issueContext ? `<context>
|
|
4295
|
-
|
|
4296
|
-
${issueContext}
|
|
4297
|
-
|
|
4298
|
-
</context>` : "";
|
|
4299
|
-
return composePrompt(template, {
|
|
4300
|
-
TICKET: ticketContent,
|
|
4301
|
-
OUTPUT_FILE: outputFile,
|
|
4302
|
-
SCHEMA: schema,
|
|
4303
|
-
ISSUE_CONTEXT: issueContextSection
|
|
4304
3965
|
});
|
|
4305
3966
|
}
|
|
4306
|
-
function
|
|
4307
|
-
return
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
SCHEMA: schema
|
|
4315
|
-
});
|
|
4316
|
-
}
|
|
4317
|
-
function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
|
|
4318
|
-
return composePrompt(loadTemplate("ideate-auto"), {
|
|
4319
|
-
...buildPlannerBase(projectToolingSection),
|
|
4320
|
-
IDEA_TITLE: ideaTitle,
|
|
4321
|
-
IDEA_DESCRIPTION: ideaDescription,
|
|
4322
|
-
PROJECT_NAME: projectName,
|
|
4323
|
-
REPOSITORIES: repositories,
|
|
4324
|
-
SCHEMA: schema
|
|
4325
|
-
});
|
|
4326
|
-
}
|
|
4327
|
-
function renderExtraDimensions(extras) {
|
|
4328
|
-
if (extras.length === 0) {
|
|
4329
|
-
return { section: "", passBar: "", assessment: "" };
|
|
4330
|
-
}
|
|
4331
|
-
const section = extras.map(
|
|
4332
|
-
(name) => `
|
|
4333
|
-
<dimension name="${name}" floor="false">
|
|
4334
|
-
Additional task-specific dimension flagged by the planner. Apply judgment to whether the implementation satisfies this dimension given the task's verification criteria and steps.
|
|
4335
|
-
</dimension>
|
|
4336
|
-
`
|
|
4337
|
-
).join("");
|
|
4338
|
-
const passBar = extras.map((name) => `
|
|
4339
|
-
- **${name}**: Task-specific dimension flagged by the planner`).join("");
|
|
4340
|
-
return {
|
|
4341
|
-
section,
|
|
4342
|
-
passBar,
|
|
4343
|
-
assessment: extras.map((name) => `
|
|
4344
|
-
**${name}**: PASS/FAIL \u2014 [one-line finding]`).join("")
|
|
4345
|
-
};
|
|
4346
|
-
}
|
|
4347
|
-
function buildEvaluatorPrompt(ctx) {
|
|
4348
|
-
const template = loadTemplate("task-evaluation");
|
|
4349
|
-
const descriptionSection = ctx.taskDescription ? `
|
|
4350
|
-
**Description:** ${ctx.taskDescription}` : "";
|
|
4351
|
-
const stepsSection = ctx.taskSteps.length > 0 ? `
|
|
4352
|
-
**Implementation Steps:**
|
|
4353
|
-
${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
|
|
4354
|
-
const criteriaSection = ctx.verificationCriteria.length > 0 ? `
|
|
4355
|
-
**Verification Criteria:**
|
|
4356
|
-
${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
|
|
4357
|
-
const checkSection = ctx.checkScriptSection ? `
|
|
4358
|
-
|
|
4359
|
-
${ctx.checkScriptSection}` : "";
|
|
4360
|
-
const extras = renderExtraDimensions(ctx.extraDimensions);
|
|
4361
|
-
const extraAssessmentPass = extras.assessment.replace(/PASS\/FAIL/g, "PASS");
|
|
4362
|
-
return composePrompt(template, {
|
|
4363
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4364
|
-
SIGNALS: loadPartial("signals-evaluation"),
|
|
4365
|
-
TASK_NAME: ctx.taskName,
|
|
4366
|
-
TASK_DESCRIPTION_SECTION: descriptionSection,
|
|
4367
|
-
TASK_STEPS_SECTION: stepsSection,
|
|
4368
|
-
VERIFICATION_CRITERIA_SECTION: criteriaSection,
|
|
4369
|
-
PROJECT_PATH: ctx.projectPath,
|
|
4370
|
-
CHECK_SCRIPT_SECTION: checkSection,
|
|
4371
|
-
PROJECT_TOOLING: ctx.projectToolingSection,
|
|
4372
|
-
EXTRA_DIMENSIONS_SECTION: extras.section,
|
|
4373
|
-
EXTRA_DIMENSIONS_PASS_BAR: extras.passBar,
|
|
4374
|
-
EXTRA_DIMENSIONS_ASSESSMENT_PASS: extraAssessmentPass,
|
|
4375
|
-
EXTRA_DIMENSIONS_ASSESSMENT_MIXED: extras.assessment
|
|
3967
|
+
function verifyCheckScriptStep(deps) {
|
|
3968
|
+
return step("verify-check-script", (ctx) => {
|
|
3969
|
+
const cmd = ctx.checkScriptFinal;
|
|
3970
|
+
if (!cmd) return Result.ok({});
|
|
3971
|
+
if (!/^\S/.test(cmd)) {
|
|
3972
|
+
deps.logger.warn(`Check script looks malformed: ${cmd}`);
|
|
3973
|
+
}
|
|
3974
|
+
return Result.ok({});
|
|
4376
3975
|
});
|
|
4377
3976
|
}
|
|
4378
|
-
function
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
3977
|
+
function createOnboardPipeline(deps, options = {}) {
|
|
3978
|
+
return pipeline("onboard", [
|
|
3979
|
+
loadProjectStep(deps),
|
|
3980
|
+
selectRepoStep(deps, options),
|
|
3981
|
+
repoPreflightStep(deps),
|
|
3982
|
+
aiInventoryStep(deps),
|
|
3983
|
+
validateAgentsMdStep(deps.adapter),
|
|
3984
|
+
retryOnViolationStep(deps),
|
|
3985
|
+
checkDriftStep(deps),
|
|
3986
|
+
reviewAndConfirmStep(deps, options),
|
|
3987
|
+
writeArtifactsStep(deps, options),
|
|
3988
|
+
verifyCheckScriptStep(deps)
|
|
3989
|
+
]);
|
|
4391
3990
|
}
|
|
4392
3991
|
|
|
4393
3992
|
// src/integration/ai/prompts/prompt-builder-adapter.ts
|
|
@@ -4523,10 +4122,12 @@ async function importTasksAppend(tasks, sprintId) {
|
|
|
4523
4122
|
name: taskInput.name,
|
|
4524
4123
|
description: taskInput.description,
|
|
4525
4124
|
steps: taskInput.steps ?? [],
|
|
4125
|
+
verificationCriteria: taskInput.verificationCriteria ?? [],
|
|
4526
4126
|
ticketId: taskInput.ticketId,
|
|
4527
4127
|
blockedBy: [],
|
|
4528
4128
|
// Set later
|
|
4529
|
-
repoId: taskInput.repoId
|
|
4129
|
+
repoId: taskInput.repoId,
|
|
4130
|
+
extraDimensions: taskInput.extraDimensions
|
|
4530
4131
|
},
|
|
4531
4132
|
sprintId
|
|
4532
4133
|
);
|
|
@@ -4579,7 +4180,8 @@ async function importTasksReplace(tasks, sprintId) {
|
|
|
4579
4180
|
// Set in second pass
|
|
4580
4181
|
repoId: taskInput.repoId,
|
|
4581
4182
|
evaluated: false,
|
|
4582
|
-
verified: false
|
|
4183
|
+
verified: false,
|
|
4184
|
+
extraDimensions: taskInput.extraDimensions
|
|
4583
4185
|
});
|
|
4584
4186
|
}
|
|
4585
4187
|
for (let i = 0; i < tasks.length; i++) {
|
|
@@ -4595,8 +4197,8 @@ async function importTasksReplace(tasks, sprintId) {
|
|
|
4595
4197
|
|
|
4596
4198
|
// src/integration/cli/commands/ticket/refine-utils.ts
|
|
4597
4199
|
import { writeFile } from "fs/promises";
|
|
4598
|
-
import { join as
|
|
4599
|
-
import { Result as
|
|
4200
|
+
import { join as join2 } from "path";
|
|
4201
|
+
import { Result as Result2 } from "typescript-result";
|
|
4600
4202
|
function formatTicketForPrompt(ticket) {
|
|
4601
4203
|
const lines = [];
|
|
4602
4204
|
lines.push(`### ${formatTicketDisplay(ticket)}`);
|
|
@@ -4614,7 +4216,7 @@ function formatTicketForPrompt(ticket) {
|
|
|
4614
4216
|
}
|
|
4615
4217
|
function parseRequirementsFile(content) {
|
|
4616
4218
|
const jsonStr = extractJsonArray(content);
|
|
4617
|
-
const parseR =
|
|
4219
|
+
const parseR = Result2.try(() => JSON.parse(jsonStr));
|
|
4618
4220
|
if (!parseR.ok) {
|
|
4619
4221
|
throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
|
|
4620
4222
|
}
|
|
@@ -4634,7 +4236,7 @@ ${issues}`);
|
|
|
4634
4236
|
return result.data;
|
|
4635
4237
|
}
|
|
4636
4238
|
async function runAiSession(workingDir, prompt, ticketTitle) {
|
|
4637
|
-
const contextFile =
|
|
4239
|
+
const contextFile = join2(workingDir, "refine-context.md");
|
|
4638
4240
|
await writeFile(contextFile, prompt, "utf-8");
|
|
4639
4241
|
const provider = await getActiveProvider();
|
|
4640
4242
|
const startPrompt = `I need help refining the requirements for "${ticketTitle}". The full context is in refine-context.md. Please read that file now and follow the instructions to help refine the ticket requirements.`;
|
|
@@ -4652,7 +4254,7 @@ async function runAiSession(workingDir, prompt, ticketTitle) {
|
|
|
4652
4254
|
}
|
|
4653
4255
|
|
|
4654
4256
|
// src/integration/ai/evaluator.ts
|
|
4655
|
-
var DIMENSION_LINE = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)
|
|
4257
|
+
var DIMENSION_LINE = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)(?:\s*(?:—|-)\s*([^\n]*\S))?/gi;
|
|
4656
4258
|
function parseDimensionScores(output) {
|
|
4657
4259
|
const scores = [];
|
|
4658
4260
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4661,15 +4263,16 @@ function parseDimensionScores(output) {
|
|
|
4661
4263
|
while ((match = DIMENSION_LINE.exec(output)) !== null) {
|
|
4662
4264
|
const rawName = match[1];
|
|
4663
4265
|
const verdict = match[2];
|
|
4664
|
-
const finding = match[3];
|
|
4665
|
-
if (!rawName || !verdict
|
|
4266
|
+
const finding = (match[3] ?? "").trim();
|
|
4267
|
+
if (!rawName || !verdict) continue;
|
|
4666
4268
|
const name = rawName.toLowerCase();
|
|
4667
4269
|
if (seen.has(name)) continue;
|
|
4668
4270
|
seen.add(name);
|
|
4271
|
+
const hasJustification = finding.length > 0;
|
|
4669
4272
|
scores.push({
|
|
4670
4273
|
dimension: name,
|
|
4671
|
-
passed: verdict.toUpperCase() === "PASS",
|
|
4672
|
-
finding
|
|
4274
|
+
passed: verdict.toUpperCase() === "PASS" && hasJustification,
|
|
4275
|
+
finding
|
|
4673
4276
|
});
|
|
4674
4277
|
}
|
|
4675
4278
|
return scores;
|
|
@@ -4689,132 +4292,6 @@ function parseEvaluationResult(output) {
|
|
|
4689
4292
|
return { passed: false, status: "malformed", output, dimensions };
|
|
4690
4293
|
}
|
|
4691
4294
|
|
|
4692
|
-
// src/integration/signals/parser.ts
|
|
4693
|
-
var SIGNAL_PATTERNS = {
|
|
4694
|
-
progress: /<progress>([\s\S]*?)<\/progress>/g,
|
|
4695
|
-
progressWithFiles: /<progress>([\s\S]*?)<\/progress>/,
|
|
4696
|
-
evaluation_passed: /<evaluation-passed>/,
|
|
4697
|
-
evaluation_failed: /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/,
|
|
4698
|
-
task_verified: /<task-verified>([\s\S]*?)<\/task-verified>/,
|
|
4699
|
-
task_complete: /<task-complete>/,
|
|
4700
|
-
task_blocked: /<task-blocked>([\s\S]*?)<\/task-blocked>/,
|
|
4701
|
-
note: /<note>([\s\S]*?)<\/note>/g
|
|
4702
|
-
};
|
|
4703
|
-
var DIMENSION_LINE2 = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/gi;
|
|
4704
|
-
function parseDimensionScores2(output) {
|
|
4705
|
-
const scores = [];
|
|
4706
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4707
|
-
DIMENSION_LINE2.lastIndex = 0;
|
|
4708
|
-
let match;
|
|
4709
|
-
while ((match = DIMENSION_LINE2.exec(output)) !== null) {
|
|
4710
|
-
const rawName = match[1];
|
|
4711
|
-
const verdict = match[2];
|
|
4712
|
-
const finding = match[3];
|
|
4713
|
-
if (!rawName || !verdict || !finding) continue;
|
|
4714
|
-
const name = rawName.toLowerCase();
|
|
4715
|
-
if (seen.has(name)) continue;
|
|
4716
|
-
seen.add(name);
|
|
4717
|
-
scores.push({
|
|
4718
|
-
dimension: name,
|
|
4719
|
-
passed: verdict.toUpperCase() === "PASS",
|
|
4720
|
-
finding: finding.trim()
|
|
4721
|
-
});
|
|
4722
|
-
}
|
|
4723
|
-
return scores;
|
|
4724
|
-
}
|
|
4725
|
-
var SignalParser = class {
|
|
4726
|
-
parseSignals(output) {
|
|
4727
|
-
const signals = [];
|
|
4728
|
-
const timestamp = /* @__PURE__ */ new Date();
|
|
4729
|
-
let progressMatch;
|
|
4730
|
-
while ((progressMatch = SIGNAL_PATTERNS.progress.exec(output)) !== null) {
|
|
4731
|
-
const summary = progressMatch[1]?.trim();
|
|
4732
|
-
if (summary) {
|
|
4733
|
-
const progressSignal = {
|
|
4734
|
-
type: "progress",
|
|
4735
|
-
summary,
|
|
4736
|
-
// Note: Phase 1 doesn't parse files attribute; added in Phase 2+
|
|
4737
|
-
timestamp
|
|
4738
|
-
};
|
|
4739
|
-
signals.push(progressSignal);
|
|
4740
|
-
}
|
|
4741
|
-
}
|
|
4742
|
-
if (output.includes("<evaluation-passed>")) {
|
|
4743
|
-
const dimensions = parseDimensionScores2(output);
|
|
4744
|
-
const evaluationSignal = {
|
|
4745
|
-
type: "evaluation",
|
|
4746
|
-
status: "passed",
|
|
4747
|
-
dimensions,
|
|
4748
|
-
timestamp
|
|
4749
|
-
};
|
|
4750
|
-
signals.push(evaluationSignal);
|
|
4751
|
-
} else {
|
|
4752
|
-
const failedMatch = SIGNAL_PATTERNS.evaluation_failed.exec(output);
|
|
4753
|
-
if (failedMatch?.[1]) {
|
|
4754
|
-
const critique = failedMatch[1].trim();
|
|
4755
|
-
const dimensions = parseDimensionScores2(output);
|
|
4756
|
-
const evaluationSignal = {
|
|
4757
|
-
type: "evaluation",
|
|
4758
|
-
status: dimensions.length > 0 ? "failed" : "malformed",
|
|
4759
|
-
dimensions,
|
|
4760
|
-
critique: dimensions.length > 0 ? critique : void 0,
|
|
4761
|
-
timestamp
|
|
4762
|
-
};
|
|
4763
|
-
signals.push(evaluationSignal);
|
|
4764
|
-
} else if (parseDimensionScores2(output).length > 0) {
|
|
4765
|
-
const dimensions = parseDimensionScores2(output);
|
|
4766
|
-
const evaluationSignal = {
|
|
4767
|
-
type: "evaluation",
|
|
4768
|
-
status: "failed",
|
|
4769
|
-
dimensions,
|
|
4770
|
-
timestamp
|
|
4771
|
-
};
|
|
4772
|
-
signals.push(evaluationSignal);
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
const taskVerifiedMatch = SIGNAL_PATTERNS.task_verified.exec(output);
|
|
4776
|
-
if (taskVerifiedMatch?.[1]) {
|
|
4777
|
-
const verificationOutput = taskVerifiedMatch[1].trim();
|
|
4778
|
-
const verifiedSignal = {
|
|
4779
|
-
type: "task-verified",
|
|
4780
|
-
output: verificationOutput,
|
|
4781
|
-
timestamp
|
|
4782
|
-
};
|
|
4783
|
-
signals.push(verifiedSignal);
|
|
4784
|
-
}
|
|
4785
|
-
if (output.includes("<task-complete>")) {
|
|
4786
|
-
const completeSignal = {
|
|
4787
|
-
type: "task-complete",
|
|
4788
|
-
timestamp
|
|
4789
|
-
};
|
|
4790
|
-
signals.push(completeSignal);
|
|
4791
|
-
}
|
|
4792
|
-
const taskBlockedMatch = SIGNAL_PATTERNS.task_blocked.exec(output);
|
|
4793
|
-
if (taskBlockedMatch?.[1]) {
|
|
4794
|
-
const reason = taskBlockedMatch[1].trim();
|
|
4795
|
-
const blockedSignal = {
|
|
4796
|
-
type: "task-blocked",
|
|
4797
|
-
reason,
|
|
4798
|
-
timestamp
|
|
4799
|
-
};
|
|
4800
|
-
signals.push(blockedSignal);
|
|
4801
|
-
}
|
|
4802
|
-
let noteMatch;
|
|
4803
|
-
while ((noteMatch = SIGNAL_PATTERNS.note.exec(output)) !== null) {
|
|
4804
|
-
const text = noteMatch[1]?.trim();
|
|
4805
|
-
if (text) {
|
|
4806
|
-
const noteSignal = {
|
|
4807
|
-
type: "note",
|
|
4808
|
-
text,
|
|
4809
|
-
timestamp
|
|
4810
|
-
};
|
|
4811
|
-
signals.push(noteSignal);
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
return signals;
|
|
4815
|
-
}
|
|
4816
|
-
};
|
|
4817
|
-
|
|
4818
4295
|
// src/integration/ai/output/parser.ts
|
|
4819
4296
|
var signalParser = new SignalParser();
|
|
4820
4297
|
function parseExecutionResult(output) {
|
|
@@ -4965,8 +4442,8 @@ var AutoUserAdapter = class {
|
|
|
4965
4442
|
};
|
|
4966
4443
|
|
|
4967
4444
|
// src/integration/ai/project-tooling.ts
|
|
4968
|
-
import { existsSync
|
|
4969
|
-
import { join as
|
|
4445
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
4446
|
+
import { join as join3 } from "path";
|
|
4970
4447
|
var EMPTY_TOOLING = {
|
|
4971
4448
|
agents: [],
|
|
4972
4449
|
skills: [],
|
|
@@ -4977,7 +4454,7 @@ var EMPTY_TOOLING = {
|
|
|
4977
4454
|
};
|
|
4978
4455
|
function safeListDir(path, predicate) {
|
|
4979
4456
|
try {
|
|
4980
|
-
if (!
|
|
4457
|
+
if (!existsSync(path)) return [];
|
|
4981
4458
|
return readdirSync(path).filter(predicate).sort();
|
|
4982
4459
|
} catch {
|
|
4983
4460
|
return [];
|
|
@@ -4985,23 +4462,23 @@ function safeListDir(path, predicate) {
|
|
|
4985
4462
|
}
|
|
4986
4463
|
var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
|
|
4987
4464
|
function detectAgents(projectPath) {
|
|
4988
|
-
const agentsDir =
|
|
4465
|
+
const agentsDir = join3(projectPath, ".claude", "agents");
|
|
4989
4466
|
return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
|
|
4990
4467
|
}
|
|
4991
4468
|
function detectSkills(projectPath) {
|
|
4992
|
-
const skillsDir =
|
|
4469
|
+
const skillsDir = join3(projectPath, ".claude", "skills");
|
|
4993
4470
|
try {
|
|
4994
|
-
if (!
|
|
4471
|
+
if (!existsSync(skillsDir)) return [];
|
|
4995
4472
|
return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
4996
4473
|
} catch {
|
|
4997
4474
|
return [];
|
|
4998
4475
|
}
|
|
4999
4476
|
}
|
|
5000
4477
|
function detectMcpServers(projectPath) {
|
|
5001
|
-
const mcpFile =
|
|
5002
|
-
if (!
|
|
4478
|
+
const mcpFile = join3(projectPath, ".mcp.json");
|
|
4479
|
+
if (!existsSync(mcpFile)) return [];
|
|
5003
4480
|
try {
|
|
5004
|
-
const raw =
|
|
4481
|
+
const raw = readFileSync(mcpFile, "utf-8");
|
|
5005
4482
|
const parsed = JSON.parse(raw);
|
|
5006
4483
|
const servers = parsed.mcpServers;
|
|
5007
4484
|
if (!servers || typeof servers !== "object") return [];
|
|
@@ -5011,16 +4488,16 @@ function detectMcpServers(projectPath) {
|
|
|
5011
4488
|
}
|
|
5012
4489
|
}
|
|
5013
4490
|
function detectProjectTooling(projectPath) {
|
|
5014
|
-
if (!projectPath || !
|
|
4491
|
+
if (!projectPath || !existsSync(projectPath)) {
|
|
5015
4492
|
return EMPTY_TOOLING;
|
|
5016
4493
|
}
|
|
5017
4494
|
return {
|
|
5018
4495
|
agents: detectAgents(projectPath),
|
|
5019
4496
|
skills: detectSkills(projectPath),
|
|
5020
4497
|
mcpServers: detectMcpServers(projectPath),
|
|
5021
|
-
hasClaudeMd:
|
|
5022
|
-
hasAgentsMd:
|
|
5023
|
-
hasCopilotInstructions:
|
|
4498
|
+
hasClaudeMd: existsSync(join3(projectPath, "CLAUDE.md")),
|
|
4499
|
+
hasAgentsMd: existsSync(join3(projectPath, "AGENTS.md")),
|
|
4500
|
+
hasCopilotInstructions: existsSync(join3(projectPath, ".github", "copilot-instructions.md"))
|
|
5024
4501
|
};
|
|
5025
4502
|
}
|
|
5026
4503
|
function detectProjectToolingAcrossPaths(projectPaths) {
|
|
@@ -5131,7 +4608,7 @@ function describeMcpHint(name) {
|
|
|
5131
4608
|
}
|
|
5132
4609
|
|
|
5133
4610
|
// src/integration/external/lifecycle.ts
|
|
5134
|
-
import { spawnSync
|
|
4611
|
+
import { spawnSync } from "child_process";
|
|
5135
4612
|
var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5136
4613
|
function getHookTimeoutMs() {
|
|
5137
4614
|
const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
|
|
@@ -5144,7 +4621,7 @@ function getHookTimeoutMs() {
|
|
|
5144
4621
|
function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
5145
4622
|
assertSafeCwd(projectPath);
|
|
5146
4623
|
const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
|
|
5147
|
-
const result =
|
|
4624
|
+
const result = spawnSync(script, {
|
|
5148
4625
|
cwd: projectPath,
|
|
5149
4626
|
shell: true,
|
|
5150
4627
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5158,9 +4635,9 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
|
5158
4635
|
|
|
5159
4636
|
// src/integration/ai/task-context.ts
|
|
5160
4637
|
import { execSync } from "child_process";
|
|
5161
|
-
import { Result as
|
|
4638
|
+
import { Result as Result3 } from "typescript-result";
|
|
5162
4639
|
function getRecentGitHistory(projectPath, count = 20) {
|
|
5163
|
-
const r =
|
|
4640
|
+
const r = Result3.try(() => {
|
|
5164
4641
|
assertSafeCwd(projectPath);
|
|
5165
4642
|
const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
|
|
5166
4643
|
cwd: projectPath,
|
|
@@ -5173,7 +4650,7 @@ function getRecentGitHistory(projectPath, count = 20) {
|
|
|
5173
4650
|
}
|
|
5174
4651
|
|
|
5175
4652
|
// src/integration/external/git.ts
|
|
5176
|
-
import { spawnSync as
|
|
4653
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5177
4654
|
var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
|
|
5178
4655
|
var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
|
|
5179
4656
|
function isValidBranchName(name) {
|
|
@@ -5186,7 +4663,7 @@ function isValidBranchName(name) {
|
|
|
5186
4663
|
}
|
|
5187
4664
|
function getCurrentBranch(cwd) {
|
|
5188
4665
|
assertSafeCwd(cwd);
|
|
5189
|
-
const result =
|
|
4666
|
+
const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
5190
4667
|
cwd,
|
|
5191
4668
|
encoding: "utf-8",
|
|
5192
4669
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5201,7 +4678,7 @@ function branchExists(cwd, name) {
|
|
|
5201
4678
|
if (!isValidBranchName(name)) {
|
|
5202
4679
|
throw new Error(`Invalid branch name: ${name}`);
|
|
5203
4680
|
}
|
|
5204
|
-
const result =
|
|
4681
|
+
const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
|
|
5205
4682
|
cwd,
|
|
5206
4683
|
encoding: "utf-8",
|
|
5207
4684
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5218,7 +4695,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5218
4695
|
return;
|
|
5219
4696
|
}
|
|
5220
4697
|
if (branchExists(cwd, name)) {
|
|
5221
|
-
const result =
|
|
4698
|
+
const result = spawnSync2("git", ["checkout", name], {
|
|
5222
4699
|
cwd,
|
|
5223
4700
|
encoding: "utf-8",
|
|
5224
4701
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5227,7 +4704,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5227
4704
|
throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
|
|
5228
4705
|
}
|
|
5229
4706
|
} else {
|
|
5230
|
-
const result =
|
|
4707
|
+
const result = spawnSync2("git", ["checkout", "-b", name], {
|
|
5231
4708
|
cwd,
|
|
5232
4709
|
encoding: "utf-8",
|
|
5233
4710
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5243,7 +4720,7 @@ function verifyCurrentBranch(cwd, expected) {
|
|
|
5243
4720
|
}
|
|
5244
4721
|
function getDefaultBranch(cwd) {
|
|
5245
4722
|
assertSafeCwd(cwd);
|
|
5246
|
-
const result =
|
|
4723
|
+
const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
5247
4724
|
cwd,
|
|
5248
4725
|
encoding: "utf-8",
|
|
5249
4726
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5264,7 +4741,7 @@ function getDefaultBranch(cwd) {
|
|
|
5264
4741
|
function getHeadSha(cwd) {
|
|
5265
4742
|
try {
|
|
5266
4743
|
assertSafeCwd(cwd);
|
|
5267
|
-
const result =
|
|
4744
|
+
const result = spawnSync2("git", ["rev-parse", "HEAD"], {
|
|
5268
4745
|
cwd,
|
|
5269
4746
|
encoding: "utf-8",
|
|
5270
4747
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5277,7 +4754,7 @@ function getHeadSha(cwd) {
|
|
|
5277
4754
|
}
|
|
5278
4755
|
function hasUncommittedChanges(cwd) {
|
|
5279
4756
|
assertSafeCwd(cwd);
|
|
5280
|
-
const result =
|
|
4757
|
+
const result = spawnSync2("git", ["status", "--porcelain"], {
|
|
5281
4758
|
cwd,
|
|
5282
4759
|
encoding: "utf-8",
|
|
5283
4760
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5287,9 +4764,28 @@ function hasUncommittedChanges(cwd) {
|
|
|
5287
4764
|
}
|
|
5288
4765
|
return result.stdout.trim().length > 0;
|
|
5289
4766
|
}
|
|
4767
|
+
function hardResetWorkingTree(cwd) {
|
|
4768
|
+
assertSafeCwd(cwd);
|
|
4769
|
+
const reset = spawnSync2("git", ["reset", "--hard", "HEAD"], {
|
|
4770
|
+
cwd,
|
|
4771
|
+
encoding: "utf-8",
|
|
4772
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4773
|
+
});
|
|
4774
|
+
if (reset.status !== 0) {
|
|
4775
|
+
throw new StorageError(`Failed to reset working tree in ${cwd}: ${reset.stderr.trim() || reset.stdout.trim()}`);
|
|
4776
|
+
}
|
|
4777
|
+
const clean = spawnSync2("git", ["clean", "-fd"], {
|
|
4778
|
+
cwd,
|
|
4779
|
+
encoding: "utf-8",
|
|
4780
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4781
|
+
});
|
|
4782
|
+
if (clean.status !== 0) {
|
|
4783
|
+
throw new StorageError(`Failed to clean working tree in ${cwd}: ${clean.stderr.trim() || clean.stdout.trim()}`);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
5290
4786
|
function autoCommit(cwd, message) {
|
|
5291
4787
|
assertSafeCwd(cwd);
|
|
5292
|
-
const add =
|
|
4788
|
+
const add = spawnSync2("git", ["add", "-A"], {
|
|
5293
4789
|
cwd,
|
|
5294
4790
|
encoding: "utf-8",
|
|
5295
4791
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5297,7 +4793,7 @@ function autoCommit(cwd, message) {
|
|
|
5297
4793
|
if (add.status !== 0) {
|
|
5298
4794
|
throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
|
|
5299
4795
|
}
|
|
5300
|
-
const commit =
|
|
4796
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
5301
4797
|
cwd,
|
|
5302
4798
|
encoding: "utf-8",
|
|
5303
4799
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5310,14 +4806,14 @@ function generateBranchName(sprintId) {
|
|
|
5310
4806
|
return `ralphctl/${sprintId}`;
|
|
5311
4807
|
}
|
|
5312
4808
|
function isGhAvailable() {
|
|
5313
|
-
const result =
|
|
4809
|
+
const result = spawnSync2("gh", ["--version"], {
|
|
5314
4810
|
encoding: "utf-8",
|
|
5315
4811
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5316
4812
|
});
|
|
5317
4813
|
return result.status === 0;
|
|
5318
4814
|
}
|
|
5319
4815
|
function isGlabAvailable() {
|
|
5320
|
-
const result =
|
|
4816
|
+
const result = spawnSync2("glab", ["--version"], {
|
|
5321
4817
|
encoding: "utf-8",
|
|
5322
4818
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5323
4819
|
});
|
|
@@ -5365,6 +4861,9 @@ var DefaultExternalAdapter = class {
|
|
|
5365
4861
|
hasUncommittedChanges(projectPath) {
|
|
5366
4862
|
return hasUncommittedChanges(projectPath);
|
|
5367
4863
|
}
|
|
4864
|
+
hardResetWorkingTree(projectPath) {
|
|
4865
|
+
hardResetWorkingTree(projectPath);
|
|
4866
|
+
}
|
|
5368
4867
|
autoCommit(projectPath, message) {
|
|
5369
4868
|
autoCommit(projectPath, message);
|
|
5370
4869
|
return Promise.resolve();
|
|
@@ -5392,6 +4891,265 @@ var DefaultExternalAdapter = class {
|
|
|
5392
4891
|
}
|
|
5393
4892
|
};
|
|
5394
4893
|
|
|
4894
|
+
// src/integration/external/onboard-adapter.ts
|
|
4895
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
4896
|
+
import { join as join6 } from "path";
|
|
4897
|
+
|
|
4898
|
+
// src/integration/external/agents-md-linter.ts
|
|
4899
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
4900
|
+
import { join as join4 } from "path";
|
|
4901
|
+
var MAX_H2 = 7;
|
|
4902
|
+
var MAX_LINES = 300;
|
|
4903
|
+
var MIN_FLESCH = 40;
|
|
4904
|
+
var REQUIRED_H2_SECTIONS = [
|
|
4905
|
+
"Project Overview",
|
|
4906
|
+
"Build & Run",
|
|
4907
|
+
"Testing",
|
|
4908
|
+
"Architecture",
|
|
4909
|
+
"Implementation Style",
|
|
4910
|
+
"Security & Safety",
|
|
4911
|
+
"Performance Constraints"
|
|
4912
|
+
];
|
|
4913
|
+
function normalizeHeading(raw) {
|
|
4914
|
+
return raw.replace(/^#+\s*/, "").replace(/[*_`]/g, "").trim().toLowerCase();
|
|
4915
|
+
}
|
|
4916
|
+
function lintAgentsMd(content) {
|
|
4917
|
+
const violations = [];
|
|
4918
|
+
const lines = content.split("\n");
|
|
4919
|
+
if (lines.length >= MAX_LINES) {
|
|
4920
|
+
violations.push({
|
|
4921
|
+
rule: "max-lines",
|
|
4922
|
+
message: `Project context file is ${String(lines.length)} lines (must be under ${String(MAX_LINES)}).`
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
let inCodeFence = false;
|
|
4926
|
+
let h1Count = 0;
|
|
4927
|
+
let h2Count = 0;
|
|
4928
|
+
const h2Titles = [];
|
|
4929
|
+
for (const line of lines) {
|
|
4930
|
+
if (line.startsWith("```")) {
|
|
4931
|
+
inCodeFence = !inCodeFence;
|
|
4932
|
+
continue;
|
|
4933
|
+
}
|
|
4934
|
+
if (inCodeFence) continue;
|
|
4935
|
+
const match = /^(#+)\s/.exec(line);
|
|
4936
|
+
if (!match) continue;
|
|
4937
|
+
const depth = match[1]?.length ?? 0;
|
|
4938
|
+
if (depth === 1) h1Count++;
|
|
4939
|
+
else if (depth === 2) {
|
|
4940
|
+
h2Count++;
|
|
4941
|
+
h2Titles.push(normalizeHeading(line));
|
|
4942
|
+
} else if (depth >= 4) {
|
|
4943
|
+
violations.push({
|
|
4944
|
+
rule: "no-h4-plus",
|
|
4945
|
+
message: `H${String(depth)} heading is too deep \u2014 keep structure flat (H1/H2/H3 only): "${line.trim()}"`
|
|
4946
|
+
});
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
for (const required of REQUIRED_H2_SECTIONS) {
|
|
4950
|
+
if (!h2Titles.includes(required.toLowerCase())) {
|
|
4951
|
+
violations.push({
|
|
4952
|
+
rule: "required-section",
|
|
4953
|
+
message: `Missing required H2 section: "## ${required}".`
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
if (h1Count !== 1) {
|
|
4958
|
+
violations.push({
|
|
4959
|
+
rule: "single-h1",
|
|
4960
|
+
message: `Expected exactly one H1, found ${String(h1Count)}.`
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4963
|
+
if (h2Count > MAX_H2) {
|
|
4964
|
+
violations.push({
|
|
4965
|
+
rule: "max-h2",
|
|
4966
|
+
message: `Too many H2 sections (${String(h2Count)}); keep at most ${String(MAX_H2)}.`
|
|
4967
|
+
});
|
|
4968
|
+
}
|
|
4969
|
+
const flesch = fleschReadingEase(content);
|
|
4970
|
+
if (Number.isFinite(flesch) && flesch < MIN_FLESCH) {
|
|
4971
|
+
violations.push({
|
|
4972
|
+
rule: "readability",
|
|
4973
|
+
message: `Flesch score ${flesch.toFixed(1)} is below ${String(MIN_FLESCH)} \u2014 simplify long sentences.`
|
|
4974
|
+
});
|
|
4975
|
+
}
|
|
4976
|
+
return { ok: violations.length === 0, violations };
|
|
4977
|
+
}
|
|
4978
|
+
function fleschReadingEase(content) {
|
|
4979
|
+
const prose = stripNonProse(content);
|
|
4980
|
+
const words = prose.match(/[A-Za-z][A-Za-z'-]*/g) ?? [];
|
|
4981
|
+
if (words.length === 0) return 100;
|
|
4982
|
+
const sentences = Math.max(1, (prose.match(/[.!?]+(?:\s|$)/g) ?? []).length);
|
|
4983
|
+
const syllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
|
|
4984
|
+
return 206.835 - 1.015 * (words.length / sentences) - 84.6 * (syllables / words.length);
|
|
4985
|
+
}
|
|
4986
|
+
function stripNonProse(content) {
|
|
4987
|
+
return content.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^#+\s+.*$/gm, " ").replace(/^\s*[-*+]\s+/gm, "");
|
|
4988
|
+
}
|
|
4989
|
+
function countSyllables(word) {
|
|
4990
|
+
const lower = word.toLowerCase();
|
|
4991
|
+
const groups = lower.match(/[aeiouy]+/g) ?? [];
|
|
4992
|
+
let count = groups.length;
|
|
4993
|
+
if (lower.at(-1) === "e" && count > 1) count--;
|
|
4994
|
+
return Math.max(1, count);
|
|
4995
|
+
}
|
|
4996
|
+
function detectCommandDrift(content, repoPath) {
|
|
4997
|
+
const warnings = [];
|
|
4998
|
+
const pkgPath = join4(repoPath, "package.json");
|
|
4999
|
+
if (!existsSync2(pkgPath)) return warnings;
|
|
5000
|
+
let scripts = {};
|
|
5001
|
+
try {
|
|
5002
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
5003
|
+
const parsed = JSON.parse(raw);
|
|
5004
|
+
if (isRecord(parsed) && isRecord(parsed["scripts"])) {
|
|
5005
|
+
const entries = Object.entries(parsed["scripts"]).filter((pair) => {
|
|
5006
|
+
return typeof pair[1] === "string";
|
|
5007
|
+
});
|
|
5008
|
+
scripts = Object.fromEntries(entries);
|
|
5009
|
+
}
|
|
5010
|
+
} catch {
|
|
5011
|
+
return warnings;
|
|
5012
|
+
}
|
|
5013
|
+
const re = /\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([a-z][a-z0-9:_-]*)/gi;
|
|
5014
|
+
let match;
|
|
5015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5016
|
+
while ((match = re.exec(content)) !== null) {
|
|
5017
|
+
const name = match[1];
|
|
5018
|
+
if (!name) continue;
|
|
5019
|
+
if (seen.has(name)) continue;
|
|
5020
|
+
seen.add(name);
|
|
5021
|
+
if (name === "install" || name === "test" || name === "start") continue;
|
|
5022
|
+
if (!(name in scripts)) {
|
|
5023
|
+
warnings.push(`Referenced script "${name}" not defined in package.json`);
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
return warnings;
|
|
5027
|
+
}
|
|
5028
|
+
function isRecord(value) {
|
|
5029
|
+
return typeof value === "object" && value !== null;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
// src/integration/external/agents-md-writer.ts
|
|
5033
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
|
|
5034
|
+
import { dirname, join as join5 } from "path";
|
|
5035
|
+
import { randomBytes } from "crypto";
|
|
5036
|
+
var RALPHCTL_MARKER = "<!-- managed by ralphctl onboard -->";
|
|
5037
|
+
function providerInstructionsPath(repoPath, provider) {
|
|
5038
|
+
if (provider === "claude") return join5(repoPath, "CLAUDE.md");
|
|
5039
|
+
return join5(repoPath, ".github", "copilot-instructions.md");
|
|
5040
|
+
}
|
|
5041
|
+
function readExistingProviderInstructions(repoPath, provider) {
|
|
5042
|
+
const path = providerInstructionsPath(repoPath, provider);
|
|
5043
|
+
if (!existsSync3(path)) return { content: null, authored: false };
|
|
5044
|
+
let content;
|
|
5045
|
+
try {
|
|
5046
|
+
content = readFileSync3(path, "utf-8");
|
|
5047
|
+
} catch {
|
|
5048
|
+
return { content: null, authored: false };
|
|
5049
|
+
}
|
|
5050
|
+
const managed = content.includes(RALPHCTL_MARKER);
|
|
5051
|
+
return { content, authored: !managed };
|
|
5052
|
+
}
|
|
5053
|
+
function writeProviderInstructionsAtomic(repoPath, content, provider) {
|
|
5054
|
+
const target = providerInstructionsPath(repoPath, provider);
|
|
5055
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
5056
|
+
const body = content.endsWith("\n") ? content : `${content}
|
|
5057
|
+
`;
|
|
5058
|
+
const stamped = body.includes(RALPHCTL_MARKER) ? body : `${body}
|
|
5059
|
+
${RALPHCTL_MARKER}
|
|
5060
|
+
`;
|
|
5061
|
+
const tempPath = `${target}.${randomBytes(6).toString("hex")}.tmp`;
|
|
5062
|
+
writeFileSync(tempPath, stamped, { encoding: "utf-8", mode: 420 });
|
|
5063
|
+
renameSync(tempPath, target);
|
|
5064
|
+
return { path: target };
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
// src/integration/ai/discover-agents-md.ts
|
|
5068
|
+
var DISCOVERY_TIMEOUT_MS = 12e4;
|
|
5069
|
+
async function discoverAgentsMdWithAi(ctx, aiSession, signalParser2) {
|
|
5070
|
+
const prompt = buildRepoOnboardPrompt(ctx);
|
|
5071
|
+
const session = aiSession.spawnHeadless(prompt, { cwd: ctx.repoPath });
|
|
5072
|
+
const timeout = new Promise((resolve) => {
|
|
5073
|
+
setTimeout(() => {
|
|
5074
|
+
resolve(null);
|
|
5075
|
+
}, DISCOVERY_TIMEOUT_MS).unref();
|
|
5076
|
+
});
|
|
5077
|
+
try {
|
|
5078
|
+
const result = await Promise.race([session, timeout]);
|
|
5079
|
+
if (!result) return { agentsMd: null, checkScript: null, changes: null };
|
|
5080
|
+
const signals = signalParser2.parseSignals(result.output);
|
|
5081
|
+
const agentsSignal = signals.find((s) => s.type === "agents-md-proposal");
|
|
5082
|
+
const checkSignal = signals.find((s) => s.type === "check-script-discovery");
|
|
5083
|
+
const changes = extractChanges(result.output);
|
|
5084
|
+
return {
|
|
5085
|
+
agentsMd: agentsSignal ? agentsSignal.content : null,
|
|
5086
|
+
checkScript: checkSignal ? checkSignal.command : null,
|
|
5087
|
+
changes
|
|
5088
|
+
};
|
|
5089
|
+
} catch {
|
|
5090
|
+
return { agentsMd: null, checkScript: null, changes: null };
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
function extractChanges(output) {
|
|
5094
|
+
const match = /<changes>([\s\S]*?)<\/changes>/.exec(output);
|
|
5095
|
+
if (!match?.[1]) return null;
|
|
5096
|
+
const body = match[1].trim();
|
|
5097
|
+
return body.length > 0 ? body : null;
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
// src/integration/external/onboard-adapter.ts
|
|
5101
|
+
var DefaultOnboardAdapter = class {
|
|
5102
|
+
constructor(aiSession, signalParser2) {
|
|
5103
|
+
this.aiSession = aiSession;
|
|
5104
|
+
this.signalParser = signalParser2;
|
|
5105
|
+
}
|
|
5106
|
+
aiSession;
|
|
5107
|
+
signalParser;
|
|
5108
|
+
readExistingInstructions(repoPath, provider) {
|
|
5109
|
+
return readExistingProviderInstructions(repoPath, provider);
|
|
5110
|
+
}
|
|
5111
|
+
validateRepoPath(path) {
|
|
5112
|
+
let exists;
|
|
5113
|
+
try {
|
|
5114
|
+
exists = existsSync4(path) && statSync(path).isDirectory();
|
|
5115
|
+
} catch {
|
|
5116
|
+
exists = false;
|
|
5117
|
+
}
|
|
5118
|
+
if (!exists) return { exists: false, isGitRepo: false };
|
|
5119
|
+
const isGitRepo = existsSync4(join6(path, ".git"));
|
|
5120
|
+
return { exists: true, isGitRepo };
|
|
5121
|
+
}
|
|
5122
|
+
lintAgentsMd(content) {
|
|
5123
|
+
return lintAgentsMd(content);
|
|
5124
|
+
}
|
|
5125
|
+
detectCommandDrift(content, repoPath) {
|
|
5126
|
+
return detectCommandDrift(content, repoPath);
|
|
5127
|
+
}
|
|
5128
|
+
async discoverAgentsMd(input) {
|
|
5129
|
+
return discoverAgentsMdWithAi(input, this.aiSession, this.signalParser);
|
|
5130
|
+
}
|
|
5131
|
+
inferProjectType(repoPath) {
|
|
5132
|
+
const checks = [
|
|
5133
|
+
["package.json", "node"],
|
|
5134
|
+
["pyproject.toml", "python"],
|
|
5135
|
+
["requirements.txt", "python"],
|
|
5136
|
+
["Cargo.toml", "rust"],
|
|
5137
|
+
["go.mod", "go"],
|
|
5138
|
+
["pom.xml", "java"],
|
|
5139
|
+
["build.gradle", "java"],
|
|
5140
|
+
["Makefile", "makefile"]
|
|
5141
|
+
];
|
|
5142
|
+
const hints = [];
|
|
5143
|
+
for (const [file, label] of checks) {
|
|
5144
|
+
if (existsSync4(join6(repoPath, file))) hints.push(label);
|
|
5145
|
+
}
|
|
5146
|
+
return hints.length === 0 ? "unknown" : hints.join(", ");
|
|
5147
|
+
}
|
|
5148
|
+
writeProviderInstructions(repoPath, content, provider) {
|
|
5149
|
+
return writeProviderInstructionsAtomic(repoPath, content, provider);
|
|
5150
|
+
}
|
|
5151
|
+
};
|
|
5152
|
+
|
|
5395
5153
|
// src/application/factories.ts
|
|
5396
5154
|
function createAiDeps(auto) {
|
|
5397
5155
|
return {
|
|
@@ -5451,6 +5209,22 @@ function createIdeatePipeline2(shared, idea, options = {}) {
|
|
|
5451
5209
|
options
|
|
5452
5210
|
);
|
|
5453
5211
|
}
|
|
5212
|
+
function createOnboardPipeline2(shared, options = {}) {
|
|
5213
|
+
const aiSession = new ProviderAiSessionAdapter();
|
|
5214
|
+
const adapter = new DefaultOnboardAdapter(aiSession, shared.signalParser);
|
|
5215
|
+
return createOnboardPipeline(
|
|
5216
|
+
{
|
|
5217
|
+
persistence: shared.persistence,
|
|
5218
|
+
adapter,
|
|
5219
|
+
logger: shared.logger,
|
|
5220
|
+
prompt: shared.prompt,
|
|
5221
|
+
updateProjectRepos: async (name, repositories) => {
|
|
5222
|
+
return updateProject(name, { repositories });
|
|
5223
|
+
}
|
|
5224
|
+
},
|
|
5225
|
+
options
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5454
5228
|
function createExecuteSprintPipeline2(shared, options = {}) {
|
|
5455
5229
|
const { aiSession, promptBuilder, parser, ui, external } = createAiDeps(false);
|
|
5456
5230
|
return createExecuteSprintPipeline(
|
|
@@ -5467,7 +5241,9 @@ function createExecuteSprintPipeline2(shared, options = {}) {
|
|
|
5467
5241
|
signalHandler: shared.signalHandler,
|
|
5468
5242
|
signalBus: shared.signalBus,
|
|
5469
5243
|
createRateLimitCoordinator: shared.createRateLimitCoordinator,
|
|
5470
|
-
processLifecycle: shared.processLifecycle
|
|
5244
|
+
processLifecycle: shared.processLifecycle,
|
|
5245
|
+
prompt: shared.prompt,
|
|
5246
|
+
isTTY
|
|
5471
5247
|
},
|
|
5472
5248
|
options
|
|
5473
5249
|
);
|
|
@@ -5547,10 +5323,17 @@ function parseArgs(args) {
|
|
|
5547
5323
|
options.noEvaluate = true;
|
|
5548
5324
|
} else if (arg === "--no-feedback") {
|
|
5549
5325
|
options.noFeedback = true;
|
|
5326
|
+
} else if (arg === "--resume-dirty") {
|
|
5327
|
+
options.resumeDirty = true;
|
|
5328
|
+
} else if (arg === "--reset-on-resume") {
|
|
5329
|
+
options.resetOnResume = true;
|
|
5550
5330
|
} else if (!arg?.startsWith("-")) {
|
|
5551
5331
|
sprintId = arg;
|
|
5552
5332
|
}
|
|
5553
5333
|
}
|
|
5334
|
+
if (options.resumeDirty && options.resetOnResume) {
|
|
5335
|
+
throw new Error("--resume-dirty and --reset-on-resume are mutually exclusive");
|
|
5336
|
+
}
|
|
5554
5337
|
return { sprintId, options };
|
|
5555
5338
|
}
|
|
5556
5339
|
async function sprintStartCommand(args) {
|
|
@@ -5629,6 +5412,7 @@ async function sprintStartCommand(args) {
|
|
|
5629
5412
|
}
|
|
5630
5413
|
|
|
5631
5414
|
export {
|
|
5415
|
+
executePipeline,
|
|
5632
5416
|
getTasks,
|
|
5633
5417
|
saveTasks,
|
|
5634
5418
|
getTask,
|
|
@@ -5644,29 +5428,20 @@ export {
|
|
|
5644
5428
|
areAllTasksDone,
|
|
5645
5429
|
reorderByDependencies,
|
|
5646
5430
|
validateImportTasks,
|
|
5647
|
-
getCurrentBranch,
|
|
5648
|
-
branchExists,
|
|
5649
|
-
getDefaultBranch,
|
|
5650
|
-
isGhAvailable,
|
|
5651
|
-
isGlabAvailable,
|
|
5652
|
-
executePipeline,
|
|
5653
|
-
processLifecycleAdapter,
|
|
5654
|
-
resolveProvider,
|
|
5655
|
-
providerDisplayName,
|
|
5656
|
-
enterAltScreen,
|
|
5657
|
-
exitAltScreen,
|
|
5658
|
-
registerTuiInstance,
|
|
5659
|
-
withSuspendedTui,
|
|
5660
|
-
buildTicketRefinePrompt,
|
|
5661
5431
|
renderParsedTasksTable,
|
|
5662
5432
|
importTasks,
|
|
5663
5433
|
formatTicketForPrompt,
|
|
5664
5434
|
parseRequirementsFile,
|
|
5665
5435
|
runAiSession,
|
|
5666
|
-
|
|
5436
|
+
getCurrentBranch,
|
|
5437
|
+
branchExists,
|
|
5438
|
+
getDefaultBranch,
|
|
5439
|
+
isGhAvailable,
|
|
5440
|
+
isGlabAvailable,
|
|
5667
5441
|
createRefinePipeline2 as createRefinePipeline,
|
|
5668
5442
|
createPlanPipeline2 as createPlanPipeline,
|
|
5669
5443
|
createIdeatePipeline2 as createIdeatePipeline,
|
|
5444
|
+
createOnboardPipeline2 as createOnboardPipeline,
|
|
5670
5445
|
createExecuteSprintPipeline2 as createExecuteSprintPipeline,
|
|
5671
5446
|
parseSprintStartArgs,
|
|
5672
5447
|
sprintStartCommand
|