@neriros/ralphy 3.10.8 → 3.10.10

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 (2) hide show
  1. package/dist/shell/index.js +1430 -972
  2. package/package.json +1 -1
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
18928
18928
  import { resolve } from "path";
18929
18929
  function getVersion() {
18930
18930
  try {
18931
- if ("3.10.8")
18932
- return "3.10.8";
18931
+ if ("3.10.10")
18932
+ return "3.10.10";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -19376,6 +19376,764 @@ var init_src2 = __esm(() => {
19376
19376
  noopFallback = createNoopBus();
19377
19377
  });
19378
19378
 
19379
+ // packages/workflow/src/fields.ts
19380
+ function fieldsForMode(mode, answers = {}, restrictTo) {
19381
+ const all = mode === "customized" ? CUSTOMIZED_FIELDS : QUICK_FIELDS;
19382
+ const allowed = restrictTo ? new Set(restrictTo) : null;
19383
+ return all.filter((field) => {
19384
+ if (HIDDEN_FIELD_IDS.has(field.id))
19385
+ return false;
19386
+ if (allowed && !allowed.has(field.id))
19387
+ return false;
19388
+ return !field.when || field.when(answers);
19389
+ });
19390
+ }
19391
+ function findField(id) {
19392
+ return CUSTOMIZED_FIELDS.find((field) => field.id === id);
19393
+ }
19394
+ function modelOptionValues() {
19395
+ const field = findField("model");
19396
+ return field && field.spec.kind === "select" ? field.spec.options.map((o) => o.value) : [];
19397
+ }
19398
+ var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_ASSIGNEE_CHOICE_FIELD_ID = "linear.assigneeChoice", LINEAR_ASSIGNEE_VALUE_FIELD_ID = "linear.assigneeValue", LINEAR_FILTER_DESCRIPTION, LINEAR_ASSIGNEE_CHOICE, LINEAR_ASSIGNEE_VALUE, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, concurrencyForcesWorktree = (answers) => {
19399
+ const value = answers["concurrency"];
19400
+ return typeof value === "number" && value > 1;
19401
+ }, worktreeEnabled = (answers) => answers["useWorktree"] === true || concurrencyForcesWorktree(answers), HIDDEN_FIELD_IDS, CUSTOMIZED_FIELDS, COMMON_CLI_OPTIONS, FIELD_DESCRIPTIONS;
19402
+ var init_fields = __esm(() => {
19403
+ PROJECT_NAME = {
19404
+ id: "project.name",
19405
+ label: "Project name",
19406
+ description: "The project's display name. Ralphy puts it in the agent's prompt and in its logs.",
19407
+ spec: { kind: "text", placeholder: "my-project" }
19408
+ };
19409
+ LINEAR_TEAM = {
19410
+ id: "linear.team",
19411
+ label: "Linear team key",
19412
+ hint: "e.g. ENG \u2014 leave blank to match all teams",
19413
+ description: "The Linear team this repository is linked to, given by its key (e.g. ENG). Ralphy only picks up issues from this team. Leave blank to watch every team.",
19414
+ emptyLabel: "all teams",
19415
+ spec: { kind: "text" }
19416
+ };
19417
+ REPO_LINK = {
19418
+ id: "repo.link",
19419
+ label: "Record this repository in WORKFLOW.md?",
19420
+ description: "Record the detected git repository in WORKFLOW.md so Ralphy maps this project's Linear issues to it. Confirm to adopt the detected repo; decline to leave it out.",
19421
+ spec: yes(),
19422
+ when: (answers) => typeof answers["repo.name"] === "string" && answers["repo.name"] !== ""
19423
+ };
19424
+ LINEAR_FILTER_DESCRIPTION = "Global filter applied to every Linear ticket fetch, as an 'assignee = <value>' clause. " + "<value> is 'me' (issues assigned to you), 'any' (regardless of assignee), 'unassigned', " + "or a specific Linear user (email or user-id). Blank defaults to 'assignee = me'.";
19425
+ LINEAR_ASSIGNEE_CHOICE = {
19426
+ id: LINEAR_ASSIGNEE_CHOICE_FIELD_ID,
19427
+ label: "Linear assignee filter",
19428
+ description: "Which Linear issues Ralphy fetches, by assignee: 'me' (assigned to you), 'any' (regardless of assignee), 'unassigned', or a specific user you name next.",
19429
+ spec: {
19430
+ kind: "select",
19431
+ options: [
19432
+ { label: "me (assigned to you)", value: "me" },
19433
+ { label: "any (regardless of assignee)", value: "any" },
19434
+ { label: "unassigned", value: "unassigned" },
19435
+ { label: "a specific user (email or user-id)\u2026", value: "other" }
19436
+ ]
19437
+ }
19438
+ };
19439
+ LINEAR_ASSIGNEE_VALUE = {
19440
+ id: LINEAR_ASSIGNEE_VALUE_FIELD_ID,
19441
+ label: "Assignee email or user-id",
19442
+ description: "The specific Linear user to filter by \u2014 their email address or Linear user-id.",
19443
+ spec: { kind: "text", placeholder: "you@example.com" },
19444
+ when: (answers) => answers[LINEAR_ASSIGNEE_CHOICE_FIELD_ID] === "other"
19445
+ };
19446
+ QUICK_FIELDS = [
19447
+ PROJECT_NAME,
19448
+ LINEAR_TEAM,
19449
+ REPO_LINK,
19450
+ LINEAR_ASSIGNEE_CHOICE,
19451
+ LINEAR_ASSIGNEE_VALUE
19452
+ ];
19453
+ HIDDEN_FIELD_IDS = new Set([
19454
+ "appendPrompt",
19455
+ "metaPrompt.enabled",
19456
+ "metaPrompt.effort",
19457
+ "logRawStream",
19458
+ "maxConsecutiveFailuresPerTask",
19459
+ "prDraft",
19460
+ "manualMergeWhenAutoMergeDisabled",
19461
+ "finalizeNoOpAsDone",
19462
+ "linear.confirmationMode.maxConfirmationRounds",
19463
+ "openspec.reviewPhase.enabled"
19464
+ ]);
19465
+ CUSTOMIZED_FIELDS = [
19466
+ PROJECT_NAME,
19467
+ {
19468
+ id: "project.language",
19469
+ label: "Language",
19470
+ description: "Primary programming language (e.g. TypeScript). Added to the agent's prompt as context.",
19471
+ spec: { kind: "text", placeholder: "TypeScript" }
19472
+ },
19473
+ {
19474
+ id: "project.framework",
19475
+ label: "Framework",
19476
+ description: "Primary framework or toolchain (e.g. Bun + Nx). Added to the agent's prompt as context.",
19477
+ spec: { kind: "text", placeholder: "Bun + Nx" }
19478
+ },
19479
+ {
19480
+ id: "commands.test",
19481
+ label: "Test command",
19482
+ description: "Shell command Ralphy runs to check the agent's work each iteration; its exit code decides pass or fail.",
19483
+ spec: { kind: "text", placeholder: "bun test" }
19484
+ },
19485
+ {
19486
+ id: "commands.lint",
19487
+ label: "Lint command",
19488
+ description: "Shell command Ralphy runs to lint the code before a task is allowed to finish.",
19489
+ spec: { kind: "text", placeholder: "bun run lint" }
19490
+ },
19491
+ {
19492
+ id: "commands.build",
19493
+ label: "Build command",
19494
+ description: "Shell command Ralphy runs to confirm the project still compiles / builds.",
19495
+ spec: { kind: "text", placeholder: "bun run build" }
19496
+ },
19497
+ {
19498
+ id: "commands.typecheck",
19499
+ label: "Typecheck command",
19500
+ description: "Shell command Ralphy runs to confirm the project's types still pass.",
19501
+ spec: { kind: "text", placeholder: "bun run typecheck" }
19502
+ },
19503
+ {
19504
+ id: "engine",
19505
+ label: "Engine",
19506
+ description: "Which AI coding tool runs the loop: 'claude' (Claude Code) or 'codex' (OpenAI Codex).",
19507
+ spec: {
19508
+ kind: "select",
19509
+ options: [
19510
+ { label: "claude", value: "claude" },
19511
+ { label: "codex", value: "codex" }
19512
+ ]
19513
+ }
19514
+ },
19515
+ {
19516
+ id: "model",
19517
+ label: "Model tier",
19518
+ description: "Model tier the engine uses. 'opus' is the most capable, 'haiku' the cheapest and fastest; higher tiers cost more per token.",
19519
+ spec: {
19520
+ kind: "select",
19521
+ options: [
19522
+ { label: "opus", value: "opus" },
19523
+ { label: "sonnet", value: "sonnet" },
19524
+ { label: "haiku", value: "haiku" }
19525
+ ]
19526
+ }
19527
+ },
19528
+ {
19529
+ id: "logRawStream",
19530
+ label: "Log the raw engine stream to stdout?",
19531
+ description: "Print the engine's raw event stream to the terminal. Very verbose \u2014 mainly for debugging.",
19532
+ spec: no()
19533
+ },
19534
+ {
19535
+ id: "taskVerbose",
19536
+ label: "Show detailed task output?",
19537
+ description: "Show detailed per-task output (passes --verbose to the task sub-process) for extra diagnostics.",
19538
+ spec: no()
19539
+ },
19540
+ {
19541
+ id: "concurrency",
19542
+ label: "Concurrency (parallel tasks)",
19543
+ description: "How many tasks Ralphy works on at once. Higher finishes faster but uses more API quota simultaneously.",
19544
+ spec: { kind: "number", placeholder: "1" }
19545
+ },
19546
+ {
19547
+ id: "pollIntervalSeconds",
19548
+ label: "Poll interval (seconds)",
19549
+ description: "In agent mode, how often (in seconds) Ralphy checks Linear for new issues to pick up.",
19550
+ spec: { kind: "number", placeholder: "60" }
19551
+ },
19552
+ {
19553
+ id: "iterationDelaySeconds",
19554
+ label: "Delay between iterations (seconds)",
19555
+ description: "Seconds to pause between loop iterations \u2014 a throttle to slow spend. 0 means no pause.",
19556
+ spec: { kind: "number", placeholder: "0" }
19557
+ },
19558
+ {
19559
+ id: "maxIterationsPerTask",
19560
+ label: "Max iterations per task (0 = unlimited)",
19561
+ description: "Stop a task after this many loop iterations. 0 means no limit (run until done or another limit hits).",
19562
+ spec: { kind: "number", placeholder: "0" }
19563
+ },
19564
+ {
19565
+ id: "maxCostUsdPerTask",
19566
+ label: "Max cost USD per task (0 = unlimited)",
19567
+ description: "Stop a task once its API spend passes this many US dollars. 0 means no cost limit.",
19568
+ spec: { kind: "number", placeholder: "0" }
19569
+ },
19570
+ {
19571
+ id: "maxRuntimeMinutesPerTask",
19572
+ label: "Max runtime minutes per task (0 = unlimited)",
19573
+ description: "Stop a task after this many minutes of wall-clock time. 0 means no time limit.",
19574
+ spec: { kind: "number", placeholder: "0" }
19575
+ },
19576
+ {
19577
+ id: "maxConsecutiveFailuresPerTask",
19578
+ label: "Max consecutive identical failures",
19579
+ description: "Give up on a task after this many identical failures in a row \u2014 a guard against stuck loops.",
19580
+ spec: { kind: "number", placeholder: "5" }
19581
+ },
19582
+ {
19583
+ id: "useWorktree",
19584
+ label: "Run each task in an isolated git worktree?",
19585
+ description: "Run each task in its own git worktree (a separate working copy of the repo) so parallel tasks don't overwrite each other's files. Forced on when concurrency is greater than 1.",
19586
+ spec: no(),
19587
+ when: (answers) => !concurrencyForcesWorktree(answers)
19588
+ },
19589
+ {
19590
+ id: "cleanupWorktreeOnSuccess",
19591
+ label: "Delete the worktree after a successful task?",
19592
+ description: "Delete a task's worktree (its separate working copy) once it succeeds, to reclaim disk space.",
19593
+ spec: no(),
19594
+ when: worktreeEnabled
19595
+ },
19596
+ {
19597
+ id: "setupScript",
19598
+ label: "Worktree setup script (runs before each task)",
19599
+ description: "Part of the worktree flow: a shell script run once in each task's fresh worktree before the task starts \u2014 e.g. to install dependencies in the new working copy.",
19600
+ spec: { kind: "text" },
19601
+ when: worktreeEnabled
19602
+ },
19603
+ {
19604
+ id: "teardownScript",
19605
+ label: "Worktree teardown script (runs after each task)",
19606
+ description: "Part of the worktree flow: a shell script run once in each task's worktree after the task ends \u2014 e.g. to clean up before the worktree is removed.",
19607
+ spec: { kind: "text" },
19608
+ when: worktreeEnabled
19609
+ },
19610
+ {
19611
+ id: "enableManualTest",
19612
+ label: "Enable the manual-test phase?",
19613
+ description: "Add a phase that pauses for a human to manually test the change (e.g. in the UI) before the task is marked done.",
19614
+ spec: no()
19615
+ },
19616
+ {
19617
+ id: "appendPrompt",
19618
+ label: "Extra text appended to every prompt",
19619
+ description: "Free text added to the end of every prompt sent to the agent \u2014 house rules or reminders.",
19620
+ spec: { kind: "text" }
19621
+ },
19622
+ {
19623
+ id: "createPrOnSuccess",
19624
+ label: "Open a pull request when a task succeeds?",
19625
+ description: "When a task succeeds, automatically push the branch and open a GitHub pull request (PR).",
19626
+ spec: no()
19627
+ },
19628
+ {
19629
+ id: "prDraft",
19630
+ label: "Open pull requests as drafts?",
19631
+ description: "Open PRs as drafts (marked not-ready-for-review) instead of ready for review.",
19632
+ spec: no(),
19633
+ when: isOn("createPrOnSuccess")
19634
+ },
19635
+ {
19636
+ id: "prBaseBranch",
19637
+ label: "PR base branch",
19638
+ description: "The branch new pull requests merge into (their base) \u2014 e.g. main.",
19639
+ spec: { kind: "text", placeholder: "main" },
19640
+ when: isOn("createPrOnSuccess")
19641
+ },
19642
+ {
19643
+ id: "stackPrsOnDependencies",
19644
+ label: "Stack dependent issues' PRs onto their blocker's PR?",
19645
+ description: "If an issue is blocked by another that already has an open PR, base this issue's PR on that PR's branch instead of main (a 'stacked' PR).",
19646
+ spec: no(),
19647
+ when: isOn("createPrOnSuccess")
19648
+ },
19649
+ {
19650
+ id: "autoMergeStrategy",
19651
+ label: "Auto-merge strategy",
19652
+ description: "How GitHub combines the PR's commits when it auto-merges: squash (one commit), merge (a merge commit), or rebase.",
19653
+ spec: {
19654
+ kind: "select",
19655
+ options: [
19656
+ { label: "squash", value: "squash" },
19657
+ { label: "merge", value: "merge" },
19658
+ { label: "rebase", value: "rebase" }
19659
+ ]
19660
+ },
19661
+ when: isOn("createPrOnSuccess")
19662
+ },
19663
+ {
19664
+ id: "manualMergeWhenAutoMergeDisabled",
19665
+ label: "Merge manually when GitHub auto-merge is disabled?",
19666
+ description: "If the repo doesn't have GitHub's auto-merge feature enabled, have Ralphy merge the PR itself once checks pass.",
19667
+ spec: yes(),
19668
+ when: isOn("createPrOnSuccess")
19669
+ },
19670
+ {
19671
+ id: "finalizeNoOpAsDone",
19672
+ label: "Finalize a no-op (meta-only) change as done?",
19673
+ description: "If a change ended up touching only meta files (specs, task lists) and no real code, mark the issue done instead of retrying it.",
19674
+ spec: yes()
19675
+ },
19676
+ {
19677
+ id: "fixCiOnFailure",
19678
+ label: "Let the agent fix CI failures?",
19679
+ description: "After opening a PR, watch its CI (the automated checks GitHub runs) and let the agent push fixes when they fail.",
19680
+ spec: no()
19681
+ },
19682
+ {
19683
+ id: "maxCiFixAttempts",
19684
+ label: "Max CI-fix attempts per task",
19685
+ description: "Stop trying to fix failing CI after this many attempts.",
19686
+ spec: { kind: "number", placeholder: "5" },
19687
+ when: isOn("fixCiOnFailure")
19688
+ },
19689
+ {
19690
+ id: "ciPollIntervalSeconds",
19691
+ label: "CI status poll interval (seconds)",
19692
+ description: "How often (in seconds) to re-check the PR's CI status while waiting on or fixing it.",
19693
+ spec: { kind: "number", placeholder: "30" },
19694
+ when: isOn("fixCiOnFailure")
19695
+ },
19696
+ {
19697
+ id: "ignoreCiChecks",
19698
+ label: "CI checks to ignore",
19699
+ description: "Names of CI checks to ignore when deciding whether a PR is green \u2014 e.g. known-flaky jobs.",
19700
+ spec: { kind: "list", placeholder: "check name" }
19701
+ },
19702
+ {
19703
+ id: "rules",
19704
+ label: "Project rules",
19705
+ description: "House rules added to every prompt (e.g. 'never edit generated files'). One rule per entry.",
19706
+ spec: { kind: "list", placeholder: "a rule" }
19707
+ },
19708
+ {
19709
+ id: "boundaries.never_touch",
19710
+ label: "Never-touch globs",
19711
+ description: "Glob patterns for files the agent must never modify (e.g. dist/**).",
19712
+ spec: { kind: "list", placeholder: "dist/**" }
19713
+ },
19714
+ LINEAR_TEAM,
19715
+ REPO_LINK,
19716
+ LINEAR_ASSIGNEE_CHOICE,
19717
+ LINEAR_ASSIGNEE_VALUE,
19718
+ {
19719
+ id: "linear.postComments",
19720
+ label: "Post progress comments on the Linear issue?",
19721
+ description: "Post progress comments on the Linear issue while a task runs.",
19722
+ spec: yes()
19723
+ },
19724
+ {
19725
+ id: "linear.updateEveryIterations",
19726
+ label: "Post a progress update every N iterations (0 = off)",
19727
+ description: "Post a progress comment every N loop iterations. 0 turns periodic updates off.",
19728
+ spec: { kind: "number", placeholder: "10" }
19729
+ },
19730
+ {
19731
+ id: "linear.mentionTrigger",
19732
+ label: "Watch comments/PRs for @mentions?",
19733
+ description: "Watch a finished issue's comments and its PR for @mentions of Ralphy, and re-engage when mentioned.",
19734
+ spec: yes()
19735
+ },
19736
+ {
19737
+ id: "linear.mentionHandle",
19738
+ label: "Mention handle",
19739
+ description: "The @handle that, when mentioned, makes Ralphy pick the issue back up (e.g. @ralphy).",
19740
+ spec: { kind: "text", placeholder: "@ralphy" },
19741
+ when: isOn("linear.mentionTrigger")
19742
+ },
19743
+ {
19744
+ id: "linear.codeReviewTrigger",
19745
+ label: "Watch PRs for unresolved review threads?",
19746
+ description: "Watch open PRs for unresolved review comments and re-engage to address them.",
19747
+ spec: yes()
19748
+ },
19749
+ {
19750
+ id: "linear.codeReviewStaleHours",
19751
+ label: "Code-review stale window (hours)",
19752
+ description: "Ignore review comments older than this many hours, so stale threads don't re-trigger work.",
19753
+ spec: { kind: "number", placeholder: "24" },
19754
+ when: isOn("linear.codeReviewTrigger")
19755
+ },
19756
+ {
19757
+ id: "linear.syncTasksToComment",
19758
+ label: "Sync tasks into a sticky Linear comment?",
19759
+ description: "Keep one pinned ('sticky') Linear comment in sync with the task checklist (tasks.md).",
19760
+ spec: yes()
19761
+ },
19762
+ {
19763
+ id: "linear.syncSpecsAsAttachments",
19764
+ label: "Upload plan as attachments to the Linear ticket?",
19765
+ description: "Upload the OpenSpec planning docs (proposal.md, design.md) to the issue as attachments. OpenSpec is Ralphy's spec-driven planning format.",
19766
+ spec: yes()
19767
+ },
19768
+ {
19769
+ id: "linear.specAttachmentFormats",
19770
+ label: "Plan attachment formats",
19771
+ description: "Which formats to upload the spec docs in: 'md' (raw markdown), 'pdf' (a rendered PDF), or both.",
19772
+ spec: {
19773
+ kind: "multiselect",
19774
+ options: [
19775
+ { label: "md", value: "md" },
19776
+ { label: "pdf", value: "pdf" }
19777
+ ]
19778
+ },
19779
+ when: isOn("linear.syncSpecsAsAttachments")
19780
+ },
19781
+ {
19782
+ id: "linear.confirmationMode.enabled",
19783
+ label: "Enable the human confirmation gate?",
19784
+ description: "Pause after the agent finishes planning and wait for a human to approve before it writes any code (a confirmation gate).",
19785
+ spec: no()
19786
+ },
19787
+ {
19788
+ id: "linear.confirmationMode.timeoutHours",
19789
+ label: "Confirmation timeout (hours)",
19790
+ description: "If no one approves or rejects within this many hours, auto-resolve the confirmation gate.",
19791
+ spec: { kind: "number", placeholder: "48" },
19792
+ when: isOn("linear.confirmationMode.enabled")
19793
+ },
19794
+ {
19795
+ id: "linear.confirmationMode.maxConfirmationRounds",
19796
+ label: "Max confirmation rounds",
19797
+ description: "How many times the plan can be revised and re-submitted for approval before Ralphy gives up.",
19798
+ spec: { kind: "number", placeholder: "3" },
19799
+ when: isOn("linear.confirmationMode.enabled")
19800
+ },
19801
+ {
19802
+ id: "linear.indicators",
19803
+ label: "Linear lifecycle indicators",
19804
+ description: "How Ralphy maps lifecycle events to Linear statuses/labels \u2014 which issues to pick up (todo) and what to set when a task is in progress, done, or errored.",
19805
+ spec: { kind: "indicators" }
19806
+ },
19807
+ {
19808
+ id: "preExistingErrorCheck.enabled",
19809
+ label: "Enable the base-branch health gate?",
19810
+ description: "Before picking up new work, run health-check commands on the base branch and pause if it's already broken, so the agent isn't blamed for pre-existing failures.",
19811
+ spec: no()
19812
+ },
19813
+ {
19814
+ id: "preExistingErrorCheck.commands",
19815
+ label: "Health-gate commands (blank = use lint/test)",
19816
+ description: "Commands run against the base branch to judge its health. Leave empty to reuse your lint/test commands.",
19817
+ spec: { kind: "list", placeholder: "bun run lint" },
19818
+ when: isOn("preExistingErrorCheck.enabled")
19819
+ },
19820
+ {
19821
+ id: "preExistingErrorCheck.baseBranch",
19822
+ label: "Health-gate base branch",
19823
+ description: "The branch the health gate checks out and tests (usually main).",
19824
+ spec: { kind: "text", placeholder: "main" },
19825
+ when: isOn("preExistingErrorCheck.enabled")
19826
+ },
19827
+ {
19828
+ id: "preExistingErrorCheck.label",
19829
+ label: "Health-gate Linear label",
19830
+ description: "Linear label applied to the ticket Ralphy opens when the base branch is found broken.",
19831
+ spec: { kind: "text", placeholder: "ralph:pre-existing-error" },
19832
+ when: isOn("preExistingErrorCheck.enabled")
19833
+ },
19834
+ {
19835
+ id: "prTracker.enabled",
19836
+ label: "Enable the PR tracker?",
19837
+ description: "Keep watching the PRs Ralphy opened and automatically try to recover any whose merge state goes red (conflicts or failing CI).",
19838
+ spec: yes()
19839
+ },
19840
+ {
19841
+ id: "prTracker.maxRecoveryAttempts",
19842
+ label: "PR tracker max recovery attempts",
19843
+ description: "Give up auto-recovering a red PR after this many attempts, then flag it for a human.",
19844
+ spec: { kind: "number", placeholder: "3" },
19845
+ when: isOn("prTracker.enabled")
19846
+ },
19847
+ {
19848
+ id: "prTracker.advanceMergedToDone",
19849
+ label: "Advance merged PRs to done automatically?",
19850
+ description: "Move an issue to its done state as soon as its PR is merged.",
19851
+ spec: no(),
19852
+ when: isOn("prTracker.enabled")
19853
+ },
19854
+ {
19855
+ id: "metaPrompt.enabled",
19856
+ label: "Enable the meta-prompt addendum?",
19857
+ description: "Add Ralphy's task-level 'meta-prompt' layer (extra framing instructions) to each phase. Leave on unless you want raw prompts.",
19858
+ spec: yes()
19859
+ },
19860
+ {
19861
+ id: "metaPrompt.effort",
19862
+ label: "Per-ticket effort tier",
19863
+ description: "How much effort the meta-prompt nudges the agent toward per ticket. 'auto' detects it from the ticket; 'light'/'standard'/'heavy' pin every ticket to that tier.",
19864
+ spec: {
19865
+ kind: "select",
19866
+ options: [
19867
+ { label: "auto", value: "auto" },
19868
+ { label: "light", value: "light" },
19869
+ { label: "standard", value: "standard" },
19870
+ { label: "heavy", value: "heavy" }
19871
+ ]
19872
+ },
19873
+ when: isOn("metaPrompt.enabled")
19874
+ },
19875
+ {
19876
+ id: "openspec.reviewPhase.enabled",
19877
+ label: "Enable the OpenSpec review phase?",
19878
+ description: "After all tasks finish, spawn a separate reviewer agent that reads the full diff and writes review findings; open findings loop back into more work.",
19879
+ spec: no()
19880
+ },
19881
+ {
19882
+ id: "openspec.reviewPhase.maxRounds",
19883
+ label: "Review phase max rounds",
19884
+ description: "How many review\u2192fix cycles to run before the change is archived regardless.",
19885
+ spec: { kind: "number", placeholder: "1" },
19886
+ when: isOn("openspec.reviewPhase.enabled")
19887
+ },
19888
+ {
19889
+ id: "openspec.reviewPhase.reviewerModel",
19890
+ label: "Reviewer model (blank = same as main)",
19891
+ description: "Model used for the review pass. Blank reuses the main model; a cheaper tier (e.g. haiku) saves cost.",
19892
+ spec: { kind: "text", placeholder: "haiku" },
19893
+ when: isOn("openspec.reviewPhase.enabled")
19894
+ },
19895
+ {
19896
+ id: "openspec.reviewPhase.reviewerContextStrategy",
19897
+ label: "Reviewer context",
19898
+ description: "'fresh' gives the reviewer a brand-new session (unbiased); 'warm' resumes the last task's session (more context, cheaper).",
19899
+ spec: {
19900
+ kind: "select",
19901
+ options: [
19902
+ { label: "fresh", value: "fresh" },
19903
+ { label: "warm", value: "warm" }
19904
+ ]
19905
+ },
19906
+ when: isOn("openspec.reviewPhase.enabled")
19907
+ },
19908
+ {
19909
+ id: PROMPT_BODY_FIELD_ID,
19910
+ label: "Customize the prompt sent to the agent?",
19911
+ description: "The prompt the agent receives lives in the file body \u2014 a template filled with per-issue values (e.g. {{ issue.identifier }}). Edit it here, or leave it and finish to keep the default.",
19912
+ spec: { kind: "multiline" }
19913
+ }
19914
+ ];
19915
+ COMMON_CLI_OPTIONS = [
19916
+ { fieldId: "model", flag: "--model", argKey: "model", kind: "model" },
19917
+ { fieldId: "iterationDelaySeconds", flag: "--delay", argKey: "delay", kind: "int" },
19918
+ { fieldId: "maxCostUsdPerTask", flag: "--max-cost", argKey: "maxCostUsd", kind: "float" },
19919
+ {
19920
+ fieldId: "maxRuntimeMinutesPerTask",
19921
+ flag: "--max-runtime",
19922
+ argKey: "maxRuntimeMinutes",
19923
+ kind: "float"
19924
+ },
19925
+ {
19926
+ fieldId: "maxConsecutiveFailuresPerTask",
19927
+ flag: "--max-failures",
19928
+ argKey: "maxConsecutiveFailures",
19929
+ kind: "int"
19930
+ },
19931
+ {
19932
+ fieldId: "maxIterationsPerTask",
19933
+ flag: "--max-iterations",
19934
+ argKey: "maxIterations",
19935
+ kind: "int"
19936
+ },
19937
+ { fieldId: "logRawStream", flag: "--log", argKey: "log", kind: "boolean" },
19938
+ { fieldId: "taskVerbose", flag: "--verbose", argKey: "verbose", kind: "boolean" }
19939
+ ];
19940
+ FIELD_DESCRIPTIONS = [
19941
+ ...CUSTOMIZED_FIELDS.filter((field) => Boolean(field.description) && field.spec.kind !== "multiline").map((field) => ({ path: field.id.split("."), description: field.description })),
19942
+ { path: ["linear", "filter"], description: LINEAR_FILTER_DESCRIPTION }
19943
+ ];
19944
+ });
19945
+
19946
+ // packages/cli-args/src/common-args.ts
19947
+ import { resolve as resolve2 } from "path";
19948
+ function initialCommonArgs() {
19949
+ return {
19950
+ engine: "claude",
19951
+ model: "opus",
19952
+ engineSet: false,
19953
+ maxIterations: 0,
19954
+ maxCostUsd: 0,
19955
+ maxRuntimeMinutes: 0,
19956
+ maxConsecutiveFailures: 5,
19957
+ delay: 0,
19958
+ log: false,
19959
+ verbose: false,
19960
+ projectRoot: undefined,
19961
+ workflowFile: undefined,
19962
+ name: "",
19963
+ prompt: "",
19964
+ fromAgent: false
19965
+ };
19966
+ }
19967
+ function applyValueOption(option, args, raw) {
19968
+ const setter = VALUE_SETTERS[option.argKey];
19969
+ if (!setter)
19970
+ throw new Error("no value setter registered for CLI option");
19971
+ setter(args, raw);
19972
+ }
19973
+ function applyBooleanOption(option, args) {
19974
+ const setter = BOOLEAN_SETTERS[option.argKey];
19975
+ if (!setter)
19976
+ throw new Error("no boolean setter registered for CLI option");
19977
+ setter(args);
19978
+ }
19979
+ function emptyParseState() {
19980
+ return {
19981
+ pendingOption: null,
19982
+ expectClaudeModel: false,
19983
+ expectProjectRoot: false,
19984
+ expectWorkflow: false,
19985
+ expectName: false,
19986
+ expectPrompt: false,
19987
+ expectPromptFile: false,
19988
+ promptFilePath: null,
19989
+ workflowFileRaw: null
19990
+ };
19991
+ }
19992
+ function parseCommonArg(arg, args, state) {
19993
+ if (state.pendingOption) {
19994
+ applyValueOption(state.pendingOption, args, arg);
19995
+ state.pendingOption = null;
19996
+ return true;
19997
+ }
19998
+ if (state.expectClaudeModel) {
19999
+ state.expectClaudeModel = false;
20000
+ if (VALID_MODELS.has(arg)) {
20001
+ args.model = arg;
20002
+ return true;
20003
+ }
20004
+ }
20005
+ if (state.expectProjectRoot) {
20006
+ args.projectRoot = arg;
20007
+ state.expectProjectRoot = false;
20008
+ return true;
20009
+ }
20010
+ if (state.expectWorkflow) {
20011
+ state.workflowFileRaw = arg;
20012
+ args.workflowFile = resolve2(arg);
20013
+ state.expectWorkflow = false;
20014
+ return true;
20015
+ }
20016
+ if (state.expectName) {
20017
+ args.name = arg;
20018
+ state.expectName = false;
20019
+ return true;
20020
+ }
20021
+ if (state.expectPrompt) {
20022
+ args.prompt = arg;
20023
+ state.promptFilePath = null;
20024
+ state.expectPrompt = false;
20025
+ return true;
20026
+ }
20027
+ if (state.expectPromptFile) {
20028
+ state.promptFilePath = arg;
20029
+ state.expectPromptFile = false;
20030
+ return true;
20031
+ }
20032
+ const option = OPTION_BY_FLAG.get(arg);
20033
+ if (option) {
20034
+ if (option.kind === "boolean")
20035
+ applyBooleanOption(option, args);
20036
+ else
20037
+ state.pendingOption = option;
20038
+ return true;
20039
+ }
20040
+ switch (arg) {
20041
+ case "--claude":
20042
+ if (args.engineSet && args.engine !== "claude") {
20043
+ throw new Error("Choose only one engine flag: --claude or --codex");
20044
+ }
20045
+ args.engine = "claude";
20046
+ args.engineSet = true;
20047
+ state.expectClaudeModel = true;
20048
+ return true;
20049
+ case "--codex":
20050
+ if (args.engineSet && args.engine !== "codex") {
20051
+ throw new Error("Choose only one engine flag: --claude or --codex");
20052
+ }
20053
+ args.engine = "codex";
20054
+ args.engineSet = true;
20055
+ return true;
20056
+ case "--unlimited":
20057
+ args.maxIterations = 0;
20058
+ return true;
20059
+ case "--project-root":
20060
+ state.expectProjectRoot = true;
20061
+ return true;
20062
+ case "--workflow":
20063
+ state.expectWorkflow = true;
20064
+ return true;
20065
+ case "--name":
20066
+ state.expectName = true;
20067
+ return true;
20068
+ case "--prompt":
20069
+ state.expectPrompt = true;
20070
+ return true;
20071
+ case "--prompt-file":
20072
+ state.expectPromptFile = true;
20073
+ return true;
20074
+ case "--from-agent":
20075
+ args.fromAgent = true;
20076
+ return true;
20077
+ default:
20078
+ return false;
20079
+ }
20080
+ }
20081
+ async function resolvePromptFile(args, state) {
20082
+ if (state.promptFilePath !== null) {
20083
+ args.prompt = await Bun.file(state.promptFilePath).text();
20084
+ }
20085
+ }
20086
+ function resolveWorkflowFile(args, state) {
20087
+ if (state.workflowFileRaw !== null && args.projectRoot !== undefined) {
20088
+ args.workflowFile = resolve2(args.projectRoot, state.workflowFileRaw);
20089
+ }
20090
+ }
20091
+ function parseWorkflowPathArgs(argv) {
20092
+ const args = initialCommonArgs();
20093
+ const state = emptyParseState();
20094
+ for (const token of argv)
20095
+ parseCommonArg(token, args, state);
20096
+ resolveWorkflowFile(args, state);
20097
+ return { projectRoot: args.projectRoot, workflowFile: args.workflowFile };
20098
+ }
20099
+ var VALID_MODELS, OPTION_BY_FLAG, VALUE_FLAGS, VALUE_SETTERS, BOOLEAN_SETTERS;
20100
+ var init_common_args = __esm(() => {
20101
+ init_fields();
20102
+ VALID_MODELS = new Set(modelOptionValues());
20103
+ OPTION_BY_FLAG = new Map(COMMON_CLI_OPTIONS.map((option) => [option.flag, option]));
20104
+ VALUE_FLAGS = new Set(COMMON_CLI_OPTIONS.filter((option) => option.kind !== "boolean").map((option) => option.flag));
20105
+ VALUE_SETTERS = {
20106
+ model: (args, raw) => {
20107
+ if (!VALID_MODELS.has(raw))
20108
+ throw new Error("Invalid model");
20109
+ args.model = raw;
20110
+ },
20111
+ delay: (args, raw) => {
20112
+ args.delay = parseInt(raw, 10);
20113
+ },
20114
+ maxCostUsd: (args, raw) => {
20115
+ args.maxCostUsd = parseFloat(raw);
20116
+ },
20117
+ maxRuntimeMinutes: (args, raw) => {
20118
+ args.maxRuntimeMinutes = parseFloat(raw);
20119
+ },
20120
+ maxConsecutiveFailures: (args, raw) => {
20121
+ args.maxConsecutiveFailures = parseInt(raw, 10);
20122
+ },
20123
+ maxIterations: (args, raw) => {
20124
+ args.maxIterations = parseInt(raw, 10);
20125
+ }
20126
+ };
20127
+ BOOLEAN_SETTERS = {
20128
+ log: (args) => {
20129
+ args.log = true;
20130
+ },
20131
+ verbose: (args) => {
20132
+ args.verbose = true;
20133
+ }
20134
+ };
20135
+ });
20136
+
19379
20137
  // node_modules/.bun/react@18.3.1/node_modules/react/cjs/react.development.js
19380
20138
  var require_react_development = __commonJS((exports, module) => {
19381
20139
  if (true) {
@@ -21016,14 +21774,14 @@ Check the top-level render call using <` + parentName + ">.";
21016
21774
  var thenableResult = result;
21017
21775
  var wasAwaited = false;
21018
21776
  var thenable = {
21019
- then: function(resolve2, reject) {
21777
+ then: function(resolve3, reject) {
21020
21778
  wasAwaited = true;
21021
21779
  thenableResult.then(function(returnValue2) {
21022
21780
  popActScope(prevActScopeDepth);
21023
21781
  if (actScopeDepth === 0) {
21024
- recursivelyFlushAsyncActWork(returnValue2, resolve2, reject);
21782
+ recursivelyFlushAsyncActWork(returnValue2, resolve3, reject);
21025
21783
  } else {
21026
- resolve2(returnValue2);
21784
+ resolve3(returnValue2);
21027
21785
  }
21028
21786
  }, function(error2) {
21029
21787
  popActScope(prevActScopeDepth);
@@ -21052,20 +21810,20 @@ Check the top-level render call using <` + parentName + ">.";
21052
21810
  ReactCurrentActQueue.current = null;
21053
21811
  }
21054
21812
  var _thenable = {
21055
- then: function(resolve2, reject) {
21813
+ then: function(resolve3, reject) {
21056
21814
  if (ReactCurrentActQueue.current === null) {
21057
21815
  ReactCurrentActQueue.current = [];
21058
- recursivelyFlushAsyncActWork(returnValue, resolve2, reject);
21816
+ recursivelyFlushAsyncActWork(returnValue, resolve3, reject);
21059
21817
  } else {
21060
- resolve2(returnValue);
21818
+ resolve3(returnValue);
21061
21819
  }
21062
21820
  }
21063
21821
  };
21064
21822
  return _thenable;
21065
21823
  } else {
21066
21824
  var _thenable2 = {
21067
- then: function(resolve2, reject) {
21068
- resolve2(returnValue);
21825
+ then: function(resolve3, reject) {
21826
+ resolve3(returnValue);
21069
21827
  }
21070
21828
  };
21071
21829
  return _thenable2;
@@ -21081,7 +21839,7 @@ Check the top-level render call using <` + parentName + ">.";
21081
21839
  actScopeDepth = prevActScopeDepth;
21082
21840
  }
21083
21841
  }
21084
- function recursivelyFlushAsyncActWork(returnValue, resolve2, reject) {
21842
+ function recursivelyFlushAsyncActWork(returnValue, resolve3, reject) {
21085
21843
  {
21086
21844
  var queue = ReactCurrentActQueue.current;
21087
21845
  if (queue !== null) {
@@ -21090,16 +21848,16 @@ Check the top-level render call using <` + parentName + ">.";
21090
21848
  enqueueTask(function() {
21091
21849
  if (queue.length === 0) {
21092
21850
  ReactCurrentActQueue.current = null;
21093
- resolve2(returnValue);
21851
+ resolve3(returnValue);
21094
21852
  } else {
21095
- recursivelyFlushAsyncActWork(returnValue, resolve2, reject);
21853
+ recursivelyFlushAsyncActWork(returnValue, resolve3, reject);
21096
21854
  }
21097
21855
  });
21098
21856
  } catch (error2) {
21099
21857
  reject(error2);
21100
21858
  }
21101
21859
  } else {
21102
- resolve2(returnValue);
21860
+ resolve3(returnValue);
21103
21861
  }
21104
21862
  }
21105
21863
  }
@@ -59172,8 +59930,8 @@ class Ink {
59172
59930
  }
59173
59931
  }
59174
59932
  async waitUntilExit() {
59175
- this.exitPromise ||= new Promise((resolve2, reject2) => {
59176
- this.resolveExitPromise = resolve2;
59933
+ this.exitPromise ||= new Promise((resolve3, reject2) => {
59934
+ this.resolveExitPromise = resolve3;
59177
59935
  this.rejectExitPromise = reject2;
59178
59936
  });
59179
59937
  return this.exitPromise;
@@ -59633,19 +60391,23 @@ var init_build2 = __esm(async () => {
59633
60391
  // packages/paths/src/paths.ts
59634
60392
  import { exists } from "fs/promises";
59635
60393
  import { homedir as homedir3 } from "os";
59636
- import { basename, join as join4, resolve as resolve2 } from "path";
59637
- async function findProjectRoot() {
59638
- let dir = process.cwd();
60394
+ import { basename, join as join4, resolve as resolve3 } from "path";
60395
+ async function findProjectRoot(startDir = process.cwd()) {
60396
+ let dir = startDir;
59639
60397
  while (dir !== "/") {
59640
- if (await exists(join4(dir, "openspec")))
60398
+ if (await exists(join4(dir, ROOT_MARKER)))
59641
60399
  return dir;
59642
- dir = resolve2(dir, "..");
60400
+ dir = resolve3(dir, "..");
59643
60401
  }
59644
- return process.cwd();
60402
+ return startDir;
59645
60403
  }
59646
60404
  function worktreesDir(projectRoot) {
59647
60405
  return join4(homedir3(), ".ralph", basename(projectRoot), "worktrees");
59648
60406
  }
60407
+ function setupBackupPath() {
60408
+ return join4(homedir3(), ".ralph", "setup.tmp");
60409
+ }
60410
+ var ROOT_MARKER = "WORKFLOW.md";
59649
60411
  var init_paths = () => {};
59650
60412
 
59651
60413
  // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
@@ -80635,7 +81397,7 @@ var init_schema = __esm(() => {
80635
81397
  maxRuntimeMinutesPerTask: exports_external.number().nonnegative().default(0),
80636
81398
  maxConsecutiveFailuresPerTask: exports_external.number().int().nonnegative().default(5),
80637
81399
  iterationDelaySeconds: exports_external.number().int().nonnegative().default(0),
80638
- logRawStream: exports_external.boolean().default(false),
81400
+ logRawStream: exports_external.boolean().default(true),
80639
81401
  taskVerbose: exports_external.boolean().default(false),
80640
81402
  enableManualTest: exports_external.boolean().default(false),
80641
81403
  useWorktree: exports_external.boolean().default(false),
@@ -80809,13 +81571,14 @@ maxConsecutiveFailuresPerTask: 5
80809
81571
 
80810
81572
  engine: claude
80811
81573
  model: opus
80812
- logRawStream: false
81574
+ logRawStream: true
80813
81575
  taskVerbose: false
80814
81576
 
80815
81577
  useWorktree: false
80816
81578
  cleanupWorktreeOnSuccess: false
80817
81579
 
80818
81580
  createPrOnSuccess: false
81581
+ prDraft: true
80819
81582
  prBaseBranch: main
80820
81583
  stackPrsOnDependencies: false
80821
81584
  autoMergeStrategy: squash
@@ -80868,6 +81631,10 @@ linear:
80868
81631
  # filter:
80869
81632
  # - type: label
80870
81633
  # value: "ralph:auto-merge"
81634
+
81635
+ openspec:
81636
+ reviewPhase:
81637
+ enabled: true
80871
81638
  ---
80872
81639
  You are working on {{ issue.identifier }}: {{ issue.title }}.
80873
81640
 
@@ -81065,537 +81832,6 @@ function renderTemplate(src, ctx) {
81065
81832
  return renderNodes(tree, ctx);
81066
81833
  }
81067
81834
 
81068
- // packages/workflow/src/fields.ts
81069
- function fieldsForMode(mode, answers = {}, restrictTo) {
81070
- const all = mode === "customized" ? CUSTOMIZED_FIELDS : QUICK_FIELDS;
81071
- const allowed = restrictTo ? new Set(restrictTo) : null;
81072
- return all.filter((field) => {
81073
- if (allowed && !allowed.has(field.id))
81074
- return false;
81075
- return !field.when || field.when(answers);
81076
- });
81077
- }
81078
- function findField(id) {
81079
- return CUSTOMIZED_FIELDS.find((field) => field.id === id);
81080
- }
81081
- function modelOptionValues() {
81082
- const field = findField("model");
81083
- return field && field.spec.kind === "select" ? field.spec.options.map((o) => o.value) : [];
81084
- }
81085
- var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", AWAITING_STATUS_FIELD_ID = "linear.confirmationMode.awaitingStatus", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_FILTER, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, CUSTOMIZED_FIELDS, COMMON_CLI_OPTIONS, FIELD_DESCRIPTIONS;
81086
- var init_fields = __esm(() => {
81087
- PROJECT_NAME = {
81088
- id: "project.name",
81089
- label: "Project name",
81090
- description: "The project's display name. Ralphy puts it in the agent's prompt and in its logs.",
81091
- spec: { kind: "text", placeholder: "my-project" }
81092
- };
81093
- LINEAR_TEAM = {
81094
- id: "linear.team",
81095
- label: "Linear team key",
81096
- hint: "e.g. ENG \u2014 leave blank to match all teams",
81097
- description: "The Linear team this repository is linked to, given by its key (e.g. ENG). Ralphy only picks up issues from this team. Leave blank to watch every team.",
81098
- emptyLabel: "all teams",
81099
- spec: { kind: "text" }
81100
- };
81101
- REPO_LINK = {
81102
- id: "repo.link",
81103
- label: "Link this repository to the team?",
81104
- description: "Record the detected git repository in WORKFLOW.md and link it to the Linear team above. Confirm to adopt the detected repo; decline to leave it out.",
81105
- spec: yes(),
81106
- when: (answers) => typeof answers["repo.name"] === "string" && answers["repo.name"] !== ""
81107
- };
81108
- LINEAR_FILTER = {
81109
- id: "linear.filter",
81110
- label: "Linear filter",
81111
- hint: "e.g. 'assignee = me', 'assignee = any', 'assignee = unassigned', or an email/user-id",
81112
- description: "Global filter applied to every Linear ticket fetch. The only clause today is 'assignee = <value>', where <value> is 'me' (issues assigned to you), 'any' (regardless of assignee), 'unassigned', a Linear user id, or an email. Blank defaults to 'assignee = me'.",
81113
- emptyLabel: "assignee = me",
81114
- spec: { kind: "text", placeholder: "assignee = me" }
81115
- };
81116
- QUICK_FIELDS = [PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_FILTER];
81117
- CUSTOMIZED_FIELDS = [
81118
- PROJECT_NAME,
81119
- {
81120
- id: "project.language",
81121
- label: "Language",
81122
- description: "Primary programming language (e.g. TypeScript). Added to the agent's prompt as context.",
81123
- spec: { kind: "text", placeholder: "TypeScript" }
81124
- },
81125
- {
81126
- id: "project.framework",
81127
- label: "Framework",
81128
- description: "Primary framework or toolchain (e.g. Bun + Nx). Added to the agent's prompt as context.",
81129
- spec: { kind: "text", placeholder: "Bun + Nx" }
81130
- },
81131
- {
81132
- id: "commands.test",
81133
- label: "Test command",
81134
- description: "Shell command Ralphy runs to check the agent's work each iteration; its exit code decides pass or fail.",
81135
- spec: { kind: "text", placeholder: "bun test" }
81136
- },
81137
- {
81138
- id: "commands.lint",
81139
- label: "Lint command",
81140
- description: "Shell command Ralphy runs to lint the code before a task is allowed to finish.",
81141
- spec: { kind: "text", placeholder: "bun run lint" }
81142
- },
81143
- {
81144
- id: "commands.build",
81145
- label: "Build command",
81146
- description: "Shell command Ralphy runs to confirm the project still compiles / builds.",
81147
- spec: { kind: "text", placeholder: "bun run build" }
81148
- },
81149
- {
81150
- id: "commands.typecheck",
81151
- label: "Typecheck command",
81152
- description: "Shell command Ralphy runs to confirm the project's types still pass.",
81153
- spec: { kind: "text", placeholder: "bun run typecheck" }
81154
- },
81155
- {
81156
- id: "engine",
81157
- label: "Engine",
81158
- description: "Which AI coding tool runs the loop: 'claude' (Claude Code) or 'codex' (OpenAI Codex).",
81159
- spec: {
81160
- kind: "select",
81161
- options: [
81162
- { label: "claude", value: "claude" },
81163
- { label: "codex", value: "codex" }
81164
- ]
81165
- }
81166
- },
81167
- {
81168
- id: "model",
81169
- label: "Model tier",
81170
- description: "Model tier the engine uses. 'opus' is the most capable, 'haiku' the cheapest and fastest; higher tiers cost more per token.",
81171
- spec: {
81172
- kind: "select",
81173
- options: [
81174
- { label: "opus", value: "opus" },
81175
- { label: "sonnet", value: "sonnet" },
81176
- { label: "haiku", value: "haiku" }
81177
- ]
81178
- }
81179
- },
81180
- {
81181
- id: "logRawStream",
81182
- label: "Log the raw engine stream to stdout?",
81183
- description: "Print the engine's raw event stream to the terminal. Very verbose \u2014 mainly for debugging.",
81184
- spec: no()
81185
- },
81186
- {
81187
- id: "taskVerbose",
81188
- label: "Pass --verbose to the task sub-process?",
81189
- description: "Run the per-task process with --verbose for extra diagnostic output.",
81190
- spec: no()
81191
- },
81192
- {
81193
- id: "concurrency",
81194
- label: "Concurrency (parallel tasks)",
81195
- description: "How many tasks Ralphy works on at once. Higher finishes faster but uses more API quota simultaneously.",
81196
- spec: { kind: "number", placeholder: "1" }
81197
- },
81198
- {
81199
- id: "pollIntervalSeconds",
81200
- label: "Poll interval (seconds)",
81201
- description: "In agent mode, how often (in seconds) Ralphy checks Linear for new issues to pick up.",
81202
- spec: { kind: "number", placeholder: "60" }
81203
- },
81204
- {
81205
- id: "iterationDelaySeconds",
81206
- label: "Delay between iterations (seconds)",
81207
- description: "Seconds to pause between loop iterations \u2014 a throttle to slow spend. 0 means no pause.",
81208
- spec: { kind: "number", placeholder: "0" }
81209
- },
81210
- {
81211
- id: "maxIterationsPerTask",
81212
- label: "Max iterations per task (0 = unlimited)",
81213
- description: "Stop a task after this many loop iterations. 0 means no limit (run until done or another limit hits).",
81214
- spec: { kind: "number", placeholder: "0" }
81215
- },
81216
- {
81217
- id: "maxCostUsdPerTask",
81218
- label: "Max cost USD per task (0 = unlimited)",
81219
- description: "Stop a task once its API spend passes this many US dollars. 0 means no cost limit.",
81220
- spec: { kind: "number", placeholder: "0" }
81221
- },
81222
- {
81223
- id: "maxRuntimeMinutesPerTask",
81224
- label: "Max runtime minutes per task (0 = unlimited)",
81225
- description: "Stop a task after this many minutes of wall-clock time. 0 means no time limit.",
81226
- spec: { kind: "number", placeholder: "0" }
81227
- },
81228
- {
81229
- id: "maxConsecutiveFailuresPerTask",
81230
- label: "Max consecutive identical failures",
81231
- description: "Give up on a task after this many identical failures in a row \u2014 a guard against stuck loops.",
81232
- spec: { kind: "number", placeholder: "5" }
81233
- },
81234
- {
81235
- id: "useWorktree",
81236
- label: "Run each task in an isolated git worktree?",
81237
- description: "Run each task in its own git worktree (a separate working copy of the repo) so parallel tasks don't overwrite each other's files.",
81238
- spec: no()
81239
- },
81240
- {
81241
- id: "cleanupWorktreeOnSuccess",
81242
- label: "Delete the worktree after a successful task?",
81243
- description: "Delete a task's worktree (its separate working copy) once it succeeds, to reclaim disk space.",
81244
- spec: no(),
81245
- when: isOn("useWorktree")
81246
- },
81247
- {
81248
- id: "setupScript",
81249
- label: "Setup script (runs before each task)",
81250
- description: "Shell script run once before each task starts \u2014 e.g. to install dependencies.",
81251
- spec: { kind: "text" }
81252
- },
81253
- {
81254
- id: "teardownScript",
81255
- label: "Teardown script (runs after each task)",
81256
- description: "Shell script run once after each task ends \u2014 e.g. to clean up temporary state.",
81257
- spec: { kind: "text" }
81258
- },
81259
- {
81260
- id: "enableManualTest",
81261
- label: "Enable the manual-test phase?",
81262
- description: "Add a phase that pauses for a human to manually test the change (e.g. in the UI) before the task is marked done.",
81263
- spec: no()
81264
- },
81265
- {
81266
- id: "appendPrompt",
81267
- label: "Extra text appended to every prompt",
81268
- description: "Free text added to the end of every prompt sent to the agent \u2014 house rules or reminders.",
81269
- spec: { kind: "text" }
81270
- },
81271
- {
81272
- id: "createPrOnSuccess",
81273
- label: "Open a pull request when a task succeeds?",
81274
- description: "When a task succeeds, automatically push the branch and open a GitHub pull request (PR).",
81275
- spec: no()
81276
- },
81277
- {
81278
- id: "prDraft",
81279
- label: "Open pull requests as drafts?",
81280
- description: "Open PRs as drafts (marked not-ready-for-review) instead of ready for review.",
81281
- spec: no(),
81282
- when: isOn("createPrOnSuccess")
81283
- },
81284
- {
81285
- id: "prBaseBranch",
81286
- label: "PR base branch",
81287
- description: "The branch new pull requests merge into (their base) \u2014 e.g. main.",
81288
- spec: { kind: "text", placeholder: "main" },
81289
- when: isOn("createPrOnSuccess")
81290
- },
81291
- {
81292
- id: "stackPrsOnDependencies",
81293
- label: "Stack dependent issues' PRs onto their blocker's PR?",
81294
- description: "If an issue is blocked by another that already has an open PR, base this issue's PR on that PR's branch instead of main (a 'stacked' PR).",
81295
- spec: no(),
81296
- when: isOn("createPrOnSuccess")
81297
- },
81298
- {
81299
- id: "autoMergeStrategy",
81300
- label: "Auto-merge strategy",
81301
- description: "How GitHub combines the PR's commits when it auto-merges: squash (one commit), merge (a merge commit), or rebase.",
81302
- spec: {
81303
- kind: "select",
81304
- options: [
81305
- { label: "squash", value: "squash" },
81306
- { label: "merge", value: "merge" },
81307
- { label: "rebase", value: "rebase" }
81308
- ]
81309
- },
81310
- when: isOn("createPrOnSuccess")
81311
- },
81312
- {
81313
- id: "manualMergeWhenAutoMergeDisabled",
81314
- label: "Merge manually when GitHub auto-merge is disabled?",
81315
- description: "If the repo doesn't have GitHub's auto-merge feature enabled, have Ralphy merge the PR itself once checks pass.",
81316
- spec: yes(),
81317
- when: isOn("createPrOnSuccess")
81318
- },
81319
- {
81320
- id: "finalizeNoOpAsDone",
81321
- label: "Finalize a no-op (meta-only) change as done?",
81322
- description: "If a change ended up touching only meta files (specs, task lists) and no real code, mark the issue done instead of retrying it.",
81323
- spec: yes()
81324
- },
81325
- {
81326
- id: "fixCiOnFailure",
81327
- label: "Let the agent fix CI failures?",
81328
- description: "After opening a PR, watch its CI (the automated checks GitHub runs) and let the agent push fixes when they fail.",
81329
- spec: no()
81330
- },
81331
- {
81332
- id: "maxCiFixAttempts",
81333
- label: "Max CI-fix attempts per task",
81334
- description: "Stop trying to fix failing CI after this many attempts.",
81335
- spec: { kind: "number", placeholder: "5" },
81336
- when: isOn("fixCiOnFailure")
81337
- },
81338
- {
81339
- id: "ciPollIntervalSeconds",
81340
- label: "CI status poll interval (seconds)",
81341
- description: "How often (in seconds) to re-check the PR's CI status while waiting on or fixing it.",
81342
- spec: { kind: "number", placeholder: "30" },
81343
- when: isOn("fixCiOnFailure")
81344
- },
81345
- {
81346
- id: "ignoreCiChecks",
81347
- label: "CI checks to ignore",
81348
- description: "Names of CI checks to ignore when deciding whether a PR is green \u2014 e.g. known-flaky jobs.",
81349
- spec: { kind: "list", placeholder: "check name" }
81350
- },
81351
- {
81352
- id: "rules",
81353
- label: "Project rules",
81354
- description: "House rules added to every prompt (e.g. 'never edit generated files'). One rule per entry.",
81355
- spec: { kind: "list", placeholder: "a rule" }
81356
- },
81357
- {
81358
- id: "boundaries.never_touch",
81359
- label: "Never-touch globs",
81360
- description: "Glob patterns for files the agent must never modify (e.g. dist/**).",
81361
- spec: { kind: "list", placeholder: "dist/**" }
81362
- },
81363
- LINEAR_TEAM,
81364
- REPO_LINK,
81365
- LINEAR_FILTER,
81366
- {
81367
- id: "linear.postComments",
81368
- label: "Post progress comments on the Linear issue?",
81369
- description: "Post progress comments on the Linear issue while a task runs.",
81370
- spec: yes()
81371
- },
81372
- {
81373
- id: "linear.updateEveryIterations",
81374
- label: "Post a progress update every N iterations (0 = off)",
81375
- description: "Post a progress comment every N loop iterations. 0 turns periodic updates off.",
81376
- spec: { kind: "number", placeholder: "10" }
81377
- },
81378
- {
81379
- id: "linear.mentionTrigger",
81380
- label: "Watch comments/PRs for @mentions?",
81381
- description: "Watch a finished issue's comments and its PR for @mentions of Ralphy, and re-engage when mentioned.",
81382
- spec: yes()
81383
- },
81384
- {
81385
- id: "linear.mentionHandle",
81386
- label: "Mention handle",
81387
- description: "The @handle that, when mentioned, makes Ralphy pick the issue back up (e.g. @ralphy).",
81388
- spec: { kind: "text", placeholder: "@ralphy" },
81389
- when: isOn("linear.mentionTrigger")
81390
- },
81391
- {
81392
- id: "linear.codeReviewTrigger",
81393
- label: "Watch PRs for unresolved review threads?",
81394
- description: "Watch open PRs for unresolved review comments and re-engage to address them.",
81395
- spec: yes()
81396
- },
81397
- {
81398
- id: "linear.codeReviewStaleHours",
81399
- label: "Code-review stale window (hours)",
81400
- description: "Ignore review comments older than this many hours, so stale threads don't re-trigger work.",
81401
- spec: { kind: "number", placeholder: "24" },
81402
- when: isOn("linear.codeReviewTrigger")
81403
- },
81404
- {
81405
- id: "linear.syncTasksToComment",
81406
- label: "Mirror tasks.md into a sticky Linear comment?",
81407
- description: "Keep one pinned ('sticky') Linear comment in sync with the task checklist (tasks.md).",
81408
- spec: yes()
81409
- },
81410
- {
81411
- id: "linear.syncSpecsAsAttachments",
81412
- label: "Upload proposal.md / design.md as attachments?",
81413
- description: "Upload the OpenSpec planning docs (proposal.md, design.md) to the issue as attachments. OpenSpec is Ralphy's spec-driven planning format.",
81414
- spec: yes()
81415
- },
81416
- {
81417
- id: "linear.specAttachmentFormats",
81418
- label: "Spec attachment formats",
81419
- description: "Which formats to upload the spec docs in: 'md' (raw markdown), 'pdf' (a rendered PDF), or both.",
81420
- spec: {
81421
- kind: "multiselect",
81422
- options: [
81423
- { label: "md", value: "md" },
81424
- { label: "pdf", value: "pdf" }
81425
- ]
81426
- },
81427
- when: isOn("linear.syncSpecsAsAttachments")
81428
- },
81429
- {
81430
- id: "linear.confirmationMode.enabled",
81431
- label: "Enable the human confirmation gate?",
81432
- description: "Pause after the agent finishes planning and wait for a human to approve before it writes any code (a confirmation gate).",
81433
- spec: no()
81434
- },
81435
- {
81436
- id: "linear.confirmationMode.timeoutHours",
81437
- label: "Confirmation timeout (hours)",
81438
- description: "If no one approves or rejects within this many hours, auto-resolve the confirmation gate.",
81439
- spec: { kind: "number", placeholder: "48" },
81440
- when: isOn("linear.confirmationMode.enabled")
81441
- },
81442
- {
81443
- id: "linear.confirmationMode.maxConfirmationRounds",
81444
- label: "Max confirmation rounds",
81445
- description: "How many times the plan can be revised and re-submitted for approval before Ralphy gives up.",
81446
- spec: { kind: "number", placeholder: "3" },
81447
- when: isOn("linear.confirmationMode.enabled")
81448
- },
81449
- {
81450
- id: AWAITING_STATUS_FIELD_ID,
81451
- label: "Park awaiting-approval tickets in a status?",
81452
- hint: "e.g. Planned \u2014 blank keeps them In Progress",
81453
- description: "When the confirmation gate opens, move the ticket to this Linear status so the board shows it waiting on a human (it must be a real status in your team). Ralphy also adds it to the in-progress pickup filter so the parked ticket keeps being polled, and re-asserts In Progress on approval. Leave blank to keep parked tickets in In Progress. Pairs with status-based indicators.",
81454
- spec: { kind: "text", placeholder: "Planned" },
81455
- when: isOn("linear.confirmationMode.enabled")
81456
- },
81457
- {
81458
- id: "linear.indicators",
81459
- label: "Linear lifecycle indicators",
81460
- description: "How Ralphy maps lifecycle events to Linear statuses/labels \u2014 which issues to pick up (todo) and what to set when a task is in progress, done, or errored.",
81461
- spec: { kind: "indicators" }
81462
- },
81463
- {
81464
- id: "preExistingErrorCheck.enabled",
81465
- label: "Enable the base-branch health gate?",
81466
- description: "Before picking up new work, run health-check commands on the base branch and pause if it's already broken, so the agent isn't blamed for pre-existing failures.",
81467
- spec: no()
81468
- },
81469
- {
81470
- id: "preExistingErrorCheck.commands",
81471
- label: "Health-gate commands (blank = use lint/test)",
81472
- description: "Commands run against the base branch to judge its health. Leave empty to reuse your lint/test commands.",
81473
- spec: { kind: "list", placeholder: "bun run lint" },
81474
- when: isOn("preExistingErrorCheck.enabled")
81475
- },
81476
- {
81477
- id: "preExistingErrorCheck.baseBranch",
81478
- label: "Health-gate base branch",
81479
- description: "The branch the health gate checks out and tests (usually main).",
81480
- spec: { kind: "text", placeholder: "main" },
81481
- when: isOn("preExistingErrorCheck.enabled")
81482
- },
81483
- {
81484
- id: "preExistingErrorCheck.label",
81485
- label: "Health-gate Linear label",
81486
- description: "Linear label applied to the ticket Ralphy opens when the base branch is found broken.",
81487
- spec: { kind: "text", placeholder: "ralph:pre-existing-error" },
81488
- when: isOn("preExistingErrorCheck.enabled")
81489
- },
81490
- {
81491
- id: "prTracker.enabled",
81492
- label: "Enable the PR tracker?",
81493
- description: "Keep watching the PRs Ralphy opened and automatically try to recover any whose merge state goes red (conflicts or failing CI).",
81494
- spec: yes()
81495
- },
81496
- {
81497
- id: "prTracker.maxRecoveryAttempts",
81498
- label: "PR tracker max recovery attempts",
81499
- description: "Give up auto-recovering a red PR after this many attempts, then flag it for a human.",
81500
- spec: { kind: "number", placeholder: "3" },
81501
- when: isOn("prTracker.enabled")
81502
- },
81503
- {
81504
- id: "prTracker.advanceMergedToDone",
81505
- label: "Advance merged PRs to done automatically?",
81506
- description: "Move an issue to its done state as soon as its PR is merged.",
81507
- spec: no(),
81508
- when: isOn("prTracker.enabled")
81509
- },
81510
- {
81511
- id: "metaPrompt.enabled",
81512
- label: "Enable the meta-prompt addendum?",
81513
- description: "Add Ralphy's task-level 'meta-prompt' layer (extra framing instructions) to each phase. Leave on unless you want raw prompts.",
81514
- spec: yes()
81515
- },
81516
- {
81517
- id: "metaPrompt.effort",
81518
- label: "Per-ticket effort tier",
81519
- description: "How much effort the meta-prompt nudges the agent toward per ticket. 'auto' detects it from the ticket; 'light'/'standard'/'heavy' pin every ticket to that tier.",
81520
- spec: {
81521
- kind: "select",
81522
- options: [
81523
- { label: "auto", value: "auto" },
81524
- { label: "light", value: "light" },
81525
- { label: "standard", value: "standard" },
81526
- { label: "heavy", value: "heavy" }
81527
- ]
81528
- },
81529
- when: isOn("metaPrompt.enabled")
81530
- },
81531
- {
81532
- id: "openspec.reviewPhase.enabled",
81533
- label: "Enable the OpenSpec review phase?",
81534
- description: "After all tasks finish, spawn a separate reviewer agent that reads the full diff and writes review findings; open findings loop back into more work.",
81535
- spec: no()
81536
- },
81537
- {
81538
- id: "openspec.reviewPhase.maxRounds",
81539
- label: "Review phase max rounds",
81540
- description: "How many review\u2192fix cycles to run before the change is archived regardless.",
81541
- spec: { kind: "number", placeholder: "1" },
81542
- when: isOn("openspec.reviewPhase.enabled")
81543
- },
81544
- {
81545
- id: "openspec.reviewPhase.reviewerModel",
81546
- label: "Reviewer model (blank = same as main)",
81547
- description: "Model used for the review pass. Blank reuses the main model; a cheaper tier (e.g. haiku) saves cost.",
81548
- spec: { kind: "text", placeholder: "haiku" },
81549
- when: isOn("openspec.reviewPhase.enabled")
81550
- },
81551
- {
81552
- id: "openspec.reviewPhase.reviewerContextStrategy",
81553
- label: "Reviewer context",
81554
- description: "'fresh' gives the reviewer a brand-new session (unbiased); 'warm' resumes the last task's session (more context, cheaper).",
81555
- spec: {
81556
- kind: "select",
81557
- options: [
81558
- { label: "fresh", value: "fresh" },
81559
- { label: "warm", value: "warm" }
81560
- ]
81561
- },
81562
- when: isOn("openspec.reviewPhase.enabled")
81563
- },
81564
- {
81565
- id: PROMPT_BODY_FIELD_ID,
81566
- label: "Customize the prompt sent to the agent?",
81567
- description: "The prompt the agent receives lives in the file body \u2014 a template filled with per-issue values (e.g. {{ issue.identifier }}). Edit it here, or leave it and finish to keep the default.",
81568
- spec: { kind: "multiline" }
81569
- }
81570
- ];
81571
- COMMON_CLI_OPTIONS = [
81572
- { fieldId: "model", flag: "--model", argKey: "model", kind: "model" },
81573
- { fieldId: "iterationDelaySeconds", flag: "--delay", argKey: "delay", kind: "int" },
81574
- { fieldId: "maxCostUsdPerTask", flag: "--max-cost", argKey: "maxCostUsd", kind: "float" },
81575
- {
81576
- fieldId: "maxRuntimeMinutesPerTask",
81577
- flag: "--max-runtime",
81578
- argKey: "maxRuntimeMinutes",
81579
- kind: "float"
81580
- },
81581
- {
81582
- fieldId: "maxConsecutiveFailuresPerTask",
81583
- flag: "--max-failures",
81584
- argKey: "maxConsecutiveFailures",
81585
- kind: "int"
81586
- },
81587
- {
81588
- fieldId: "maxIterationsPerTask",
81589
- flag: "--max-iterations",
81590
- argKey: "maxIterations",
81591
- kind: "int"
81592
- },
81593
- { fieldId: "logRawStream", flag: "--log", argKey: "log", kind: "boolean" },
81594
- { fieldId: "taskVerbose", flag: "--verbose", argKey: "verbose", kind: "boolean" }
81595
- ];
81596
- FIELD_DESCRIPTIONS = CUSTOMIZED_FIELDS.filter((field) => Boolean(field.description) && field.spec.kind !== "multiline").map((field) => ({ path: field.id.split("."), description: field.description }));
81597
- });
81598
-
81599
81835
  // packages/workflow/src/wizard.ts
81600
81836
  function indicatorsForPreset(preset) {
81601
81837
  if (preset === "status-standard") {
@@ -81689,6 +81925,87 @@ var init_wizard = __esm(() => {
81689
81925
  import_yaml = __toESM(require_dist(), 1);
81690
81926
  });
81691
81927
 
81928
+ // packages/workflow/src/migrate/normalize.ts
81929
+ function defaultLeafEntries() {
81930
+ const defaults2 = WorkflowConfigSchema.parse({});
81931
+ const entries = [];
81932
+ const walk = (node2, prefix) => {
81933
+ if (Array.isArray(node2)) {
81934
+ entries.push({ path: prefix, value: node2 });
81935
+ return;
81936
+ }
81937
+ if (node2 && typeof node2 === "object") {
81938
+ const keys2 = Object.keys(node2);
81939
+ if (keys2.length === 0)
81940
+ return;
81941
+ for (const key of keys2)
81942
+ walk(node2[key], [...prefix, key]);
81943
+ return;
81944
+ }
81945
+ entries.push({ path: prefix, value: node2 });
81946
+ };
81947
+ walk(defaults2, []);
81948
+ return entries.filter((entry) => !(entry.path.length === 1 && entry.path[0] === "version"));
81949
+ }
81950
+ function stampDescription(document2, path) {
81951
+ const match = FIELD_DESCRIPTIONS.find((description) => description.path.length === path.length && description.path.every((segment, index) => segment === path[index]));
81952
+ if (!match)
81953
+ return;
81954
+ const parent = path.length === 1 ? document2.contents : document2.getIn(path.slice(0, -1), true);
81955
+ if (!import_yaml2.default.isMap(parent))
81956
+ return;
81957
+ const leaf = path[path.length - 1];
81958
+ const pair = parent.items.find((item) => import_yaml2.default.isScalar(item.key) && String(item.key.value) === leaf);
81959
+ if (!pair || !import_yaml2.default.isScalar(pair.key))
81960
+ return;
81961
+ pair.key.commentBefore = toCommentLines(match.description);
81962
+ }
81963
+ function normalizeWorkflowMarkdown(markdown) {
81964
+ const match = FRONTMATTER_RE.exec(markdown);
81965
+ if (!match)
81966
+ return { markdown, changed: false, added: [] };
81967
+ const document2 = import_yaml2.default.parseDocument(match[1] ?? "");
81968
+ if (!import_yaml2.default.isMap(document2.contents))
81969
+ return { markdown, changed: false, added: [] };
81970
+ const body = match[2] ?? "";
81971
+ const added = [];
81972
+ for (const { path, value } of defaultLeafEntries()) {
81973
+ if (document2.getIn(path) !== undefined)
81974
+ continue;
81975
+ document2.setIn(path, value);
81976
+ stampDescription(document2, path);
81977
+ added.push(path.join("."));
81978
+ }
81979
+ const gateEnabled = document2.getIn(["linear", "confirmationMode", "enabled"]) === true;
81980
+ const hasGetApproved = document2.getIn(["linear", "indicators", "getApproved"]) !== undefined;
81981
+ if (gateEnabled && !hasGetApproved) {
81982
+ document2.setIn(["linear", "indicators", "getApproved"], DEFAULT_APPROVAL_INDICATORS.getApproved);
81983
+ if (document2.getIn(["linear", "indicators", "clearApproved"]) === undefined) {
81984
+ document2.setIn(["linear", "indicators", "clearApproved"], DEFAULT_APPROVAL_INDICATORS.clearApproved);
81985
+ }
81986
+ added.push("linear.indicators.getApproved");
81987
+ }
81988
+ if (added.length === 0)
81989
+ return { markdown, changed: false, added: [] };
81990
+ const frontmatter = document2.toString({ flowCollectionPadding: false }).replace(/\n+$/, "");
81991
+ return { markdown: `---
81992
+ ${frontmatter}
81993
+ ---
81994
+ ${body}`, changed: true, added };
81995
+ }
81996
+ var import_yaml2, DEFAULT_APPROVAL_INDICATORS;
81997
+ var init_normalize = __esm(() => {
81998
+ init_schema();
81999
+ init_default();
82000
+ init_fields();
82001
+ init_wizard();
82002
+ import_yaml2 = __toESM(require_dist(), 1);
82003
+ DEFAULT_APPROVAL_INDICATORS = {
82004
+ getApproved: { filter: [{ type: "label", value: "approved" }] },
82005
+ clearApproved: { type: "label", value: "approved" }
82006
+ };
82007
+ });
82008
+
81692
82009
  // packages/workflow/src/confirmation.ts
81693
82010
  function matchesIndicator(indicator, ticket) {
81694
82011
  if (!indicator || indicator.filter.length === 0)
@@ -81796,6 +82113,7 @@ __export(exports_workflow, {
81796
82113
  renderTemplate: () => renderTemplate,
81797
82114
  parseWorkflow: () => parseWorkflow,
81798
82115
  parseLinearFilter: () => parseLinearFilter,
82116
+ normalizeWorkflowMarkdown: () => normalizeWorkflowMarkdown,
81799
82117
  matchesIndicator: () => matchesIndicator,
81800
82118
  loadWorkflow: () => loadWorkflow,
81801
82119
  ensureWorkflow: () => ensureWorkflow,
@@ -81805,6 +82123,7 @@ __export(exports_workflow, {
81805
82123
  WORKFLOW_FILE: () => WORKFLOW_FILE,
81806
82124
  FRONTMATTER_RE: () => FRONTMATTER_RE,
81807
82125
  DEFAULT_WORKFLOW_MD: () => DEFAULT_WORKFLOW_MD,
82126
+ DEFAULT_APPROVAL_INDICATORS: () => DEFAULT_APPROVAL_INDICATORS,
81808
82127
  CURRENT_WORKFLOW_VERSION: () => CURRENT_WORKFLOW_VERSION
81809
82128
  });
81810
82129
  import { join as join5 } from "path";
@@ -81818,7 +82137,7 @@ function parseWorkflow(text, path = "") {
81818
82137
  const body = m[2] ?? "";
81819
82138
  let raw;
81820
82139
  try {
81821
- raw = import_yaml2.default.parse(yamlText, { schema: "core" });
82140
+ raw = import_yaml3.default.parse(yamlText, { schema: "core" });
81822
82141
  } catch (err) {
81823
82142
  throw new Error(`WORKFLOW.md frontmatter is not valid YAML.
81824
82143
  ` + (path ? ` File: ${path}
@@ -81906,21 +82225,24 @@ function applyAliases(cfg) {
81906
82225
  }
81907
82226
  }
81908
82227
  }
81909
- function workflowPath(projectRoot) {
81910
- return join5(projectRoot, WORKFLOW_FILE);
82228
+ function workflowPath(projectRoot, workflowFile) {
82229
+ return workflowFile ?? join5(projectRoot, WORKFLOW_FILE);
81911
82230
  }
81912
- async function loadWorkflow(projectRoot) {
81913
- const path = workflowPath(projectRoot);
82231
+ async function loadWorkflow(projectRoot, workflowFile, options = {}) {
82232
+ const path = workflowPath(projectRoot, workflowFile);
81914
82233
  const file2 = Bun.file(path);
81915
82234
  if (!await file2.exists()) {
81916
82235
  const { config: config2 } = parseWorkflow(DEFAULT_WORKFLOW_MD);
81917
82236
  return { config: config2, body: extractDefaultBody(), path };
81918
82237
  }
81919
82238
  const text = await file2.text();
81920
- return parseWorkflow(text, path);
82239
+ const normalized = normalizeWorkflowMarkdown(text);
82240
+ if (normalized.changed && options.persist)
82241
+ await Bun.write(path, normalized.markdown);
82242
+ return parseWorkflow(normalized.markdown, path);
81921
82243
  }
81922
- async function ensureWorkflow(projectRoot) {
81923
- const path = workflowPath(projectRoot);
82244
+ async function ensureWorkflow(projectRoot, workflowFile) {
82245
+ const path = workflowPath(projectRoot, workflowFile);
81924
82246
  const file2 = Bun.file(path);
81925
82247
  if (await file2.exists())
81926
82248
  return path;
@@ -81952,15 +82274,17 @@ function renderWorkflowPrompt(workflow, ctx) {
81952
82274
  };
81953
82275
  return renderTemplate(workflow.body, fullCtx);
81954
82276
  }
81955
- var import_yaml2, WORKFLOW_FILE = "WORKFLOW.md";
82277
+ var import_yaml3, WORKFLOW_FILE = "WORKFLOW.md";
81956
82278
  var init_workflow = __esm(() => {
81957
82279
  init_schema();
81958
82280
  init_default();
81959
82281
  init_wizard();
82282
+ init_normalize();
81960
82283
  init_schema();
81961
82284
  init_default();
81962
82285
  init_linear_filter();
81963
- import_yaml2 = __toESM(require_dist(), 1);
82286
+ init_normalize();
82287
+ import_yaml3 = __toESM(require_dist(), 1);
81964
82288
  });
81965
82289
 
81966
82290
  // packages/core/src/repo/index.ts
@@ -82911,6 +83235,24 @@ function resolveIndicators(value) {
82911
83235
  }
82912
83236
  function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
82913
83237
  const values2 = { ...answers };
83238
+ const concurrencyValue = values2["concurrency"];
83239
+ if (typeof concurrencyValue === "number" && concurrencyValue > 1) {
83240
+ values2["useWorktree"] = true;
83241
+ }
83242
+ const assigneeChoice = values2[LINEAR_ASSIGNEE_CHOICE_FIELD_ID];
83243
+ if (typeof assigneeChoice === "string") {
83244
+ let assignee;
83245
+ if (assigneeChoice === "other") {
83246
+ const raw = values2[LINEAR_ASSIGNEE_VALUE_FIELD_ID];
83247
+ assignee = typeof raw === "string" && raw.trim() !== "" ? raw.trim() : undefined;
83248
+ } else {
83249
+ assignee = assigneeChoice;
83250
+ }
83251
+ if (assignee)
83252
+ values2["linear.filter"] = `assignee = ${assignee}`;
83253
+ }
83254
+ delete values2[LINEAR_ASSIGNEE_CHOICE_FIELD_ID];
83255
+ delete values2[LINEAR_ASSIGNEE_VALUE_FIELD_ID];
82914
83256
  if ("linear.indicators" in values2) {
82915
83257
  const indicators = resolveIndicators(values2["linear.indicators"]);
82916
83258
  if (indicators)
@@ -82926,20 +83268,20 @@ function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
82926
83268
  values2["linear.indicators"] = map3;
82927
83269
  }
82928
83270
  }
82929
- const parkStatusRaw = values2[AWAITING_STATUS_FIELD_ID];
82930
- const parkStatus = typeof parkStatusRaw === "string" ? parkStatusRaw.trim() : "";
82931
- if (values2["linear.confirmationMode.enabled"] === true && parkStatus && values2["linear.indicators"] && typeof values2["linear.indicators"] === "object") {
83271
+ if (values2["linear.confirmationMode.enabled"] === true && values2["linear.indicators"] && typeof values2["linear.indicators"] === "object") {
82932
83272
  const map3 = { ...values2["linear.indicators"] };
82933
- map3.setAwaitingConfirmation = { type: "status", value: parkStatus };
82934
- const existing = map3.getInProgress;
82935
- const filter2 = existing && !Array.isArray(existing) && "filter" in existing ? [...existing.filter] : [];
82936
- if (!filter2.some((m) => m.type === "status" && m.value === parkStatus)) {
82937
- filter2.push({ type: "status", value: parkStatus });
83273
+ const awaiting = map3.setAwaitingConfirmation;
83274
+ const parkMarker = Array.isArray(awaiting) ? awaiting.find((marker) => marker.type === "status") : awaiting;
83275
+ if (parkMarker && !Array.isArray(parkMarker) && "type" in parkMarker && parkMarker.type === "status") {
83276
+ const existing = map3.getInProgress;
83277
+ const filter2 = existing && !Array.isArray(existing) && "filter" in existing ? [...existing.filter] : [];
83278
+ if (!filter2.some((marker) => marker.type === "status" && marker.value === parkMarker.value)) {
83279
+ filter2.push({ type: "status", value: parkMarker.value });
83280
+ map3.getInProgress = { filter: filter2 };
83281
+ values2["linear.indicators"] = map3;
83282
+ }
82938
83283
  }
82939
- map3.getInProgress = { filter: filter2 };
82940
- values2["linear.indicators"] = map3;
82941
83284
  }
82942
- delete values2[AWAITING_STATUS_FIELD_ID];
82943
83285
  const linkRepo = values2[REPO_LINK_FIELD_ID] === true;
82944
83286
  delete values2[REPO_LINK_FIELD_ID];
82945
83287
  if (!linkRepo) {
@@ -83028,7 +83370,8 @@ function SetupWizard({
83028
83370
  buildMarkdown,
83029
83371
  onlyFields,
83030
83372
  initialBody,
83031
- detectedRepo
83373
+ detectedRepo,
83374
+ onAnswersChange
83032
83375
  }) {
83033
83376
  const { exit } = use_app_default();
83034
83377
  const startValues = initialValues ?? {};
@@ -83103,6 +83446,7 @@ function SetupWizard({
83103
83446
  setAnswers(source);
83104
83447
  setIndex(target);
83105
83448
  initEditing(fieldsFor(mode, source)[target], source);
83449
+ onAnswersChange?.({ mode, values: source });
83106
83450
  };
83107
83451
  const valuesToWrite = (source) => {
83108
83452
  if (!onlyFields)
@@ -83140,6 +83484,7 @@ function SetupWizard({
83140
83484
  setIndex(0);
83141
83485
  setVisited(new Set([fieldsFor(chosen, answers)[0].id]));
83142
83486
  initEditing(fieldsFor(chosen, answers)[0], answers);
83487
+ onAnswersChange?.({ mode: chosen, values: answers });
83143
83488
  }
83144
83489
  return;
83145
83490
  }
@@ -83240,7 +83585,15 @@ ${draft.slice(at2)}`, at2 + 1);
83240
83585
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83241
83586
  bold: true,
83242
83587
  children: "Ralphy setup"
83243
- }, undefined, false, undefined, this)
83588
+ }, undefined, false, undefined, this),
83589
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83590
+ dimColor: true,
83591
+ children: [
83592
+ " \xB7 ",
83593
+ "v",
83594
+ VERSION
83595
+ ]
83596
+ }, undefined, true, undefined, this)
83244
83597
  ]
83245
83598
  }, undefined, true, undefined, this),
83246
83599
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
@@ -83301,6 +83654,9 @@ ${draft.slice(at2)}`, at2 + 1);
83301
83654
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83302
83655
  dimColor: true,
83303
83656
  children: [
83657
+ " \xB7 ",
83658
+ "v",
83659
+ VERSION,
83304
83660
  " \xB7 ",
83305
83661
  mode,
83306
83662
  " \xB7 step ",
@@ -83393,7 +83749,7 @@ function hintFor(kind) {
83393
83749
  return `\u2191\u2193 to switch \xB7 enter to confirm and continue \xB7 ${nav}`;
83394
83750
  }
83395
83751
  if (kind === "multiselect") {
83396
- return `\u2191\u2193 to move \xB7 space to toggle \xB7 enter to confirm and continue \xB7 ${nav}`;
83752
+ return `\u2191\u2193 to move \xB7 space to select \xB7 enter to confirm \xB7 ${nav}`;
83397
83753
  }
83398
83754
  if (kind === "list") {
83399
83755
  return `type + enter to add \xB7 empty enter to finish \xB7 ${nav}`;
@@ -83656,6 +84012,16 @@ function ChoicePrompt({
83656
84012
  ]
83657
84013
  }, undefined, true, undefined, this);
83658
84014
  }
84015
+ function ResumeOrFreshPrompt({
84016
+ onChoice
84017
+ }) {
84018
+ return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(ChoicePrompt, {
84019
+ title: "Unfinished setup found",
84020
+ subtitle: "A previous setup session was interrupted \u2014 resume it or start over",
84021
+ options: RESUME_FRESH_OPTIONS,
84022
+ onChoice
84023
+ }, undefined, false, undefined, this);
84024
+ }
83659
84025
  function EditOrExitPrompt({ onChoice }) {
83660
84026
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(ChoicePrompt, {
83661
84027
  title: "WORKFLOW.md already exists",
@@ -83831,9 +84197,7 @@ function IndicatorBuilder({
83831
84197
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83832
84198
  dimColor: true,
83833
84199
  children: [
83834
- " \xB7 ",
83835
- state.label,
83836
- " \xB7 ",
84200
+ " \xB7 step ",
83837
84201
  stateIndex + 1,
83838
84202
  "/",
83839
84203
  states.length
@@ -83841,6 +84205,16 @@ function IndicatorBuilder({
83841
84205
  }, undefined, true, undefined, this)
83842
84206
  ]
83843
84207
  }, undefined, true, undefined, this),
84208
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
84209
+ children: [
84210
+ " Configuring: ",
84211
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
84212
+ bold: true,
84213
+ color: "cyan",
84214
+ children: state.label
84215
+ }, undefined, false, undefined, this)
84216
+ ]
84217
+ }, undefined, true, undefined, this),
83844
84218
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83845
84219
  dimColor: true,
83846
84220
  children: [
@@ -83862,8 +84236,15 @@ function IndicatorBuilder({
83862
84236
  children: phase === "type" ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV(jsx_dev_runtime.Fragment, {
83863
84237
  children: [
83864
84238
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
83865
- children: "Choose a marker type for this state (or skip):"
83866
- }, undefined, false, undefined, this),
84239
+ children: [
84240
+ "Choose a marker type for ",
84241
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
84242
+ bold: true,
84243
+ children: state.label
84244
+ }, undefined, false, undefined, this),
84245
+ " (or skip):"
84246
+ ]
84247
+ }, undefined, true, undefined, this),
83867
84248
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(OptionList, {
83868
84249
  options: typeChoices,
83869
84250
  highlight: typeIndex
@@ -83908,8 +84289,9 @@ function IndicatorBuilder({
83908
84289
  ]
83909
84290
  }, undefined, true, undefined, this);
83910
84291
  }
83911
- var import_react22, jsx_dev_runtime, REPO_ANSWER_IDS, MODE_OPTIONS, INDICATOR_OPTIONS, CONFIRM_OPTIONS, EDIT_EXIT_OPTIONS, RECREATE_EXIT_OPTIONS, MIGRATE_OPTIONS, CORE_STATES, CONFIRMATION_STATES, ALL_TYPES;
84292
+ var import_react22, jsx_dev_runtime, REPO_ANSWER_IDS, MODE_OPTIONS, INDICATOR_OPTIONS, CONFIRM_OPTIONS, RESUME_FRESH_OPTIONS, EDIT_EXIT_OPTIONS, RECREATE_EXIT_OPTIONS, MIGRATE_OPTIONS, CORE_STATES, CONFIRMATION_STATES, ALL_TYPES;
83912
84293
  var init_SetupWizard = __esm(async () => {
84294
+ init_version();
83913
84295
  init_wizard();
83914
84296
  init_fields();
83915
84297
  await init_build2();
@@ -83923,14 +84305,18 @@ var init_SetupWizard = __esm(async () => {
83923
84305
  ];
83924
84306
  INDICATOR_OPTIONS = [
83925
84307
  { label: "None \u2014 configure later in WORKFLOW.md", value: "none" },
83926
- { label: "Status-based (Todo \u2192 In Progress \u2192 In Review)", value: "status-standard" },
83927
- { label: "Label-based (ralph:todo / in-progress / done)", value: "label-standard" },
83928
- { label: "Custom \u2014 build markers per lifecycle slot", value: "custom" }
84308
+ { label: "Status-based preset (Todo \u2192 In Progress \u2192 In Review)", value: "status-standard" },
84309
+ { label: "Label-based preset (ralph:todo / in-progress / done)", value: "label-standard" },
84310
+ { label: "Custom \u2014 open a guided builder (enter opens it)", value: "custom" }
83929
84311
  ];
83930
84312
  CONFIRM_OPTIONS = [
83931
84313
  { label: "Yes", value: "true" },
83932
84314
  { label: "No", value: "false" }
83933
84315
  ];
84316
+ RESUME_FRESH_OPTIONS = [
84317
+ { label: "Resume where I left off", value: "resume" },
84318
+ { label: "Start fresh (discard the saved answers)", value: "fresh" }
84319
+ ];
83934
84320
  EDIT_EXIT_OPTIONS = [
83935
84321
  { label: "Edit it with the setup wizard", value: "edit" },
83936
84322
  { label: "Exit without changes", value: "exit" }
@@ -84056,7 +84442,7 @@ var init_migrations = __esm(() => {
84056
84442
  {
84057
84443
  version: 3,
84058
84444
  description: "The per-workflow `linear.assignee` setting is replaced by a global " + "`linear.filter` expression (e.g. `assignee = me`) applied to every " + "ticket fetch. Existing `assignee` values are folded in automatically; " + "note that an empty filter now defaults to `assignee = me` (it previously " + "meant unassigned-only).",
84059
- fields: ["linear.filter"]
84445
+ fields: ["linear.assigneeChoice", "linear.assigneeValue"]
84060
84446
  },
84061
84447
  {
84062
84448
  version: 4,
@@ -84067,6 +84453,129 @@ var init_migrations = __esm(() => {
84067
84453
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
84068
84454
  });
84069
84455
 
84456
+ // apps/init/src/project-detect.ts
84457
+ import { join as join6 } from "path";
84458
+ async function readPackageJson(projectRoot) {
84459
+ const file2 = Bun.file(join6(projectRoot, "package.json"));
84460
+ if (!await file2.exists())
84461
+ return null;
84462
+ try {
84463
+ return JSON.parse(await file2.text());
84464
+ } catch {
84465
+ return null;
84466
+ }
84467
+ }
84468
+ async function fileExists(projectRoot, name) {
84469
+ return Bun.file(join6(projectRoot, name)).exists();
84470
+ }
84471
+ async function detectRunPrefix(projectRoot) {
84472
+ const lockfiles = [
84473
+ { file: "bun.lock", prefix: "bun run" },
84474
+ { file: "bun.lockb", prefix: "bun run" },
84475
+ { file: "pnpm-lock.yaml", prefix: "pnpm run" },
84476
+ { file: "yarn.lock", prefix: "yarn run" },
84477
+ { file: "package-lock.json", prefix: "npm run" }
84478
+ ];
84479
+ for (const { file: file2, prefix } of lockfiles) {
84480
+ if (await fileExists(projectRoot, file2))
84481
+ return prefix;
84482
+ }
84483
+ return "bun run";
84484
+ }
84485
+ async function detectCommandsFromPackageJson(projectRoot) {
84486
+ const pkg = await readPackageJson(projectRoot);
84487
+ const scripts = pkg?.scripts ?? {};
84488
+ const runPrefix = await detectRunPrefix(projectRoot);
84489
+ const commands = {};
84490
+ for (const { field, scripts: names } of COMMAND_FIELD_SCRIPTS) {
84491
+ const name = names.find((candidate) => typeof scripts[candidate] === "string" && scripts[candidate] !== "");
84492
+ if (name)
84493
+ commands[field] = `${runPrefix} ${name}`;
84494
+ }
84495
+ return commands;
84496
+ }
84497
+ async function detectFramework(projectRoot) {
84498
+ const pkg = await readPackageJson(projectRoot);
84499
+ const dependencies = { ...pkg?.dependencies, ...pkg?.devDependencies };
84500
+ const detected = [];
84501
+ if (await fileExists(projectRoot, "bun.lock") || await fileExists(projectRoot, "bun.lockb")) {
84502
+ detected.push("Bun");
84503
+ }
84504
+ if (await fileExists(projectRoot, "nx.json"))
84505
+ detected.push("Nx");
84506
+ for (const { dependency, name } of FRAMEWORK_MARKERS) {
84507
+ if (dependencies[dependency] && !detected.includes(name)) {
84508
+ detected.push(name);
84509
+ break;
84510
+ }
84511
+ }
84512
+ return detected.length > 0 ? detected.join(" + ") : undefined;
84513
+ }
84514
+ async function gitText(projectRoot, args) {
84515
+ try {
84516
+ const proc = Bun.spawn({
84517
+ cmd: ["git", ...args],
84518
+ cwd: projectRoot,
84519
+ stdout: "pipe",
84520
+ stderr: "ignore",
84521
+ stdin: "ignore"
84522
+ });
84523
+ const out = await new Response(proc.stdout).text();
84524
+ await proc.exited;
84525
+ return out.trim();
84526
+ } catch {
84527
+ return "";
84528
+ }
84529
+ }
84530
+ async function detectDefaultBranch(projectRoot) {
84531
+ const head3 = await gitText(projectRoot, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
84532
+ if (head3) {
84533
+ const branch = head3.replace(/^origin\//, "").trim();
84534
+ if (branch)
84535
+ return branch;
84536
+ }
84537
+ for (const candidate of ["main", "master"]) {
84538
+ const verified = await gitText(projectRoot, ["rev-parse", "--verify", "--quiet", candidate]);
84539
+ if (verified)
84540
+ return candidate;
84541
+ }
84542
+ return;
84543
+ }
84544
+ async function detectInitialValues(projectRoot) {
84545
+ const values2 = { ...await detectCommandsFromPackageJson(projectRoot) };
84546
+ const framework = await detectFramework(projectRoot);
84547
+ if (framework)
84548
+ values2["project.framework"] = framework;
84549
+ const defaultBranch = await detectDefaultBranch(projectRoot);
84550
+ if (defaultBranch)
84551
+ values2["prBaseBranch"] = defaultBranch;
84552
+ return values2;
84553
+ }
84554
+ var COMMAND_FIELD_SCRIPTS, FRAMEWORK_MARKERS;
84555
+ var init_project_detect = __esm(() => {
84556
+ COMMAND_FIELD_SCRIPTS = [
84557
+ { field: "commands.test", scripts: ["test"] },
84558
+ { field: "commands.lint", scripts: ["lint"] },
84559
+ { field: "commands.build", scripts: ["build"] },
84560
+ { field: "commands.typecheck", scripts: ["typecheck", "type-check", "tsc"] }
84561
+ ];
84562
+ FRAMEWORK_MARKERS = [
84563
+ { dependency: "next", name: "Next.js" },
84564
+ { dependency: "@remix-run/react", name: "Remix" },
84565
+ { dependency: "@nestjs/core", name: "NestJS" },
84566
+ { dependency: "@angular/core", name: "Angular" },
84567
+ { dependency: "@sveltejs/kit", name: "SvelteKit" },
84568
+ { dependency: "svelte", name: "Svelte" },
84569
+ { dependency: "nuxt", name: "Nuxt" },
84570
+ { dependency: "vue", name: "Vue" },
84571
+ { dependency: "astro", name: "Astro" },
84572
+ { dependency: "react", name: "React" },
84573
+ { dependency: "@nestjs/common", name: "NestJS" },
84574
+ { dependency: "fastify", name: "Fastify" },
84575
+ { dependency: "express", name: "Express" }
84576
+ ];
84577
+ });
84578
+
84070
84579
  // apps/init/src/index.ts
84071
84580
  var exports_src = {};
84072
84581
  __export(exports_src, {
@@ -84074,6 +84583,33 @@ __export(exports_src, {
84074
84583
  maybeRunSetupWizard: () => maybeRunSetupWizard,
84075
84584
  main: () => main
84076
84585
  });
84586
+ async function readSetupBackup(projectRoot) {
84587
+ const file2 = Bun.file(setupBackupPath());
84588
+ if (!await file2.exists())
84589
+ return null;
84590
+ try {
84591
+ const data = JSON.parse(await file2.text());
84592
+ if (data.projectRoot !== projectRoot)
84593
+ return null;
84594
+ if (data.mode !== "quick" && data.mode !== "permissive" && data.mode !== "customized") {
84595
+ return null;
84596
+ }
84597
+ if (!data.values || typeof data.values !== "object")
84598
+ return null;
84599
+ return { mode: data.mode, values: data.values };
84600
+ } catch {
84601
+ return null;
84602
+ }
84603
+ }
84604
+ async function writeSetupBackup(projectRoot, mode, values2) {
84605
+ const backup = { projectRoot, mode, values: values2 };
84606
+ await Bun.write(setupBackupPath(), JSON.stringify(backup, null, 2));
84607
+ }
84608
+ async function clearSetupBackup() {
84609
+ const file2 = Bun.file(setupBackupPath());
84610
+ if (await file2.exists())
84611
+ await file2.delete();
84612
+ }
84077
84613
  function withDetectedRepo(initial2, repo) {
84078
84614
  if (!repo)
84079
84615
  return initial2;
@@ -84094,7 +84630,7 @@ async function runSetupWizard(projectRoot, options = {}) {
84094
84630
  let markdown = null;
84095
84631
  const buildMarkdown = options.existing ? (answers, bodyOverride) => applyAnswersToWorkflow(options.existing, answers, bodyOverride) : undefined;
84096
84632
  const initialBody = workflowBody(options.existing ?? DEFAULT_WORKFLOW_MD);
84097
- const initialValues = withDetectedRepo(options.initialValues, options.detectedRepo);
84633
+ const initialValues = options.resumeValues ?? withDetectedRepo(options.initialValues, options.detectedRepo);
84098
84634
  clearScreen2();
84099
84635
  const { waitUntilExit } = render_default(import_react23.createElement(SetupWizard, {
84100
84636
  onComplete: (md) => {
@@ -84108,21 +84644,33 @@ async function runSetupWizard(projectRoot, options = {}) {
84108
84644
  ...initialValues ? { initialValues } : {},
84109
84645
  ...options.onlyFields ? { onlyFields: options.onlyFields } : {},
84110
84646
  ...options.detectedRepo ? { detectedRepo: { owner: options.detectedRepo.owner, name: options.detectedRepo.name } } : {},
84111
- ...buildMarkdown ? { buildMarkdown } : {}
84647
+ ...buildMarkdown ? { buildMarkdown } : {},
84648
+ ...options.trackBackup ? {
84649
+ onAnswersChange: (state) => {
84650
+ writeSetupBackup(projectRoot, state.mode, state.values);
84651
+ }
84652
+ } : {}
84112
84653
  }));
84113
84654
  await waitUntilExit();
84114
84655
  if (markdown === null)
84115
84656
  return false;
84116
- await Bun.write(workflowPath(projectRoot), markdown);
84657
+ const { markdown: healed } = normalizeWorkflowMarkdown(markdown);
84658
+ await Bun.write(workflowPath(projectRoot, options.workflowFile), healed);
84659
+ await clearSetupBackup();
84117
84660
  return true;
84118
84661
  }
84119
- async function maybeRunSetupWizard(projectRoot) {
84662
+ async function maybeRunSetupWizard(projectRoot, workflowFile) {
84120
84663
  const root = projectRoot ?? await findProjectRoot();
84121
- if (await Bun.file(workflowPath(root)).exists())
84664
+ if (await Bun.file(workflowPath(root, workflowFile)).exists())
84122
84665
  return false;
84123
84666
  if (!process.stdin.isTTY || !process.stdout.isTTY)
84124
84667
  return false;
84125
- return runSetupWizard(root);
84668
+ const detected = await detectInitialValues(root);
84669
+ return runSetupWizard(root, {
84670
+ trackBackup: true,
84671
+ ...workflowFile ? { workflowFile } : {},
84672
+ ...Object.keys(detected).length > 0 ? { initialValues: detected } : {}
84673
+ });
84126
84674
  }
84127
84675
  function initialValuesFromConfig(config2) {
84128
84676
  const values2 = {};
@@ -84149,8 +84697,16 @@ function initialValuesFromConfig(config2) {
84149
84697
  values2["useWorktree"] = config2.useWorktree;
84150
84698
  if (config2.linear.team)
84151
84699
  values2["linear.team"] = config2.linear.team;
84152
- if (config2.linear.filter)
84153
- values2["linear.filter"] = config2.linear.filter;
84700
+ if (config2.linear.filter) {
84701
+ const match = /^assignee\s*=\s*(.+)$/i.exec(config2.linear.filter.trim());
84702
+ const assignee = match ? match[1].trim() : "";
84703
+ if (assignee === "me" || assignee === "any" || assignee === "unassigned") {
84704
+ values2["linear.assigneeChoice"] = assignee;
84705
+ } else if (assignee !== "") {
84706
+ values2["linear.assigneeChoice"] = "other";
84707
+ values2["linear.assigneeValue"] = assignee;
84708
+ }
84709
+ }
84154
84710
  return values2;
84155
84711
  }
84156
84712
  async function promptEditOrExit() {
@@ -84164,6 +84720,17 @@ async function promptEditOrExit() {
84164
84720
  await waitUntilExit();
84165
84721
  return choice;
84166
84722
  }
84723
+ async function promptResumeOrFresh() {
84724
+ let choice = "fresh";
84725
+ clearScreen2();
84726
+ const { waitUntilExit } = render_default(import_react23.createElement(ResumeOrFreshPrompt, {
84727
+ onChoice: (value) => {
84728
+ choice = value;
84729
+ }
84730
+ }));
84731
+ await waitUntilExit();
84732
+ return choice;
84733
+ }
84167
84734
  async function promptRecreateOrExit() {
84168
84735
  let choice = "exit";
84169
84736
  clearScreen2();
@@ -84189,14 +84756,16 @@ async function promptMigrate(fromVersion) {
84189
84756
  await waitUntilExit();
84190
84757
  return choice;
84191
84758
  }
84192
- async function editExisting(projectRoot, path, config2, onlyFields) {
84759
+ async function editExisting(projectRoot, path, config2, workflowFile, onlyFields) {
84193
84760
  const existing = await Bun.file(path).text();
84194
84761
  const detectedRepo = await detectRepoIdentity(projectRoot);
84762
+ const detected = await detectInitialValues(projectRoot);
84195
84763
  const wrote = await runSetupWizard(projectRoot, {
84196
84764
  existing,
84197
84765
  initialMode: "customized",
84198
- initialValues: initialValuesFromConfig(config2),
84766
+ initialValues: { ...detected, ...initialValuesFromConfig(config2) },
84199
84767
  ...detectedRepo ? { detectedRepo } : {},
84768
+ ...workflowFile ? { workflowFile } : {},
84200
84769
  ...onlyFields ? { onlyFields } : {}
84201
84770
  });
84202
84771
  process.stdout.write(wrote ? `
@@ -84212,8 +84781,9 @@ async function main(argv) {
84212
84781
  `);
84213
84782
  return 0;
84214
84783
  }
84215
- const projectRoot = await findProjectRoot();
84216
- const path = workflowPath(projectRoot);
84784
+ const { projectRoot: rootOverride, workflowFile } = parseWorkflowPathArgs(argv);
84785
+ const projectRoot = rootOverride ?? await findProjectRoot();
84786
+ const path = workflowPath(projectRoot, workflowFile);
84217
84787
  const exists2 = await Bun.file(path).exists();
84218
84788
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
84219
84789
  if (exists2) {
@@ -84224,7 +84794,7 @@ async function main(argv) {
84224
84794
  }
84225
84795
  let config2;
84226
84796
  try {
84227
- ({ config: config2 } = await loadWorkflow(projectRoot));
84797
+ ({ config: config2 } = await loadWorkflow(projectRoot, workflowFile));
84228
84798
  } catch {
84229
84799
  const choice2 = await promptRecreateOrExit();
84230
84800
  if (choice2 === "exit") {
@@ -84233,7 +84803,10 @@ async function main(argv) {
84233
84803
  return 0;
84234
84804
  }
84235
84805
  const detectedRepo2 = await detectRepoIdentity(projectRoot);
84236
- const wrote2 = await runSetupWizard(projectRoot, detectedRepo2 ? { detectedRepo: detectedRepo2 } : {});
84806
+ const wrote2 = await runSetupWizard(projectRoot, {
84807
+ ...detectedRepo2 ? { detectedRepo: detectedRepo2 } : {},
84808
+ ...workflowFile ? { workflowFile } : {}
84809
+ });
84237
84810
  process.stdout.write(wrote2 ? `
84238
84811
  \u2713 Recreated ${path}
84239
84812
  ` : `
@@ -84249,7 +84822,7 @@ Setup cancelled \u2014 no file written.
84249
84822
  return 0;
84250
84823
  }
84251
84824
  const onlyFields = choice2 === "diff" ? fieldsAddedSince(config2.version) : undefined;
84252
- return editExisting(projectRoot, path, config2, onlyFields);
84825
+ return editExisting(projectRoot, path, config2, workflowFile, onlyFields);
84253
84826
  }
84254
84827
  const choice = await promptEditOrExit();
84255
84828
  if (choice === "exit") {
@@ -84257,17 +84830,42 @@ Setup cancelled \u2014 no file written.
84257
84830
  `);
84258
84831
  return 0;
84259
84832
  }
84260
- return editExisting(projectRoot, path, config2);
84833
+ return editExisting(projectRoot, path, config2, workflowFile);
84261
84834
  }
84262
84835
  if (!interactive) {
84263
84836
  const { ensureWorkflow: ensureWorkflow2 } = await Promise.resolve().then(() => (init_workflow(), exports_workflow));
84264
- const written = await ensureWorkflow2(projectRoot);
84837
+ const written = await ensureWorkflow2(projectRoot, workflowFile);
84265
84838
  process.stdout.write(`Non-interactive shell \u2014 wrote default WORKFLOW.md: ${written}
84266
84839
  `);
84267
84840
  return 0;
84268
84841
  }
84842
+ const backup = await readSetupBackup(projectRoot);
84843
+ if (backup) {
84844
+ const choice = await promptResumeOrFresh();
84845
+ if (choice === "resume") {
84846
+ const wrote2 = await runSetupWizard(projectRoot, {
84847
+ initialMode: backup.mode,
84848
+ resumeValues: backup.values,
84849
+ trackBackup: true,
84850
+ ...workflowFile ? { workflowFile } : {}
84851
+ });
84852
+ process.stdout.write(wrote2 ? `
84853
+ \u2713 Created ${path}
84854
+ ` : `
84855
+ Setup cancelled \u2014 no file written.
84856
+ `);
84857
+ return 0;
84858
+ }
84859
+ await clearSetupBackup();
84860
+ }
84269
84861
  const detectedRepo = await detectRepoIdentity(projectRoot);
84270
- const wrote = await runSetupWizard(projectRoot, detectedRepo ? { detectedRepo } : {});
84862
+ const detected = await detectInitialValues(projectRoot);
84863
+ const wrote = await runSetupWizard(projectRoot, {
84864
+ trackBackup: true,
84865
+ ...detectedRepo ? { detectedRepo } : {},
84866
+ ...workflowFile ? { workflowFile } : {},
84867
+ ...Object.keys(detected).length > 0 ? { initialValues: detected } : {}
84868
+ });
84271
84869
  process.stdout.write(wrote ? `
84272
84870
  \u2713 Created ${path}
84273
84871
  ` : `
@@ -84278,10 +84876,12 @@ Setup cancelled \u2014 no file written.
84278
84876
  var import_react23, INIT_HELP;
84279
84877
  var init_src4 = __esm(async () => {
84280
84878
  init_paths();
84879
+ init_common_args();
84281
84880
  init_workflow();
84282
84881
  init_wizard();
84283
84882
  init_repo();
84284
84883
  init_migrations();
84884
+ init_project_detect();
84285
84885
  await __promiseAll([
84286
84886
  init_build2(),
84287
84887
  init_SetupWizard()
@@ -84290,10 +84890,15 @@ var init_src4 = __esm(async () => {
84290
84890
  INIT_HELP = [
84291
84891
  "ralphy init \u2014 create or edit WORKFLOW.md with an interactive setup wizard",
84292
84892
  "",
84293
- "Usage: ralphy init",
84893
+ "Usage: ralphy init [options]",
84294
84894
  "",
84295
84895
  "Runs a short wizard (quick / permissive / customized) and writes WORKFLOW.md",
84296
- "to the project root. If WORKFLOW.md already exists, offers to edit it."
84896
+ "to the project root. If WORKFLOW.md already exists, offers to edit it.",
84897
+ "",
84898
+ "Options:",
84899
+ " --project-root <path> Directory to treat as the project root (default: detected)",
84900
+ " --workflow <path> Path to read / write WORKFLOW.md (default: <project>/WORKFLOW.md)",
84901
+ " --help, -h Show this help message"
84297
84902
  ].join(`
84298
84903
  `);
84299
84904
  });
@@ -84362,6 +84967,12 @@ function getLayout() {
84362
84967
  throw new Error("No layout in context. Set layout when calling runWithContext().");
84363
84968
  return ctx.layout;
84364
84969
  }
84970
+ function getArgs() {
84971
+ const ctx = getContext();
84972
+ if (!ctx.args)
84973
+ throw new Error("No args in context. Set args when calling runWithContext().");
84974
+ return ctx.args;
84975
+ }
84365
84976
  function runWithContext(ctx, fn) {
84366
84977
  return contextStore.run(ctx, fn);
84367
84978
  }
@@ -84374,18 +84985,18 @@ var init_context = __esm(() => {
84374
84985
  });
84375
84986
 
84376
84987
  // packages/core/src/layout.ts
84377
- import { join as join6 } from "path";
84988
+ import { join as join7 } from "path";
84378
84989
  function projectLayout(root) {
84379
- const statesDir = join6(root, ".ralph", "tasks");
84380
- const tasksDir = join6(root, "openspec", "changes");
84990
+ const statesDir = join7(root, ".ralph", "tasks");
84991
+ const tasksDir = join7(root, "openspec", "changes");
84381
84992
  return {
84382
84993
  root,
84383
84994
  statesDir,
84384
84995
  tasksDir,
84385
- agentStateFile: join6(root, ".ralph", "agent-state.json"),
84386
- changeDir: (name) => join6(tasksDir, name),
84387
- taskStateDir: (name) => join6(statesDir, name),
84388
- stateFile: (name) => join6(statesDir, name, STATE_FILE)
84996
+ agentStateFile: join7(root, ".ralph", "agent-state.json"),
84997
+ changeDir: (name) => join7(tasksDir, name),
84998
+ taskStateDir: (name) => join7(statesDir, name),
84999
+ stateFile: (name) => join7(statesDir, name, STATE_FILE)
84389
85000
  };
84390
85001
  }
84391
85002
  var STATE_FILE = ".ralph-state.json", GAVEUP_COUNT_FILE = ".ralph-gaveup-count";
@@ -84394,13 +85005,13 @@ var init_layout = __esm(() => {
84394
85005
  });
84395
85006
 
84396
85007
  // packages/openspec/src/openspec-bin.ts
84397
- import { dirname as dirname3, join as join7 } from "path";
85008
+ import { dirname as dirname3, join as join8 } from "path";
84398
85009
  function findPackageRoot(startDir) {
84399
85010
  let dir = startDir;
84400
85011
  for (let i = 0;i < 8; i++) {
84401
- if (Bun.file(join7(dir, "package.json")).size >= 0) {
85012
+ if (Bun.file(join8(dir, "package.json")).size >= 0) {
84402
85013
  try {
84403
- if (Bun.file(join7(dir, "package.json")).size > 0)
85014
+ if (Bun.file(join8(dir, "package.json")).size > 0)
84404
85015
  return dir;
84405
85016
  } catch {}
84406
85017
  }
@@ -84436,11 +85047,11 @@ function ensureOpenspecInstalled(fromDir, runner) {
84436
85047
  function resolveOpenspecBin(fromDir, runner = bunInstallRunner) {
84437
85048
  try {
84438
85049
  const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
84439
- return join7(dirname3(pkgJsonPath), "bin", "openspec.js");
85050
+ return join8(dirname3(pkgJsonPath), "bin", "openspec.js");
84440
85051
  } catch {
84441
85052
  ensureOpenspecInstalled(fromDir, runner);
84442
85053
  const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
84443
- return join7(dirname3(pkgJsonPath), "bin", "openspec.js");
85054
+ return join8(dirname3(pkgJsonPath), "bin", "openspec.js");
84444
85055
  }
84445
85056
  }
84446
85057
  var bunInstallRunner;
@@ -84462,7 +85073,7 @@ var init_openspec_bin = __esm(() => {
84462
85073
  });
84463
85074
 
84464
85075
  // packages/openspec/src/openspec-change-store.ts
84465
- import { dirname as dirname4, join as join8 } from "path";
85076
+ import { dirname as dirname4, join as join9 } from "path";
84466
85077
  import { readdir, mkdir as mkdir2 } from "fs/promises";
84467
85078
  function runOpenspec(args, options = {}) {
84468
85079
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -84532,7 +85143,7 @@ class OpenSpecChangeStore {
84532
85143
  }
84533
85144
  }
84534
85145
  getChangeDirectory(name) {
84535
- return join8("openspec", "changes", name);
85146
+ return join9("openspec", "changes", name);
84536
85147
  }
84537
85148
  async listChanges() {
84538
85149
  const result2 = runOpenspec(["list", "--json"]);
@@ -84546,7 +85157,7 @@ class OpenSpecChangeStore {
84546
85157
  }
84547
85158
  } catch {}
84548
85159
  }
84549
- const changesDir = join8("openspec", "changes");
85160
+ const changesDir = join9("openspec", "changes");
84550
85161
  try {
84551
85162
  const entries = await readdir(changesDir, { withFileTypes: true });
84552
85163
  return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
@@ -84555,18 +85166,18 @@ class OpenSpecChangeStore {
84555
85166
  }
84556
85167
  }
84557
85168
  async readTaskList(name) {
84558
- const file2 = Bun.file(join8("openspec", "changes", name, "tasks.md"));
85169
+ const file2 = Bun.file(join9("openspec", "changes", name, "tasks.md"));
84559
85170
  if (!await file2.exists())
84560
85171
  return "";
84561
85172
  return await file2.text();
84562
85173
  }
84563
85174
  async writeTaskList(name, content) {
84564
- const path = join8("openspec", "changes", name, "tasks.md");
85175
+ const path = join9("openspec", "changes", name, "tasks.md");
84565
85176
  await mkdir2(dirname4(path), { recursive: true });
84566
85177
  await Bun.write(path, content);
84567
85178
  }
84568
85179
  async appendSteering(name, message) {
84569
- const path = join8("openspec", "changes", name, "steering.md");
85180
+ const path = join9("openspec", "changes", name, "steering.md");
84570
85181
  const file2 = Bun.file(path);
84571
85182
  const existing = await file2.exists() ? await file2.text() : null;
84572
85183
  const updated = existing ? `${message}
@@ -84578,7 +85189,7 @@ ${existing.trimStart()}` : `${message}
84578
85189
  const firstLine = message.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? message.trim();
84579
85190
  if (firstLine.length === 0)
84580
85191
  return;
84581
- const tasksPath = join8("openspec", "changes", name, "tasks.md");
85192
+ const tasksPath = join9("openspec", "changes", name, "tasks.md");
84582
85193
  const tasksFile = Bun.file(tasksPath);
84583
85194
  const existingTasks = await tasksFile.exists() ? await tasksFile.text() : "";
84584
85195
  const taskLine = `- [ ] Address steering: ${firstLine}`;
@@ -84711,171 +85322,6 @@ var init_output2 = __esm(() => {
84711
85322
  };
84712
85323
  });
84713
85324
 
84714
- // packages/cli-args/src/common-args.ts
84715
- function initialCommonArgs() {
84716
- return {
84717
- engine: "claude",
84718
- model: "opus",
84719
- engineSet: false,
84720
- maxIterations: 0,
84721
- maxCostUsd: 0,
84722
- maxRuntimeMinutes: 0,
84723
- maxConsecutiveFailures: 5,
84724
- delay: 0,
84725
- log: false,
84726
- verbose: false,
84727
- projectRoot: undefined,
84728
- name: "",
84729
- prompt: "",
84730
- fromAgent: false
84731
- };
84732
- }
84733
- function applyValueOption(option, args, raw) {
84734
- const setter = VALUE_SETTERS[option.argKey];
84735
- if (!setter)
84736
- throw new Error("no value setter registered for CLI option");
84737
- setter(args, raw);
84738
- }
84739
- function applyBooleanOption(option, args) {
84740
- const setter = BOOLEAN_SETTERS[option.argKey];
84741
- if (!setter)
84742
- throw new Error("no boolean setter registered for CLI option");
84743
- setter(args);
84744
- }
84745
- function emptyParseState() {
84746
- return {
84747
- pendingOption: null,
84748
- expectClaudeModel: false,
84749
- expectProjectRoot: false,
84750
- expectName: false,
84751
- expectPrompt: false,
84752
- expectPromptFile: false,
84753
- promptFilePath: null
84754
- };
84755
- }
84756
- function parseCommonArg(arg, args, state) {
84757
- if (state.pendingOption) {
84758
- applyValueOption(state.pendingOption, args, arg);
84759
- state.pendingOption = null;
84760
- return true;
84761
- }
84762
- if (state.expectClaudeModel) {
84763
- state.expectClaudeModel = false;
84764
- if (VALID_MODELS.has(arg)) {
84765
- args.model = arg;
84766
- return true;
84767
- }
84768
- }
84769
- if (state.expectProjectRoot) {
84770
- args.projectRoot = arg;
84771
- state.expectProjectRoot = false;
84772
- return true;
84773
- }
84774
- if (state.expectName) {
84775
- args.name = arg;
84776
- state.expectName = false;
84777
- return true;
84778
- }
84779
- if (state.expectPrompt) {
84780
- args.prompt = arg;
84781
- state.promptFilePath = null;
84782
- state.expectPrompt = false;
84783
- return true;
84784
- }
84785
- if (state.expectPromptFile) {
84786
- state.promptFilePath = arg;
84787
- state.expectPromptFile = false;
84788
- return true;
84789
- }
84790
- const option = OPTION_BY_FLAG.get(arg);
84791
- if (option) {
84792
- if (option.kind === "boolean")
84793
- applyBooleanOption(option, args);
84794
- else
84795
- state.pendingOption = option;
84796
- return true;
84797
- }
84798
- switch (arg) {
84799
- case "--claude":
84800
- if (args.engineSet && args.engine !== "claude") {
84801
- throw new Error("Choose only one engine flag: --claude or --codex");
84802
- }
84803
- args.engine = "claude";
84804
- args.engineSet = true;
84805
- state.expectClaudeModel = true;
84806
- return true;
84807
- case "--codex":
84808
- if (args.engineSet && args.engine !== "codex") {
84809
- throw new Error("Choose only one engine flag: --claude or --codex");
84810
- }
84811
- args.engine = "codex";
84812
- args.engineSet = true;
84813
- return true;
84814
- case "--unlimited":
84815
- args.maxIterations = 0;
84816
- return true;
84817
- case "--project-root":
84818
- state.expectProjectRoot = true;
84819
- return true;
84820
- case "--name":
84821
- state.expectName = true;
84822
- return true;
84823
- case "--prompt":
84824
- state.expectPrompt = true;
84825
- return true;
84826
- case "--prompt-file":
84827
- state.expectPromptFile = true;
84828
- return true;
84829
- case "--from-agent":
84830
- args.fromAgent = true;
84831
- return true;
84832
- default:
84833
- return false;
84834
- }
84835
- }
84836
- async function resolvePromptFile(args, state) {
84837
- if (state.promptFilePath !== null) {
84838
- args.prompt = await Bun.file(state.promptFilePath).text();
84839
- }
84840
- }
84841
- var VALID_MODELS, OPTION_BY_FLAG, VALUE_FLAGS, VALUE_SETTERS, BOOLEAN_SETTERS;
84842
- var init_common_args = __esm(() => {
84843
- init_fields();
84844
- VALID_MODELS = new Set(modelOptionValues());
84845
- OPTION_BY_FLAG = new Map(COMMON_CLI_OPTIONS.map((option) => [option.flag, option]));
84846
- VALUE_FLAGS = new Set(COMMON_CLI_OPTIONS.filter((option) => option.kind !== "boolean").map((option) => option.flag));
84847
- VALUE_SETTERS = {
84848
- model: (args, raw) => {
84849
- if (!VALID_MODELS.has(raw))
84850
- throw new Error("Invalid model");
84851
- args.model = raw;
84852
- },
84853
- delay: (args, raw) => {
84854
- args.delay = parseInt(raw, 10);
84855
- },
84856
- maxCostUsd: (args, raw) => {
84857
- args.maxCostUsd = parseFloat(raw);
84858
- },
84859
- maxRuntimeMinutes: (args, raw) => {
84860
- args.maxRuntimeMinutes = parseFloat(raw);
84861
- },
84862
- maxConsecutiveFailures: (args, raw) => {
84863
- args.maxConsecutiveFailures = parseInt(raw, 10);
84864
- },
84865
- maxIterations: (args, raw) => {
84866
- args.maxIterations = parseInt(raw, 10);
84867
- }
84868
- };
84869
- BOOLEAN_SETTERS = {
84870
- log: (args) => {
84871
- args.log = true;
84872
- },
84873
- verbose: (args) => {
84874
- args.verbose = true;
84875
- }
84876
- };
84877
- });
84878
-
84879
85325
  // apps/loop/src/cli.ts
84880
85326
  function printLoopHelp() {
84881
85327
  log(HELP_TEXT);
@@ -84944,6 +85390,7 @@ async function parseLoopArgs(argv) {
84944
85390
  }
84945
85391
  }
84946
85392
  await resolvePromptFile(result2, state);
85393
+ resolveWorkflowFile(result2, state);
84947
85394
  return result2;
84948
85395
  }
84949
85396
  var VALID_MODES, HELP_TEXT;
@@ -84966,6 +85413,7 @@ var init_cli = __esm(() => {
84966
85413
  "",
84967
85414
  "Options:",
84968
85415
  " --name <name> Change name (required for most commands)",
85416
+ " --workflow <path> Path to an alternate WORKFLOW.md (default: <project>/WORKFLOW.md)",
84969
85417
  " --prompt <text> Task description",
84970
85418
  " --prompt-file <path> Read prompt from file",
84971
85419
  " --model <model> Set model (haiku|sonnet|opus)",
@@ -85022,6 +85470,7 @@ async function parseTaskArgs(argv) {
85022
85470
  }
85023
85471
  }
85024
85472
  await resolvePromptFile(result2, state);
85473
+ resolveWorkflowFile(result2, state);
85025
85474
  if (!phaseSet) {
85026
85475
  throw new Error(`Missing phase. Valid phases: research, plan, execute, review. Run 'ralphy task --help' for usage information.`);
85027
85476
  }
@@ -85049,6 +85498,7 @@ var init_task_cli = __esm(() => {
85049
85498
  "",
85050
85499
  "Options:",
85051
85500
  " --name <name> Change name (required)",
85501
+ " --workflow <path> Path to an alternate WORKFLOW.md (default: <project>/WORKFLOW.md)",
85052
85502
  " --prompt <text> Task description",
85053
85503
  " --prompt-file <path> Read prompt from file",
85054
85504
  " --model <model> Set model (haiku|sonnet|opus)",
@@ -85088,10 +85538,10 @@ var init_schema2 = __esm(() => {
85088
85538
  });
85089
85539
 
85090
85540
  // packages/core/src/state/sidecar.ts
85091
- import { dirname as dirname5, join as join9 } from "path";
85541
+ import { dirname as dirname5, join as join10 } from "path";
85092
85542
  import { mkdir as mkdir3, rename, unlink } from "fs/promises";
85093
85543
  function slotSidecarPath(changeDir, slot) {
85094
- return join9(changeDir, `${CORE_STATE_FILE.replace(/\.json$/, "")}.${slot}.json`);
85544
+ return join10(changeDir, `${CORE_STATE_FILE.replace(/\.json$/, "")}.${slot}.json`);
85095
85545
  }
85096
85546
  function parseObject(text) {
85097
85547
  if (text === null)
@@ -89273,7 +89723,7 @@ function formatTaskName(name) {
89273
89723
  }
89274
89724
 
89275
89725
  // packages/core/src/state.ts
89276
- import { join as join10 } from "path";
89726
+ import { join as join11 } from "path";
89277
89727
  function stripOwnedSlots(state) {
89278
89728
  const out = { ...state };
89279
89729
  for (const slot of ALL_OWNED_SLOTS)
@@ -89281,7 +89731,7 @@ function stripOwnedSlots(state) {
89281
89731
  return out;
89282
89732
  }
89283
89733
  function readState(changeDir) {
89284
- const filePath = join10(changeDir, STATE_FILE2);
89734
+ const filePath = join11(changeDir, STATE_FILE2);
89285
89735
  const raw = getStorage().read(filePath);
89286
89736
  if (raw === null)
89287
89737
  throw new Error(".ralph-state.json not found");
@@ -89290,7 +89740,7 @@ function readState(changeDir) {
89290
89740
  return StateSchema.parse(base2);
89291
89741
  }
89292
89742
  function tryReadStateRaw(changeDir) {
89293
- const filePath = join10(changeDir, STATE_FILE2);
89743
+ const filePath = join11(changeDir, STATE_FILE2);
89294
89744
  const text = getStorage().read(filePath);
89295
89745
  if (text === null)
89296
89746
  return { state: null, raw: null };
@@ -89306,7 +89756,7 @@ function tryReadStateRaw(changeDir) {
89306
89756
  return { state: result2.success ? result2.data : null, raw };
89307
89757
  }
89308
89758
  function writeState(changeDir, state) {
89309
- const filePath = join10(changeDir, STATE_FILE2);
89759
+ const filePath = join11(changeDir, STATE_FILE2);
89310
89760
  const core2 = stripOwnedSlots(state);
89311
89761
  getStorage().write(filePath, JSON.stringify(core2, null, 2) + `
89312
89762
  `);
@@ -89347,7 +89797,7 @@ function buildInitialState(options) {
89347
89797
  });
89348
89798
  }
89349
89799
  function ensureState(changeDir) {
89350
- const filePath = join10(changeDir, STATE_FILE2);
89800
+ const filePath = join11(changeDir, STATE_FILE2);
89351
89801
  const storage = getStorage();
89352
89802
  if (storage.read(filePath) !== null) {
89353
89803
  return readState(changeDir);
@@ -89366,7 +89816,7 @@ var init_state = __esm(() => {
89366
89816
  });
89367
89817
 
89368
89818
  // packages/core/src/state/store.ts
89369
- import { join as join11 } from "path";
89819
+ import { join as join12 } from "path";
89370
89820
  async function readJson(filePath) {
89371
89821
  const file2 = Bun.file(filePath);
89372
89822
  if (!await file2.exists())
@@ -89390,7 +89840,7 @@ async function writeField(changeDir, featureName, path, value) {
89390
89840
  if (!allowed.includes(topSlot)) {
89391
89841
  throw new OwnershipError(featureName, path, `feature '${featureName}' may not write '${path}' (owns ${allowed.join(", ")})`);
89392
89842
  }
89393
- const inline = (await readJson(join11(changeDir, STATE_FILE3)))[topSlot];
89843
+ const inline = (await readJson(join12(changeDir, STATE_FILE3)))[topSlot];
89394
89844
  const seed = inline && typeof inline === "object" && !Array.isArray(inline) ? inline : undefined;
89395
89845
  await writeSlotField(changeDir, path, value, seed);
89396
89846
  }
@@ -89413,14 +89863,14 @@ var init_store = __esm(() => {
89413
89863
  });
89414
89864
 
89415
89865
  // apps/loop/src/components/TaskStatus.tsx
89416
- import { join as join12 } from "path";
89866
+ import { join as join13 } from "path";
89417
89867
  function TaskStatus({ state, stateDir }) {
89418
89868
  const storage = getStorage();
89419
89869
  const cost = Math.round(state.usage.total_cost_usd * 100) / 100;
89420
89870
  const time3 = Math.round(state.usage.total_duration_ms / 1000 * 10) / 10 + "s";
89421
89871
  const artifacts = OPENSPEC_ARTIFACTS.map((name) => ({
89422
89872
  name,
89423
- exists: storage.read(join12(stateDir, name)) !== null
89873
+ exists: storage.read(join13(stateDir, name)) !== null
89424
89874
  }));
89425
89875
  const recent = state.history.slice(-10);
89426
89876
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
@@ -97382,10 +97832,10 @@ var require_xstate_development_cjs = __commonJS((exports) => {
97382
97832
  }
97383
97833
  }
97384
97834
  function toPromise(actor) {
97385
- return new Promise((resolve3, reject2) => {
97835
+ return new Promise((resolve4, reject2) => {
97386
97836
  actor.subscribe({
97387
97837
  complete: () => {
97388
- resolve3(actor.getSnapshot().output);
97838
+ resolve4(actor.getSnapshot().output);
97389
97839
  },
97390
97840
  error: reject2
97391
97841
  });
@@ -98138,7 +98588,7 @@ var init_rate_limit_detection = __esm(() => {
98138
98588
 
98139
98589
  // packages/engine/src/agents/claude.ts
98140
98590
  import { mkdtemp, unlink as unlink2 } from "fs/promises";
98141
- import { join as join13 } from "path";
98591
+ import { join as join14 } from "path";
98142
98592
  import { tmpdir } from "os";
98143
98593
  function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, reviewerModel) {
98144
98594
  const effectiveModel = reviewerModel ?? model;
@@ -98159,7 +98609,7 @@ function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, review
98159
98609
  }
98160
98610
  async function runInteractive(req) {
98161
98611
  const { model, prompt, taskDir } = req;
98162
- const promptFile = taskDir ? join13(taskDir, "_interactive_prompt.md") : join13(await mkdtemp(join13(tmpdir(), "ralph-")), "prompt.md");
98612
+ const promptFile = taskDir ? join14(taskDir, "_interactive_prompt.md") : join14(await mkdtemp(join14(tmpdir(), "ralph-")), "prompt.md");
98163
98613
  await Bun.write(promptFile, prompt);
98164
98614
  try {
98165
98615
  const cmd = [
@@ -98186,7 +98636,7 @@ async function runInteractive(req) {
98186
98636
  env: scrubClaudeEnv(process.env)
98187
98637
  });
98188
98638
  const exitCode = await proc.exited;
98189
- const doneFile = taskDir ? join13(taskDir, "_interactive_done") : null;
98639
+ const doneFile = taskDir ? join14(taskDir, "_interactive_done") : null;
98190
98640
  if (doneFile && await Bun.file(doneFile).exists()) {
98191
98641
  return { exitCode: 0, usage: null, sessionId: null, rateLimited: false };
98192
98642
  }
@@ -98816,10 +99266,10 @@ async function runEngine(opts) {
98816
99266
  await mkdir4(dirname6(opts.logFile), { recursive: true });
98817
99267
  rawWriter = createWriteStream(opts.logFile, { flags: "a" });
98818
99268
  }
98819
- const closeRaw = () => new Promise((resolve3) => {
99269
+ const closeRaw = () => new Promise((resolve4) => {
98820
99270
  if (!rawWriter)
98821
- return resolve3();
98822
- rawWriter.end(resolve3);
99271
+ return resolve4();
99272
+ rawWriter.end(resolve4);
98823
99273
  });
98824
99274
  const userOnFeedEvent = opts.onFeedEvent;
98825
99275
  const onFeedEvent = (event) => {
@@ -98963,8 +99413,8 @@ var init_flow_machine = __esm(() => {
98963
99413
  } catch {}
98964
99414
  const exited = await Promise.race([
98965
99415
  worker.exited.then(() => "exited"),
98966
- new Promise((resolve3) => {
98967
- const t = setTimeout(() => resolve3("timeout"), graceMs);
99416
+ new Promise((resolve4) => {
99417
+ const t = setTimeout(() => resolve4("timeout"), graceMs);
98968
99418
  t.unref();
98969
99419
  })
98970
99420
  ]);
@@ -99689,11 +100139,11 @@ var init_meta_prompt = __esm(() => {
99689
100139
  });
99690
100140
 
99691
100141
  // packages/core/src/loop.ts
99692
- import { join as join14 } from "path";
100142
+ import { join as join15 } from "path";
99693
100143
  function buildTaskPrompt(state, taskDir, reviewPhase) {
99694
100144
  const storage = getStorage();
99695
100145
  let prompt = "";
99696
- const steeringContent = storage.read(join14(taskDir, "steering.md"));
100146
+ const steeringContent = storage.read(join15(taskDir, "steering.md"));
99697
100147
  if (steeringContent !== null) {
99698
100148
  const steeringLines = steeringContent.split(`
99699
100149
  `).filter((line) => !line.startsWith("#")).filter((line) => line.trim()).slice(0, STEERING_MAX_LINES);
@@ -99712,8 +100162,8 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99712
100162
  `;
99713
100163
  }
99714
100164
  }
99715
- const agentTasksPath = join14(taskDir, AGENT_TASKS_FILENAME);
99716
- const missionTasksPath = join14(taskDir, MISSION_TASKS_FILENAME);
100165
+ const agentTasksPath = join15(taskDir, AGENT_TASKS_FILENAME);
100166
+ const missionTasksPath = join15(taskDir, MISSION_TASKS_FILENAME);
99717
100167
  const agentTasksContent = storage.read(agentTasksPath);
99718
100168
  const missionTasksContent = storage.read(missionTasksPath);
99719
100169
  let activePath = null;
@@ -99789,7 +100239,7 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99789
100239
  }
99790
100240
  }
99791
100241
  if (reviewPhase?.enabled) {
99792
- const reviewFindingsPath = join14(taskDir, "review-findings.md");
100242
+ const reviewFindingsPath = join15(taskDir, "review-findings.md");
99793
100243
  const reviewFindingsContent = storage.read(reviewFindingsPath);
99794
100244
  const hasUncheckedMission = missionTasksContent !== null && /^- \[ \]/m.test(missionTasksContent);
99795
100245
  const hasUncheckedAgent = agentTasksContent !== null && /^- \[ \]/m.test(agentTasksContent);
@@ -99863,7 +100313,7 @@ When all tasks are complete and all files are committed, push your branch and op
99863
100313
  }
99864
100314
  function buildSteeringBlock(taskDir) {
99865
100315
  const storage = getStorage();
99866
- const steeringContent = storage.read(join14(taskDir, "steering.md"));
100316
+ const steeringContent = storage.read(join15(taskDir, "steering.md"));
99867
100317
  if (steeringContent === null)
99868
100318
  return "";
99869
100319
  const steeringLines = steeringContent.split(`
@@ -99961,7 +100411,7 @@ function buildPlanPrompt(state, taskDir) {
99961
100411
  return prompt;
99962
100412
  }
99963
100413
  function buildReviewPrompt(state, taskDir) {
99964
- const reviewFindingsPath = join14(taskDir, "review-findings.md");
100414
+ const reviewFindingsPath = join15(taskDir, "review-findings.md");
99965
100415
  let prompt = buildSteeringBlock(taskDir);
99966
100416
  prompt += `---
99967
100417
 
@@ -100026,7 +100476,7 @@ function buildPhasePrompt(phase, state, taskDir, reviewPhase, metaPromptOptions)
100026
100476
  }
100027
100477
  function checkStopSignal(taskDir, stateDir) {
100028
100478
  const storage = getStorage();
100029
- const stopFile = join14(taskDir, "STOP");
100479
+ const stopFile = join15(taskDir, "STOP");
100030
100480
  const reason = storage.read(stopFile);
100031
100481
  if (reason === null)
100032
100482
  return null;
@@ -100086,7 +100536,7 @@ function updateStateIteration(stateDir, result2, startedAt, engine, model, usage
100086
100536
  }
100087
100537
  function appendSteeringMessage(taskDir, message) {
100088
100538
  const storage = getStorage();
100089
- const steeringPath = join14(taskDir, "steering.md");
100539
+ const steeringPath = join15(taskDir, "steering.md");
100090
100540
  const existing = storage.read(steeringPath);
100091
100541
  const updated = existing ? `${message}
100092
100542
 
@@ -100136,9 +100586,9 @@ var init_loop2 = __esm(() => {
100136
100586
  });
100137
100587
 
100138
100588
  // apps/loop/src/hooks/useLoop.ts
100139
- import { join as join15 } from "path";
100589
+ import { join as join16 } from "path";
100140
100590
  function sleep(seconds) {
100141
- return new Promise((resolve3) => setTimeout(resolve3, seconds * 1000));
100591
+ return new Promise((resolve4) => setTimeout(resolve4, seconds * 1000));
100142
100592
  }
100143
100593
  function useLoop(opts) {
100144
100594
  const outerLayoutRef = import_react57.useRef(null);
@@ -100252,8 +100702,8 @@ function useLoop(opts) {
100252
100702
  setState(currentState);
100253
100703
  if (!actor.getSnapshot().matches("running"))
100254
100704
  break;
100255
- const tasksContent = storage.read(join15(tasksDir, MISSION_TASKS_FILENAME));
100256
- const agentTasksContent = storage.read(join15(tasksDir, AGENT_TASKS_FILENAME));
100705
+ const tasksContent = storage.read(join16(tasksDir, MISSION_TASKS_FILENAME));
100706
+ const agentTasksContent = storage.read(join16(tasksDir, AGENT_TASKS_FILENAME));
100257
100707
  if (tasksContent === null && currentState.iteration > 0 && typeof opts.changeStore.listChanges === "function") {
100258
100708
  let stillActive = true;
100259
100709
  try {
@@ -100290,7 +100740,7 @@ function useLoop(opts) {
100290
100740
  const agentDone = agentTasksContent === null || allCompleted(agentTasksContent);
100291
100741
  if (missionDone && agentDone && tasksContent !== null) {
100292
100742
  if (opts.reviewPhase?.enabled) {
100293
- const reviewFindingsPath = join15(tasksDir, "review-findings.md");
100743
+ const reviewFindingsPath = join16(tasksDir, "review-findings.md");
100294
100744
  const reviewFindingsFile = Bun.file(reviewFindingsPath);
100295
100745
  const findingsExists = await reviewFindingsFile.exists();
100296
100746
  const findingsContent = findingsExists ? await reviewFindingsFile.text() : null;
@@ -100319,7 +100769,7 @@ function useLoop(opts) {
100319
100769
  model: opts.reviewPhase.reviewerModel ?? opts.model,
100320
100770
  prompt: reviewPrompt,
100321
100771
  logFlag: opts.log,
100322
- logFile: join15(stateDir, `log-review-${roundNum}.json`),
100772
+ logFile: join16(stateDir, `log-review-${roundNum}.json`),
100323
100773
  taskDir: tasksDir,
100324
100774
  reviewerContextStrategy: opts.reviewPhase.reviewerContextStrategy ?? "fresh",
100325
100775
  onFeedEvent: addFeedEvent
@@ -100393,8 +100843,8 @@ function useLoop(opts) {
100393
100843
  const time3 = new Date().toLocaleTimeString("en-US", { hour12: false });
100394
100844
  addIterationHeader(localIter, time3);
100395
100845
  addInfo(`Iteration ${localIter} (total: ${currentState.iteration})`);
100396
- const proposalContent = storage.read(join15(tasksDir, "proposal.md"));
100397
- const designContent = storage.read(join15(tasksDir, "design.md"));
100846
+ const proposalContent = storage.read(join16(tasksDir, "proposal.md"));
100847
+ const designContent = storage.read(join16(tasksDir, "design.md"));
100398
100848
  const routedPhase = routeTaskPhase(opts.phase, {
100399
100849
  proposal: proposalContent,
100400
100850
  design: designContent,
@@ -100414,7 +100864,7 @@ function useLoop(opts) {
100414
100864
  model: opts.model,
100415
100865
  prompt,
100416
100866
  logFlag: opts.log,
100417
- logFile: join15(stateDir, "log.json"),
100867
+ logFile: join16(stateDir, "log.json"),
100418
100868
  taskDir: tasksDir,
100419
100869
  interactive: false,
100420
100870
  onFeedEvent: addFeedEvent,
@@ -100437,7 +100887,7 @@ function useLoop(opts) {
100437
100887
  model: opts.model,
100438
100888
  prompt: buildSteeringPrompt(steerMessage),
100439
100889
  logFlag: opts.log,
100440
- logFile: join15(stateDir, "log.json"),
100890
+ logFile: join16(stateDir, "log.json"),
100441
100891
  taskDir: tasksDir,
100442
100892
  onFeedEvent: addResumeFeedEvent,
100443
100893
  signal: resumeController.signal,
@@ -100744,7 +101194,7 @@ var init_TaskLoop = __esm(async () => {
100744
101194
  });
100745
101195
 
100746
101196
  // apps/loop/src/components/App.tsx
100747
- import { join as join16 } from "path";
101197
+ import { join as join17 } from "path";
100748
101198
  function ExitAfterRender({ children }) {
100749
101199
  const { exit } = use_app_default();
100750
101200
  import_react59.useEffect(() => {
@@ -100797,7 +101247,7 @@ function App2({ args, taskPhase }) {
100797
101247
  }
100798
101248
  const layout = getLayout();
100799
101249
  const stateDir = layout.taskStateDir(args.name);
100800
- if (getStorage().read(join16(stateDir, ".ralph-state.json")) === null) {
101250
+ if (getStorage().read(join17(stateDir, ".ralph-state.json")) === null) {
100801
101251
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
100802
101252
  message: `Error: change '${args.name}' not found`
100803
101253
  }, undefined, false, undefined, this);
@@ -100851,7 +101301,7 @@ var init_App2 = __esm(async () => {
100851
101301
 
100852
101302
  // packages/log/src/log.ts
100853
101303
  import { appendFile } from "fs/promises";
100854
- import { join as join17, dirname as dirname7 } from "path";
101304
+ import { join as join18, dirname as dirname7 } from "path";
100855
101305
  import { homedir as homedir4 } from "os";
100856
101306
  import { mkdir as mkdir5 } from "fs/promises";
100857
101307
  function fmt(type, text) {
@@ -100900,14 +101350,14 @@ var init_log = __esm(() => {
100900
101350
  init_version();
100901
101351
  jsonLogChains = new Map;
100902
101352
  ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
100903
- AGENT_LOG_PATH = join17(homedir4(), ".ralph", "agent-mode.log");
101353
+ AGENT_LOG_PATH = join18(homedir4(), ".ralph", "agent-mode.log");
100904
101354
  mkdir5(dirname7(AGENT_LOG_PATH), { recursive: true }).catch(() => {
100905
101355
  return;
100906
101356
  });
100907
101357
  });
100908
101358
 
100909
101359
  // apps/loop/src/debug.ts
100910
- import { join as join18 } from "path";
101360
+ import { join as join19 } from "path";
100911
101361
  function fmtTs(d) {
100912
101362
  return d.toISOString().replace("T", " ").slice(0, 23);
100913
101363
  }
@@ -101019,7 +101469,7 @@ function detectDebugStuck(lines) {
101019
101469
  };
101020
101470
  }
101021
101471
  async function inspectBinary(projectRoot) {
101022
- const binPath = join18(projectRoot, ".ralph", "bin", "cli.js");
101472
+ const binPath = join19(projectRoot, ".ralph", "bin", "cli.js");
101023
101473
  const file2 = Bun.file(binPath);
101024
101474
  if (!await file2.exists())
101025
101475
  return null;
@@ -101044,7 +101494,7 @@ async function inspectBinary(projectRoot) {
101044
101494
  async function resolveDebugTarget(projectRoot, opts) {
101045
101495
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
101046
101496
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
101047
- const jsonlLogFile = Bun.file(join18(projectRoot, ".ralph", "agent.log"));
101497
+ const jsonlLogFile = Bun.file(join19(projectRoot, ".ralph", "agent.log"));
101048
101498
  const jsonlLines = await jsonlLogFile.exists() ? parseJsonlLog(await jsonlLogFile.text()) : [];
101049
101499
  const allLines = [...textLines, ...jsonlLines];
101050
101500
  if (opts.name && !opts.issue) {
@@ -101149,7 +101599,7 @@ async function runDebug(opts) {
101149
101599
  `);
101150
101600
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
101151
101601
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
101152
- const jsonlLogPath = join18(projectRoot, ".ralph", "agent.log");
101602
+ const jsonlLogPath = join19(projectRoot, ".ralph", "agent.log");
101153
101603
  const jsonlLogFile = Bun.file(jsonlLogPath);
101154
101604
  const hasJsonlLog = await jsonlLogFile.exists();
101155
101605
  let { changeName, identifier: issueIdentifier } = await resolveDebugTarget(projectRoot, {
@@ -101163,7 +101613,7 @@ async function runDebug(opts) {
101163
101613
  }
101164
101614
  const jsonlLines = hasJsonlLog ? parseJsonlLog(await jsonlLogFile.text(), changeName) : [];
101165
101615
  const relevantText = textLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
101166
- const workerLogFile = Bun.file(join18(projectRoot, ".ralph", "logs", `${changeName}.log`));
101616
+ const workerLogFile = Bun.file(join19(projectRoot, ".ralph", "logs", `${changeName}.log`));
101167
101617
  const workerLines = await workerLogFile.exists() ? parseTextLog(await workerLogFile.text()) : [];
101168
101618
  const merged = [...relevantText, ...jsonlLines, ...workerLines].sort((a, b) => +a.ts - +b.ts);
101169
101619
  const seen = new Set;
@@ -101322,8 +101772,8 @@ async function runDebug(opts) {
101322
101772
  out(" \u26A0 PR currently has merge conflicts");
101323
101773
  if (pr?.checks.some((c) => c.conclusion === "FAILURE"))
101324
101774
  out(" \u26A0 PR has failing CI checks");
101325
- const worktreePath = join18(projectRoot, ".ralph", "worktrees", changeName);
101326
- if (await Bun.file(join18(worktreePath, ".git")).exists()) {
101775
+ const worktreePath = join19(projectRoot, ".ralph", "worktrees", changeName);
101776
+ if (await Bun.file(join19(worktreePath, ".git")).exists()) {
101327
101777
  out(` Worktree : ${worktreePath}`);
101328
101778
  }
101329
101779
  if (!timeline.length)
@@ -101343,12 +101793,12 @@ __export(exports_src2, {
101343
101793
  taskMain: () => taskMain,
101344
101794
  main: () => main2
101345
101795
  });
101346
- import { join as join19 } from "path";
101796
+ import { join as join20 } from "path";
101347
101797
  import { exists as exists2, mkdir as mkdir6, rm as rm2 } from "fs/promises";
101348
101798
  async function ensureRalphGitignore(projectRoot) {
101349
- const ralphDir = join19(projectRoot, ".ralph");
101799
+ const ralphDir = join20(projectRoot, ".ralph");
101350
101800
  await mkdir6(ralphDir, { recursive: true });
101351
- const gitignorePath = join19(ralphDir, ".gitignore");
101801
+ const gitignorePath = join20(ralphDir, ".gitignore");
101352
101802
  const file2 = Bun.file(gitignorePath);
101353
101803
  if (await file2.exists()) {
101354
101804
  const existing = await file2.text();
@@ -101390,14 +101840,14 @@ async function main2(argv) {
101390
101840
  await mkdir6(statesDir, { recursive: true });
101391
101841
  await ensureRalphGitignore(projectRoot);
101392
101842
  const { ensureWorkflow: ensureWorkflow2 } = await Promise.resolve().then(() => (init_workflow(), exports_workflow));
101393
- const workflowPath2 = await ensureWorkflow2(projectRoot);
101843
+ const workflowPath2 = await ensureWorkflow2(projectRoot, args.workflowFile);
101394
101844
  process.stdout.write(`Workflow config: ${workflowPath2}
101395
101845
  `);
101396
101846
  const openspecBin = resolveOpenspecBin(import.meta.dir);
101397
101847
  Bun.spawnSync({
101398
101848
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
101399
101849
  stdio: ["inherit", "inherit", "inherit"],
101400
- cwd: process.cwd()
101850
+ cwd: projectRoot
101401
101851
  });
101402
101852
  }
101403
101853
  if (args.mode === "debug") {
@@ -101415,9 +101865,9 @@ async function main2(argv) {
101415
101865
  `);
101416
101866
  return 1;
101417
101867
  }
101418
- const worktreeDir = join19(worktreesDir(projectRoot), args.name);
101419
- const changeDir = join19(tasksDir, args.name);
101420
- const stateDir = join19(statesDir, args.name);
101868
+ const worktreeDir = join20(worktreesDir(projectRoot), args.name);
101869
+ const changeDir = join20(tasksDir, args.name);
101870
+ const stateDir = join20(statesDir, args.name);
101421
101871
  const branch = `ralph/${args.name}`;
101422
101872
  const removed = [];
101423
101873
  if (await exists2(worktreeDir)) {
@@ -101468,8 +101918,8 @@ async function main2(argv) {
101468
101918
  return 0;
101469
101919
  }
101470
101920
  if (args.mode === "task" && args.name) {
101471
- await mkdir6(join19(statesDir, args.name), { recursive: true });
101472
- await mkdir6(join19(tasksDir, args.name), { recursive: true });
101921
+ await mkdir6(join20(statesDir, args.name), { recursive: true });
101922
+ await mkdir6(join20(tasksDir, args.name), { recursive: true });
101473
101923
  await ensureRalphGitignore(projectRoot);
101474
101924
  }
101475
101925
  await runWithContext(createDefaultContext({ layout, args }), async () => {
@@ -101497,8 +101947,8 @@ async function taskMain(argv) {
101497
101947
  const layout = projectLayout(projectRoot);
101498
101948
  const statesDir = layout.statesDir;
101499
101949
  const tasksDir = layout.tasksDir;
101500
- await mkdir6(join19(statesDir, args.name), { recursive: true });
101501
- await mkdir6(join19(tasksDir, args.name), { recursive: true });
101950
+ await mkdir6(join20(statesDir, args.name), { recursive: true });
101951
+ await mkdir6(join20(tasksDir, args.name), { recursive: true });
101502
101952
  await ensureRalphGitignore(projectRoot);
101503
101953
  await runWithContext(createDefaultContext({ layout, args }), async () => {
101504
101954
  const { waitUntilExit } = render_default(import_react60.createElement(App2, {
@@ -101741,6 +102191,7 @@ async function parseAgentArgs(argv) {
101741
102191
  }
101742
102192
  }
101743
102193
  await resolvePromptFile(result2, state);
102194
+ resolveWorkflowFile(result2, state);
101744
102195
  if (result2.fixCi && !result2.createPr) {
101745
102196
  throw new Error("--fix-ci requires --create-pr");
101746
102197
  }
@@ -101778,6 +102229,7 @@ var init_cli2 = __esm(() => {
101778
102229
  "",
101779
102230
  "Options:",
101780
102231
  " --name <id> Change name / ticket identifier (list / debug filter)",
102232
+ " --workflow <path> Path to an alternate WORKFLOW.md (default: <project>/WORKFLOW.md)",
101781
102233
  " --prompt <text> Task description appended to every scaffolded proposal",
101782
102234
  " --prompt-file <path> Read prompt from file",
101783
102235
  " --model <model> Set model (haiku|sonnet|opus)",
@@ -101838,12 +102290,12 @@ __export(exports_config, {
101838
102290
  loadRalphyConfig: () => loadRalphyConfig,
101839
102291
  ensureRalphyConfig: () => ensureRalphyConfig
101840
102292
  });
101841
- async function loadRalphyConfig(projectRoot) {
101842
- const { config: config2 } = await loadWorkflow(projectRoot);
102293
+ async function loadRalphyConfig(projectRoot, workflowFile) {
102294
+ const { config: config2 } = await loadWorkflow(projectRoot, workflowFile);
101843
102295
  return config2;
101844
102296
  }
101845
- async function ensureRalphyConfig(projectRoot) {
101846
- return ensureWorkflow(projectRoot);
102297
+ async function ensureRalphyConfig(projectRoot, workflowFile) {
102298
+ return ensureWorkflow(projectRoot, workflowFile);
101847
102299
  }
101848
102300
  var init_config = __esm(() => {
101849
102301
  init_workflow();
@@ -101897,7 +102349,7 @@ function formatError2(err) {
101897
102349
  }
101898
102350
 
101899
102351
  // apps/agent/src/shared/capabilities/fs-change.ts
101900
- import { join as join20, dirname as dirname8 } from "path";
102352
+ import { join as join21, dirname as dirname8 } from "path";
101901
102353
  import { mkdir as mkdir7 } from "fs/promises";
101902
102354
  var scaffold, prependTask, appendSteering, fsChange;
101903
102355
  var init_fs_change = __esm(() => {
@@ -101910,11 +102362,11 @@ var init_fs_change = __esm(() => {
101910
102362
  errorFormatter: formatError2,
101911
102363
  run: async (args) => {
101912
102364
  await mkdir7(args.changeDir, { recursive: true });
101913
- await mkdir7(join20(args.changeDir, "specs"), { recursive: true });
102365
+ await mkdir7(join21(args.changeDir, "specs"), { recursive: true });
101914
102366
  await mkdir7(args.stateDir, { recursive: true });
101915
- await Bun.write(join20(args.changeDir, "proposal.md"), args.proposal);
101916
- await Bun.write(join20(args.changeDir, "tasks.md"), args.tasks);
101917
- await Bun.write(join20(args.changeDir, "design.md"), args.design);
102367
+ await Bun.write(join21(args.changeDir, "proposal.md"), args.proposal);
102368
+ await Bun.write(join21(args.changeDir, "tasks.md"), args.tasks);
102369
+ await Bun.write(join21(args.changeDir, "design.md"), args.design);
101918
102370
  }
101919
102371
  };
101920
102372
  prependTask = {
@@ -101932,7 +102384,7 @@ var init_fs_change = __esm(() => {
101932
102384
  retryPolicy: NO_RETRY,
101933
102385
  errorFormatter: formatError2,
101934
102386
  run: async (args) => {
101935
- const path = join20(args.changeDir, "steering.md");
102387
+ const path = join21(args.changeDir, "steering.md");
101936
102388
  const f2 = Bun.file(path);
101937
102389
  const existing = await f2.exists() ? await f2.text() : null;
101938
102390
  const updated = existing ? `${args.message}
@@ -101947,11 +102399,11 @@ ${existing.trimStart()}` : `${args.message}
101947
102399
  });
101948
102400
 
101949
102401
  // apps/agent/src/agent/worktree.ts
101950
- import { basename as basename2, join as join21 } from "path";
102402
+ import { basename as basename2, join as join22 } from "path";
101951
102403
  import { homedir as homedir5 } from "os";
101952
102404
  import { exists as exists3 } from "fs/promises";
101953
102405
  function worktreesDir2(projectRoot) {
101954
- return join21(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
102406
+ return join22(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
101955
102407
  }
101956
102408
  function branchForChange(changeName) {
101957
102409
  return `ralph/${changeName}`;
@@ -101970,7 +102422,7 @@ function createWorktree(projectRoot, changeName, baseBranch, runner) {
101970
102422
  }
101971
102423
  async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
101972
102424
  const dir = worktreesDir2(projectRoot);
101973
- const cwd2 = join21(dir, changeName);
102425
+ const cwd2 = join22(dir, changeName);
101974
102426
  const branch = branchForChange(changeName);
101975
102427
  const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
101976
102428
  if (list.stdout.includes(`worktree ${cwd2}
@@ -101995,7 +102447,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
101995
102447
  return { cwd: cwd2, branch };
101996
102448
  }
101997
102449
  async function installPrePushHook(cwd2, runner) {
101998
- const hookPath = join21(cwd2, ".ralph-hooks", "pre-push");
102450
+ const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
101999
102451
  await Bun.write(hookPath, PRE_PUSH_HOOK_SCRIPT);
102000
102452
  const chmod = Bun.spawn(["chmod", "+x", hookPath]);
102001
102453
  await chmod.exited;
@@ -102041,8 +102493,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
102041
102493
  return { safe: true, dirty, unpushedCommits };
102042
102494
  }
102043
102495
  async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
102044
- const dst = join21(worktreeCwd, ".mcp.json");
102045
- const src = join21(projectRoot, ".mcp.json");
102496
+ const dst = join22(worktreeCwd, ".mcp.json");
102497
+ const src = join22(projectRoot, ".mcp.json");
102046
102498
  const source = await exists3(dst) ? dst : await exists3(src) ? src : null;
102047
102499
  if (!source)
102048
102500
  return;
@@ -102056,7 +102508,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
102056
102508
  if (servers && typeof servers === "object") {
102057
102509
  for (const cfg of Object.values(servers)) {
102058
102510
  if (Array.isArray(cfg.args)) {
102059
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join21(projectRoot, a) : a);
102511
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join22(projectRoot, a) : a);
102060
102512
  }
102061
102513
  }
102062
102514
  }
@@ -102150,7 +102602,7 @@ async function runCapability(cap, args, ctx = {}) {
102150
102602
  throw lastError;
102151
102603
  }
102152
102604
  function sleepMs(ms) {
102153
- return new Promise((resolve3) => setTimeout(resolve3, ms));
102605
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
102154
102606
  }
102155
102607
 
102156
102608
  // packages/workflow/src/boundaries.ts
@@ -103906,7 +104358,7 @@ function emitFeatureSkipped(bus, id, reason) {
103906
104358
  var init_run_feature = () => {};
103907
104359
 
103908
104360
  // apps/agent/src/agent/post-task.ts
103909
- import { join as join22, dirname as dirname9 } from "path";
104361
+ import { join as join23, dirname as dirname9 } from "path";
103910
104362
  function summarizeUncommittedStatus(stdout) {
103911
104363
  const lines = stdout.split(`
103912
104364
  `).filter((line) => line.length > 0);
@@ -103978,7 +104430,7 @@ async function reactivateState(stateFilePath, log3, changeName) {
103978
104430
  async function runWorkerWithFixTask(ctx, heading, body) {
103979
104431
  try {
103980
104432
  await runCapability(fsChange.prependTask, {
103981
- tasksPath: join22(ctx.changeDir, AGENT_TASKS_FILENAME),
104433
+ tasksPath: join23(ctx.changeDir, AGENT_TASKS_FILENAME),
103982
104434
  heading,
103983
104435
  failureOutput: body
103984
104436
  });
@@ -104529,7 +104981,7 @@ async function runValidateOnlyPhase(input, deps) {
104529
104981
  emit3("validate-fix", command);
104530
104982
  log3(`! validation check failed: ${command}`, "yellow");
104531
104983
  try {
104532
- await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104984
+ await prependFixTask(join23(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104533
104985
  } catch (err) {
104534
104986
  log3(`! could not prepend fix task: ${err.message}`, "red");
104535
104987
  return 1;
@@ -104540,7 +104992,7 @@ async function runValidateOnlyPhase(input, deps) {
104540
104992
  }
104541
104993
  }
104542
104994
  try {
104543
- await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
104995
+ await prependFixTask(join23(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
104544
104996
  `Run \`bunx openspec validate ${changeName}\` to validate the change artifacts.`,
104545
104997
  `Commit any pending changes before running the validation command.`
104546
104998
  ].join(`
@@ -104553,7 +105005,7 @@ async function runValidateOnlyPhase(input, deps) {
104553
105005
  return respawnWorker();
104554
105006
  }
104555
105007
  async function recordGaveUp(stateFilePath, log3, changeName) {
104556
- const path = join22(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
105008
+ const path = join23(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
104557
105009
  try {
104558
105010
  const file2 = Bun.file(path);
104559
105011
  const current = await file2.exists() ? Number.parseInt(await file2.text(), 10) || 0 : 0;
@@ -105960,15 +106412,15 @@ var init_coordinator2 = __esm(() => {
105960
106412
  });
105961
106413
 
105962
106414
  // apps/agent/src/agent/scaffold.ts
105963
- import { join as join23 } from "path";
106415
+ import { join as join24 } from "path";
105964
106416
  function changeNameForIssue(issue2) {
105965
106417
  const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
105966
106418
  return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
105967
106419
  }
105968
106420
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
105969
106421
  const name = changeNameForIssue(issue2);
105970
- const changeDir = join23(tasksDir, name);
105971
- const stateDir = join23(statesDir, name);
106422
+ const changeDir = join24(tasksDir, name);
106423
+ const stateDir = join24(statesDir, name);
105972
106424
  const commentsBlock = comments.length > 0 ? [
105973
106425
  "",
105974
106426
  "## Linear comments",
@@ -106104,7 +106556,7 @@ var init_detections = __esm(() => {
106104
106556
  });
106105
106557
 
106106
106558
  // apps/agent/src/features/confirmation/state.ts
106107
- import { dirname as dirname10, join as join24 } from "path";
106559
+ import { dirname as dirname10, join as join25 } from "path";
106108
106560
  async function readInlineConfirmation(statePath) {
106109
106561
  const f2 = Bun.file(statePath);
106110
106562
  if (!await f2.exists())
@@ -106143,8 +106595,8 @@ async function restartFromDesign(changeDir, changeName) {
106143
106595
  ""
106144
106596
  ].join(`
106145
106597
  `);
106146
- await Bun.write(join24(changeDir, "design.md"), designStub);
106147
- const tasksPath = join24(changeDir, "tasks.md");
106598
+ await Bun.write(join25(changeDir, "design.md"), designStub);
106599
+ const tasksPath = join25(changeDir, "tasks.md");
106148
106600
  if (await Bun.file(tasksPath).exists()) {
106149
106601
  await Bun.write(tasksPath, `# Tasks
106150
106602
 
@@ -106374,7 +106826,7 @@ var init_inspect = __esm(() => {
106374
106826
  });
106375
106827
 
106376
106828
  // apps/agent/src/features/confirmation/awaiting.ts
106377
- import { join as join25 } from "path";
106829
+ import { join as join26 } from "path";
106378
106830
  async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106379
106831
  const tracked = deps.cwdOf(changeName);
106380
106832
  if (tracked)
@@ -106382,12 +106834,12 @@ async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106382
106834
  if (!deps.useWorktree)
106383
106835
  return deps.projectRoot;
106384
106836
  const root = worktreesDir2(deps.projectRoot);
106385
- const canonical = join25(root, worktreeDirNameForIssue(issue2));
106386
- if (await Bun.file(join25(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106837
+ const canonical = join26(root, worktreeDirNameForIssue(issue2));
106838
+ if (await Bun.file(join26(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106387
106839
  return canonical;
106388
106840
  }
106389
- const legacy = join25(root, changeName);
106390
- if (await Bun.file(join25(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106841
+ const legacy = join26(root, changeName);
106842
+ if (await Bun.file(join26(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106391
106843
  return legacy;
106392
106844
  }
106393
106845
  return deps.projectRoot;
@@ -106522,9 +106974,9 @@ async function processAwaitingForIssue(issue2, deps) {
106522
106974
  const layout = projectLayout(cwd2);
106523
106975
  const changeDir = layout.changeDir(changeName);
106524
106976
  const statePath = layout.stateFile(changeName);
106525
- const tasks2 = await readTextOrNull(join25(changeDir, "tasks.md"));
106526
- const proposal = await readTextOrNull(join25(changeDir, "proposal.md"));
106527
- const design = await readTextOrNull(join25(changeDir, "design.md"));
106977
+ const tasks2 = await readTextOrNull(join26(changeDir, "tasks.md"));
106978
+ const proposal = await readTextOrNull(join26(changeDir, "proposal.md"));
106979
+ const design = await readTextOrNull(join26(changeDir, "design.md"));
106528
106980
  let commentsCache = null;
106529
106981
  const getComments = async () => {
106530
106982
  if (commentsCache)
@@ -107068,7 +107520,7 @@ var init_linear_resolvers = __esm(() => {
107068
107520
 
107069
107521
  // apps/agent/src/agent/wire/prepare.ts
107070
107522
  import { mkdir as mkdir8 } from "fs/promises";
107071
- import { join as join26 } from "path";
107523
+ import { join as join27 } from "path";
107072
107524
  function createPrepareHelpers(input) {
107073
107525
  const {
107074
107526
  args,
@@ -107132,7 +107584,7 @@ function createPrepareHelpers(input) {
107132
107584
  let changeName;
107133
107585
  const wtLayoutPre = projectLayout(workerCwd);
107134
107586
  const derivedName = changeNameForIssue(issue2);
107135
- const tasksMdPath = join26(wtLayoutPre.changeDir(derivedName), "tasks.md");
107587
+ const tasksMdPath = join27(wtLayoutPre.changeDir(derivedName), "tasks.md");
107136
107588
  const tasksMdExists = await Bun.file(tasksMdPath).exists();
107137
107589
  const isFresh = !tasksMdExists;
107138
107590
  if (isFresh) {
@@ -107150,7 +107602,7 @@ function createPrepareHelpers(input) {
107150
107602
  }
107151
107603
  let workflowPrompt = "";
107152
107604
  try {
107153
- const workflow = await loadWorkflow(projectRoot);
107605
+ const workflow = await loadWorkflow(projectRoot, args.workflowFile);
107154
107606
  workflowPrompt = renderWorkflowPrompt(workflow, {
107155
107607
  issue: {
107156
107608
  identifier: issue2.identifier,
@@ -107210,7 +107662,7 @@ function createPrepareHelpers(input) {
107210
107662
  if (!workerCwd)
107211
107663
  return;
107212
107664
  const wtLayout = projectLayout(workerCwd);
107213
- const tasksFile = join26(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107665
+ const tasksFile = join27(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107214
107666
  if (trigger === "review") {
107215
107667
  let body2;
107216
107668
  let heading;
@@ -107503,14 +107955,14 @@ var init_pr_discovery = __esm(() => {
107503
107955
  });
107504
107956
 
107505
107957
  // apps/agent/src/features/review-followup/scan.ts
107506
- import { dirname as dirname11, join as join27 } from "path";
107958
+ import { dirname as dirname11, join as join28 } from "path";
107507
107959
  async function resolveReviewStateDir(changeName, deps) {
107508
107960
  const root = deps.cwdOf(changeName);
107509
107961
  if (root)
107510
107962
  return dirname11(projectLayout(root).stateFile(changeName));
107511
107963
  if (!deps.useWorktree)
107512
107964
  return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
107513
- const wtPath = join27(worktreesDir2(deps.projectRoot), changeName);
107965
+ const wtPath = join28(worktreesDir2(deps.projectRoot), changeName);
107514
107966
  const statePath = projectLayout(wtPath).stateFile(changeName);
107515
107967
  if (await Bun.file(statePath).exists())
107516
107968
  return dirname11(statePath);
@@ -107520,7 +107972,7 @@ async function readReviewWatermark(stateDir) {
107520
107972
  const sidecar = await readSlotSidecar(stateDir, "review");
107521
107973
  if (sidecar)
107522
107974
  return sidecar.lastConsumedCommentAt ?? null;
107523
- const file2 = Bun.file(join27(stateDir, ".ralph-state.json"));
107975
+ const file2 = Bun.file(join28(stateDir, ".ralph-state.json"));
107524
107976
  if (!await file2.exists())
107525
107977
  return null;
107526
107978
  try {
@@ -107732,7 +108184,7 @@ var init_github = __esm(() => {
107732
108184
 
107733
108185
  // apps/agent/src/agent/wire/mention-scan.ts
107734
108186
  import { readdir as readdir2 } from "fs/promises";
107735
- import { join as join28 } from "path";
108187
+ import { join as join29 } from "path";
107736
108188
  function createMentionScanner(input) {
107737
108189
  const {
107738
108190
  apiKey,
@@ -107898,7 +108350,7 @@ function createMentionScanner(input) {
107898
108350
  async function isChangeArchivedForIssue(issue2, cwdByChange, projectRoot) {
107899
108351
  const changeName = changeNameForIssue(issue2);
107900
108352
  const root = cwdByChange.get(changeName) ?? projectRoot;
107901
- const archiveDir = join28(projectLayout(root).tasksDir, "archive");
108353
+ const archiveDir = join29(projectLayout(root).tasksDir, "archive");
107902
108354
  let entries;
107903
108355
  try {
107904
108356
  entries = await readdir2(archiveDir);
@@ -107922,9 +108374,9 @@ var init_mention_scan = __esm(() => {
107922
108374
  });
107923
108375
 
107924
108376
  // apps/agent/src/agent/wire/spawn/default.ts
107925
- import { join as join29 } from "path";
108377
+ import { join as join30 } from "path";
107926
108378
  function defaultSpawn(changeName, cmd, cwd2, logsDir, onWorkerOutput, note) {
107927
- const logFilePath = join29(logsDir, `${changeName}.log`);
108379
+ const logFilePath = join30(logsDir, `${changeName}.log`);
107928
108380
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
107929
108381
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
107930
108382
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -107985,16 +108437,16 @@ var init_default2 = __esm(() => {
107985
108437
  });
107986
108438
 
107987
108439
  // apps/agent/src/agent/state/agent-run-state.ts
107988
- import { basename as basename3, join as join30 } from "path";
108440
+ import { basename as basename3, join as join31 } from "path";
107989
108441
  import { homedir as homedir6 } from "os";
107990
108442
  import { mkdir as mkdir9, writeFile } from "fs/promises";
107991
108443
  function agentRunStatePath(projectRoot) {
107992
- return join30(homedir6(), ".ralph", basename3(projectRoot), "agent-state.json");
108444
+ return join31(homedir6(), ".ralph", basename3(projectRoot), "agent-state.json");
107993
108445
  }
107994
108446
  async function writeAgentRunState(state) {
107995
108447
  const path = agentRunStatePath(state.projectRoot);
107996
108448
  try {
107997
- await mkdir9(join30(homedir6(), ".ralph", basename3(state.projectRoot)), { recursive: true });
108449
+ await mkdir9(join31(homedir6(), ".ralph", basename3(state.projectRoot)), { recursive: true });
107998
108450
  await writeFile(path, JSON.stringify(state, null, 2) + `
107999
108451
  `, "utf-8");
108000
108452
  } catch {}
@@ -108021,17 +108473,17 @@ var CI_FAILED_EXIT2 = 70, PR_FAILED_EXIT2 = 71, NO_CHANGES_EXIT2 = 72;
108021
108473
  // packages/retro/src/paths.ts
108022
108474
  import { homedir as homedir7 } from "os";
108023
108475
  import { mkdir as mkdir10 } from "fs/promises";
108024
- import { join as join31 } from "path";
108476
+ import { join as join32 } from "path";
108025
108477
  function retroDir() {
108026
- return join31(homedir7(), ".ralph", "retro");
108478
+ return join32(homedir7(), ".ralph", "retro");
108027
108479
  }
108028
108480
  async function resolveRetroOutputPath(identifier, date5, dir = retroDir()) {
108029
108481
  await mkdir10(dir, { recursive: true });
108030
- const base2 = join31(dir, `${identifier}-${date5}.md`);
108482
+ const base2 = join32(dir, `${identifier}-${date5}.md`);
108031
108483
  if (!await Bun.file(base2).exists())
108032
108484
  return base2;
108033
108485
  for (let n = 2;; n++) {
108034
- const candidate = join31(dir, `${identifier}-${date5}-${n}.md`);
108486
+ const candidate = join32(dir, `${identifier}-${date5}-${n}.md`);
108035
108487
  if (!await Bun.file(candidate).exists())
108036
108488
  return candidate;
108037
108489
  }
@@ -108145,7 +108597,7 @@ var init_retro = __esm(() => {
108145
108597
  });
108146
108598
 
108147
108599
  // apps/agent/src/agent/wire/spawn/worker.ts
108148
- import { join as join32 } from "path";
108600
+ import { join as join33 } from "path";
108149
108601
  function localDateStamp(d) {
108150
108602
  const y = d.getFullYear();
108151
108603
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -108276,7 +108728,7 @@ function createSpawnWorker(input) {
108276
108728
  paths: {
108277
108729
  changeDir: info.changeDir,
108278
108730
  stateFilePath: info.stateFilePath,
108279
- logFile: join32(logsDir, `${info.changeName}.log`),
108731
+ logFile: join33(logsDir, `${info.changeName}.log`),
108280
108732
  jsonLogFile: args.jsonLogFile ?? null,
108281
108733
  agentStateFile: agentRunStatePath(projectRoot)
108282
108734
  }
@@ -108293,7 +108745,7 @@ function createSpawnWorker(input) {
108293
108745
  return function spawnWorker(changeName, _issue, trigger) {
108294
108746
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
108295
108747
  const injected = runners?.spawnWorker;
108296
- const missionTasksPath = join32(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
108748
+ const missionTasksPath = join33(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
108297
108749
  const prevTasksPromise = (async () => {
108298
108750
  const f2 = Bun.file(missionTasksPath);
108299
108751
  return await f2.exists() ? await f2.text() : "";
@@ -108301,7 +108753,7 @@ function createSpawnWorker(input) {
108301
108753
  let logFilePath;
108302
108754
  let handle;
108303
108755
  if (injected) {
108304
- logFilePath = join32(logsDir, `${changeName}.log`);
108756
+ logFilePath = join33(logsDir, `${changeName}.log`);
108305
108757
  handle = injected(buildTaskCmdFor(changeName), cwd2);
108306
108758
  } else {
108307
108759
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, logsDir, onWorkerOutput, `spawn at ${new Date().toISOString()}`);
@@ -108323,7 +108775,7 @@ function createSpawnWorker(input) {
108323
108775
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
108324
108776
  const wrapped = handle.exited.then(async (code) => {
108325
108777
  const workerLayout = projectLayout(cwd2);
108326
- const validateSpecPath = join32(workerLayout.changeDir(changeName), "specs", "validate.md");
108778
+ const validateSpecPath = join33(workerLayout.changeDir(changeName), "specs", "validate.md");
108327
108779
  const hasValidateSpec = await Bun.file(validateSpecPath).exists();
108328
108780
  const wantValidateOnly = hasValidateSpec && !wantPrBase;
108329
108781
  if (hasValidateSpec) {
@@ -108807,7 +109259,7 @@ var init_linear_sync = __esm(() => {
108807
109259
  });
108808
109260
 
108809
109261
  // apps/agent/src/agent/linear-sync/comment-sync.ts
108810
- import { dirname as dirname12, join as join33 } from "path";
109262
+ import { dirname as dirname12, join as join34 } from "path";
108811
109263
  async function readInlineLinearComments(statePath) {
108812
109264
  const file2 = Bun.file(statePath);
108813
109265
  if (!await file2.exists())
@@ -108848,7 +109300,7 @@ function isCommentNotFoundError(err) {
108848
109300
  return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
108849
109301
  }
108850
109302
  async function readTasksMd(changeDir, log3) {
108851
- const file2 = Bun.file(join33(changeDir, "tasks.md"));
109303
+ const file2 = Bun.file(join34(changeDir, "tasks.md"));
108852
109304
  if (!await file2.exists()) {
108853
109305
  log3(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
108854
109306
  return null;
@@ -108954,14 +109406,14 @@ async function postPlanCommentOnce(deps) {
108954
109406
  const check2 = parsePlanningSection(tasksMd);
108955
109407
  if (!check2.allChecked)
108956
109408
  return null;
108957
- const proposalPath = join33(deps.changeDir, "proposal.md");
109409
+ const proposalPath = join34(deps.changeDir, "proposal.md");
108958
109410
  const why = await readSection(proposalPath, "Why");
108959
109411
  const whatChanges = await readSection(proposalPath, "What Changes");
108960
109412
  if (!why && !whatChanges) {
108961
109413
  deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
108962
109414
  return null;
108963
109415
  }
108964
- const designSummary = await readFirstParagraph(join33(deps.changeDir, "design.md"));
109416
+ const designSummary = await readFirstParagraph(join34(deps.changeDir, "design.md"));
108965
109417
  const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
108966
109418
  if (why) {
108967
109419
  parts.push("", "**Why**", "", why);
@@ -111615,11 +112067,11 @@ var require_tslib = __commonJS((exports, module) => {
111615
112067
  };
111616
112068
  __awaiter = function(thisArg, _arguments, P, generator) {
111617
112069
  function adopt(value) {
111618
- return value instanceof P ? value : new P(function(resolve3) {
111619
- resolve3(value);
112070
+ return value instanceof P ? value : new P(function(resolve4) {
112071
+ resolve4(value);
111620
112072
  });
111621
112073
  }
111622
- return new (P || (P = Promise))(function(resolve3, reject2) {
112074
+ return new (P || (P = Promise))(function(resolve4, reject2) {
111623
112075
  function fulfilled(value) {
111624
112076
  try {
111625
112077
  step(generator.next(value));
@@ -111635,7 +112087,7 @@ var require_tslib = __commonJS((exports, module) => {
111635
112087
  }
111636
112088
  }
111637
112089
  function step(result2) {
111638
- result2.done ? resolve3(result2.value) : adopt(result2.value).then(fulfilled, rejected);
112090
+ result2.done ? resolve4(result2.value) : adopt(result2.value).then(fulfilled, rejected);
111639
112091
  }
111640
112092
  step((generator = generator.apply(thisArg, _arguments || [])).next());
111641
112093
  });
@@ -111864,14 +112316,14 @@ var require_tslib = __commonJS((exports, module) => {
111864
112316
  }, i);
111865
112317
  function verb(n) {
111866
112318
  i[n] = o[n] && function(v) {
111867
- return new Promise(function(resolve3, reject2) {
111868
- v = o[n](v), settle(resolve3, reject2, v.done, v.value);
112319
+ return new Promise(function(resolve4, reject2) {
112320
+ v = o[n](v), settle(resolve4, reject2, v.done, v.value);
111869
112321
  });
111870
112322
  };
111871
112323
  }
111872
- function settle(resolve3, reject2, d, v) {
112324
+ function settle(resolve4, reject2, d, v) {
111873
112325
  Promise.resolve(v).then(function(v2) {
111874
- resolve3({ value: v2, done: d });
112326
+ resolve4({ value: v2, done: d });
111875
112327
  }, reject2);
111876
112328
  }
111877
112329
  };
@@ -112734,9 +113186,9 @@ var require_clone = __commonJS((exports, module) => {
112734
113186
  } else if (_instanceof2(parent2, nativeSet)) {
112735
113187
  child = new nativeSet;
112736
113188
  } else if (_instanceof2(parent2, nativePromise)) {
112737
- child = new nativePromise(function(resolve3, reject2) {
113189
+ child = new nativePromise(function(resolve4, reject2) {
112738
113190
  parent2.then(function(value) {
112739
- resolve3(_clone(value, depth2 - 1));
113191
+ resolve4(_clone(value, depth2 - 1));
112740
113192
  }, function(err) {
112741
113193
  reject2(_clone(err, depth2 - 1));
112742
113194
  });
@@ -260785,7 +261237,7 @@ function toPdfSafe(text) {
260785
261237
  return out;
260786
261238
  }
260787
261239
  function renderMarkdownToPdf(md, title) {
260788
- return new Promise((resolve3, reject2) => {
261240
+ return new Promise((resolve4, reject2) => {
260789
261241
  try {
260790
261242
  const doc2 = new PDFDocument({
260791
261243
  size: PAGE_SIZE,
@@ -260794,7 +261246,7 @@ function renderMarkdownToPdf(md, title) {
260794
261246
  });
260795
261247
  const chunks = [];
260796
261248
  doc2.on("data", (chunk2) => chunks.push(chunk2));
260797
- doc2.on("end", () => resolve3(new Uint8Array(Buffer.concat(chunks))));
261249
+ doc2.on("end", () => resolve4(new Uint8Array(Buffer.concat(chunks))));
260798
261250
  doc2.on("error", reject2);
260799
261251
  doc2.fillColor(COLOR_TEXT).font(FONT_BODY).fontSize(BODY_SIZE);
260800
261252
  const tokens = g.lexer(md);
@@ -261237,7 +261689,7 @@ var init_render_pdf = __esm(() => {
261237
261689
  });
261238
261690
 
261239
261691
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
261240
- import { dirname as dirname13, join as join34 } from "path";
261692
+ import { dirname as dirname13, join as join35 } from "path";
261241
261693
  function describeLinearError(err) {
261242
261694
  const e = err;
261243
261695
  const parts = [e.message ?? String(err)];
@@ -261346,7 +261798,7 @@ async function syncSlot(deps, slot) {
261346
261798
  const [primaryName, ...trailingNames] = spec.sourceFiles;
261347
261799
  if (!primaryName)
261348
261800
  return;
261349
- const primary = Bun.file(join34(deps.changeDir, primaryName));
261801
+ const primary = Bun.file(join35(deps.changeDir, primaryName));
261350
261802
  if (!await primary.exists()) {
261351
261803
  deps.log(` spec-attachments: ${primaryName} missing, skipping`, "gray");
261352
261804
  return;
@@ -261365,7 +261817,7 @@ async function syncSlot(deps, slot) {
261365
261817
  const parts = [primaryBytes];
261366
261818
  const enc = new TextEncoder;
261367
261819
  for (const name of trailingNames) {
261368
- const f2 = Bun.file(join34(deps.changeDir, name));
261820
+ const f2 = Bun.file(join35(deps.changeDir, name));
261369
261821
  if (!await f2.exists())
261370
261822
  continue;
261371
261823
  let raw;
@@ -261633,9 +262085,9 @@ var init_comment_sync2 = __esm(() => {
261633
262085
  });
261634
262086
 
261635
262087
  // apps/agent/src/features/pr-tracker/state.ts
261636
- import { join as join35 } from "path";
262088
+ import { join as join36 } from "path";
261637
262089
  async function readState2(projectRoot) {
261638
- const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
262090
+ const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
261639
262091
  const file2 = Bun.file(path);
261640
262092
  if (!await file2.exists())
261641
262093
  return {};
@@ -261651,7 +262103,7 @@ async function readState2(projectRoot) {
261651
262103
  }
261652
262104
  }
261653
262105
  async function writeState2(projectRoot, state) {
261654
- const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
262106
+ const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
261655
262107
  await Bun.write(path, JSON.stringify(state, null, 2));
261656
262108
  }
261657
262109
  var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
@@ -261730,7 +262182,7 @@ var init_pr_tracker = __esm(() => {
261730
262182
  });
261731
262183
 
261732
262184
  // apps/agent/src/agent/wire.ts
261733
- import { join as join36 } from "path";
262185
+ import { join as join37 } from "path";
261734
262186
  function buildAgentCoordinator(input) {
261735
262187
  const {
261736
262188
  args,
@@ -261749,7 +262201,7 @@ function buildAgentCoordinator(input) {
261749
262201
  onWorkerCmd,
261750
262202
  onAwaitingTicket
261751
262203
  } = input;
261752
- const logsDir = join36(projectRoot, ".ralph", "logs");
262204
+ const logsDir = join37(projectRoot, ".ralph", "logs");
261753
262205
  const bus = createBus();
261754
262206
  subscribeAgentDiag(bus, onLog);
261755
262207
  const diag = (area, message, color) => {
@@ -261775,7 +262227,11 @@ function buildAgentCoordinator(input) {
261775
262227
  const awaitingChangeSet = new Set;
261776
262228
  const coordRef = { current: null };
261777
262229
  let pollContext = new PollContext;
261778
- const useWorktree = args.worktree || cfg.useWorktree;
262230
+ let useWorktree = args.worktree || cfg.useWorktree;
262231
+ if (concurrency > 1 && !useWorktree) {
262232
+ diag("config", `! concurrency is ${concurrency} but useWorktree is off \u2014 forcing worktrees on so parallel tasks get isolated working copies`, "yellow");
262233
+ useWorktree = true;
262234
+ }
261779
262235
  const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
261780
262236
  const proc = Bun.spawn({
261781
262237
  cmd: ["sh", "-c", cmd],
@@ -261980,7 +262436,7 @@ function buildAgentCoordinator(input) {
261980
262436
  const changeDir = projectLayout(root).changeDir(changeName);
261981
262437
  const parts = [];
261982
262438
  for (const name of ["tasks.md", "proposal.md", "design.md"]) {
261983
- const file2 = Bun.file(join36(changeDir, name));
262439
+ const file2 = Bun.file(join37(changeDir, name));
261984
262440
  if (!await file2.exists())
261985
262441
  continue;
261986
262442
  parts.push(`${name}:${file2.lastModified}:${file2.size}`);
@@ -262025,7 +262481,7 @@ function buildAgentCoordinator(input) {
262025
262481
  getGaveUpTotal: async () => {
262026
262482
  let total = 0;
262027
262483
  for (const [changeName, root] of cwdByChange) {
262028
- const file2 = Bun.file(join36(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
262484
+ const file2 = Bun.file(join37(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
262029
262485
  if (!await file2.exists())
262030
262486
  continue;
262031
262487
  try {
@@ -262092,7 +262548,7 @@ async function waitForActiveWorkers(deps) {
262092
262548
  const budgetMs = deps.budgetMs ?? 1e4;
262093
262549
  const warnAtMs = deps.warnAtMs ?? 5000;
262094
262550
  deps.stop();
262095
- await new Promise((resolve3) => {
262551
+ await new Promise((resolve4) => {
262096
262552
  const start = Date.now();
262097
262553
  let warned = false;
262098
262554
  const wait = setInterval(() => {
@@ -262100,7 +262556,7 @@ async function waitForActiveWorkers(deps) {
262100
262556
  const elapsed = Date.now() - start;
262101
262557
  if (active === 0) {
262102
262558
  clearInterval(wait);
262103
- resolve3();
262559
+ resolve4();
262104
262560
  return;
262105
262561
  }
262106
262562
  if (!warned && elapsed >= warnAtMs) {
@@ -262110,7 +262566,7 @@ async function waitForActiveWorkers(deps) {
262110
262566
  if (elapsed >= budgetMs) {
262111
262567
  clearInterval(wait);
262112
262568
  deps.onTimeout?.(active);
262113
- resolve3();
262569
+ resolve4();
262114
262570
  }
262115
262571
  }, 100);
262116
262572
  });
@@ -262313,7 +262769,7 @@ var init_output_utils = __esm(() => {
262313
262769
  });
262314
262770
 
262315
262771
  // apps/agent/src/agent/state/worker-state-poll.ts
262316
- import { join as join37 } from "path";
262772
+ import { join as join38 } from "path";
262317
262773
  function parseSubtasks(tasksMd) {
262318
262774
  const out = [];
262319
262775
  let skipSection = false;
@@ -262346,7 +262802,7 @@ function initialWorkerSnapshot() {
262346
262802
  async function readWorkerSnapshot(input) {
262347
262803
  const next = { ...input.prev };
262348
262804
  try {
262349
- const file2 = Bun.file(join37(input.statesDir, input.changeName, ".ralph-state.json"));
262805
+ const file2 = Bun.file(join38(input.statesDir, input.changeName, ".ralph-state.json"));
262350
262806
  if (await file2.exists()) {
262351
262807
  const json2 = await file2.json();
262352
262808
  next.iter = json2.iteration ?? next.iter;
@@ -262355,10 +262811,10 @@ async function readWorkerSnapshot(input) {
262355
262811
  } catch {}
262356
262812
  if (input.changeDir) {
262357
262813
  try {
262358
- const tasksFile = Bun.file(join37(input.changeDir, "tasks.md"));
262359
- const proposalFile = Bun.file(join37(input.changeDir, "proposal.md"));
262360
- const designFile = Bun.file(join37(input.changeDir, "design.md"));
262361
- const reviewFindingsFile = Bun.file(join37(input.changeDir, "review-findings.md"));
262814
+ const tasksFile = Bun.file(join38(input.changeDir, "tasks.md"));
262815
+ const proposalFile = Bun.file(join38(input.changeDir, "proposal.md"));
262816
+ const designFile = Bun.file(join38(input.changeDir, "design.md"));
262817
+ const reviewFindingsFile = Bun.file(join38(input.changeDir, "review-findings.md"));
262362
262818
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
262363
262819
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
262364
262820
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -262421,7 +262877,7 @@ var init_worker_state_poll = __esm(() => {
262421
262877
  });
262422
262878
 
262423
262879
  // apps/agent/src/components/AgentMode.tsx
262424
- import { join as join38 } from "path";
262880
+ import { join as join39 } from "path";
262425
262881
  async function appendSteeringImpl(changeDir, message) {
262426
262882
  await runWithContext(createDefaultContext(), async () => {
262427
262883
  appendSteeringMessage(changeDir, message);
@@ -262695,8 +263151,8 @@ function AgentMode({
262695
263151
  let cancelled = false;
262696
263152
  async function init2() {
262697
263153
  logSession(`=== session start ${SESSION_START} ===`);
262698
- const cfgPath = await ensureConfig(projectRoot);
262699
- const cfg2 = await loadConfig(projectRoot);
263154
+ const cfgPath = await ensureConfig(projectRoot, args.workflowFile);
263155
+ const cfg2 = await loadConfig(projectRoot, args.workflowFile);
262700
263156
  cfgRef.current = cfg2;
262701
263157
  appendLog(`agent mode v${VERSION} \u2014 config: ${cfgPath}`, "gray");
262702
263158
  const apiKey = process.env["LINEAR_API_KEY"];
@@ -263994,7 +264450,7 @@ function AgentMode({
263994
264450
  },
263995
264451
  onSubmit: async (message) => {
263996
264452
  try {
263997
- await appendSteering2(join38(tasksDir, w2.changeName), message);
264453
+ await appendSteering2(join39(tasksDir, w2.changeName), message);
263998
264454
  fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
263999
264455
  } catch (err) {
264000
264456
  const text = err.message;
@@ -264298,7 +264754,7 @@ __export(exports_list, {
264298
264754
  buildBuckets: () => buildBuckets,
264299
264755
  backlogRankByIssueId: () => backlogRankByIssueId
264300
264756
  });
264301
- import { join as join39 } from "path";
264757
+ import { join as join40 } from "path";
264302
264758
  function countTaskItems(content) {
264303
264759
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
264304
264760
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -264314,13 +264770,13 @@ function buildLocalRows() {
264314
264770
  const sources = [{ dir: statesDir, label: "main" }];
264315
264771
  const worktreesRoot = worktreesDir2(projectRoot);
264316
264772
  for (const wt of storage.list(worktreesRoot)) {
264317
- sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
264773
+ sources.push({ dir: join40(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
264318
264774
  }
264319
264775
  for (const { dir, label } of sources) {
264320
264776
  for (const entry of storage.list(dir)) {
264321
264777
  if (seen.has(entry))
264322
264778
  continue;
264323
- const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
264779
+ const raw = storage.read(join40(dir, entry, ".ralph-state.json"));
264324
264780
  if (raw === null)
264325
264781
  continue;
264326
264782
  let state;
@@ -264335,7 +264791,7 @@ function buildLocalRows() {
264335
264791
  const firstLine = promptRaw.split(`
264336
264792
  `).find((l3) => l3.trim() !== "") ?? "";
264337
264793
  let progress = "\u2014";
264338
- const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
264794
+ const tasksContent = storage.read(join40(dir, entry, "tasks.md"));
264339
264795
  if (tasksContent !== null) {
264340
264796
  const { checked, unchecked } = countTaskItems(tasksContent);
264341
264797
  const total = checked + unchecked;
@@ -264600,7 +265056,7 @@ async function runList(input) {
264600
265056
  }
264601
265057
  const rows = buildLocalRows();
264602
265058
  printLocalRows(rows);
264603
- const cfg = await loadRalphyConfig(projectRoot);
265059
+ const cfg = await loadRalphyConfig(projectRoot, getArgs().workflowFile);
264604
265060
  const apiKey = process.env["LINEAR_API_KEY"];
264605
265061
  const indicators = cfg.linear.indicators;
264606
265062
  const team = input.linearTeamOverride || cfg.linear.team;
@@ -264724,7 +265180,7 @@ async function runListDebug(input) {
264724
265180
  process.exitCode = 1;
264725
265181
  return;
264726
265182
  }
264727
- const cfg = await loadRalphyConfig(projectRoot);
265183
+ const cfg = await loadRalphyConfig(projectRoot, getArgs().workflowFile);
264728
265184
  const indicators = cfg.linear.indicators;
264729
265185
  const team = input.linearTeamOverride || cfg.linear.team;
264730
265186
  const { assignee, anyAssignee } = resolveLinearFilter(input.linearFilterOverride, input.linearAssigneeOverride, cfg.linear.filter);
@@ -264835,7 +265291,7 @@ var exports_json_runner = {};
264835
265291
  __export(exports_json_runner, {
264836
265292
  runAgentJson: () => runAgentJson
264837
265293
  });
264838
- import { join as join40 } from "path";
265294
+ import { join as join41 } from "path";
264839
265295
  import { mkdir as mkdir12 } from "fs/promises";
264840
265296
  import { homedir as homedir8 } from "os";
264841
265297
  function makeEmit(fileSink) {
@@ -264857,13 +265313,13 @@ async function runAgentJson({
264857
265313
  tasksDir,
264858
265314
  runPreflight: runPreflight2 = runPreflight
264859
265315
  }) {
264860
- await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
265316
+ await mkdir12(join41(homedir8(), ".ralph"), { recursive: true }).catch(() => {
264861
265317
  return;
264862
265318
  });
264863
265319
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
264864
265320
  const emit3 = makeEmit(fileSink);
264865
- const cfgPath = await ensureRalphyConfig(projectRoot);
264866
- const cfg = await loadRalphyConfig(projectRoot);
265321
+ const cfgPath = await ensureRalphyConfig(projectRoot, args.workflowFile);
265322
+ const cfg = await loadRalphyConfig(projectRoot, args.workflowFile);
264867
265323
  await writeAgentRunState({
264868
265324
  projectRoot,
264869
265325
  configPath: cfgPath,
@@ -265020,7 +265476,7 @@ async function runAgentJson({
265020
265476
  }
265021
265477
  })();
265022
265478
  }, 1000);
265023
- await new Promise((resolve3) => {
265479
+ await new Promise((resolve4) => {
265024
265480
  let shuttingDown = false;
265025
265481
  const onSig = () => {
265026
265482
  if (shuttingDown) {
@@ -265050,7 +265506,7 @@ async function runAgentJson({
265050
265506
  });
265051
265507
  setTimeout(() => process.exit(1), 50);
265052
265508
  }
265053
- }).then(() => resolve3());
265509
+ }).then(() => resolve4());
265054
265510
  };
265055
265511
  process.once("SIGINT", onSig);
265056
265512
  process.once("SIGTERM", onSig);
@@ -265074,7 +265530,7 @@ __export(exports_src3, {
265074
265530
  main: () => main3
265075
265531
  });
265076
265532
  import { mkdir as mkdir13 } from "fs/promises";
265077
- import { join as join41 } from "path";
265533
+ import { join as join42 } from "path";
265078
265534
  async function main3(argv) {
265079
265535
  if (argv.includes("--help") || argv.includes("-h")) {
265080
265536
  printAgentHelp();
@@ -265128,7 +265584,7 @@ async function main3(argv) {
265128
265584
  if (args.ticketTokens.length > 0) {
265129
265585
  const { loadRalphyConfig: loadRalphyConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
265130
265586
  const { resolveTicketNumbers: resolveTicketNumbers2, formatTicketError: formatTicketError2 } = await Promise.resolve().then(() => (init_linear_client(), exports_linear_client));
265131
- const cfg = await loadRalphyConfig2(projectRoot);
265587
+ const cfg = await loadRalphyConfig2(projectRoot, args.workflowFile);
265132
265588
  const team = args.linearTeam || cfg.linear.team;
265133
265589
  try {
265134
265590
  resolveTicketNumbers2(args.ticketTokens, team);
@@ -265140,7 +265596,7 @@ async function main3(argv) {
265140
265596
  }
265141
265597
  await mkdir13(statesDir, { recursive: true });
265142
265598
  await mkdir13(tasksDir, { recursive: true });
265143
- await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
265599
+ await mkdir13(join42(projectRoot, ".ralph"), { recursive: true });
265144
265600
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
265145
265601
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
265146
265602
  `);
@@ -265198,6 +265654,7 @@ var init_src8 = __esm(async () => {
265198
265654
  init_src();
265199
265655
  init_src2();
265200
265656
  init_version();
265657
+ init_common_args();
265201
265658
  if (typeof globalThis.Bun === "undefined") {
265202
265659
  process.stderr.write(`ralphy requires the Bun runtime (https://bun.sh/). It is not compatible with plain Node.js.
265203
265660
  ` + "Install Bun and re-run with `bun` or `bunx ralphy`.\n");
@@ -265282,7 +265739,8 @@ ${HELP}
265282
265739
  if (shouldOfferSetup(subcommand, argv.slice(1))) {
265283
265740
  try {
265284
265741
  const { maybeRunSetupWizard: maybeRunSetupWizard2 } = await init_src4().then(() => exports_src);
265285
- await maybeRunSetupWizard2();
265742
+ const { projectRoot, workflowFile } = parseWorkflowPathArgs(argv.slice(1));
265743
+ await maybeRunSetupWizard2(projectRoot, workflowFile);
265286
265744
  } catch (setupErr) {
265287
265745
  captureError("setup_wizard_error", setupErr, { subcommand });
265288
265746
  }