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.
Files changed (31) hide show
  1. package/README.md +13 -11
  2. package/dist/{add-CIM72NE3.mjs → add-DVPVHENV.mjs} +7 -7
  3. package/dist/{add-GX7P7XTT.mjs → add-YVXM34RP.mjs} +6 -5
  4. package/dist/{chunk-GL7MKLLS.mjs → chunk-ACRMBVEE.mjs} +458 -181
  5. package/dist/{chunk-NUYQK5MN.mjs → chunk-BSB4EDGR.mjs} +2 -2
  6. package/dist/{chunk-YCDUVPRT.mjs → chunk-CBMFRQ4Y.mjs} +5 -73
  7. package/dist/{chunk-3QBEBKMZ.mjs → chunk-FNAAA32W.mjs} +7 -7
  8. package/dist/{chunk-JOQO4HMM.mjs → chunk-GQ2WFKBN.mjs} +11 -11
  9. package/dist/{chunk-TKPTT2UG.mjs → chunk-OFILN7QL.mjs} +798 -1023
  10. package/dist/{chunk-7JLZQICD.mjs → chunk-OGEXYSFS.mjs} +7 -7
  11. package/dist/{chunk-D2YGPLIV.mjs → chunk-PYZEQ2VK.mjs} +214 -9
  12. package/dist/{chunk-57UWLHRH.mjs → chunk-VAZ3LJBI.mjs} +12 -1
  13. package/dist/{chunk-CTP2A436.mjs → chunk-WDMLPXOD.mjs} +11 -4
  14. package/dist/{chunk-FKMKOWLA.mjs → chunk-XN2UIHBY.mjs} +84 -3
  15. package/dist/chunk-ZLWSPLWI.mjs +1117 -0
  16. package/dist/cli.mjs +72 -21
  17. package/dist/create-Z635FQKO.mjs +15 -0
  18. package/dist/{handle-BBAZJ44Y.mjs → handle-23EFF3BE.mjs} +1 -1
  19. package/dist/{mount-ISHZM36X.mjs → mount-VEV3TESX.mjs} +1702 -1202
  20. package/dist/{project-2IE7VWDB.mjs → project-DQHF4ISP.mjs} +3 -3
  21. package/dist/prompts/check-script-discover.md +69 -0
  22. package/dist/prompts/repo-onboard.md +111 -0
  23. package/dist/prompts/sprint-feedback.md +4 -0
  24. package/dist/prompts/task-evaluation.md +44 -2
  25. package/dist/prompts/task-execution.md +5 -0
  26. package/dist/{resolver-EOE5WUMV.mjs → resolver-OVPYVW6Q.mjs} +4 -4
  27. package/dist/{sprint-OGOFEJJH.mjs → sprint-4E26AB5F.mjs} +4 -4
  28. package/dist/start-2WH4BTDB.mjs +19 -0
  29. package/package.json +6 -6
  30. package/dist/create-7WFSCMP4.mjs +0 -15
  31. 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-JOQO4HMM.mjs";
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-YCDUVPRT.mjs";
41
+ } from "./chunk-CBMFRQ4Y.mjs";
27
42
  import {
28
- emoji,
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-FKMKOWLA.mjs";
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-CTP2A436.mjs";
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-57UWLHRH.mjs";
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 services = opts.createServices?.() ?? defaultServices();
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
- try {
3132
- if (external.hasUncommittedChanges(projectPath)) {
3133
- return Result.error(
3134
- new StorageError(
3135
- `Repository at ${projectPath} has uncommitted changes. Commit or stash them before starting.`
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/integration/ai/session/session.ts
3555
- import { spawn, spawnSync } from "child_process";
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/integration/ai/session/process-manager.ts
3558
- var GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5e3;
3559
- var FORCE_QUIT_WINDOW_MS = 5e3;
3560
- var ProcessManager = class _ProcessManager {
3561
- static instance = null;
3562
- /** All active AI child processes */
3563
- children = /* @__PURE__ */ new Set();
3564
- /** Cleanup callbacks (for stopping spinners, removing temp files) */
3565
- cleanupCallbacks = /* @__PURE__ */ new Set();
3566
- /** Whether we're currently shutting down */
3567
- exiting = false;
3568
- /** Whether signal handlers have been installed */
3569
- handlersInstalled = false;
3570
- /** Timestamp of first SIGINT (for double-signal detection) */
3571
- firstSigintAt = null;
3572
- /** Stored signal handler references for cleanup */
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
- if (this.exiting) {
3691
- return;
3692
- }
3693
- this.exiting = true;
3694
- if (signal === "SIGINT") {
3695
- this.firstSigintAt = Date.now();
3696
- }
3697
- log.dim("\n\nShutting down gracefully... (press Ctrl+C again to force-quit)");
3698
- for (const callback of this.cleanupCallbacks) {
3699
- try {
3700
- callback();
3701
- } catch (err) {
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
- this.cleanupCallbacks.clear();
3706
- this.killAll("SIGINT");
3707
- const waitStart = Date.now();
3708
- while (this.children.size > 0 && Date.now() - waitStart < GRACEFUL_SHUTDOWN_TIMEOUT_MS) {
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 (this.children.size > 0) {
3712
- log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
3713
- this.killAll("SIGKILL");
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
- process.exit(signal === "SIGINT" ? EXIT_INTERRUPTED : 1);
3716
- }
3717
- /**
3718
- * Clean up all resources (for testing).
3719
- * @internal
3720
- */
3721
- dispose() {
3722
- if (this.sigintHandler) {
3723
- process.removeListener("SIGINT", this.sigintHandler);
3724
- this.sigintHandler = null;
3725
- }
3726
- if (this.sigtermHandler) {
3727
- process.removeListener("SIGTERM", this.sigtermHandler);
3728
- this.sigtermHandler = null;
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
- this.children.clear();
3731
- this.cleanupCallbacks.clear();
3732
- this.exiting = false;
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
- // src/integration/ai/session/session.ts
3912
- function spawnInteractive(prompt, options, provider) {
3913
- assertSafeCwd(options.cwd);
3914
- const args = prompt ? provider.buildInteractiveArgs(prompt, options.args ?? []) : [...provider.baseArgs, ...options.args ?? []];
3915
- const env = options.env ? { ...process.env, ...options.env } : void 0;
3916
- const result = spawnSync(provider.binary, args, {
3917
- cwd: options.cwd,
3918
- stdio: "inherit",
3919
- env
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
- async function spawnHeadless(options, provider) {
3927
- assertSafeCwd(options.cwd);
3928
- const p = provider ?? await getActiveProvider();
3929
- return new Promise((resolve, reject) => {
3930
- const allArgs = p.buildHeadlessArgs(options.args ?? []);
3931
- if (options.resumeSessionId) {
3932
- try {
3933
- allArgs.push(...p.buildResumeArgs(options.resumeSessionId));
3934
- } catch {
3935
- reject(new SpawnError("Invalid session ID format", "", 1));
3936
- return;
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
- const child = spawn(p.binary, allArgs, {
3940
- cwd: options.cwd,
3941
- stdio: ["pipe", "pipe", "pipe"],
3942
- env: options.env ? { ...process.env, ...options.env } : void 0
3943
- });
3944
- const manager = ProcessManager.getInstance();
3945
- try {
3946
- manager.registerChild(child);
3947
- } catch {
3948
- reject(new SpawnError("Cannot spawn during shutdown", "", 1));
3949
- return;
3950
- }
3951
- const MAX_STDOUT_SIZE = 1e7;
3952
- const MAX_PROMPT_SIZE = 1e6;
3953
- if (options.prompt) {
3954
- if (options.prompt.length > MAX_PROMPT_SIZE) {
3955
- reject(new SpawnError("Prompt exceeds maximum size (1MB)", "", 1));
3956
- return;
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
- child.stdin.write(options.prompt);
3959
- }
3960
- child.stdin.end();
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
- child.stderr.on("data", (data) => {
3969
- stderr += data.toString();
3970
- });
3971
- child.on("close", (code) => {
3972
- void (async () => {
3973
- const exitCode = code ?? 1;
3974
- const { result, sessionId: parsedSessionId, model: parsedModel } = p.parseJsonOutput(rawStdout);
3975
- const sessionId = parsedSessionId ?? await p.extractSessionId?.(options.cwd) ?? null;
3976
- if (exitCode !== 0) {
3977
- reject(
3978
- new SpawnError(
3979
- `${p.displayName} CLI exited with code ${String(exitCode)}: ${stderr}`,
3980
- stderr,
3981
- exitCode,
3982
- sessionId
3983
- )
3984
- );
3985
- } else {
3986
- resolve({ stdout: result, stderr, exitCode: 0, sessionId, model: parsedModel });
3987
- }
3988
- })().catch((err) => {
3989
- reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, "", 1));
3990
- });
3991
- });
3992
- child.on("error", (err) => {
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
- var DEFAULT_MAX_RETRIES = 5;
3998
- var BASE_DELAY_MS = 2e3;
3999
- var MAX_DELAY_MS = 12e4;
4000
- var DEFAULT_TOTAL_TIMEOUT_MS = 6e5;
4001
- function sleep(ms) {
4002
- return new Promise((resolve) => setTimeout(resolve, ms));
4003
- }
4004
- function jitter() {
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 delay = Math.min(err.retryAfterMs ?? BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + jitter();
4031
- retryOptions?.onRetry?.(attempt + 1, delay, err);
4032
- await sleep(delay);
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
- process.on("uncaughtException", (err) => {
4065
- restore();
4066
- setImmediate(() => {
4067
- throw err;
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 enterAltScreen() {
4072
- if (altScreenActive) return;
4073
- if (!process.stdout.isTTY) return;
4074
- installSafetyNets();
4075
- altScreenActive = true;
4076
- writeRaw(ENTER_ALT_SCREEN);
4077
- writeRaw(CLEAR_SCREEN);
4078
- writeRaw(HIDE_CURSOR);
4079
- }
4080
- function exitAltScreen() {
4081
- restore();
4082
- }
4083
-
4084
- // src/integration/ui/tui/runtime/suspend.ts
4085
- var activeInstance = null;
4086
- function registerTuiInstance(instance) {
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
- if (result.error) {
4133
- throw new Error(result.error);
4134
- }
4135
- });
4136
- }
4137
- async spawnHeadless(prompt, options) {
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
- return this.provider.displayName;
4204
- }
4205
- getSpawnEnv() {
4206
- if (!this.provider) {
4207
- throw new Error("Provider not yet resolved. Call an async method first.");
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 buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema, projectToolingSection) {
4307
- return composePrompt(loadTemplate("ideate"), {
4308
- ...buildPlannerBase(projectToolingSection),
4309
- IDEA_TITLE: ideaTitle,
4310
- IDEA_DESCRIPTION: ideaDescription,
4311
- PROJECT_NAME: projectName,
4312
- REPOSITORIES: repositories,
4313
- OUTPUT_FILE: outputFile,
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 buildSprintFeedbackPrompt(sprintName, completedTasks, feedback, branch) {
4379
- const template = loadTemplate("sprint-feedback");
4380
- const branchSection = branch ? `
4381
- **Branch:** ${branch}
4382
- ` : "";
4383
- return composePrompt(template, {
4384
- HARNESS_CONTEXT: loadPartial("harness-context"),
4385
- SIGNALS: loadPartial("signals-task"),
4386
- SPRINT_NAME: sprintName,
4387
- BRANCH_SECTION: branchSection,
4388
- COMPLETED_TASKS: completedTasks,
4389
- FEEDBACK: feedback
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 join4 } from "path";
4599
- import { Result as Result4 } from "typescript-result";
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 = Result4.try(() => JSON.parse(jsonStr));
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 = join4(workingDir, "refine-context.md");
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)\s*(?:—|-)\s*(.+)/gi;
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 || !finding) continue;
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: finding.trim()
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 as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
4969
- import { join as join5 } from "path";
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 (!existsSync2(path)) return [];
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 = join5(projectPath, ".claude", "agents");
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 = join5(projectPath, ".claude", "skills");
4469
+ const skillsDir = join3(projectPath, ".claude", "skills");
4993
4470
  try {
4994
- if (!existsSync2(skillsDir)) return [];
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 = join5(projectPath, ".mcp.json");
5002
- if (!existsSync2(mcpFile)) return [];
4478
+ const mcpFile = join3(projectPath, ".mcp.json");
4479
+ if (!existsSync(mcpFile)) return [];
5003
4480
  try {
5004
- const raw = readFileSync2(mcpFile, "utf-8");
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 || !existsSync2(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: existsSync2(join5(projectPath, "CLAUDE.md")),
5022
- hasAgentsMd: existsSync2(join5(projectPath, "AGENTS.md")),
5023
- hasCopilotInstructions: existsSync2(join5(projectPath, ".github", "copilot-instructions.md"))
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 as spawnSync2 } from "child_process";
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 = spawnSync2(script, {
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 Result5 } from "typescript-result";
4638
+ import { Result as Result3 } from "typescript-result";
5162
4639
  function getRecentGitHistory(projectPath, count = 20) {
5163
- const r = Result5.try(() => {
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 spawnSync3 } from "child_process";
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 = spawnSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
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 = spawnSync3("git", ["show-ref", "--verify", `refs/heads/${name}`], {
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 = spawnSync3("git", ["checkout", name], {
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 = spawnSync3("git", ["checkout", "-b", name], {
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 = spawnSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
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 = spawnSync3("git", ["rev-parse", "HEAD"], {
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 = spawnSync3("git", ["status", "--porcelain"], {
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 = spawnSync3("git", ["add", "-A"], {
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 = spawnSync3("git", ["commit", "-m", message], {
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 = spawnSync3("gh", ["--version"], {
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 = spawnSync3("glab", ["--version"], {
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
- SignalParser,
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