ralphctl 0.4.2 → 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.
@@ -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,843 +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));
3710
- }
3711
- if (this.children.size > 0) {
3712
- log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
3713
- this.killAll("SIGKILL");
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 });
3714
3627
  }
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;
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 });
3725
3632
  }
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
- 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
3856
  });
4305
3857
  }
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
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({});
4376
3866
  });
4377
3867
  }
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
- });
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
+ ]);
4391
3881
  }
4392
3882
 
4393
3883
  // src/integration/ai/prompts/prompt-builder-adapter.ts
@@ -4595,8 +4085,8 @@ async function importTasksReplace(tasks, sprintId) {
4595
4085
 
4596
4086
  // src/integration/cli/commands/ticket/refine-utils.ts
4597
4087
  import { writeFile } from "fs/promises";
4598
- import { join as join4 } from "path";
4599
- import { Result as Result4 } from "typescript-result";
4088
+ import { join as join2 } from "path";
4089
+ import { Result as Result2 } from "typescript-result";
4600
4090
  function formatTicketForPrompt(ticket) {
4601
4091
  const lines = [];
4602
4092
  lines.push(`### ${formatTicketDisplay(ticket)}`);
@@ -4614,7 +4104,7 @@ function formatTicketForPrompt(ticket) {
4614
4104
  }
4615
4105
  function parseRequirementsFile(content) {
4616
4106
  const jsonStr = extractJsonArray(content);
4617
- const parseR = Result4.try(() => JSON.parse(jsonStr));
4107
+ const parseR = Result2.try(() => JSON.parse(jsonStr));
4618
4108
  if (!parseR.ok) {
4619
4109
  throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
4620
4110
  }
@@ -4634,7 +4124,7 @@ ${issues}`);
4634
4124
  return result.data;
4635
4125
  }
4636
4126
  async function runAiSession(workingDir, prompt, ticketTitle) {
4637
- const contextFile = join4(workingDir, "refine-context.md");
4127
+ const contextFile = join2(workingDir, "refine-context.md");
4638
4128
  await writeFile(contextFile, prompt, "utf-8");
4639
4129
  const provider = await getActiveProvider();
4640
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.`;
@@ -4689,132 +4179,6 @@ function parseEvaluationResult(output) {
4689
4179
  return { passed: false, status: "malformed", output, dimensions };
4690
4180
  }
4691
4181
 
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
4182
  // src/integration/ai/output/parser.ts
4819
4183
  var signalParser = new SignalParser();
4820
4184
  function parseExecutionResult(output) {
@@ -4965,8 +4329,8 @@ var AutoUserAdapter = class {
4965
4329
  };
4966
4330
 
4967
4331
  // src/integration/ai/project-tooling.ts
4968
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
4969
- import { join as join5 } from "path";
4332
+ import { existsSync, readdirSync, readFileSync } from "fs";
4333
+ import { join as join3 } from "path";
4970
4334
  var EMPTY_TOOLING = {
4971
4335
  agents: [],
4972
4336
  skills: [],
@@ -4977,7 +4341,7 @@ var EMPTY_TOOLING = {
4977
4341
  };
4978
4342
  function safeListDir(path, predicate) {
4979
4343
  try {
4980
- if (!existsSync2(path)) return [];
4344
+ if (!existsSync(path)) return [];
4981
4345
  return readdirSync(path).filter(predicate).sort();
4982
4346
  } catch {
4983
4347
  return [];
@@ -4985,23 +4349,23 @@ function safeListDir(path, predicate) {
4985
4349
  }
4986
4350
  var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
4987
4351
  function detectAgents(projectPath) {
4988
- const agentsDir = join5(projectPath, ".claude", "agents");
4352
+ const agentsDir = join3(projectPath, ".claude", "agents");
4989
4353
  return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
4990
4354
  }
4991
4355
  function detectSkills(projectPath) {
4992
- const skillsDir = join5(projectPath, ".claude", "skills");
4356
+ const skillsDir = join3(projectPath, ".claude", "skills");
4993
4357
  try {
4994
- if (!existsSync2(skillsDir)) return [];
4358
+ if (!existsSync(skillsDir)) return [];
4995
4359
  return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
4996
4360
  } catch {
4997
4361
  return [];
4998
4362
  }
4999
4363
  }
5000
4364
  function detectMcpServers(projectPath) {
5001
- const mcpFile = join5(projectPath, ".mcp.json");
5002
- if (!existsSync2(mcpFile)) return [];
4365
+ const mcpFile = join3(projectPath, ".mcp.json");
4366
+ if (!existsSync(mcpFile)) return [];
5003
4367
  try {
5004
- const raw = readFileSync2(mcpFile, "utf-8");
4368
+ const raw = readFileSync(mcpFile, "utf-8");
5005
4369
  const parsed = JSON.parse(raw);
5006
4370
  const servers = parsed.mcpServers;
5007
4371
  if (!servers || typeof servers !== "object") return [];
@@ -5011,16 +4375,16 @@ function detectMcpServers(projectPath) {
5011
4375
  }
5012
4376
  }
5013
4377
  function detectProjectTooling(projectPath) {
5014
- if (!projectPath || !existsSync2(projectPath)) {
4378
+ if (!projectPath || !existsSync(projectPath)) {
5015
4379
  return EMPTY_TOOLING;
5016
4380
  }
5017
4381
  return {
5018
4382
  agents: detectAgents(projectPath),
5019
4383
  skills: detectSkills(projectPath),
5020
4384
  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"))
4385
+ hasClaudeMd: existsSync(join3(projectPath, "CLAUDE.md")),
4386
+ hasAgentsMd: existsSync(join3(projectPath, "AGENTS.md")),
4387
+ hasCopilotInstructions: existsSync(join3(projectPath, ".github", "copilot-instructions.md"))
5024
4388
  };
5025
4389
  }
5026
4390
  function detectProjectToolingAcrossPaths(projectPaths) {
@@ -5131,7 +4495,7 @@ function describeMcpHint(name) {
5131
4495
  }
5132
4496
 
5133
4497
  // src/integration/external/lifecycle.ts
5134
- import { spawnSync as spawnSync2 } from "child_process";
4498
+ import { spawnSync } from "child_process";
5135
4499
  var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
5136
4500
  function getHookTimeoutMs() {
5137
4501
  const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
@@ -5144,7 +4508,7 @@ function getHookTimeoutMs() {
5144
4508
  function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
5145
4509
  assertSafeCwd(projectPath);
5146
4510
  const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
5147
- const result = spawnSync2(script, {
4511
+ const result = spawnSync(script, {
5148
4512
  cwd: projectPath,
5149
4513
  shell: true,
5150
4514
  stdio: ["pipe", "pipe", "pipe"],
@@ -5158,9 +4522,9 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
5158
4522
 
5159
4523
  // src/integration/ai/task-context.ts
5160
4524
  import { execSync } from "child_process";
5161
- import { Result as Result5 } from "typescript-result";
4525
+ import { Result as Result3 } from "typescript-result";
5162
4526
  function getRecentGitHistory(projectPath, count = 20) {
5163
- const r = Result5.try(() => {
4527
+ const r = Result3.try(() => {
5164
4528
  assertSafeCwd(projectPath);
5165
4529
  const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
5166
4530
  cwd: projectPath,
@@ -5173,7 +4537,7 @@ function getRecentGitHistory(projectPath, count = 20) {
5173
4537
  }
5174
4538
 
5175
4539
  // src/integration/external/git.ts
5176
- import { spawnSync as spawnSync3 } from "child_process";
4540
+ import { spawnSync as spawnSync2 } from "child_process";
5177
4541
  var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
5178
4542
  var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
5179
4543
  function isValidBranchName(name) {
@@ -5186,7 +4550,7 @@ function isValidBranchName(name) {
5186
4550
  }
5187
4551
  function getCurrentBranch(cwd) {
5188
4552
  assertSafeCwd(cwd);
5189
- const result = spawnSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4553
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
5190
4554
  cwd,
5191
4555
  encoding: "utf-8",
5192
4556
  stdio: ["pipe", "pipe", "pipe"]
@@ -5201,7 +4565,7 @@ function branchExists(cwd, name) {
5201
4565
  if (!isValidBranchName(name)) {
5202
4566
  throw new Error(`Invalid branch name: ${name}`);
5203
4567
  }
5204
- const result = spawnSync3("git", ["show-ref", "--verify", `refs/heads/${name}`], {
4568
+ const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
5205
4569
  cwd,
5206
4570
  encoding: "utf-8",
5207
4571
  stdio: ["pipe", "pipe", "pipe"]
@@ -5218,7 +4582,7 @@ function createAndCheckoutBranch(cwd, name) {
5218
4582
  return;
5219
4583
  }
5220
4584
  if (branchExists(cwd, name)) {
5221
- const result = spawnSync3("git", ["checkout", name], {
4585
+ const result = spawnSync2("git", ["checkout", name], {
5222
4586
  cwd,
5223
4587
  encoding: "utf-8",
5224
4588
  stdio: ["pipe", "pipe", "pipe"]
@@ -5227,7 +4591,7 @@ function createAndCheckoutBranch(cwd, name) {
5227
4591
  throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
5228
4592
  }
5229
4593
  } else {
5230
- const result = spawnSync3("git", ["checkout", "-b", name], {
4594
+ const result = spawnSync2("git", ["checkout", "-b", name], {
5231
4595
  cwd,
5232
4596
  encoding: "utf-8",
5233
4597
  stdio: ["pipe", "pipe", "pipe"]
@@ -5243,7 +4607,7 @@ function verifyCurrentBranch(cwd, expected) {
5243
4607
  }
5244
4608
  function getDefaultBranch(cwd) {
5245
4609
  assertSafeCwd(cwd);
5246
- const result = spawnSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4610
+ const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5247
4611
  cwd,
5248
4612
  encoding: "utf-8",
5249
4613
  stdio: ["pipe", "pipe", "pipe"]
@@ -5264,7 +4628,7 @@ function getDefaultBranch(cwd) {
5264
4628
  function getHeadSha(cwd) {
5265
4629
  try {
5266
4630
  assertSafeCwd(cwd);
5267
- const result = spawnSync3("git", ["rev-parse", "HEAD"], {
4631
+ const result = spawnSync2("git", ["rev-parse", "HEAD"], {
5268
4632
  cwd,
5269
4633
  encoding: "utf-8",
5270
4634
  stdio: ["pipe", "pipe", "pipe"]
@@ -5277,7 +4641,7 @@ function getHeadSha(cwd) {
5277
4641
  }
5278
4642
  function hasUncommittedChanges(cwd) {
5279
4643
  assertSafeCwd(cwd);
5280
- const result = spawnSync3("git", ["status", "--porcelain"], {
4644
+ const result = spawnSync2("git", ["status", "--porcelain"], {
5281
4645
  cwd,
5282
4646
  encoding: "utf-8",
5283
4647
  stdio: ["pipe", "pipe", "pipe"]
@@ -5289,7 +4653,7 @@ function hasUncommittedChanges(cwd) {
5289
4653
  }
5290
4654
  function autoCommit(cwd, message) {
5291
4655
  assertSafeCwd(cwd);
5292
- const add = spawnSync3("git", ["add", "-A"], {
4656
+ const add = spawnSync2("git", ["add", "-A"], {
5293
4657
  cwd,
5294
4658
  encoding: "utf-8",
5295
4659
  stdio: ["pipe", "pipe", "pipe"]
@@ -5297,7 +4661,7 @@ function autoCommit(cwd, message) {
5297
4661
  if (add.status !== 0) {
5298
4662
  throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
5299
4663
  }
5300
- const commit = spawnSync3("git", ["commit", "-m", message], {
4664
+ const commit = spawnSync2("git", ["commit", "-m", message], {
5301
4665
  cwd,
5302
4666
  encoding: "utf-8",
5303
4667
  stdio: ["pipe", "pipe", "pipe"]
@@ -5310,14 +4674,14 @@ function generateBranchName(sprintId) {
5310
4674
  return `ralphctl/${sprintId}`;
5311
4675
  }
5312
4676
  function isGhAvailable() {
5313
- const result = spawnSync3("gh", ["--version"], {
4677
+ const result = spawnSync2("gh", ["--version"], {
5314
4678
  encoding: "utf-8",
5315
4679
  stdio: ["pipe", "pipe", "pipe"]
5316
4680
  });
5317
4681
  return result.status === 0;
5318
4682
  }
5319
4683
  function isGlabAvailable() {
5320
- const result = spawnSync3("glab", ["--version"], {
4684
+ const result = spawnSync2("glab", ["--version"], {
5321
4685
  encoding: "utf-8",
5322
4686
  stdio: ["pipe", "pipe", "pipe"]
5323
4687
  });
@@ -5392,6 +4756,265 @@ var DefaultExternalAdapter = class {
5392
4756
  }
5393
4757
  };
5394
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
+
5395
5018
  // src/application/factories.ts
5396
5019
  function createAiDeps(auto) {
5397
5020
  return {
@@ -5451,6 +5074,22 @@ function createIdeatePipeline2(shared, idea, options = {}) {
5451
5074
  options
5452
5075
  );
5453
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
+ }
5454
5093
  function createExecuteSprintPipeline2(shared, options = {}) {
5455
5094
  const { aiSession, promptBuilder, parser, ui, external } = createAiDeps(false);
5456
5095
  return createExecuteSprintPipeline(
@@ -5629,6 +5268,7 @@ async function sprintStartCommand(args) {
5629
5268
  }
5630
5269
 
5631
5270
  export {
5271
+ executePipeline,
5632
5272
  getTasks,
5633
5273
  saveTasks,
5634
5274
  getTask,
@@ -5644,29 +5284,20 @@ export {
5644
5284
  areAllTasksDone,
5645
5285
  reorderByDependencies,
5646
5286
  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
5287
  renderParsedTasksTable,
5662
5288
  importTasks,
5663
5289
  formatTicketForPrompt,
5664
5290
  parseRequirementsFile,
5665
5291
  runAiSession,
5666
- SignalParser,
5292
+ getCurrentBranch,
5293
+ branchExists,
5294
+ getDefaultBranch,
5295
+ isGhAvailable,
5296
+ isGlabAvailable,
5667
5297
  createRefinePipeline2 as createRefinePipeline,
5668
5298
  createPlanPipeline2 as createPlanPipeline,
5669
5299
  createIdeatePipeline2 as createIdeatePipeline,
5300
+ createOnboardPipeline2 as createOnboardPipeline,
5670
5301
  createExecuteSprintPipeline2 as createExecuteSprintPipeline,
5671
5302
  parseSprintStartArgs,
5672
5303
  sprintStartCommand