ralphctl 0.4.1 → 0.4.3

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 (36) hide show
  1. package/README.md +13 -11
  2. package/dist/{add-CIM72NE3.mjs → add-MG26JWBP.mjs} +6 -6
  3. package/dist/{add-GX7P7XTT.mjs → add-ZZYL4BSF.mjs} +5 -4
  4. package/dist/chunk-2FT37OZX.mjs +1071 -0
  5. package/dist/{chunk-CTP2A436.mjs → chunk-D2HWXEHH.mjs} +9 -2
  6. package/dist/{chunk-JOQO4HMM.mjs → chunk-EGUFQNRB.mjs} +10 -10
  7. package/dist/{chunk-3HJNVQ7N.mjs → chunk-LCY32RW4.mjs} +621 -976
  8. package/dist/{chunk-NUYQK5MN.mjs → chunk-LDSG7G2T.mjs} +1 -1
  9. package/dist/{chunk-7JLZQICD.mjs → chunk-MDE6KPJQ.mjs} +6 -6
  10. package/dist/{chunk-3QBEBKMZ.mjs → chunk-Q4AVHUZL.mjs} +7 -7
  11. package/dist/{chunk-YCDUVPRT.mjs → chunk-RQGD5WS6.mjs} +4 -72
  12. package/dist/{chunk-D2YGPLIV.mjs → chunk-TDBEEHTS.mjs} +213 -8
  13. package/dist/{chunk-SM4GGZSU.mjs → chunk-WOMGKKZY.mjs} +152 -179
  14. package/dist/{chunk-FKMKOWLA.mjs → chunk-WZTY77GY.mjs} +75 -1
  15. package/dist/cli.mjs +68 -19
  16. package/dist/{create-7WFSCMP4.mjs → create-PQK6KKRD.mjs} +5 -5
  17. package/dist/{handle-BBAZJ44Y.mjs → handle-SYVCFI6Y.mjs} +1 -1
  18. package/dist/{mount-2N6H5CWA.mjs → mount-2ANLHHQE.mjs} +556 -318
  19. package/dist/{project-2IE7VWDB.mjs → project-JF47ZWMF.mjs} +2 -2
  20. package/dist/prompts/check-script-discover.md +69 -0
  21. package/dist/prompts/ideate-auto.md +26 -1
  22. package/dist/prompts/ideate.md +5 -1
  23. package/dist/prompts/plan-auto.md +30 -2
  24. package/dist/prompts/plan-common-examples.md +82 -0
  25. package/dist/prompts/plan-common.md +26 -78
  26. package/dist/prompts/plan-interactive.md +6 -2
  27. package/dist/prompts/repo-onboard.md +111 -0
  28. package/dist/prompts/sprint-feedback.md +6 -2
  29. package/dist/prompts/task-evaluation.md +25 -10
  30. package/dist/prompts/task-execution.md +13 -13
  31. package/dist/prompts/ticket-refine.md +4 -0
  32. package/dist/prompts/validation-checklist.md +4 -0
  33. package/dist/{resolver-EOE5WUMV.mjs → resolver-PG2DZEBX.mjs} +3 -3
  34. package/dist/{sprint-OGOFEJJH.mjs → sprint-54DOSIJK.mjs} +3 -3
  35. package/dist/{start-IUDCXIEA.mjs → start-2SZTBKGF.mjs} +7 -5
  36. package/package.json +6 -6
@@ -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-2FT37OZX.mjs";
2
17
  import {
3
18
  fetchIssueFromUrl,
4
19
  formatIssueContext,
5
20
  formatTicketDisplay,
6
21
  truncate
7
- } from "./chunk-JOQO4HMM.mjs";
22
+ } from "./chunk-EGUFQNRB.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,17 @@ import {
15
29
  getPrompt,
16
30
  getSharedDeps
17
31
  } from "./chunk-747KW2RW.mjs";
32
+ import {
33
+ updateProject
34
+ } from "./chunk-LDSG7G2T.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-RQGD5WS6.mjs";
27
42
  import {
28
- emoji,
29
43
  log,
30
44
  printHeader,
31
45
  renderTable,
@@ -35,13 +49,14 @@ import {
35
49
  showSuccess,
36
50
  showWarning,
37
51
  terminalBell
38
- } from "./chunk-FKMKOWLA.mjs";
52
+ } from "./chunk-WZTY77GY.mjs";
39
53
  import {
40
54
  ensureError,
41
55
  unwrapOrThrow,
42
56
  wrapAsync
43
57
  } from "./chunk-IWXBJD2D.mjs";
44
58
  import {
59
+ CURRENT_ONBOARDING_VERSION,
45
60
  IdeateOutputSchema,
46
61
  ImportTasksSchema,
47
62
  RefinedRequirementsSchema,
@@ -53,12 +68,11 @@ import {
53
68
  getTasksFilePath,
54
69
  readValidatedJson,
55
70
  writeValidatedJson
56
- } from "./chunk-CTP2A436.mjs";
71
+ } from "./chunk-D2HWXEHH.mjs";
57
72
  import {
58
73
  BranchPreflightError,
59
74
  DependencyCycleError,
60
75
  DomainError,
61
- IOError,
62
76
  ParseError,
63
77
  ProjectNotFoundError,
64
78
  SpawnError,
@@ -2139,6 +2153,10 @@ ${repoPath}`);
2139
2153
  case "note":
2140
2154
  await this.signalHandler.handleNote(signal, ctx);
2141
2155
  break;
2156
+ case "check-script-discovery":
2157
+ break;
2158
+ case "agents-md-proposal":
2159
+ break;
2142
2160
  default: {
2143
2161
  const _exhaustive = signal;
2144
2162
  void _exhaustive;
@@ -3551,829 +3569,315 @@ function createExecuteSprintPipeline(deps, options = {}) {
3551
3569
  ]);
3552
3570
  }
3553
3571
 
3554
- // src/integration/ai/session/session.ts
3555
- import { spawn, spawnSync } from "child_process";
3572
+ // src/business/pipelines/steps/validate-agents-md.ts
3573
+ function validateAgentsMdStep(adapter) {
3574
+ return step("validate-agents-md", (ctx) => {
3575
+ const draft = ctx.agentsMdDraft;
3576
+ if (!draft || draft.trim().length === 0) {
3577
+ return Result.error(new ParseError("Project context file draft is empty \u2014 AI discovery produced no content."));
3578
+ }
3579
+ const { violations } = adapter.lintAgentsMd(draft);
3580
+ const partial = { agentsMdViolations: violations };
3581
+ return Result.ok(partial);
3582
+ });
3583
+ }
3556
3584
 
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;
3585
+ // src/business/pipelines/onboard.ts
3586
+ function providerInstructionsFileName(provider) {
3587
+ if (provider === "claude") return "CLAUDE.md";
3588
+ return ".github/copilot-instructions.md";
3589
+ }
3590
+ function loadProjectStep(deps) {
3591
+ return step("load-project", async (ctx) => {
3592
+ try {
3593
+ const project = await deps.persistence.getProject(ctx.projectName);
3594
+ const config = await deps.persistence.getConfig();
3595
+ if (!config.aiProvider) {
3596
+ return Result.error(
3597
+ new ParseError(
3598
+ "No AI provider configured \u2014 run `ralphctl config set provider <claude|copilot>` before onboarding."
3599
+ )
3600
+ );
3688
3601
  }
3602
+ const partial = { project, provider: config.aiProvider };
3603
+ return Result.ok(partial);
3604
+ } catch (err) {
3605
+ if (err instanceof ProjectNotFoundError) return Result.error(err);
3606
+ return Result.error(new ParseError(err instanceof Error ? err.message : String(err)));
3689
3607
  }
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)}`);
3608
+ });
3609
+ }
3610
+ function selectRepoStep(deps, options) {
3611
+ return step("select-repo", async (ctx) => {
3612
+ const project = ctx.project;
3613
+ if (!project) return Result.error(new ParseError("Project not loaded."));
3614
+ const repos = project.repositories;
3615
+ if (repos.length === 0) return Result.error(new ParseError("Project has no repositories."));
3616
+ if (options.repo) {
3617
+ const match = repos.find((r) => r.name === options.repo);
3618
+ if (!match) {
3619
+ return Result.error(new ParseError(`No repository named "${options.repo}" in project "${project.name}".`));
3703
3620
  }
3621
+ return Result.ok({ repo: match });
3704
3622
  }
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));
3623
+ if (repos.length === 1) {
3624
+ const only = repos[0];
3625
+ if (!only) return Result.error(new ParseError("Project has no repositories."));
3626
+ return Result.ok({ repo: only });
3710
3627
  }
3711
- if (this.children.size > 0) {
3712
- log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
3713
- this.killAll("SIGKILL");
3628
+ if (options.auto) {
3629
+ const first = repos[0];
3630
+ if (!first) return Result.error(new ParseError("Project has no repositories."));
3631
+ return Result.ok({ repo: first });
3714
3632
  }
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;
3633
+ const choice = await deps.prompt.select({
3634
+ message: `Select a repository to onboard in "${project.name}":`,
3635
+ choices: repos.map((r) => ({ label: `${r.name} \u2014 ${r.path}`, value: r.id }))
3636
+ });
3637
+ const selected = repos.find((r) => r.id === choice);
3638
+ if (!selected) return Result.error(new ParseError("Invalid repository selection."));
3639
+ return Result.ok({ repo: selected });
3640
+ });
3641
+ }
3642
+ function repoPreflightStep(deps) {
3643
+ return step("repo-preflight", (ctx) => {
3644
+ const repo = ctx.repo;
3645
+ const provider = ctx.provider;
3646
+ if (!repo) return Result.error(new ParseError("Repository not resolved."));
3647
+ if (!provider) return Result.error(new ParseError("AI provider not resolved."));
3648
+ const validation = deps.adapter.validateRepoPath(repo.path);
3649
+ if (!validation.exists) {
3650
+ return Result.error(new ParseError(`Repository path does not exist or is not a directory: ${repo.path}`));
3651
+ }
3652
+ if (!validation.isGitRepo) {
3653
+ return Result.error(new ParseError(`Repository is not a git repository: ${repo.path}`));
3654
+ }
3655
+ const existing = deps.adapter.readExistingInstructions(repo.path, provider);
3656
+ let mode;
3657
+ if (existing.content === null) {
3658
+ mode = "bootstrap";
3659
+ } else if (repo.onboardingVersion != null) {
3660
+ mode = "update";
3661
+ } else {
3662
+ mode = "adopt";
3729
3663
  }
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
3664
+ const partial = {
3665
+ mode,
3666
+ existingAgentsMd: existing.content
3783
3667
  };
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(() => {
3668
+ return Result.ok(partial);
3669
+ });
3670
+ }
3671
+ function aiInventoryStep(deps) {
3672
+ return step("ai-inventory", async (ctx) => {
3673
+ const repo = ctx.repo;
3674
+ const mode = ctx.mode;
3675
+ const provider = ctx.provider;
3676
+ if (!repo || !mode || !provider)
3677
+ return Result.error(new ParseError("Preflight did not populate repo/mode/provider."));
3678
+ deps.logger.info(`Asking AI to inventory ${repo.name}...`);
3679
+ let result;
3680
+ try {
3681
+ result = await deps.adapter.discoverAgentsMd({
3682
+ repoPath: repo.path,
3683
+ mode,
3684
+ existingAgentsMd: ctx.existingAgentsMd ?? null,
3685
+ projectType: deps.adapter.inferProjectType(repo.path),
3686
+ checkScriptSuggestion: repo.checkScript ?? "",
3687
+ fileName: providerInstructionsFileName(provider)
3860
3688
  });
3689
+ } catch (err) {
3690
+ return Result.error(new ParseError(`AI inventory failed: ${err instanceof Error ? err.message : String(err)}`));
3861
3691
  }
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
- ]
3692
+ if (!result.agentsMd) {
3693
+ return Result.error(
3694
+ new ParseError("AI returned no project context file proposal \u2014 try again, or edit the file manually.")
3695
+ );
3696
+ }
3697
+ const partial = {
3698
+ agentsMdDraft: result.agentsMd,
3699
+ checkScriptDraft: result.checkScript,
3700
+ changes: result.changes
3701
+ };
3702
+ return Result.ok(partial);
3889
3703
  });
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
3704
  }
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
3920
- });
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
- }
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;
3705
+ function retryOnViolationStep(deps) {
3706
+ return step(
3707
+ "retry-agents-md-on-violation",
3708
+ async (ctx) => {
3709
+ const violations = ctx.agentsMdViolations ?? [];
3710
+ if (violations.length === 0) return Result.ok({});
3711
+ const repo = ctx.repo;
3712
+ const mode = ctx.mode;
3713
+ const provider = ctx.provider;
3714
+ const draft = ctx.agentsMdDraft;
3715
+ if (!repo || !mode || !provider || !draft) {
3716
+ return Result.error(new ParseError("Retry requires repo, mode, provider, and an existing draft."));
3937
3717
  }
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;
3718
+ deps.logger.warn(
3719
+ `Project context file draft failed ${String(violations.length)} rule(s); asking AI for a fix...`
3720
+ );
3721
+ const violationSummary = violations.map((v) => `- [${v.rule}] ${v.message}`).join("\n");
3722
+ const feedbackContext = [
3723
+ ctx.existingAgentsMd ?? "",
3724
+ "",
3725
+ "---",
3726
+ "",
3727
+ "Your previous draft (below) violated these rules:",
3728
+ violationSummary,
3729
+ "",
3730
+ "Fix every violation and re-emit the full project context file plus check-script.",
3731
+ "",
3732
+ draft
3733
+ ].join("\n");
3734
+ let retry;
3735
+ try {
3736
+ retry = await deps.adapter.discoverAgentsMd({
3737
+ repoPath: repo.path,
3738
+ mode,
3739
+ existingAgentsMd: feedbackContext,
3740
+ projectType: deps.adapter.inferProjectType(repo.path),
3741
+ checkScriptSuggestion: ctx.checkScriptDraft ?? repo.checkScript ?? "",
3742
+ fileName: providerInstructionsFileName(provider)
3743
+ });
3744
+ } catch (err) {
3745
+ return Result.error(new ParseError(`Retry failed: ${err instanceof Error ? err.message : String(err)}`));
3957
3746
  }
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();
3747
+ if (!retry.agentsMd) {
3748
+ deps.logger.warn("Retry produced no new proposal \u2014 keeping original draft.");
3749
+ return Result.ok({});
3966
3750
  }
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
- });
3751
+ const { violations: retryViolations } = deps.adapter.lintAgentsMd(retry.agentsMd);
3752
+ const partial = {
3753
+ agentsMdDraft: retry.agentsMd,
3754
+ checkScriptDraft: retry.checkScript ?? ctx.checkScriptDraft,
3755
+ agentsMdViolations: retryViolations
3756
+ };
3757
+ return Result.ok(partial);
3758
+ }
3759
+ );
3760
+ }
3761
+ function checkDriftStep(deps) {
3762
+ return step("check-drift", (ctx) => {
3763
+ const draft = ctx.agentsMdDraft;
3764
+ const repo = ctx.repo;
3765
+ if (!draft || !repo) return Result.error(new ParseError("check-drift requires a draft and repo."));
3766
+ const warnings = deps.adapter.detectCommandDrift(draft, repo.path);
3767
+ const residual = ctx.agentsMdViolations ?? [];
3768
+ for (const v of residual) {
3769
+ warnings.push(`lint[${v.rule}]: ${v.message}`);
3770
+ }
3771
+ const alreadyCurrent = ctx.mode === "update" && warnings.length === 0 && (!ctx.changes || ctx.changes.trim().length === 0);
3772
+ const partial = {
3773
+ driftWarnings: warnings,
3774
+ alreadyCurrent
3775
+ };
3776
+ return Result.ok(partial);
3995
3777
  });
3996
3778
  }
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;
3779
+ function reviewAndConfirmStep(deps, options) {
3780
+ return step("review-and-confirm", async (ctx) => {
3781
+ if (ctx.alreadyCurrent || options.auto || options.dryRun) {
3782
+ const partial2 = {
3783
+ agentsMdFinal: ctx.agentsMdDraft,
3784
+ checkScriptFinal: ctx.checkScriptDraft ?? null
3785
+ };
3786
+ return Result.ok(partial2);
4029
3787
  }
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);
3788
+ const fileName = ctx.provider ? providerInstructionsFileName(ctx.provider) : "project context file";
3789
+ const edited = await deps.prompt.editor({
3790
+ message: `Review ${fileName} (save to accept, cancel to abort):`,
3791
+ default: ctx.agentsMdDraft ?? ""
4062
3792
  });
4063
- }
4064
- process.on("uncaughtException", (err) => {
4065
- restore();
4066
- setImmediate(() => {
4067
- throw err;
3793
+ if (edited === null) {
3794
+ return Result.error(new ParseError("User cancelled project context file review."));
3795
+ }
3796
+ const checkEdited = await deps.prompt.input({
3797
+ message: "Check script (optional; empty skips):",
3798
+ default: ctx.checkScriptDraft ?? ""
4068
3799
  });
3800
+ const finalCheck = checkEdited.trim() === "" ? null : checkEdited.trim();
3801
+ const partial = {
3802
+ agentsMdFinal: edited,
3803
+ checkScriptFinal: finalCheck
3804
+ };
3805
+ return Result.ok(partial);
4069
3806
  });
4070
3807
  }
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
3808
+ function writeArtifactsStep(deps, options) {
3809
+ return step("write-artifacts", async (ctx) => {
3810
+ if (options.dryRun || ctx.alreadyCurrent) {
3811
+ deps.logger.info(options.dryRun ? "Dry run \u2014 skipping writes." : "Already up to date \u2014 skipping writes.");
3812
+ return Result.ok({});
3813
+ }
3814
+ const repo = ctx.repo;
3815
+ const project = ctx.project;
3816
+ const provider = ctx.provider;
3817
+ const content = ctx.agentsMdFinal;
3818
+ if (!repo || !project || !provider || !content) {
3819
+ return Result.error(new ParseError("write-artifacts requires repo, project, provider, and final content."));
3820
+ }
3821
+ if (ctx.mode === "adopt") {
3822
+ deps.logger.warn(
3823
+ "Adopt mode \u2014 existing project context file left untouched. Review the proposed additions and apply them manually."
4131
3824
  );
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.");
3825
+ return Result.ok({
3826
+ driftWarnings: [
3827
+ ...ctx.driftWarnings ?? [],
3828
+ "adopt-mode: authored file preserved; proposed additions not written \u2014 apply manually."
3829
+ ]
3830
+ });
4202
3831
  }
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.");
3832
+ try {
3833
+ const written = deps.adapter.writeProviderInstructions(repo.path, content, provider);
3834
+ const updatedRepos = project.repositories.map((r) => {
3835
+ if (r.id !== repo.id) return r;
3836
+ const next = {
3837
+ ...r,
3838
+ onboardingVersion: CURRENT_ONBOARDING_VERSION
3839
+ };
3840
+ const cs = ctx.checkScriptFinal;
3841
+ if (cs && cs.length > 0) {
3842
+ next.checkScript = cs;
3843
+ } else if (cs === null) {
3844
+ delete next.checkScript;
3845
+ }
3846
+ return next;
3847
+ });
3848
+ await deps.updateProjectRepos(project.name, updatedRepos);
3849
+ const partial = {
3850
+ writtenPath: written.path
3851
+ };
3852
+ return Result.ok(partial);
3853
+ } catch (err) {
3854
+ return Result.error(new ParseError(`Write failed: ${err instanceof Error ? err.message : String(err)}`));
4208
3855
  }
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
- function buildPlanCommon(projectToolingSection) {
4243
- return composePrompt(loadPartial("plan-common"), {
4244
- PROJECT_TOOLING: projectToolingSection
4245
- });
4246
- }
4247
- function buildPlannerBase(projectToolingSection) {
4248
- return {
4249
- HARNESS_CONTEXT: loadPartial("harness-context"),
4250
- COMMON: buildPlanCommon(projectToolingSection),
4251
- VALIDATION: loadPartial("validation-checklist"),
4252
- SIGNALS: loadPartial("signals-planning")
4253
- };
4254
- }
4255
- function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
4256
- return composePrompt(loadTemplate("plan-interactive"), {
4257
- ...buildPlannerBase(projectToolingSection),
4258
- CONTEXT: context,
4259
- OUTPUT_FILE: outputFile,
4260
- SCHEMA: schema
4261
- });
4262
- }
4263
- function buildAutoPrompt(context, schema, projectToolingSection) {
4264
- return composePrompt(loadTemplate("plan-auto"), {
4265
- ...buildPlannerBase(projectToolingSection),
4266
- CONTEXT: context,
4267
- SCHEMA: schema
4268
- });
4269
- }
4270
- function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName, projectToolingSection = "") {
4271
- const template = loadTemplate("task-execution");
4272
- const commitStep = noCommit ? "" : "\n - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
4273
- const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.\n";
4274
- return composePrompt(template, {
4275
- HARNESS_CONTEXT: loadPartial("harness-context"),
4276
- SIGNALS: loadPartial("signals-task"),
4277
- PROGRESS_FILE: progressFilePath,
4278
- COMMIT_STEP: commitStep,
4279
- COMMIT_CONSTRAINT: commitConstraint,
4280
- CONTEXT_FILE: contextFileName,
4281
- PROJECT_TOOLING: projectToolingSection
4282
- });
4283
- }
4284
- function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
4285
- const template = loadTemplate("ticket-refine");
4286
- return composePrompt(template, {
4287
- TICKET: ticketContent,
4288
- OUTPUT_FILE: outputFile,
4289
- SCHEMA: schema,
4290
- ISSUE_CONTEXT: issueContext
4291
- });
4292
- }
4293
- function buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema, projectToolingSection) {
4294
- return composePrompt(loadTemplate("ideate"), {
4295
- ...buildPlannerBase(projectToolingSection),
4296
- IDEA_TITLE: ideaTitle,
4297
- IDEA_DESCRIPTION: ideaDescription,
4298
- PROJECT_NAME: projectName,
4299
- REPOSITORIES: repositories,
4300
- OUTPUT_FILE: outputFile,
4301
- SCHEMA: schema
4302
- });
4303
- }
4304
- function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
4305
- return composePrompt(loadTemplate("ideate-auto"), {
4306
- ...buildPlannerBase(projectToolingSection),
4307
- IDEA_TITLE: ideaTitle,
4308
- IDEA_DESCRIPTION: ideaDescription,
4309
- PROJECT_NAME: projectName,
4310
- REPOSITORIES: repositories,
4311
- SCHEMA: schema
4312
3856
  });
4313
3857
  }
4314
- function renderExtraDimensions(extras) {
4315
- if (extras.length === 0) {
4316
- return { section: "", passBar: "", assessment: "" };
4317
- }
4318
- const section = extras.map(
4319
- (name, i) => `
4320
- **Dimension ${String(5 + i)} \u2014 ${name}**
4321
- 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.
4322
- `
4323
- ).join("");
4324
- const passBar = extras.map((name) => `
4325
- - **${name}**: Task-specific dimension flagged by the planner`).join("");
4326
- return {
4327
- section,
4328
- passBar,
4329
- assessment: extras.map((name) => `
4330
- **${name}**: PASS/FAIL \u2014 [one-line finding]`).join("")
4331
- };
4332
- }
4333
- function buildEvaluatorPrompt(ctx) {
4334
- const template = loadTemplate("task-evaluation");
4335
- const descriptionSection = ctx.taskDescription ? `
4336
- **Description:** ${ctx.taskDescription}` : "";
4337
- const stepsSection = ctx.taskSteps.length > 0 ? `
4338
- **Implementation Steps:**
4339
- ${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
4340
- const criteriaSection = ctx.verificationCriteria.length > 0 ? `
4341
- **Verification Criteria:**
4342
- ${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
4343
- const checkSection = ctx.checkScriptSection ? `
4344
-
4345
- ${ctx.checkScriptSection}` : "";
4346
- const extras = renderExtraDimensions(ctx.extraDimensions);
4347
- const extraAssessmentPass = extras.assessment.replace(/PASS\/FAIL/g, "PASS");
4348
- return composePrompt(template, {
4349
- HARNESS_CONTEXT: loadPartial("harness-context"),
4350
- SIGNALS: loadPartial("signals-evaluation"),
4351
- TASK_NAME: ctx.taskName,
4352
- TASK_DESCRIPTION_SECTION: descriptionSection,
4353
- TASK_STEPS_SECTION: stepsSection,
4354
- VERIFICATION_CRITERIA_SECTION: criteriaSection,
4355
- PROJECT_PATH: ctx.projectPath,
4356
- CHECK_SCRIPT_SECTION: checkSection,
4357
- PROJECT_TOOLING: ctx.projectToolingSection,
4358
- EXTRA_DIMENSIONS_SECTION: extras.section,
4359
- EXTRA_DIMENSIONS_PASS_BAR: extras.passBar,
4360
- EXTRA_DIMENSIONS_ASSESSMENT_PASS: extraAssessmentPass,
4361
- EXTRA_DIMENSIONS_ASSESSMENT_MIXED: extras.assessment
3858
+ function verifyCheckScriptStep(deps) {
3859
+ return step("verify-check-script", (ctx) => {
3860
+ const cmd = ctx.checkScriptFinal;
3861
+ if (!cmd) return Result.ok({});
3862
+ if (!/^\S/.test(cmd)) {
3863
+ deps.logger.warn(`Check script looks malformed: ${cmd}`);
3864
+ }
3865
+ return Result.ok({});
4362
3866
  });
4363
3867
  }
4364
- function buildSprintFeedbackPrompt(sprintName, completedTasks, feedback, branch) {
4365
- const template = loadTemplate("sprint-feedback");
4366
- const branchSection = branch ? `
4367
- **Branch:** ${branch}
4368
- ` : "";
4369
- return composePrompt(template, {
4370
- HARNESS_CONTEXT: loadPartial("harness-context"),
4371
- SIGNALS: loadPartial("signals-task"),
4372
- SPRINT_NAME: sprintName,
4373
- BRANCH_SECTION: branchSection,
4374
- COMPLETED_TASKS: completedTasks,
4375
- FEEDBACK: feedback
4376
- });
3868
+ function createOnboardPipeline(deps, options = {}) {
3869
+ return pipeline("onboard", [
3870
+ loadProjectStep(deps),
3871
+ selectRepoStep(deps, options),
3872
+ repoPreflightStep(deps),
3873
+ aiInventoryStep(deps),
3874
+ validateAgentsMdStep(deps.adapter),
3875
+ retryOnViolationStep(deps),
3876
+ checkDriftStep(deps),
3877
+ reviewAndConfirmStep(deps, options),
3878
+ writeArtifactsStep(deps, options),
3879
+ verifyCheckScriptStep(deps)
3880
+ ]);
4377
3881
  }
4378
3882
 
4379
3883
  // src/integration/ai/prompts/prompt-builder-adapter.ts
@@ -4581,8 +4085,8 @@ async function importTasksReplace(tasks, sprintId) {
4581
4085
 
4582
4086
  // src/integration/cli/commands/ticket/refine-utils.ts
4583
4087
  import { writeFile } from "fs/promises";
4584
- import { join as join4 } from "path";
4585
- import { Result as Result4 } from "typescript-result";
4088
+ import { join as join2 } from "path";
4089
+ import { Result as Result2 } from "typescript-result";
4586
4090
  function formatTicketForPrompt(ticket) {
4587
4091
  const lines = [];
4588
4092
  lines.push(`### ${formatTicketDisplay(ticket)}`);
@@ -4600,7 +4104,7 @@ function formatTicketForPrompt(ticket) {
4600
4104
  }
4601
4105
  function parseRequirementsFile(content) {
4602
4106
  const jsonStr = extractJsonArray(content);
4603
- const parseR = Result4.try(() => JSON.parse(jsonStr));
4107
+ const parseR = Result2.try(() => JSON.parse(jsonStr));
4604
4108
  if (!parseR.ok) {
4605
4109
  throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
4606
4110
  }
@@ -4620,7 +4124,7 @@ ${issues}`);
4620
4124
  return result.data;
4621
4125
  }
4622
4126
  async function runAiSession(workingDir, prompt, ticketTitle) {
4623
- const contextFile = join4(workingDir, "refine-context.md");
4127
+ const contextFile = join2(workingDir, "refine-context.md");
4624
4128
  await writeFile(contextFile, prompt, "utf-8");
4625
4129
  const provider = await getActiveProvider();
4626
4130
  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.`;
@@ -4675,132 +4179,6 @@ function parseEvaluationResult(output) {
4675
4179
  return { passed: false, status: "malformed", output, dimensions };
4676
4180
  }
4677
4181
 
4678
- // src/integration/signals/parser.ts
4679
- var SIGNAL_PATTERNS = {
4680
- progress: /<progress>([\s\S]*?)<\/progress>/g,
4681
- progressWithFiles: /<progress>([\s\S]*?)<\/progress>/,
4682
- evaluation_passed: /<evaluation-passed>/,
4683
- evaluation_failed: /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/,
4684
- task_verified: /<task-verified>([\s\S]*?)<\/task-verified>/,
4685
- task_complete: /<task-complete>/,
4686
- task_blocked: /<task-blocked>([\s\S]*?)<\/task-blocked>/,
4687
- note: /<note>([\s\S]*?)<\/note>/g
4688
- };
4689
- var DIMENSION_LINE2 = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/gi;
4690
- function parseDimensionScores2(output) {
4691
- const scores = [];
4692
- const seen = /* @__PURE__ */ new Set();
4693
- DIMENSION_LINE2.lastIndex = 0;
4694
- let match;
4695
- while ((match = DIMENSION_LINE2.exec(output)) !== null) {
4696
- const rawName = match[1];
4697
- const verdict = match[2];
4698
- const finding = match[3];
4699
- if (!rawName || !verdict || !finding) continue;
4700
- const name = rawName.toLowerCase();
4701
- if (seen.has(name)) continue;
4702
- seen.add(name);
4703
- scores.push({
4704
- dimension: name,
4705
- passed: verdict.toUpperCase() === "PASS",
4706
- finding: finding.trim()
4707
- });
4708
- }
4709
- return scores;
4710
- }
4711
- var SignalParser = class {
4712
- parseSignals(output) {
4713
- const signals = [];
4714
- const timestamp = /* @__PURE__ */ new Date();
4715
- let progressMatch;
4716
- while ((progressMatch = SIGNAL_PATTERNS.progress.exec(output)) !== null) {
4717
- const summary = progressMatch[1]?.trim();
4718
- if (summary) {
4719
- const progressSignal = {
4720
- type: "progress",
4721
- summary,
4722
- // Note: Phase 1 doesn't parse files attribute; added in Phase 2+
4723
- timestamp
4724
- };
4725
- signals.push(progressSignal);
4726
- }
4727
- }
4728
- if (output.includes("<evaluation-passed>")) {
4729
- const dimensions = parseDimensionScores2(output);
4730
- const evaluationSignal = {
4731
- type: "evaluation",
4732
- status: "passed",
4733
- dimensions,
4734
- timestamp
4735
- };
4736
- signals.push(evaluationSignal);
4737
- } else {
4738
- const failedMatch = SIGNAL_PATTERNS.evaluation_failed.exec(output);
4739
- if (failedMatch?.[1]) {
4740
- const critique = failedMatch[1].trim();
4741
- const dimensions = parseDimensionScores2(output);
4742
- const evaluationSignal = {
4743
- type: "evaluation",
4744
- status: dimensions.length > 0 ? "failed" : "malformed",
4745
- dimensions,
4746
- critique: dimensions.length > 0 ? critique : void 0,
4747
- timestamp
4748
- };
4749
- signals.push(evaluationSignal);
4750
- } else if (parseDimensionScores2(output).length > 0) {
4751
- const dimensions = parseDimensionScores2(output);
4752
- const evaluationSignal = {
4753
- type: "evaluation",
4754
- status: "failed",
4755
- dimensions,
4756
- timestamp
4757
- };
4758
- signals.push(evaluationSignal);
4759
- }
4760
- }
4761
- const taskVerifiedMatch = SIGNAL_PATTERNS.task_verified.exec(output);
4762
- if (taskVerifiedMatch?.[1]) {
4763
- const verificationOutput = taskVerifiedMatch[1].trim();
4764
- const verifiedSignal = {
4765
- type: "task-verified",
4766
- output: verificationOutput,
4767
- timestamp
4768
- };
4769
- signals.push(verifiedSignal);
4770
- }
4771
- if (output.includes("<task-complete>")) {
4772
- const completeSignal = {
4773
- type: "task-complete",
4774
- timestamp
4775
- };
4776
- signals.push(completeSignal);
4777
- }
4778
- const taskBlockedMatch = SIGNAL_PATTERNS.task_blocked.exec(output);
4779
- if (taskBlockedMatch?.[1]) {
4780
- const reason = taskBlockedMatch[1].trim();
4781
- const blockedSignal = {
4782
- type: "task-blocked",
4783
- reason,
4784
- timestamp
4785
- };
4786
- signals.push(blockedSignal);
4787
- }
4788
- let noteMatch;
4789
- while ((noteMatch = SIGNAL_PATTERNS.note.exec(output)) !== null) {
4790
- const text = noteMatch[1]?.trim();
4791
- if (text) {
4792
- const noteSignal = {
4793
- type: "note",
4794
- text,
4795
- timestamp
4796
- };
4797
- signals.push(noteSignal);
4798
- }
4799
- }
4800
- return signals;
4801
- }
4802
- };
4803
-
4804
4182
  // src/integration/ai/output/parser.ts
4805
4183
  var signalParser = new SignalParser();
4806
4184
  function parseExecutionResult(output) {
@@ -4951,8 +4329,8 @@ var AutoUserAdapter = class {
4951
4329
  };
4952
4330
 
4953
4331
  // src/integration/ai/project-tooling.ts
4954
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
4955
- import { join as join5 } from "path";
4332
+ import { existsSync, readdirSync, readFileSync } from "fs";
4333
+ import { join as join3 } from "path";
4956
4334
  var EMPTY_TOOLING = {
4957
4335
  agents: [],
4958
4336
  skills: [],
@@ -4963,7 +4341,7 @@ var EMPTY_TOOLING = {
4963
4341
  };
4964
4342
  function safeListDir(path, predicate) {
4965
4343
  try {
4966
- if (!existsSync2(path)) return [];
4344
+ if (!existsSync(path)) return [];
4967
4345
  return readdirSync(path).filter(predicate).sort();
4968
4346
  } catch {
4969
4347
  return [];
@@ -4971,23 +4349,23 @@ function safeListDir(path, predicate) {
4971
4349
  }
4972
4350
  var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
4973
4351
  function detectAgents(projectPath) {
4974
- const agentsDir = join5(projectPath, ".claude", "agents");
4352
+ const agentsDir = join3(projectPath, ".claude", "agents");
4975
4353
  return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
4976
4354
  }
4977
4355
  function detectSkills(projectPath) {
4978
- const skillsDir = join5(projectPath, ".claude", "skills");
4356
+ const skillsDir = join3(projectPath, ".claude", "skills");
4979
4357
  try {
4980
- if (!existsSync2(skillsDir)) return [];
4358
+ if (!existsSync(skillsDir)) return [];
4981
4359
  return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
4982
4360
  } catch {
4983
4361
  return [];
4984
4362
  }
4985
4363
  }
4986
4364
  function detectMcpServers(projectPath) {
4987
- const mcpFile = join5(projectPath, ".mcp.json");
4988
- if (!existsSync2(mcpFile)) return [];
4365
+ const mcpFile = join3(projectPath, ".mcp.json");
4366
+ if (!existsSync(mcpFile)) return [];
4989
4367
  try {
4990
- const raw = readFileSync2(mcpFile, "utf-8");
4368
+ const raw = readFileSync(mcpFile, "utf-8");
4991
4369
  const parsed = JSON.parse(raw);
4992
4370
  const servers = parsed.mcpServers;
4993
4371
  if (!servers || typeof servers !== "object") return [];
@@ -4997,16 +4375,16 @@ function detectMcpServers(projectPath) {
4997
4375
  }
4998
4376
  }
4999
4377
  function detectProjectTooling(projectPath) {
5000
- if (!projectPath || !existsSync2(projectPath)) {
4378
+ if (!projectPath || !existsSync(projectPath)) {
5001
4379
  return EMPTY_TOOLING;
5002
4380
  }
5003
4381
  return {
5004
4382
  agents: detectAgents(projectPath),
5005
4383
  skills: detectSkills(projectPath),
5006
4384
  mcpServers: detectMcpServers(projectPath),
5007
- hasClaudeMd: existsSync2(join5(projectPath, "CLAUDE.md")),
5008
- hasAgentsMd: existsSync2(join5(projectPath, "AGENTS.md")),
5009
- hasCopilotInstructions: existsSync2(join5(projectPath, ".github", "copilot-instructions.md"))
4385
+ hasClaudeMd: existsSync(join3(projectPath, "CLAUDE.md")),
4386
+ hasAgentsMd: existsSync(join3(projectPath, "AGENTS.md")),
4387
+ hasCopilotInstructions: existsSync(join3(projectPath, ".github", "copilot-instructions.md"))
5010
4388
  };
5011
4389
  }
5012
4390
  function detectProjectToolingAcrossPaths(projectPaths) {
@@ -5117,7 +4495,7 @@ function describeMcpHint(name) {
5117
4495
  }
5118
4496
 
5119
4497
  // src/integration/external/lifecycle.ts
5120
- import { spawnSync as spawnSync2 } from "child_process";
4498
+ import { spawnSync } from "child_process";
5121
4499
  var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
5122
4500
  function getHookTimeoutMs() {
5123
4501
  const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
@@ -5130,7 +4508,7 @@ function getHookTimeoutMs() {
5130
4508
  function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
5131
4509
  assertSafeCwd(projectPath);
5132
4510
  const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
5133
- const result = spawnSync2(script, {
4511
+ const result = spawnSync(script, {
5134
4512
  cwd: projectPath,
5135
4513
  shell: true,
5136
4514
  stdio: ["pipe", "pipe", "pipe"],
@@ -5144,9 +4522,9 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
5144
4522
 
5145
4523
  // src/integration/ai/task-context.ts
5146
4524
  import { execSync } from "child_process";
5147
- import { Result as Result5 } from "typescript-result";
4525
+ import { Result as Result3 } from "typescript-result";
5148
4526
  function getRecentGitHistory(projectPath, count = 20) {
5149
- const r = Result5.try(() => {
4527
+ const r = Result3.try(() => {
5150
4528
  assertSafeCwd(projectPath);
5151
4529
  const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
5152
4530
  cwd: projectPath,
@@ -5159,7 +4537,7 @@ function getRecentGitHistory(projectPath, count = 20) {
5159
4537
  }
5160
4538
 
5161
4539
  // src/integration/external/git.ts
5162
- import { spawnSync as spawnSync3 } from "child_process";
4540
+ import { spawnSync as spawnSync2 } from "child_process";
5163
4541
  var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
5164
4542
  var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
5165
4543
  function isValidBranchName(name) {
@@ -5172,7 +4550,7 @@ function isValidBranchName(name) {
5172
4550
  }
5173
4551
  function getCurrentBranch(cwd) {
5174
4552
  assertSafeCwd(cwd);
5175
- const result = spawnSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4553
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
5176
4554
  cwd,
5177
4555
  encoding: "utf-8",
5178
4556
  stdio: ["pipe", "pipe", "pipe"]
@@ -5187,7 +4565,7 @@ function branchExists(cwd, name) {
5187
4565
  if (!isValidBranchName(name)) {
5188
4566
  throw new Error(`Invalid branch name: ${name}`);
5189
4567
  }
5190
- const result = spawnSync3("git", ["show-ref", "--verify", `refs/heads/${name}`], {
4568
+ const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
5191
4569
  cwd,
5192
4570
  encoding: "utf-8",
5193
4571
  stdio: ["pipe", "pipe", "pipe"]
@@ -5204,7 +4582,7 @@ function createAndCheckoutBranch(cwd, name) {
5204
4582
  return;
5205
4583
  }
5206
4584
  if (branchExists(cwd, name)) {
5207
- const result = spawnSync3("git", ["checkout", name], {
4585
+ const result = spawnSync2("git", ["checkout", name], {
5208
4586
  cwd,
5209
4587
  encoding: "utf-8",
5210
4588
  stdio: ["pipe", "pipe", "pipe"]
@@ -5213,7 +4591,7 @@ function createAndCheckoutBranch(cwd, name) {
5213
4591
  throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
5214
4592
  }
5215
4593
  } else {
5216
- const result = spawnSync3("git", ["checkout", "-b", name], {
4594
+ const result = spawnSync2("git", ["checkout", "-b", name], {
5217
4595
  cwd,
5218
4596
  encoding: "utf-8",
5219
4597
  stdio: ["pipe", "pipe", "pipe"]
@@ -5229,7 +4607,7 @@ function verifyCurrentBranch(cwd, expected) {
5229
4607
  }
5230
4608
  function getDefaultBranch(cwd) {
5231
4609
  assertSafeCwd(cwd);
5232
- const result = spawnSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4610
+ const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5233
4611
  cwd,
5234
4612
  encoding: "utf-8",
5235
4613
  stdio: ["pipe", "pipe", "pipe"]
@@ -5250,7 +4628,7 @@ function getDefaultBranch(cwd) {
5250
4628
  function getHeadSha(cwd) {
5251
4629
  try {
5252
4630
  assertSafeCwd(cwd);
5253
- const result = spawnSync3("git", ["rev-parse", "HEAD"], {
4631
+ const result = spawnSync2("git", ["rev-parse", "HEAD"], {
5254
4632
  cwd,
5255
4633
  encoding: "utf-8",
5256
4634
  stdio: ["pipe", "pipe", "pipe"]
@@ -5263,7 +4641,7 @@ function getHeadSha(cwd) {
5263
4641
  }
5264
4642
  function hasUncommittedChanges(cwd) {
5265
4643
  assertSafeCwd(cwd);
5266
- const result = spawnSync3("git", ["status", "--porcelain"], {
4644
+ const result = spawnSync2("git", ["status", "--porcelain"], {
5267
4645
  cwd,
5268
4646
  encoding: "utf-8",
5269
4647
  stdio: ["pipe", "pipe", "pipe"]
@@ -5275,7 +4653,7 @@ function hasUncommittedChanges(cwd) {
5275
4653
  }
5276
4654
  function autoCommit(cwd, message) {
5277
4655
  assertSafeCwd(cwd);
5278
- const add = spawnSync3("git", ["add", "-A"], {
4656
+ const add = spawnSync2("git", ["add", "-A"], {
5279
4657
  cwd,
5280
4658
  encoding: "utf-8",
5281
4659
  stdio: ["pipe", "pipe", "pipe"]
@@ -5283,7 +4661,7 @@ function autoCommit(cwd, message) {
5283
4661
  if (add.status !== 0) {
5284
4662
  throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
5285
4663
  }
5286
- const commit = spawnSync3("git", ["commit", "-m", message], {
4664
+ const commit = spawnSync2("git", ["commit", "-m", message], {
5287
4665
  cwd,
5288
4666
  encoding: "utf-8",
5289
4667
  stdio: ["pipe", "pipe", "pipe"]
@@ -5296,14 +4674,14 @@ function generateBranchName(sprintId) {
5296
4674
  return `ralphctl/${sprintId}`;
5297
4675
  }
5298
4676
  function isGhAvailable() {
5299
- const result = spawnSync3("gh", ["--version"], {
4677
+ const result = spawnSync2("gh", ["--version"], {
5300
4678
  encoding: "utf-8",
5301
4679
  stdio: ["pipe", "pipe", "pipe"]
5302
4680
  });
5303
4681
  return result.status === 0;
5304
4682
  }
5305
4683
  function isGlabAvailable() {
5306
- const result = spawnSync3("glab", ["--version"], {
4684
+ const result = spawnSync2("glab", ["--version"], {
5307
4685
  encoding: "utf-8",
5308
4686
  stdio: ["pipe", "pipe", "pipe"]
5309
4687
  });
@@ -5378,6 +4756,265 @@ var DefaultExternalAdapter = class {
5378
4756
  }
5379
4757
  };
5380
4758
 
4759
+ // src/integration/external/onboard-adapter.ts
4760
+ import { existsSync as existsSync4, statSync } from "fs";
4761
+ import { join as join6 } from "path";
4762
+
4763
+ // src/integration/external/agents-md-linter.ts
4764
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4765
+ import { join as join4 } from "path";
4766
+ var MAX_H2 = 7;
4767
+ var MAX_LINES = 300;
4768
+ var MIN_FLESCH = 40;
4769
+ var REQUIRED_H2_SECTIONS = [
4770
+ "Project Overview",
4771
+ "Build & Run",
4772
+ "Testing",
4773
+ "Architecture",
4774
+ "Implementation Style",
4775
+ "Security & Safety",
4776
+ "Performance Constraints"
4777
+ ];
4778
+ function normalizeHeading(raw) {
4779
+ return raw.replace(/^#+\s*/, "").replace(/[*_`]/g, "").trim().toLowerCase();
4780
+ }
4781
+ function lintAgentsMd(content) {
4782
+ const violations = [];
4783
+ const lines = content.split("\n");
4784
+ if (lines.length >= MAX_LINES) {
4785
+ violations.push({
4786
+ rule: "max-lines",
4787
+ message: `Project context file is ${String(lines.length)} lines (must be under ${String(MAX_LINES)}).`
4788
+ });
4789
+ }
4790
+ let inCodeFence = false;
4791
+ let h1Count = 0;
4792
+ let h2Count = 0;
4793
+ const h2Titles = [];
4794
+ for (const line of lines) {
4795
+ if (line.startsWith("```")) {
4796
+ inCodeFence = !inCodeFence;
4797
+ continue;
4798
+ }
4799
+ if (inCodeFence) continue;
4800
+ const match = /^(#+)\s/.exec(line);
4801
+ if (!match) continue;
4802
+ const depth = match[1]?.length ?? 0;
4803
+ if (depth === 1) h1Count++;
4804
+ else if (depth === 2) {
4805
+ h2Count++;
4806
+ h2Titles.push(normalizeHeading(line));
4807
+ } else if (depth >= 4) {
4808
+ violations.push({
4809
+ rule: "no-h4-plus",
4810
+ message: `H${String(depth)} heading is too deep \u2014 keep structure flat (H1/H2/H3 only): "${line.trim()}"`
4811
+ });
4812
+ }
4813
+ }
4814
+ for (const required of REQUIRED_H2_SECTIONS) {
4815
+ if (!h2Titles.includes(required.toLowerCase())) {
4816
+ violations.push({
4817
+ rule: "required-section",
4818
+ message: `Missing required H2 section: "## ${required}".`
4819
+ });
4820
+ }
4821
+ }
4822
+ if (h1Count !== 1) {
4823
+ violations.push({
4824
+ rule: "single-h1",
4825
+ message: `Expected exactly one H1, found ${String(h1Count)}.`
4826
+ });
4827
+ }
4828
+ if (h2Count > MAX_H2) {
4829
+ violations.push({
4830
+ rule: "max-h2",
4831
+ message: `Too many H2 sections (${String(h2Count)}); keep at most ${String(MAX_H2)}.`
4832
+ });
4833
+ }
4834
+ const flesch = fleschReadingEase(content);
4835
+ if (Number.isFinite(flesch) && flesch < MIN_FLESCH) {
4836
+ violations.push({
4837
+ rule: "readability",
4838
+ message: `Flesch score ${flesch.toFixed(1)} is below ${String(MIN_FLESCH)} \u2014 simplify long sentences.`
4839
+ });
4840
+ }
4841
+ return { ok: violations.length === 0, violations };
4842
+ }
4843
+ function fleschReadingEase(content) {
4844
+ const prose = stripNonProse(content);
4845
+ const words = prose.match(/[A-Za-z][A-Za-z'-]*/g) ?? [];
4846
+ if (words.length === 0) return 100;
4847
+ const sentences = Math.max(1, (prose.match(/[.!?]+(?:\s|$)/g) ?? []).length);
4848
+ const syllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
4849
+ return 206.835 - 1.015 * (words.length / sentences) - 84.6 * (syllables / words.length);
4850
+ }
4851
+ function stripNonProse(content) {
4852
+ return content.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^#+\s+.*$/gm, " ").replace(/^\s*[-*+]\s+/gm, "");
4853
+ }
4854
+ function countSyllables(word) {
4855
+ const lower = word.toLowerCase();
4856
+ const groups = lower.match(/[aeiouy]+/g) ?? [];
4857
+ let count = groups.length;
4858
+ if (lower.at(-1) === "e" && count > 1) count--;
4859
+ return Math.max(1, count);
4860
+ }
4861
+ function detectCommandDrift(content, repoPath) {
4862
+ const warnings = [];
4863
+ const pkgPath = join4(repoPath, "package.json");
4864
+ if (!existsSync2(pkgPath)) return warnings;
4865
+ let scripts = {};
4866
+ try {
4867
+ const raw = readFileSync2(pkgPath, "utf-8");
4868
+ const parsed = JSON.parse(raw);
4869
+ if (isRecord(parsed) && isRecord(parsed["scripts"])) {
4870
+ const entries = Object.entries(parsed["scripts"]).filter((pair) => {
4871
+ return typeof pair[1] === "string";
4872
+ });
4873
+ scripts = Object.fromEntries(entries);
4874
+ }
4875
+ } catch {
4876
+ return warnings;
4877
+ }
4878
+ const re = /\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([a-z][a-z0-9:_-]*)/gi;
4879
+ let match;
4880
+ const seen = /* @__PURE__ */ new Set();
4881
+ while ((match = re.exec(content)) !== null) {
4882
+ const name = match[1];
4883
+ if (!name) continue;
4884
+ if (seen.has(name)) continue;
4885
+ seen.add(name);
4886
+ if (name === "install" || name === "test" || name === "start") continue;
4887
+ if (!(name in scripts)) {
4888
+ warnings.push(`Referenced script "${name}" not defined in package.json`);
4889
+ }
4890
+ }
4891
+ return warnings;
4892
+ }
4893
+ function isRecord(value) {
4894
+ return typeof value === "object" && value !== null;
4895
+ }
4896
+
4897
+ // src/integration/external/agents-md-writer.ts
4898
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
4899
+ import { dirname, join as join5 } from "path";
4900
+ import { randomBytes } from "crypto";
4901
+ var RALPHCTL_MARKER = "<!-- managed by ralphctl onboard -->";
4902
+ function providerInstructionsPath(repoPath, provider) {
4903
+ if (provider === "claude") return join5(repoPath, "CLAUDE.md");
4904
+ return join5(repoPath, ".github", "copilot-instructions.md");
4905
+ }
4906
+ function readExistingProviderInstructions(repoPath, provider) {
4907
+ const path = providerInstructionsPath(repoPath, provider);
4908
+ if (!existsSync3(path)) return { content: null, authored: false };
4909
+ let content;
4910
+ try {
4911
+ content = readFileSync3(path, "utf-8");
4912
+ } catch {
4913
+ return { content: null, authored: false };
4914
+ }
4915
+ const managed = content.includes(RALPHCTL_MARKER);
4916
+ return { content, authored: !managed };
4917
+ }
4918
+ function writeProviderInstructionsAtomic(repoPath, content, provider) {
4919
+ const target = providerInstructionsPath(repoPath, provider);
4920
+ mkdirSync(dirname(target), { recursive: true });
4921
+ const body = content.endsWith("\n") ? content : `${content}
4922
+ `;
4923
+ const stamped = body.includes(RALPHCTL_MARKER) ? body : `${body}
4924
+ ${RALPHCTL_MARKER}
4925
+ `;
4926
+ const tempPath = `${target}.${randomBytes(6).toString("hex")}.tmp`;
4927
+ writeFileSync(tempPath, stamped, { encoding: "utf-8", mode: 420 });
4928
+ renameSync(tempPath, target);
4929
+ return { path: target };
4930
+ }
4931
+
4932
+ // src/integration/ai/discover-agents-md.ts
4933
+ var DISCOVERY_TIMEOUT_MS = 12e4;
4934
+ async function discoverAgentsMdWithAi(ctx, aiSession, signalParser2) {
4935
+ const prompt = buildRepoOnboardPrompt(ctx);
4936
+ const session = aiSession.spawnHeadless(prompt, { cwd: ctx.repoPath });
4937
+ const timeout = new Promise((resolve) => {
4938
+ setTimeout(() => {
4939
+ resolve(null);
4940
+ }, DISCOVERY_TIMEOUT_MS).unref();
4941
+ });
4942
+ try {
4943
+ const result = await Promise.race([session, timeout]);
4944
+ if (!result) return { agentsMd: null, checkScript: null, changes: null };
4945
+ const signals = signalParser2.parseSignals(result.output);
4946
+ const agentsSignal = signals.find((s) => s.type === "agents-md-proposal");
4947
+ const checkSignal = signals.find((s) => s.type === "check-script-discovery");
4948
+ const changes = extractChanges(result.output);
4949
+ return {
4950
+ agentsMd: agentsSignal ? agentsSignal.content : null,
4951
+ checkScript: checkSignal ? checkSignal.command : null,
4952
+ changes
4953
+ };
4954
+ } catch {
4955
+ return { agentsMd: null, checkScript: null, changes: null };
4956
+ }
4957
+ }
4958
+ function extractChanges(output) {
4959
+ const match = /<changes>([\s\S]*?)<\/changes>/.exec(output);
4960
+ if (!match?.[1]) return null;
4961
+ const body = match[1].trim();
4962
+ return body.length > 0 ? body : null;
4963
+ }
4964
+
4965
+ // src/integration/external/onboard-adapter.ts
4966
+ var DefaultOnboardAdapter = class {
4967
+ constructor(aiSession, signalParser2) {
4968
+ this.aiSession = aiSession;
4969
+ this.signalParser = signalParser2;
4970
+ }
4971
+ aiSession;
4972
+ signalParser;
4973
+ readExistingInstructions(repoPath, provider) {
4974
+ return readExistingProviderInstructions(repoPath, provider);
4975
+ }
4976
+ validateRepoPath(path) {
4977
+ let exists;
4978
+ try {
4979
+ exists = existsSync4(path) && statSync(path).isDirectory();
4980
+ } catch {
4981
+ exists = false;
4982
+ }
4983
+ if (!exists) return { exists: false, isGitRepo: false };
4984
+ const isGitRepo = existsSync4(join6(path, ".git"));
4985
+ return { exists: true, isGitRepo };
4986
+ }
4987
+ lintAgentsMd(content) {
4988
+ return lintAgentsMd(content);
4989
+ }
4990
+ detectCommandDrift(content, repoPath) {
4991
+ return detectCommandDrift(content, repoPath);
4992
+ }
4993
+ async discoverAgentsMd(input) {
4994
+ return discoverAgentsMdWithAi(input, this.aiSession, this.signalParser);
4995
+ }
4996
+ inferProjectType(repoPath) {
4997
+ const checks = [
4998
+ ["package.json", "node"],
4999
+ ["pyproject.toml", "python"],
5000
+ ["requirements.txt", "python"],
5001
+ ["Cargo.toml", "rust"],
5002
+ ["go.mod", "go"],
5003
+ ["pom.xml", "java"],
5004
+ ["build.gradle", "java"],
5005
+ ["Makefile", "makefile"]
5006
+ ];
5007
+ const hints = [];
5008
+ for (const [file, label] of checks) {
5009
+ if (existsSync4(join6(repoPath, file))) hints.push(label);
5010
+ }
5011
+ return hints.length === 0 ? "unknown" : hints.join(", ");
5012
+ }
5013
+ writeProviderInstructions(repoPath, content, provider) {
5014
+ return writeProviderInstructionsAtomic(repoPath, content, provider);
5015
+ }
5016
+ };
5017
+
5381
5018
  // src/application/factories.ts
5382
5019
  function createAiDeps(auto) {
5383
5020
  return {
@@ -5437,6 +5074,22 @@ function createIdeatePipeline2(shared, idea, options = {}) {
5437
5074
  options
5438
5075
  );
5439
5076
  }
5077
+ function createOnboardPipeline2(shared, options = {}) {
5078
+ const aiSession = new ProviderAiSessionAdapter();
5079
+ const adapter = new DefaultOnboardAdapter(aiSession, shared.signalParser);
5080
+ return createOnboardPipeline(
5081
+ {
5082
+ persistence: shared.persistence,
5083
+ adapter,
5084
+ logger: shared.logger,
5085
+ prompt: shared.prompt,
5086
+ updateProjectRepos: async (name, repositories) => {
5087
+ return updateProject(name, { repositories });
5088
+ }
5089
+ },
5090
+ options
5091
+ );
5092
+ }
5440
5093
  function createExecuteSprintPipeline2(shared, options = {}) {
5441
5094
  const { aiSession, promptBuilder, parser, ui, external } = createAiDeps(false);
5442
5095
  return createExecuteSprintPipeline(
@@ -5615,6 +5268,7 @@ async function sprintStartCommand(args) {
5615
5268
  }
5616
5269
 
5617
5270
  export {
5271
+ executePipeline,
5618
5272
  getTasks,
5619
5273
  saveTasks,
5620
5274
  getTask,
@@ -5630,29 +5284,20 @@ export {
5630
5284
  areAllTasksDone,
5631
5285
  reorderByDependencies,
5632
5286
  validateImportTasks,
5633
- getCurrentBranch,
5634
- branchExists,
5635
- getDefaultBranch,
5636
- isGhAvailable,
5637
- isGlabAvailable,
5638
- executePipeline,
5639
- processLifecycleAdapter,
5640
- resolveProvider,
5641
- providerDisplayName,
5642
- enterAltScreen,
5643
- exitAltScreen,
5644
- registerTuiInstance,
5645
- withSuspendedTui,
5646
- buildTicketRefinePrompt,
5647
5287
  renderParsedTasksTable,
5648
5288
  importTasks,
5649
5289
  formatTicketForPrompt,
5650
5290
  parseRequirementsFile,
5651
5291
  runAiSession,
5652
- SignalParser,
5292
+ getCurrentBranch,
5293
+ branchExists,
5294
+ getDefaultBranch,
5295
+ isGhAvailable,
5296
+ isGlabAvailable,
5653
5297
  createRefinePipeline2 as createRefinePipeline,
5654
5298
  createPlanPipeline2 as createPlanPipeline,
5655
5299
  createIdeatePipeline2 as createIdeatePipeline,
5300
+ createOnboardPipeline2 as createOnboardPipeline,
5656
5301
  createExecuteSprintPipeline2 as createExecuteSprintPipeline,
5657
5302
  parseSprintStartArgs,
5658
5303
  sprintStartCommand