@neriros/ralphy 3.10.9 → 3.10.11

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.
@@ -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.9")
18932
- return "3.10.9";
18931
+ if ("3.10.11")
18932
+ return "3.10.11";
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}
@@ -81909,7 +82228,7 @@ function applyAliases(cfg) {
81909
82228
  function workflowPath(projectRoot, workflowFile) {
81910
82229
  return workflowFile ?? join5(projectRoot, WORKFLOW_FILE);
81911
82230
  }
81912
- async function loadWorkflow(projectRoot, workflowFile) {
82231
+ async function loadWorkflow(projectRoot, workflowFile, options = {}) {
81913
82232
  const path = workflowPath(projectRoot, workflowFile);
81914
82233
  const file2 = Bun.file(path);
81915
82234
  if (!await file2.exists()) {
@@ -81917,7 +82236,10 @@ async function loadWorkflow(projectRoot, workflowFile) {
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
82244
  async function ensureWorkflow(projectRoot, workflowFile) {
81923
82245
  const path = workflowPath(projectRoot, workflowFile);
@@ -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
  });
@@ -84380,18 +84985,18 @@ var init_context = __esm(() => {
84380
84985
  });
84381
84986
 
84382
84987
  // packages/core/src/layout.ts
84383
- import { join as join6 } from "path";
84988
+ import { join as join7 } from "path";
84384
84989
  function projectLayout(root) {
84385
- const statesDir = join6(root, ".ralph", "tasks");
84386
- const tasksDir = join6(root, "openspec", "changes");
84990
+ const statesDir = join7(root, ".ralph", "tasks");
84991
+ const tasksDir = join7(root, "openspec", "changes");
84387
84992
  return {
84388
84993
  root,
84389
84994
  statesDir,
84390
84995
  tasksDir,
84391
- agentStateFile: join6(root, ".ralph", "agent-state.json"),
84392
- changeDir: (name) => join6(tasksDir, name),
84393
- taskStateDir: (name) => join6(statesDir, name),
84394
- 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)
84395
85000
  };
84396
85001
  }
84397
85002
  var STATE_FILE = ".ralph-state.json", GAVEUP_COUNT_FILE = ".ralph-gaveup-count";
@@ -84400,13 +85005,13 @@ var init_layout = __esm(() => {
84400
85005
  });
84401
85006
 
84402
85007
  // packages/openspec/src/openspec-bin.ts
84403
- import { dirname as dirname3, join as join7 } from "path";
85008
+ import { dirname as dirname3, join as join8 } from "path";
84404
85009
  function findPackageRoot(startDir) {
84405
85010
  let dir = startDir;
84406
85011
  for (let i = 0;i < 8; i++) {
84407
- if (Bun.file(join7(dir, "package.json")).size >= 0) {
85012
+ if (Bun.file(join8(dir, "package.json")).size >= 0) {
84408
85013
  try {
84409
- if (Bun.file(join7(dir, "package.json")).size > 0)
85014
+ if (Bun.file(join8(dir, "package.json")).size > 0)
84410
85015
  return dir;
84411
85016
  } catch {}
84412
85017
  }
@@ -84442,11 +85047,11 @@ function ensureOpenspecInstalled(fromDir, runner) {
84442
85047
  function resolveOpenspecBin(fromDir, runner = bunInstallRunner) {
84443
85048
  try {
84444
85049
  const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
84445
- return join7(dirname3(pkgJsonPath), "bin", "openspec.js");
85050
+ return join8(dirname3(pkgJsonPath), "bin", "openspec.js");
84446
85051
  } catch {
84447
85052
  ensureOpenspecInstalled(fromDir, runner);
84448
85053
  const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
84449
- return join7(dirname3(pkgJsonPath), "bin", "openspec.js");
85054
+ return join8(dirname3(pkgJsonPath), "bin", "openspec.js");
84450
85055
  }
84451
85056
  }
84452
85057
  var bunInstallRunner;
@@ -84468,7 +85073,7 @@ var init_openspec_bin = __esm(() => {
84468
85073
  });
84469
85074
 
84470
85075
  // packages/openspec/src/openspec-change-store.ts
84471
- import { dirname as dirname4, join as join8 } from "path";
85076
+ import { dirname as dirname4, join as join9 } from "path";
84472
85077
  import { readdir, mkdir as mkdir2 } from "fs/promises";
84473
85078
  function runOpenspec(args, options = {}) {
84474
85079
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -84538,7 +85143,7 @@ class OpenSpecChangeStore {
84538
85143
  }
84539
85144
  }
84540
85145
  getChangeDirectory(name) {
84541
- return join8("openspec", "changes", name);
85146
+ return join9("openspec", "changes", name);
84542
85147
  }
84543
85148
  async listChanges() {
84544
85149
  const result2 = runOpenspec(["list", "--json"]);
@@ -84552,7 +85157,7 @@ class OpenSpecChangeStore {
84552
85157
  }
84553
85158
  } catch {}
84554
85159
  }
84555
- const changesDir = join8("openspec", "changes");
85160
+ const changesDir = join9("openspec", "changes");
84556
85161
  try {
84557
85162
  const entries = await readdir(changesDir, { withFileTypes: true });
84558
85163
  return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
@@ -84561,18 +85166,18 @@ class OpenSpecChangeStore {
84561
85166
  }
84562
85167
  }
84563
85168
  async readTaskList(name) {
84564
- const file2 = Bun.file(join8("openspec", "changes", name, "tasks.md"));
85169
+ const file2 = Bun.file(join9("openspec", "changes", name, "tasks.md"));
84565
85170
  if (!await file2.exists())
84566
85171
  return "";
84567
85172
  return await file2.text();
84568
85173
  }
84569
85174
  async writeTaskList(name, content) {
84570
- const path = join8("openspec", "changes", name, "tasks.md");
85175
+ const path = join9("openspec", "changes", name, "tasks.md");
84571
85176
  await mkdir2(dirname4(path), { recursive: true });
84572
85177
  await Bun.write(path, content);
84573
85178
  }
84574
85179
  async appendSteering(name, message) {
84575
- const path = join8("openspec", "changes", name, "steering.md");
85180
+ const path = join9("openspec", "changes", name, "steering.md");
84576
85181
  const file2 = Bun.file(path);
84577
85182
  const existing = await file2.exists() ? await file2.text() : null;
84578
85183
  const updated = existing ? `${message}
@@ -84584,7 +85189,7 @@ ${existing.trimStart()}` : `${message}
84584
85189
  const firstLine = message.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? message.trim();
84585
85190
  if (firstLine.length === 0)
84586
85191
  return;
84587
- const tasksPath = join8("openspec", "changes", name, "tasks.md");
85192
+ const tasksPath = join9("openspec", "changes", name, "tasks.md");
84588
85193
  const tasksFile = Bun.file(tasksPath);
84589
85194
  const existingTasks = await tasksFile.exists() ? await tasksFile.text() : "";
84590
85195
  const taskLine = `- [ ] Address steering: ${firstLine}`;
@@ -84717,182 +85322,6 @@ var init_output2 = __esm(() => {
84717
85322
  };
84718
85323
  });
84719
85324
 
84720
- // packages/cli-args/src/common-args.ts
84721
- import { resolve as resolve3 } from "path";
84722
- function initialCommonArgs() {
84723
- return {
84724
- engine: "claude",
84725
- model: "opus",
84726
- engineSet: false,
84727
- maxIterations: 0,
84728
- maxCostUsd: 0,
84729
- maxRuntimeMinutes: 0,
84730
- maxConsecutiveFailures: 5,
84731
- delay: 0,
84732
- log: false,
84733
- verbose: false,
84734
- projectRoot: undefined,
84735
- workflowFile: undefined,
84736
- name: "",
84737
- prompt: "",
84738
- fromAgent: false
84739
- };
84740
- }
84741
- function applyValueOption(option, args, raw) {
84742
- const setter = VALUE_SETTERS[option.argKey];
84743
- if (!setter)
84744
- throw new Error("no value setter registered for CLI option");
84745
- setter(args, raw);
84746
- }
84747
- function applyBooleanOption(option, args) {
84748
- const setter = BOOLEAN_SETTERS[option.argKey];
84749
- if (!setter)
84750
- throw new Error("no boolean setter registered for CLI option");
84751
- setter(args);
84752
- }
84753
- function emptyParseState() {
84754
- return {
84755
- pendingOption: null,
84756
- expectClaudeModel: false,
84757
- expectProjectRoot: false,
84758
- expectWorkflow: false,
84759
- expectName: false,
84760
- expectPrompt: false,
84761
- expectPromptFile: false,
84762
- promptFilePath: null
84763
- };
84764
- }
84765
- function parseCommonArg(arg, args, state) {
84766
- if (state.pendingOption) {
84767
- applyValueOption(state.pendingOption, args, arg);
84768
- state.pendingOption = null;
84769
- return true;
84770
- }
84771
- if (state.expectClaudeModel) {
84772
- state.expectClaudeModel = false;
84773
- if (VALID_MODELS.has(arg)) {
84774
- args.model = arg;
84775
- return true;
84776
- }
84777
- }
84778
- if (state.expectProjectRoot) {
84779
- args.projectRoot = arg;
84780
- state.expectProjectRoot = false;
84781
- return true;
84782
- }
84783
- if (state.expectWorkflow) {
84784
- args.workflowFile = resolve3(arg);
84785
- state.expectWorkflow = false;
84786
- return true;
84787
- }
84788
- if (state.expectName) {
84789
- args.name = arg;
84790
- state.expectName = false;
84791
- return true;
84792
- }
84793
- if (state.expectPrompt) {
84794
- args.prompt = arg;
84795
- state.promptFilePath = null;
84796
- state.expectPrompt = false;
84797
- return true;
84798
- }
84799
- if (state.expectPromptFile) {
84800
- state.promptFilePath = arg;
84801
- state.expectPromptFile = false;
84802
- return true;
84803
- }
84804
- const option = OPTION_BY_FLAG.get(arg);
84805
- if (option) {
84806
- if (option.kind === "boolean")
84807
- applyBooleanOption(option, args);
84808
- else
84809
- state.pendingOption = option;
84810
- return true;
84811
- }
84812
- switch (arg) {
84813
- case "--claude":
84814
- if (args.engineSet && args.engine !== "claude") {
84815
- throw new Error("Choose only one engine flag: --claude or --codex");
84816
- }
84817
- args.engine = "claude";
84818
- args.engineSet = true;
84819
- state.expectClaudeModel = true;
84820
- return true;
84821
- case "--codex":
84822
- if (args.engineSet && args.engine !== "codex") {
84823
- throw new Error("Choose only one engine flag: --claude or --codex");
84824
- }
84825
- args.engine = "codex";
84826
- args.engineSet = true;
84827
- return true;
84828
- case "--unlimited":
84829
- args.maxIterations = 0;
84830
- return true;
84831
- case "--project-root":
84832
- state.expectProjectRoot = true;
84833
- return true;
84834
- case "--workflow":
84835
- state.expectWorkflow = true;
84836
- return true;
84837
- case "--name":
84838
- state.expectName = true;
84839
- return true;
84840
- case "--prompt":
84841
- state.expectPrompt = true;
84842
- return true;
84843
- case "--prompt-file":
84844
- state.expectPromptFile = true;
84845
- return true;
84846
- case "--from-agent":
84847
- args.fromAgent = true;
84848
- return true;
84849
- default:
84850
- return false;
84851
- }
84852
- }
84853
- async function resolvePromptFile(args, state) {
84854
- if (state.promptFilePath !== null) {
84855
- args.prompt = await Bun.file(state.promptFilePath).text();
84856
- }
84857
- }
84858
- var VALID_MODELS, OPTION_BY_FLAG, VALUE_FLAGS, VALUE_SETTERS, BOOLEAN_SETTERS;
84859
- var init_common_args = __esm(() => {
84860
- init_fields();
84861
- VALID_MODELS = new Set(modelOptionValues());
84862
- OPTION_BY_FLAG = new Map(COMMON_CLI_OPTIONS.map((option) => [option.flag, option]));
84863
- VALUE_FLAGS = new Set(COMMON_CLI_OPTIONS.filter((option) => option.kind !== "boolean").map((option) => option.flag));
84864
- VALUE_SETTERS = {
84865
- model: (args, raw) => {
84866
- if (!VALID_MODELS.has(raw))
84867
- throw new Error("Invalid model");
84868
- args.model = raw;
84869
- },
84870
- delay: (args, raw) => {
84871
- args.delay = parseInt(raw, 10);
84872
- },
84873
- maxCostUsd: (args, raw) => {
84874
- args.maxCostUsd = parseFloat(raw);
84875
- },
84876
- maxRuntimeMinutes: (args, raw) => {
84877
- args.maxRuntimeMinutes = parseFloat(raw);
84878
- },
84879
- maxConsecutiveFailures: (args, raw) => {
84880
- args.maxConsecutiveFailures = parseInt(raw, 10);
84881
- },
84882
- maxIterations: (args, raw) => {
84883
- args.maxIterations = parseInt(raw, 10);
84884
- }
84885
- };
84886
- BOOLEAN_SETTERS = {
84887
- log: (args) => {
84888
- args.log = true;
84889
- },
84890
- verbose: (args) => {
84891
- args.verbose = true;
84892
- }
84893
- };
84894
- });
84895
-
84896
85325
  // apps/loop/src/cli.ts
84897
85326
  function printLoopHelp() {
84898
85327
  log(HELP_TEXT);
@@ -84961,6 +85390,7 @@ async function parseLoopArgs(argv) {
84961
85390
  }
84962
85391
  }
84963
85392
  await resolvePromptFile(result2, state);
85393
+ resolveWorkflowFile(result2, state);
84964
85394
  return result2;
84965
85395
  }
84966
85396
  var VALID_MODES, HELP_TEXT;
@@ -85040,6 +85470,7 @@ async function parseTaskArgs(argv) {
85040
85470
  }
85041
85471
  }
85042
85472
  await resolvePromptFile(result2, state);
85473
+ resolveWorkflowFile(result2, state);
85043
85474
  if (!phaseSet) {
85044
85475
  throw new Error(`Missing phase. Valid phases: research, plan, execute, review. Run 'ralphy task --help' for usage information.`);
85045
85476
  }
@@ -85107,10 +85538,10 @@ var init_schema2 = __esm(() => {
85107
85538
  });
85108
85539
 
85109
85540
  // packages/core/src/state/sidecar.ts
85110
- import { dirname as dirname5, join as join9 } from "path";
85541
+ import { dirname as dirname5, join as join10 } from "path";
85111
85542
  import { mkdir as mkdir3, rename, unlink } from "fs/promises";
85112
85543
  function slotSidecarPath(changeDir, slot) {
85113
- return join9(changeDir, `${CORE_STATE_FILE.replace(/\.json$/, "")}.${slot}.json`);
85544
+ return join10(changeDir, `${CORE_STATE_FILE.replace(/\.json$/, "")}.${slot}.json`);
85114
85545
  }
85115
85546
  function parseObject(text) {
85116
85547
  if (text === null)
@@ -89159,7 +89590,7 @@ var init_zod2 = __esm(() => {
89159
89590
  function markersOf(set3) {
89160
89591
  return Array.isArray(set3) ? set3 : [set3];
89161
89592
  }
89162
- var IterationUsageSchema, UsageSchema, HistoryEntrySchema, StateSchema, PhaseFrontmatterSchema;
89593
+ var IterationUsageSchema, UsageSchema, HistoryEntrySchema, RevisionSchema, StateSchema, PhaseFrontmatterSchema;
89163
89594
  var init_types2 = __esm(() => {
89164
89595
  init_zod2();
89165
89596
  IterationUsageSchema = exports_external2.object({
@@ -89192,6 +89623,12 @@ var init_types2 = __esm(() => {
89192
89623
  appVersion: exports_external2.string().optional(),
89193
89624
  usage: IterationUsageSchema.partial().optional()
89194
89625
  });
89626
+ RevisionSchema = exports_external2.object({
89627
+ version: exports_external2.number(),
89628
+ attachmentId: exports_external2.string(),
89629
+ sha256: exports_external2.string(),
89630
+ trigger: exports_external2.string()
89631
+ });
89195
89632
  StateSchema = exports_external2.object({
89196
89633
  version: exports_external2.literal("2"),
89197
89634
  name: exports_external2.string(),
@@ -89240,12 +89677,16 @@ var init_types2 = __esm(() => {
89240
89677
  attachmentId: exports_external2.string().nullable().default(null),
89241
89678
  sha256: exports_external2.string().nullable().default(null)
89242
89679
  }).default({ attachmentId: null, sha256: null }),
89680
+ designRevisions: exports_external2.array(RevisionSchema).default([]),
89681
+ designPdfRevisions: exports_external2.array(RevisionSchema).default([]),
89243
89682
  legacyProposalPurged: exports_external2.boolean().default(false)
89244
89683
  }).default({
89245
89684
  proposal: { attachmentId: null, sha256: null },
89246
89685
  design: { attachmentId: null, sha256: null },
89247
89686
  proposalPdf: { attachmentId: null, sha256: null },
89248
89687
  designPdf: { attachmentId: null, sha256: null },
89688
+ designRevisions: [],
89689
+ designPdfRevisions: [],
89249
89690
  legacyProposalPurged: false
89250
89691
  }),
89251
89692
  confirmation: exports_external2.object({
@@ -89292,7 +89733,7 @@ function formatTaskName(name) {
89292
89733
  }
89293
89734
 
89294
89735
  // packages/core/src/state.ts
89295
- import { join as join10 } from "path";
89736
+ import { join as join11 } from "path";
89296
89737
  function stripOwnedSlots(state) {
89297
89738
  const out = { ...state };
89298
89739
  for (const slot of ALL_OWNED_SLOTS)
@@ -89300,7 +89741,7 @@ function stripOwnedSlots(state) {
89300
89741
  return out;
89301
89742
  }
89302
89743
  function readState(changeDir) {
89303
- const filePath = join10(changeDir, STATE_FILE2);
89744
+ const filePath = join11(changeDir, STATE_FILE2);
89304
89745
  const raw = getStorage().read(filePath);
89305
89746
  if (raw === null)
89306
89747
  throw new Error(".ralph-state.json not found");
@@ -89309,7 +89750,7 @@ function readState(changeDir) {
89309
89750
  return StateSchema.parse(base2);
89310
89751
  }
89311
89752
  function tryReadStateRaw(changeDir) {
89312
- const filePath = join10(changeDir, STATE_FILE2);
89753
+ const filePath = join11(changeDir, STATE_FILE2);
89313
89754
  const text = getStorage().read(filePath);
89314
89755
  if (text === null)
89315
89756
  return { state: null, raw: null };
@@ -89325,7 +89766,7 @@ function tryReadStateRaw(changeDir) {
89325
89766
  return { state: result2.success ? result2.data : null, raw };
89326
89767
  }
89327
89768
  function writeState(changeDir, state) {
89328
- const filePath = join10(changeDir, STATE_FILE2);
89769
+ const filePath = join11(changeDir, STATE_FILE2);
89329
89770
  const core2 = stripOwnedSlots(state);
89330
89771
  getStorage().write(filePath, JSON.stringify(core2, null, 2) + `
89331
89772
  `);
@@ -89366,7 +89807,7 @@ function buildInitialState(options) {
89366
89807
  });
89367
89808
  }
89368
89809
  function ensureState(changeDir) {
89369
- const filePath = join10(changeDir, STATE_FILE2);
89810
+ const filePath = join11(changeDir, STATE_FILE2);
89370
89811
  const storage = getStorage();
89371
89812
  if (storage.read(filePath) !== null) {
89372
89813
  return readState(changeDir);
@@ -89385,7 +89826,7 @@ var init_state = __esm(() => {
89385
89826
  });
89386
89827
 
89387
89828
  // packages/core/src/state/store.ts
89388
- import { join as join11 } from "path";
89829
+ import { join as join12 } from "path";
89389
89830
  async function readJson(filePath) {
89390
89831
  const file2 = Bun.file(filePath);
89391
89832
  if (!await file2.exists())
@@ -89409,7 +89850,7 @@ async function writeField(changeDir, featureName, path, value) {
89409
89850
  if (!allowed.includes(topSlot)) {
89410
89851
  throw new OwnershipError(featureName, path, `feature '${featureName}' may not write '${path}' (owns ${allowed.join(", ")})`);
89411
89852
  }
89412
- const inline = (await readJson(join11(changeDir, STATE_FILE3)))[topSlot];
89853
+ const inline = (await readJson(join12(changeDir, STATE_FILE3)))[topSlot];
89413
89854
  const seed = inline && typeof inline === "object" && !Array.isArray(inline) ? inline : undefined;
89414
89855
  await writeSlotField(changeDir, path, value, seed);
89415
89856
  }
@@ -89432,14 +89873,14 @@ var init_store = __esm(() => {
89432
89873
  });
89433
89874
 
89434
89875
  // apps/loop/src/components/TaskStatus.tsx
89435
- import { join as join12 } from "path";
89876
+ import { join as join13 } from "path";
89436
89877
  function TaskStatus({ state, stateDir }) {
89437
89878
  const storage = getStorage();
89438
89879
  const cost = Math.round(state.usage.total_cost_usd * 100) / 100;
89439
89880
  const time3 = Math.round(state.usage.total_duration_ms / 1000 * 10) / 10 + "s";
89440
89881
  const artifacts = OPENSPEC_ARTIFACTS.map((name) => ({
89441
89882
  name,
89442
- exists: storage.read(join12(stateDir, name)) !== null
89883
+ exists: storage.read(join13(stateDir, name)) !== null
89443
89884
  }));
89444
89885
  const recent = state.history.slice(-10);
89445
89886
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
@@ -98157,7 +98598,7 @@ var init_rate_limit_detection = __esm(() => {
98157
98598
 
98158
98599
  // packages/engine/src/agents/claude.ts
98159
98600
  import { mkdtemp, unlink as unlink2 } from "fs/promises";
98160
- import { join as join13 } from "path";
98601
+ import { join as join14 } from "path";
98161
98602
  import { tmpdir } from "os";
98162
98603
  function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, reviewerModel) {
98163
98604
  const effectiveModel = reviewerModel ?? model;
@@ -98178,7 +98619,7 @@ function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, review
98178
98619
  }
98179
98620
  async function runInteractive(req) {
98180
98621
  const { model, prompt, taskDir } = req;
98181
- const promptFile = taskDir ? join13(taskDir, "_interactive_prompt.md") : join13(await mkdtemp(join13(tmpdir(), "ralph-")), "prompt.md");
98622
+ const promptFile = taskDir ? join14(taskDir, "_interactive_prompt.md") : join14(await mkdtemp(join14(tmpdir(), "ralph-")), "prompt.md");
98182
98623
  await Bun.write(promptFile, prompt);
98183
98624
  try {
98184
98625
  const cmd = [
@@ -98205,7 +98646,7 @@ async function runInteractive(req) {
98205
98646
  env: scrubClaudeEnv(process.env)
98206
98647
  });
98207
98648
  const exitCode = await proc.exited;
98208
- const doneFile = taskDir ? join13(taskDir, "_interactive_done") : null;
98649
+ const doneFile = taskDir ? join14(taskDir, "_interactive_done") : null;
98209
98650
  if (doneFile && await Bun.file(doneFile).exists()) {
98210
98651
  return { exitCode: 0, usage: null, sessionId: null, rateLimited: false };
98211
98652
  }
@@ -99708,11 +100149,11 @@ var init_meta_prompt = __esm(() => {
99708
100149
  });
99709
100150
 
99710
100151
  // packages/core/src/loop.ts
99711
- import { join as join14 } from "path";
100152
+ import { join as join15 } from "path";
99712
100153
  function buildTaskPrompt(state, taskDir, reviewPhase) {
99713
100154
  const storage = getStorage();
99714
100155
  let prompt = "";
99715
- const steeringContent = storage.read(join14(taskDir, "steering.md"));
100156
+ const steeringContent = storage.read(join15(taskDir, "steering.md"));
99716
100157
  if (steeringContent !== null) {
99717
100158
  const steeringLines = steeringContent.split(`
99718
100159
  `).filter((line) => !line.startsWith("#")).filter((line) => line.trim()).slice(0, STEERING_MAX_LINES);
@@ -99731,8 +100172,8 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99731
100172
  `;
99732
100173
  }
99733
100174
  }
99734
- const agentTasksPath = join14(taskDir, AGENT_TASKS_FILENAME);
99735
- const missionTasksPath = join14(taskDir, MISSION_TASKS_FILENAME);
100175
+ const agentTasksPath = join15(taskDir, AGENT_TASKS_FILENAME);
100176
+ const missionTasksPath = join15(taskDir, MISSION_TASKS_FILENAME);
99736
100177
  const agentTasksContent = storage.read(agentTasksPath);
99737
100178
  const missionTasksContent = storage.read(missionTasksPath);
99738
100179
  let activePath = null;
@@ -99808,7 +100249,7 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99808
100249
  }
99809
100250
  }
99810
100251
  if (reviewPhase?.enabled) {
99811
- const reviewFindingsPath = join14(taskDir, "review-findings.md");
100252
+ const reviewFindingsPath = join15(taskDir, "review-findings.md");
99812
100253
  const reviewFindingsContent = storage.read(reviewFindingsPath);
99813
100254
  const hasUncheckedMission = missionTasksContent !== null && /^- \[ \]/m.test(missionTasksContent);
99814
100255
  const hasUncheckedAgent = agentTasksContent !== null && /^- \[ \]/m.test(agentTasksContent);
@@ -99882,7 +100323,7 @@ When all tasks are complete and all files are committed, push your branch and op
99882
100323
  }
99883
100324
  function buildSteeringBlock(taskDir) {
99884
100325
  const storage = getStorage();
99885
- const steeringContent = storage.read(join14(taskDir, "steering.md"));
100326
+ const steeringContent = storage.read(join15(taskDir, "steering.md"));
99886
100327
  if (steeringContent === null)
99887
100328
  return "";
99888
100329
  const steeringLines = steeringContent.split(`
@@ -99980,7 +100421,7 @@ function buildPlanPrompt(state, taskDir) {
99980
100421
  return prompt;
99981
100422
  }
99982
100423
  function buildReviewPrompt(state, taskDir) {
99983
- const reviewFindingsPath = join14(taskDir, "review-findings.md");
100424
+ const reviewFindingsPath = join15(taskDir, "review-findings.md");
99984
100425
  let prompt = buildSteeringBlock(taskDir);
99985
100426
  prompt += `---
99986
100427
 
@@ -100045,7 +100486,7 @@ function buildPhasePrompt(phase, state, taskDir, reviewPhase, metaPromptOptions)
100045
100486
  }
100046
100487
  function checkStopSignal(taskDir, stateDir) {
100047
100488
  const storage = getStorage();
100048
- const stopFile = join14(taskDir, "STOP");
100489
+ const stopFile = join15(taskDir, "STOP");
100049
100490
  const reason = storage.read(stopFile);
100050
100491
  if (reason === null)
100051
100492
  return null;
@@ -100105,7 +100546,7 @@ function updateStateIteration(stateDir, result2, startedAt, engine, model, usage
100105
100546
  }
100106
100547
  function appendSteeringMessage(taskDir, message) {
100107
100548
  const storage = getStorage();
100108
- const steeringPath = join14(taskDir, "steering.md");
100549
+ const steeringPath = join15(taskDir, "steering.md");
100109
100550
  const existing = storage.read(steeringPath);
100110
100551
  const updated = existing ? `${message}
100111
100552
 
@@ -100155,7 +100596,7 @@ var init_loop2 = __esm(() => {
100155
100596
  });
100156
100597
 
100157
100598
  // apps/loop/src/hooks/useLoop.ts
100158
- import { join as join15 } from "path";
100599
+ import { join as join16 } from "path";
100159
100600
  function sleep(seconds) {
100160
100601
  return new Promise((resolve4) => setTimeout(resolve4, seconds * 1000));
100161
100602
  }
@@ -100271,8 +100712,8 @@ function useLoop(opts) {
100271
100712
  setState(currentState);
100272
100713
  if (!actor.getSnapshot().matches("running"))
100273
100714
  break;
100274
- const tasksContent = storage.read(join15(tasksDir, MISSION_TASKS_FILENAME));
100275
- const agentTasksContent = storage.read(join15(tasksDir, AGENT_TASKS_FILENAME));
100715
+ const tasksContent = storage.read(join16(tasksDir, MISSION_TASKS_FILENAME));
100716
+ const agentTasksContent = storage.read(join16(tasksDir, AGENT_TASKS_FILENAME));
100276
100717
  if (tasksContent === null && currentState.iteration > 0 && typeof opts.changeStore.listChanges === "function") {
100277
100718
  let stillActive = true;
100278
100719
  try {
@@ -100309,7 +100750,7 @@ function useLoop(opts) {
100309
100750
  const agentDone = agentTasksContent === null || allCompleted(agentTasksContent);
100310
100751
  if (missionDone && agentDone && tasksContent !== null) {
100311
100752
  if (opts.reviewPhase?.enabled) {
100312
- const reviewFindingsPath = join15(tasksDir, "review-findings.md");
100753
+ const reviewFindingsPath = join16(tasksDir, "review-findings.md");
100313
100754
  const reviewFindingsFile = Bun.file(reviewFindingsPath);
100314
100755
  const findingsExists = await reviewFindingsFile.exists();
100315
100756
  const findingsContent = findingsExists ? await reviewFindingsFile.text() : null;
@@ -100338,7 +100779,7 @@ function useLoop(opts) {
100338
100779
  model: opts.reviewPhase.reviewerModel ?? opts.model,
100339
100780
  prompt: reviewPrompt,
100340
100781
  logFlag: opts.log,
100341
- logFile: join15(stateDir, `log-review-${roundNum}.json`),
100782
+ logFile: join16(stateDir, `log-review-${roundNum}.json`),
100342
100783
  taskDir: tasksDir,
100343
100784
  reviewerContextStrategy: opts.reviewPhase.reviewerContextStrategy ?? "fresh",
100344
100785
  onFeedEvent: addFeedEvent
@@ -100412,8 +100853,8 @@ function useLoop(opts) {
100412
100853
  const time3 = new Date().toLocaleTimeString("en-US", { hour12: false });
100413
100854
  addIterationHeader(localIter, time3);
100414
100855
  addInfo(`Iteration ${localIter} (total: ${currentState.iteration})`);
100415
- const proposalContent = storage.read(join15(tasksDir, "proposal.md"));
100416
- const designContent = storage.read(join15(tasksDir, "design.md"));
100856
+ const proposalContent = storage.read(join16(tasksDir, "proposal.md"));
100857
+ const designContent = storage.read(join16(tasksDir, "design.md"));
100417
100858
  const routedPhase = routeTaskPhase(opts.phase, {
100418
100859
  proposal: proposalContent,
100419
100860
  design: designContent,
@@ -100433,7 +100874,7 @@ function useLoop(opts) {
100433
100874
  model: opts.model,
100434
100875
  prompt,
100435
100876
  logFlag: opts.log,
100436
- logFile: join15(stateDir, "log.json"),
100877
+ logFile: join16(stateDir, "log.json"),
100437
100878
  taskDir: tasksDir,
100438
100879
  interactive: false,
100439
100880
  onFeedEvent: addFeedEvent,
@@ -100456,7 +100897,7 @@ function useLoop(opts) {
100456
100897
  model: opts.model,
100457
100898
  prompt: buildSteeringPrompt(steerMessage),
100458
100899
  logFlag: opts.log,
100459
- logFile: join15(stateDir, "log.json"),
100900
+ logFile: join16(stateDir, "log.json"),
100460
100901
  taskDir: tasksDir,
100461
100902
  onFeedEvent: addResumeFeedEvent,
100462
100903
  signal: resumeController.signal,
@@ -100763,7 +101204,7 @@ var init_TaskLoop = __esm(async () => {
100763
101204
  });
100764
101205
 
100765
101206
  // apps/loop/src/components/App.tsx
100766
- import { join as join16 } from "path";
101207
+ import { join as join17 } from "path";
100767
101208
  function ExitAfterRender({ children }) {
100768
101209
  const { exit } = use_app_default();
100769
101210
  import_react59.useEffect(() => {
@@ -100816,7 +101257,7 @@ function App2({ args, taskPhase }) {
100816
101257
  }
100817
101258
  const layout = getLayout();
100818
101259
  const stateDir = layout.taskStateDir(args.name);
100819
- if (getStorage().read(join16(stateDir, ".ralph-state.json")) === null) {
101260
+ if (getStorage().read(join17(stateDir, ".ralph-state.json")) === null) {
100820
101261
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
100821
101262
  message: `Error: change '${args.name}' not found`
100822
101263
  }, undefined, false, undefined, this);
@@ -100870,7 +101311,7 @@ var init_App2 = __esm(async () => {
100870
101311
 
100871
101312
  // packages/log/src/log.ts
100872
101313
  import { appendFile } from "fs/promises";
100873
- import { join as join17, dirname as dirname7 } from "path";
101314
+ import { join as join18, dirname as dirname7 } from "path";
100874
101315
  import { homedir as homedir4 } from "os";
100875
101316
  import { mkdir as mkdir5 } from "fs/promises";
100876
101317
  function fmt(type, text) {
@@ -100919,14 +101360,14 @@ var init_log = __esm(() => {
100919
101360
  init_version();
100920
101361
  jsonLogChains = new Map;
100921
101362
  ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
100922
- AGENT_LOG_PATH = join17(homedir4(), ".ralph", "agent-mode.log");
101363
+ AGENT_LOG_PATH = join18(homedir4(), ".ralph", "agent-mode.log");
100923
101364
  mkdir5(dirname7(AGENT_LOG_PATH), { recursive: true }).catch(() => {
100924
101365
  return;
100925
101366
  });
100926
101367
  });
100927
101368
 
100928
101369
  // apps/loop/src/debug.ts
100929
- import { join as join18 } from "path";
101370
+ import { join as join19 } from "path";
100930
101371
  function fmtTs(d) {
100931
101372
  return d.toISOString().replace("T", " ").slice(0, 23);
100932
101373
  }
@@ -101038,7 +101479,7 @@ function detectDebugStuck(lines) {
101038
101479
  };
101039
101480
  }
101040
101481
  async function inspectBinary(projectRoot) {
101041
- const binPath = join18(projectRoot, ".ralph", "bin", "cli.js");
101482
+ const binPath = join19(projectRoot, ".ralph", "bin", "cli.js");
101042
101483
  const file2 = Bun.file(binPath);
101043
101484
  if (!await file2.exists())
101044
101485
  return null;
@@ -101063,7 +101504,7 @@ async function inspectBinary(projectRoot) {
101063
101504
  async function resolveDebugTarget(projectRoot, opts) {
101064
101505
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
101065
101506
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
101066
- const jsonlLogFile = Bun.file(join18(projectRoot, ".ralph", "agent.log"));
101507
+ const jsonlLogFile = Bun.file(join19(projectRoot, ".ralph", "agent.log"));
101067
101508
  const jsonlLines = await jsonlLogFile.exists() ? parseJsonlLog(await jsonlLogFile.text()) : [];
101068
101509
  const allLines = [...textLines, ...jsonlLines];
101069
101510
  if (opts.name && !opts.issue) {
@@ -101168,7 +101609,7 @@ async function runDebug(opts) {
101168
101609
  `);
101169
101610
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
101170
101611
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
101171
- const jsonlLogPath = join18(projectRoot, ".ralph", "agent.log");
101612
+ const jsonlLogPath = join19(projectRoot, ".ralph", "agent.log");
101172
101613
  const jsonlLogFile = Bun.file(jsonlLogPath);
101173
101614
  const hasJsonlLog = await jsonlLogFile.exists();
101174
101615
  let { changeName, identifier: issueIdentifier } = await resolveDebugTarget(projectRoot, {
@@ -101182,7 +101623,7 @@ async function runDebug(opts) {
101182
101623
  }
101183
101624
  const jsonlLines = hasJsonlLog ? parseJsonlLog(await jsonlLogFile.text(), changeName) : [];
101184
101625
  const relevantText = textLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
101185
- const workerLogFile = Bun.file(join18(projectRoot, ".ralph", "logs", `${changeName}.log`));
101626
+ const workerLogFile = Bun.file(join19(projectRoot, ".ralph", "logs", `${changeName}.log`));
101186
101627
  const workerLines = await workerLogFile.exists() ? parseTextLog(await workerLogFile.text()) : [];
101187
101628
  const merged = [...relevantText, ...jsonlLines, ...workerLines].sort((a, b) => +a.ts - +b.ts);
101188
101629
  const seen = new Set;
@@ -101341,8 +101782,8 @@ async function runDebug(opts) {
101341
101782
  out(" \u26A0 PR currently has merge conflicts");
101342
101783
  if (pr?.checks.some((c) => c.conclusion === "FAILURE"))
101343
101784
  out(" \u26A0 PR has failing CI checks");
101344
- const worktreePath = join18(projectRoot, ".ralph", "worktrees", changeName);
101345
- if (await Bun.file(join18(worktreePath, ".git")).exists()) {
101785
+ const worktreePath = join19(projectRoot, ".ralph", "worktrees", changeName);
101786
+ if (await Bun.file(join19(worktreePath, ".git")).exists()) {
101346
101787
  out(` Worktree : ${worktreePath}`);
101347
101788
  }
101348
101789
  if (!timeline.length)
@@ -101362,12 +101803,12 @@ __export(exports_src2, {
101362
101803
  taskMain: () => taskMain,
101363
101804
  main: () => main2
101364
101805
  });
101365
- import { join as join19 } from "path";
101806
+ import { join as join20 } from "path";
101366
101807
  import { exists as exists2, mkdir as mkdir6, rm as rm2 } from "fs/promises";
101367
101808
  async function ensureRalphGitignore(projectRoot) {
101368
- const ralphDir = join19(projectRoot, ".ralph");
101809
+ const ralphDir = join20(projectRoot, ".ralph");
101369
101810
  await mkdir6(ralphDir, { recursive: true });
101370
- const gitignorePath = join19(ralphDir, ".gitignore");
101811
+ const gitignorePath = join20(ralphDir, ".gitignore");
101371
101812
  const file2 = Bun.file(gitignorePath);
101372
101813
  if (await file2.exists()) {
101373
101814
  const existing = await file2.text();
@@ -101416,7 +101857,7 @@ async function main2(argv) {
101416
101857
  Bun.spawnSync({
101417
101858
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
101418
101859
  stdio: ["inherit", "inherit", "inherit"],
101419
- cwd: process.cwd()
101860
+ cwd: projectRoot
101420
101861
  });
101421
101862
  }
101422
101863
  if (args.mode === "debug") {
@@ -101434,9 +101875,9 @@ async function main2(argv) {
101434
101875
  `);
101435
101876
  return 1;
101436
101877
  }
101437
- const worktreeDir = join19(worktreesDir(projectRoot), args.name);
101438
- const changeDir = join19(tasksDir, args.name);
101439
- const stateDir = join19(statesDir, args.name);
101878
+ const worktreeDir = join20(worktreesDir(projectRoot), args.name);
101879
+ const changeDir = join20(tasksDir, args.name);
101880
+ const stateDir = join20(statesDir, args.name);
101440
101881
  const branch = `ralph/${args.name}`;
101441
101882
  const removed = [];
101442
101883
  if (await exists2(worktreeDir)) {
@@ -101487,8 +101928,8 @@ async function main2(argv) {
101487
101928
  return 0;
101488
101929
  }
101489
101930
  if (args.mode === "task" && args.name) {
101490
- await mkdir6(join19(statesDir, args.name), { recursive: true });
101491
- await mkdir6(join19(tasksDir, args.name), { recursive: true });
101931
+ await mkdir6(join20(statesDir, args.name), { recursive: true });
101932
+ await mkdir6(join20(tasksDir, args.name), { recursive: true });
101492
101933
  await ensureRalphGitignore(projectRoot);
101493
101934
  }
101494
101935
  await runWithContext(createDefaultContext({ layout, args }), async () => {
@@ -101516,8 +101957,8 @@ async function taskMain(argv) {
101516
101957
  const layout = projectLayout(projectRoot);
101517
101958
  const statesDir = layout.statesDir;
101518
101959
  const tasksDir = layout.tasksDir;
101519
- await mkdir6(join19(statesDir, args.name), { recursive: true });
101520
- await mkdir6(join19(tasksDir, args.name), { recursive: true });
101960
+ await mkdir6(join20(statesDir, args.name), { recursive: true });
101961
+ await mkdir6(join20(tasksDir, args.name), { recursive: true });
101521
101962
  await ensureRalphGitignore(projectRoot);
101522
101963
  await runWithContext(createDefaultContext({ layout, args }), async () => {
101523
101964
  const { waitUntilExit } = render_default(import_react60.createElement(App2, {
@@ -101760,6 +102201,7 @@ async function parseAgentArgs(argv) {
101760
102201
  }
101761
102202
  }
101762
102203
  await resolvePromptFile(result2, state);
102204
+ resolveWorkflowFile(result2, state);
101763
102205
  if (result2.fixCi && !result2.createPr) {
101764
102206
  throw new Error("--fix-ci requires --create-pr");
101765
102207
  }
@@ -101917,7 +102359,7 @@ function formatError2(err) {
101917
102359
  }
101918
102360
 
101919
102361
  // apps/agent/src/shared/capabilities/fs-change.ts
101920
- import { join as join20, dirname as dirname8 } from "path";
102362
+ import { join as join21, dirname as dirname8 } from "path";
101921
102363
  import { mkdir as mkdir7 } from "fs/promises";
101922
102364
  var scaffold, prependTask, appendSteering, fsChange;
101923
102365
  var init_fs_change = __esm(() => {
@@ -101930,11 +102372,11 @@ var init_fs_change = __esm(() => {
101930
102372
  errorFormatter: formatError2,
101931
102373
  run: async (args) => {
101932
102374
  await mkdir7(args.changeDir, { recursive: true });
101933
- await mkdir7(join20(args.changeDir, "specs"), { recursive: true });
102375
+ await mkdir7(join21(args.changeDir, "specs"), { recursive: true });
101934
102376
  await mkdir7(args.stateDir, { recursive: true });
101935
- await Bun.write(join20(args.changeDir, "proposal.md"), args.proposal);
101936
- await Bun.write(join20(args.changeDir, "tasks.md"), args.tasks);
101937
- await Bun.write(join20(args.changeDir, "design.md"), args.design);
102377
+ await Bun.write(join21(args.changeDir, "proposal.md"), args.proposal);
102378
+ await Bun.write(join21(args.changeDir, "tasks.md"), args.tasks);
102379
+ await Bun.write(join21(args.changeDir, "design.md"), args.design);
101938
102380
  }
101939
102381
  };
101940
102382
  prependTask = {
@@ -101952,7 +102394,7 @@ var init_fs_change = __esm(() => {
101952
102394
  retryPolicy: NO_RETRY,
101953
102395
  errorFormatter: formatError2,
101954
102396
  run: async (args) => {
101955
- const path = join20(args.changeDir, "steering.md");
102397
+ const path = join21(args.changeDir, "steering.md");
101956
102398
  const f2 = Bun.file(path);
101957
102399
  const existing = await f2.exists() ? await f2.text() : null;
101958
102400
  const updated = existing ? `${args.message}
@@ -101967,11 +102409,11 @@ ${existing.trimStart()}` : `${args.message}
101967
102409
  });
101968
102410
 
101969
102411
  // apps/agent/src/agent/worktree.ts
101970
- import { basename as basename2, join as join21 } from "path";
102412
+ import { basename as basename2, join as join22 } from "path";
101971
102413
  import { homedir as homedir5 } from "os";
101972
102414
  import { exists as exists3 } from "fs/promises";
101973
102415
  function worktreesDir2(projectRoot) {
101974
- return join21(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
102416
+ return join22(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
101975
102417
  }
101976
102418
  function branchForChange(changeName) {
101977
102419
  return `ralph/${changeName}`;
@@ -101990,7 +102432,7 @@ function createWorktree(projectRoot, changeName, baseBranch, runner) {
101990
102432
  }
101991
102433
  async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
101992
102434
  const dir = worktreesDir2(projectRoot);
101993
- const cwd2 = join21(dir, changeName);
102435
+ const cwd2 = join22(dir, changeName);
101994
102436
  const branch = branchForChange(changeName);
101995
102437
  const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
101996
102438
  if (list.stdout.includes(`worktree ${cwd2}
@@ -102015,7 +102457,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
102015
102457
  return { cwd: cwd2, branch };
102016
102458
  }
102017
102459
  async function installPrePushHook(cwd2, runner) {
102018
- const hookPath = join21(cwd2, ".ralph-hooks", "pre-push");
102460
+ const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
102019
102461
  await Bun.write(hookPath, PRE_PUSH_HOOK_SCRIPT);
102020
102462
  const chmod = Bun.spawn(["chmod", "+x", hookPath]);
102021
102463
  await chmod.exited;
@@ -102061,8 +102503,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
102061
102503
  return { safe: true, dirty, unpushedCommits };
102062
102504
  }
102063
102505
  async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
102064
- const dst = join21(worktreeCwd, ".mcp.json");
102065
- const src = join21(projectRoot, ".mcp.json");
102506
+ const dst = join22(worktreeCwd, ".mcp.json");
102507
+ const src = join22(projectRoot, ".mcp.json");
102066
102508
  const source = await exists3(dst) ? dst : await exists3(src) ? src : null;
102067
102509
  if (!source)
102068
102510
  return;
@@ -102076,7 +102518,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
102076
102518
  if (servers && typeof servers === "object") {
102077
102519
  for (const cfg of Object.values(servers)) {
102078
102520
  if (Array.isArray(cfg.args)) {
102079
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join21(projectRoot, a) : a);
102521
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join22(projectRoot, a) : a);
102080
102522
  }
102081
102523
  }
102082
102524
  }
@@ -103926,7 +104368,7 @@ function emitFeatureSkipped(bus, id, reason) {
103926
104368
  var init_run_feature = () => {};
103927
104369
 
103928
104370
  // apps/agent/src/agent/post-task.ts
103929
- import { join as join22, dirname as dirname9 } from "path";
104371
+ import { join as join23, dirname as dirname9 } from "path";
103930
104372
  function summarizeUncommittedStatus(stdout) {
103931
104373
  const lines = stdout.split(`
103932
104374
  `).filter((line) => line.length > 0);
@@ -103998,7 +104440,7 @@ async function reactivateState(stateFilePath, log3, changeName) {
103998
104440
  async function runWorkerWithFixTask(ctx, heading, body) {
103999
104441
  try {
104000
104442
  await runCapability(fsChange.prependTask, {
104001
- tasksPath: join22(ctx.changeDir, AGENT_TASKS_FILENAME),
104443
+ tasksPath: join23(ctx.changeDir, AGENT_TASKS_FILENAME),
104002
104444
  heading,
104003
104445
  failureOutput: body
104004
104446
  });
@@ -104549,7 +104991,7 @@ async function runValidateOnlyPhase(input, deps) {
104549
104991
  emit3("validate-fix", command);
104550
104992
  log3(`! validation check failed: ${command}`, "yellow");
104551
104993
  try {
104552
- await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104994
+ await prependFixTask(join23(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104553
104995
  } catch (err) {
104554
104996
  log3(`! could not prepend fix task: ${err.message}`, "red");
104555
104997
  return 1;
@@ -104560,7 +105002,7 @@ async function runValidateOnlyPhase(input, deps) {
104560
105002
  }
104561
105003
  }
104562
105004
  try {
104563
- await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
105005
+ await prependFixTask(join23(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
104564
105006
  `Run \`bunx openspec validate ${changeName}\` to validate the change artifacts.`,
104565
105007
  `Commit any pending changes before running the validation command.`
104566
105008
  ].join(`
@@ -104573,7 +105015,7 @@ async function runValidateOnlyPhase(input, deps) {
104573
105015
  return respawnWorker();
104574
105016
  }
104575
105017
  async function recordGaveUp(stateFilePath, log3, changeName) {
104576
- const path = join22(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
105018
+ const path = join23(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
104577
105019
  try {
104578
105020
  const file2 = Bun.file(path);
104579
105021
  const current = await file2.exists() ? Number.parseInt(await file2.text(), 10) || 0 : 0;
@@ -105038,6 +105480,7 @@ class AgentCoordinator {
105038
105480
  opts;
105039
105481
  workers = [];
105040
105482
  pendingIds = new Set;
105483
+ inFlight = new Set;
105041
105484
  queue = [];
105042
105485
  stopped = false;
105043
105486
  paused = null;
@@ -105604,7 +106047,31 @@ class AgentCoordinator {
105604
106047
  while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
105605
106048
  const next = this.queue.shift();
105606
106049
  this.pendingIds.add(next.issue.id);
105607
- this.launchWorker(next.issue, next.trigger, next.mention);
106050
+ this.track(this.launchWorker(next.issue, next.trigger, next.mention));
106051
+ }
106052
+ }
106053
+ track(p) {
106054
+ this.inFlight.add(p);
106055
+ p.finally(() => {
106056
+ this.inFlight.delete(p);
106057
+ });
106058
+ return p;
106059
+ }
106060
+ async whenSettled() {
106061
+ let consecutiveEmpty = 0;
106062
+ for (let guard = 0;guard < 1000; guard++) {
106063
+ if (this.inFlight.size > 0) {
106064
+ await Promise.allSettled(this.inFlight);
106065
+ consecutiveEmpty = 0;
106066
+ }
106067
+ await new Promise((r) => setTimeout(r, 0));
106068
+ if (this.inFlight.size === 0) {
106069
+ consecutiveEmpty += 1;
106070
+ if (consecutiveEmpty >= WHEN_SETTLED_STABLE_HOPS)
106071
+ return;
106072
+ } else {
106073
+ consecutiveEmpty = 0;
106074
+ }
105608
106075
  }
105609
106076
  }
105610
106077
  async launchWorker(issue2, trigger, mention) {
@@ -105722,7 +106189,10 @@ class AgentCoordinator {
105722
106189
  this.deps.onLog(`! sync-tasks (launch) failed for ${issue2.identifier}: ${err.message}`, "yellow");
105723
106190
  }
105724
106191
  }
105725
- handle.exited.then(async (code) => {
106192
+ handle.exited.then((code) => this.track(this.finalizeWorkerExit(worker, issue2, prep, trigger, code)));
106193
+ }
106194
+ async finalizeWorkerExit(worker, issue2, prep, trigger, code) {
106195
+ {
105726
106196
  const idx = this.workers.indexOf(worker);
105727
106197
  if (idx >= 0)
105728
106198
  this.workers.splice(idx, 1);
@@ -105770,7 +106240,7 @@ class AgentCoordinator {
105770
106240
  await this.notifyExited(issue2, prep.changeName, code, trigger);
105771
106241
  this.deps.onWorkersChanged();
105772
106242
  this.spawnNext();
105773
- });
106243
+ }
105774
106244
  }
105775
106245
  async restartWorker(changeName) {
105776
106246
  if (this.stopped)
@@ -105946,7 +106416,7 @@ var emptyPrStatus = () => ({
105946
106416
  conflicted: 0,
105947
106417
  ciFailed: 0,
105948
106418
  quarantined: 0
105949
- }), emptyPollResult = () => ({
106419
+ }), WHEN_SETTLED_STABLE_HOPS = 3, emptyPollResult = () => ({
105950
106420
  found: 0,
105951
106421
  added: 0,
105952
106422
  buckets: {
@@ -105980,15 +106450,15 @@ var init_coordinator2 = __esm(() => {
105980
106450
  });
105981
106451
 
105982
106452
  // apps/agent/src/agent/scaffold.ts
105983
- import { join as join23 } from "path";
106453
+ import { join as join24 } from "path";
105984
106454
  function changeNameForIssue(issue2) {
105985
106455
  const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
105986
106456
  return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
105987
106457
  }
105988
106458
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
105989
106459
  const name = changeNameForIssue(issue2);
105990
- const changeDir = join23(tasksDir, name);
105991
- const stateDir = join23(statesDir, name);
106460
+ const changeDir = join24(tasksDir, name);
106461
+ const stateDir = join24(statesDir, name);
105992
106462
  const commentsBlock = comments.length > 0 ? [
105993
106463
  "",
105994
106464
  "## Linear comments",
@@ -106124,7 +106594,7 @@ var init_detections = __esm(() => {
106124
106594
  });
106125
106595
 
106126
106596
  // apps/agent/src/features/confirmation/state.ts
106127
- import { dirname as dirname10, join as join24 } from "path";
106597
+ import { dirname as dirname10, join as join25 } from "path";
106128
106598
  async function readInlineConfirmation(statePath) {
106129
106599
  const f2 = Bun.file(statePath);
106130
106600
  if (!await f2.exists())
@@ -106163,8 +106633,8 @@ async function restartFromDesign(changeDir, changeName) {
106163
106633
  ""
106164
106634
  ].join(`
106165
106635
  `);
106166
- await Bun.write(join24(changeDir, "design.md"), designStub);
106167
- const tasksPath = join24(changeDir, "tasks.md");
106636
+ await Bun.write(join25(changeDir, "design.md"), designStub);
106637
+ const tasksPath = join25(changeDir, "tasks.md");
106168
106638
  if (await Bun.file(tasksPath).exists()) {
106169
106639
  await Bun.write(tasksPath, `# Tasks
106170
106640
 
@@ -106394,7 +106864,7 @@ var init_inspect = __esm(() => {
106394
106864
  });
106395
106865
 
106396
106866
  // apps/agent/src/features/confirmation/awaiting.ts
106397
- import { join as join25 } from "path";
106867
+ import { join as join26 } from "path";
106398
106868
  async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106399
106869
  const tracked = deps.cwdOf(changeName);
106400
106870
  if (tracked)
@@ -106402,12 +106872,12 @@ async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106402
106872
  if (!deps.useWorktree)
106403
106873
  return deps.projectRoot;
106404
106874
  const root = worktreesDir2(deps.projectRoot);
106405
- const canonical = join25(root, worktreeDirNameForIssue(issue2));
106406
- if (await Bun.file(join25(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106875
+ const canonical = join26(root, worktreeDirNameForIssue(issue2));
106876
+ if (await Bun.file(join26(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106407
106877
  return canonical;
106408
106878
  }
106409
- const legacy = join25(root, changeName);
106410
- if (await Bun.file(join25(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106879
+ const legacy = join26(root, changeName);
106880
+ if (await Bun.file(join26(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106411
106881
  return legacy;
106412
106882
  }
106413
106883
  return deps.projectRoot;
@@ -106542,9 +107012,9 @@ async function processAwaitingForIssue(issue2, deps) {
106542
107012
  const layout = projectLayout(cwd2);
106543
107013
  const changeDir = layout.changeDir(changeName);
106544
107014
  const statePath = layout.stateFile(changeName);
106545
- const tasks2 = await readTextOrNull(join25(changeDir, "tasks.md"));
106546
- const proposal = await readTextOrNull(join25(changeDir, "proposal.md"));
106547
- const design = await readTextOrNull(join25(changeDir, "design.md"));
107015
+ const tasks2 = await readTextOrNull(join26(changeDir, "tasks.md"));
107016
+ const proposal = await readTextOrNull(join26(changeDir, "proposal.md"));
107017
+ const design = await readTextOrNull(join26(changeDir, "design.md"));
106548
107018
  let commentsCache = null;
106549
107019
  const getComments = async () => {
106550
107020
  if (commentsCache)
@@ -107088,7 +107558,12 @@ var init_linear_resolvers = __esm(() => {
107088
107558
 
107089
107559
  // apps/agent/src/agent/wire/prepare.ts
107090
107560
  import { mkdir as mkdir8 } from "fs/promises";
107091
- import { join as join26 } from "path";
107561
+ import { join as join27 } from "path";
107562
+ function composeAppendPrompt(promptArg, cfgAppendPrompt, workflowPrompt) {
107563
+ return [promptArg || cfgAppendPrompt || "", workflowPrompt].filter(Boolean).join(`
107564
+
107565
+ `);
107566
+ }
107092
107567
  function createPrepareHelpers(input) {
107093
107568
  const {
107094
107569
  args,
@@ -107152,7 +107627,7 @@ function createPrepareHelpers(input) {
107152
107627
  let changeName;
107153
107628
  const wtLayoutPre = projectLayout(workerCwd);
107154
107629
  const derivedName = changeNameForIssue(issue2);
107155
- const tasksMdPath = join26(wtLayoutPre.changeDir(derivedName), "tasks.md");
107630
+ const tasksMdPath = join27(wtLayoutPre.changeDir(derivedName), "tasks.md");
107156
107631
  const tasksMdExists = await Bun.file(tasksMdPath).exists();
107157
107632
  const isFresh = !tasksMdExists;
107158
107633
  if (isFresh) {
@@ -107185,9 +107660,7 @@ function createPrepareHelpers(input) {
107185
107660
  } catch (err) {
107186
107661
  diag("workflow", `! workflow render failed: ${err.message}`, "yellow");
107187
107662
  }
107188
- const appendPrompt = [args.prompt || cfg.appendPrompt || "", workflowPrompt].filter(Boolean).join(`
107189
-
107190
- `);
107663
+ const appendPrompt = composeAppendPrompt(args.prompt ?? "", cfg.appendPrompt ?? "", workflowPrompt);
107191
107664
  changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue2, comments, appendPrompt, attachments);
107192
107665
  } else {
107193
107666
  changeName = derivedName;
@@ -107230,7 +107703,7 @@ function createPrepareHelpers(input) {
107230
107703
  if (!workerCwd)
107231
107704
  return;
107232
107705
  const wtLayout = projectLayout(workerCwd);
107233
- const tasksFile = join26(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107706
+ const tasksFile = join27(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107234
107707
  if (trigger === "review") {
107235
107708
  let body2;
107236
107709
  let heading;
@@ -107523,14 +107996,14 @@ var init_pr_discovery = __esm(() => {
107523
107996
  });
107524
107997
 
107525
107998
  // apps/agent/src/features/review-followup/scan.ts
107526
- import { dirname as dirname11, join as join27 } from "path";
107999
+ import { dirname as dirname11, join as join28 } from "path";
107527
108000
  async function resolveReviewStateDir(changeName, deps) {
107528
108001
  const root = deps.cwdOf(changeName);
107529
108002
  if (root)
107530
108003
  return dirname11(projectLayout(root).stateFile(changeName));
107531
108004
  if (!deps.useWorktree)
107532
108005
  return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
107533
- const wtPath = join27(worktreesDir2(deps.projectRoot), changeName);
108006
+ const wtPath = join28(worktreesDir2(deps.projectRoot), changeName);
107534
108007
  const statePath = projectLayout(wtPath).stateFile(changeName);
107535
108008
  if (await Bun.file(statePath).exists())
107536
108009
  return dirname11(statePath);
@@ -107540,7 +108013,7 @@ async function readReviewWatermark(stateDir) {
107540
108013
  const sidecar = await readSlotSidecar(stateDir, "review");
107541
108014
  if (sidecar)
107542
108015
  return sidecar.lastConsumedCommentAt ?? null;
107543
- const file2 = Bun.file(join27(stateDir, ".ralph-state.json"));
108016
+ const file2 = Bun.file(join28(stateDir, ".ralph-state.json"));
107544
108017
  if (!await file2.exists())
107545
108018
  return null;
107546
108019
  try {
@@ -107752,7 +108225,7 @@ var init_github = __esm(() => {
107752
108225
 
107753
108226
  // apps/agent/src/agent/wire/mention-scan.ts
107754
108227
  import { readdir as readdir2 } from "fs/promises";
107755
- import { join as join28 } from "path";
108228
+ import { join as join29 } from "path";
107756
108229
  function createMentionScanner(input) {
107757
108230
  const {
107758
108231
  apiKey,
@@ -107918,7 +108391,7 @@ function createMentionScanner(input) {
107918
108391
  async function isChangeArchivedForIssue(issue2, cwdByChange, projectRoot) {
107919
108392
  const changeName = changeNameForIssue(issue2);
107920
108393
  const root = cwdByChange.get(changeName) ?? projectRoot;
107921
- const archiveDir = join28(projectLayout(root).tasksDir, "archive");
108394
+ const archiveDir = join29(projectLayout(root).tasksDir, "archive");
107922
108395
  let entries;
107923
108396
  try {
107924
108397
  entries = await readdir2(archiveDir);
@@ -107942,9 +108415,9 @@ var init_mention_scan = __esm(() => {
107942
108415
  });
107943
108416
 
107944
108417
  // apps/agent/src/agent/wire/spawn/default.ts
107945
- import { join as join29 } from "path";
108418
+ import { join as join30 } from "path";
107946
108419
  function defaultSpawn(changeName, cmd, cwd2, logsDir, onWorkerOutput, note) {
107947
- const logFilePath = join29(logsDir, `${changeName}.log`);
108420
+ const logFilePath = join30(logsDir, `${changeName}.log`);
107948
108421
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
107949
108422
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
107950
108423
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -108005,16 +108478,16 @@ var init_default2 = __esm(() => {
108005
108478
  });
108006
108479
 
108007
108480
  // apps/agent/src/agent/state/agent-run-state.ts
108008
- import { basename as basename3, join as join30 } from "path";
108481
+ import { basename as basename3, join as join31 } from "path";
108009
108482
  import { homedir as homedir6 } from "os";
108010
108483
  import { mkdir as mkdir9, writeFile } from "fs/promises";
108011
108484
  function agentRunStatePath(projectRoot) {
108012
- return join30(homedir6(), ".ralph", basename3(projectRoot), "agent-state.json");
108485
+ return join31(homedir6(), ".ralph", basename3(projectRoot), "agent-state.json");
108013
108486
  }
108014
108487
  async function writeAgentRunState(state) {
108015
108488
  const path = agentRunStatePath(state.projectRoot);
108016
108489
  try {
108017
- await mkdir9(join30(homedir6(), ".ralph", basename3(state.projectRoot)), { recursive: true });
108490
+ await mkdir9(join31(homedir6(), ".ralph", basename3(state.projectRoot)), { recursive: true });
108018
108491
  await writeFile(path, JSON.stringify(state, null, 2) + `
108019
108492
  `, "utf-8");
108020
108493
  } catch {}
@@ -108041,17 +108514,17 @@ var CI_FAILED_EXIT2 = 70, PR_FAILED_EXIT2 = 71, NO_CHANGES_EXIT2 = 72;
108041
108514
  // packages/retro/src/paths.ts
108042
108515
  import { homedir as homedir7 } from "os";
108043
108516
  import { mkdir as mkdir10 } from "fs/promises";
108044
- import { join as join31 } from "path";
108517
+ import { join as join32 } from "path";
108045
108518
  function retroDir() {
108046
- return join31(homedir7(), ".ralph", "retro");
108519
+ return join32(homedir7(), ".ralph", "retro");
108047
108520
  }
108048
108521
  async function resolveRetroOutputPath(identifier, date5, dir = retroDir()) {
108049
108522
  await mkdir10(dir, { recursive: true });
108050
- const base2 = join31(dir, `${identifier}-${date5}.md`);
108523
+ const base2 = join32(dir, `${identifier}-${date5}.md`);
108051
108524
  if (!await Bun.file(base2).exists())
108052
108525
  return base2;
108053
108526
  for (let n = 2;; n++) {
108054
- const candidate = join31(dir, `${identifier}-${date5}-${n}.md`);
108527
+ const candidate = join32(dir, `${identifier}-${date5}-${n}.md`);
108055
108528
  if (!await Bun.file(candidate).exists())
108056
108529
  return candidate;
108057
108530
  }
@@ -108165,7 +108638,7 @@ var init_retro = __esm(() => {
108165
108638
  });
108166
108639
 
108167
108640
  // apps/agent/src/agent/wire/spawn/worker.ts
108168
- import { join as join32 } from "path";
108641
+ import { join as join33 } from "path";
108169
108642
  function localDateStamp(d) {
108170
108643
  const y = d.getFullYear();
108171
108644
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -108188,6 +108661,103 @@ function buildTicketDigest(issue2, comments) {
108188
108661
  function retroDepEntry(agentDebug, hook) {
108189
108662
  return agentDebug ? { runRetrospective: hook } : {};
108190
108663
  }
108664
+ function computeWantPr(wantPrBase, isAwaiting, isAwaitingConfirmation) {
108665
+ return wantPrBase && !isAwaiting && !isAwaitingConfirmation;
108666
+ }
108667
+ function computeWantValidateOnly(hasValidateSpec, wantPrBase) {
108668
+ return hasValidateSpec && !wantPrBase;
108669
+ }
108670
+ function releaseWorkerMaps(maps, changeName) {
108671
+ maps.cwdByChange.delete(changeName);
108672
+ maps.statesDirByChange.delete(changeName);
108673
+ maps.branchByChange.delete(changeName);
108674
+ maps.issueByChange.delete(changeName);
108675
+ }
108676
+ function buildTaskCmd(args, cfg, changeName) {
108677
+ const engine = args.engineSet ? args.engine : cfg.engine;
108678
+ const model = args.engineSet ? args.model : cfg.model;
108679
+ const c = [
108680
+ process.execPath,
108681
+ process.argv[1] ?? "",
108682
+ "loop",
108683
+ "task",
108684
+ "--name",
108685
+ changeName,
108686
+ "--" + engine,
108687
+ "--model",
108688
+ model
108689
+ ];
108690
+ const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
108691
+ if (maxIter > 0)
108692
+ c.push("--max-iterations", String(maxIter));
108693
+ const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
108694
+ if (maxCost > 0)
108695
+ c.push("--max-cost", String(maxCost));
108696
+ const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
108697
+ if (maxRuntime > 0)
108698
+ c.push("--max-runtime", String(maxRuntime));
108699
+ const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
108700
+ if (maxFailures !== 5)
108701
+ c.push("--max-failures", String(maxFailures));
108702
+ const delay2 = args.delay || cfg.iterationDelaySeconds;
108703
+ if (delay2 > 0)
108704
+ c.push("--delay", String(delay2));
108705
+ if (args.log || cfg.logRawStream)
108706
+ c.push("--log");
108707
+ if (args.verbose || cfg.taskVerbose)
108708
+ c.push("--verbose");
108709
+ if (args.manualTest || cfg.enableManualTest)
108710
+ c.push("--manual-test");
108711
+ const rp = cfg.openspec.reviewPhase;
108712
+ if (rp.enabled) {
108713
+ c.push("--review-enabled");
108714
+ if (rp.maxRounds !== 1)
108715
+ c.push("--review-max-rounds", String(rp.maxRounds));
108716
+ if (rp.reviewerModel !== undefined)
108717
+ c.push("--review-model", rp.reviewerModel);
108718
+ if (rp.reviewerContextStrategy !== "fresh")
108719
+ c.push("--review-context-strategy", rp.reviewerContextStrategy);
108720
+ }
108721
+ c.push("--from-agent");
108722
+ return c;
108723
+ }
108724
+ function buildPostTaskInput(input) {
108725
+ const { args, cfg } = input;
108726
+ return {
108727
+ ...input.trigger ? { mode: input.trigger } : {},
108728
+ ...input.prUrl ? { prUrl: input.prUrl } : {},
108729
+ changeName: input.changeName,
108730
+ cwd: input.cwd,
108731
+ projectRoot: input.projectRoot,
108732
+ changeDir: input.changeDir,
108733
+ stateFilePath: input.stateFilePath,
108734
+ branch: input.branch,
108735
+ issue: input.issue,
108736
+ exitCode: input.exitCode,
108737
+ useWorktree: input.useWorktree,
108738
+ wantPr: input.wantPr,
108739
+ wantFixCi: input.wantFixCi,
108740
+ wantAutoMerge: input.wantAutoMerge,
108741
+ wantValidateOnly: input.wantValidateOnly,
108742
+ cfg: {
108743
+ teardownScript: cfg.teardownScript ?? null,
108744
+ prBaseBranch: cfg.prBaseBranch,
108745
+ autoMergeStrategy: cfg.autoMergeStrategy,
108746
+ maxCiFixAttempts: cfg.maxCiFixAttempts,
108747
+ ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
108748
+ cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess,
108749
+ ignoreCiChecks: cfg.ignoreCiChecks,
108750
+ stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
108751
+ neverTouch: cfg.boundaries.never_touch,
108752
+ metaOnlyFiles: cfg.boundaries.meta_only_files,
108753
+ finalizeNoOpAsDone: cfg.finalizeNoOpAsDone,
108754
+ manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
108755
+ prDraft: cfg.prDraft,
108756
+ validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
108757
+ },
108758
+ respawnWorker: input.respawnWorker
108759
+ };
108760
+ }
108191
108761
  function createSpawnWorker(input) {
108192
108762
  const {
108193
108763
  args,
@@ -108220,54 +108790,8 @@ function createSpawnWorker(input) {
108220
108790
  onWorkerOutput,
108221
108791
  onWorkerCmd
108222
108792
  } = input;
108223
- function buildTaskCmdFor(changeName) {
108224
- const engine = args.engineSet ? args.engine : cfg.engine;
108225
- const model = args.engineSet ? args.model : cfg.model;
108226
- const c = [
108227
- process.execPath,
108228
- process.argv[1] ?? "",
108229
- "loop",
108230
- "task",
108231
- "--name",
108232
- changeName,
108233
- "--" + engine,
108234
- "--model",
108235
- model
108236
- ];
108237
- const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
108238
- if (maxIter > 0)
108239
- c.push("--max-iterations", String(maxIter));
108240
- const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
108241
- if (maxCost > 0)
108242
- c.push("--max-cost", String(maxCost));
108243
- const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
108244
- if (maxRuntime > 0)
108245
- c.push("--max-runtime", String(maxRuntime));
108246
- const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
108247
- if (maxFailures !== 5)
108248
- c.push("--max-failures", String(maxFailures));
108249
- const delay2 = args.delay || cfg.iterationDelaySeconds;
108250
- if (delay2 > 0)
108251
- c.push("--delay", String(delay2));
108252
- if (args.log || cfg.logRawStream)
108253
- c.push("--log");
108254
- if (args.verbose || cfg.taskVerbose)
108255
- c.push("--verbose");
108256
- if (args.manualTest || cfg.enableManualTest)
108257
- c.push("--manual-test");
108258
- const rp = cfg.openspec.reviewPhase;
108259
- if (rp.enabled) {
108260
- c.push("--review-enabled");
108261
- if (rp.maxRounds !== 1)
108262
- c.push("--review-max-rounds", String(rp.maxRounds));
108263
- if (rp.reviewerModel !== undefined)
108264
- c.push("--review-model", rp.reviewerModel);
108265
- if (rp.reviewerContextStrategy !== "fresh")
108266
- c.push("--review-context-strategy", rp.reviewerContextStrategy);
108267
- }
108268
- c.push("--from-agent");
108269
- return c;
108270
- }
108793
+ const doPostTask = input.runners?.runPostTask ?? runPostTask;
108794
+ const buildTaskCmdFor = (changeName) => buildTaskCmd(args, cfg, changeName);
108271
108795
  const retroSeen = new Set;
108272
108796
  const runRetrospectiveHook = async (info) => {
108273
108797
  try {
@@ -108296,7 +108820,7 @@ function createSpawnWorker(input) {
108296
108820
  paths: {
108297
108821
  changeDir: info.changeDir,
108298
108822
  stateFilePath: info.stateFilePath,
108299
- logFile: join32(logsDir, `${info.changeName}.log`),
108823
+ logFile: join33(logsDir, `${info.changeName}.log`),
108300
108824
  jsonLogFile: args.jsonLogFile ?? null,
108301
108825
  agentStateFile: agentRunStatePath(projectRoot)
108302
108826
  }
@@ -108313,7 +108837,7 @@ function createSpawnWorker(input) {
108313
108837
  return function spawnWorker(changeName, _issue, trigger) {
108314
108838
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
108315
108839
  const injected = runners?.spawnWorker;
108316
- const missionTasksPath = join32(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
108840
+ const missionTasksPath = join33(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
108317
108841
  const prevTasksPromise = (async () => {
108318
108842
  const f2 = Bun.file(missionTasksPath);
108319
108843
  return await f2.exists() ? await f2.text() : "";
@@ -108321,7 +108845,7 @@ function createSpawnWorker(input) {
108321
108845
  let logFilePath;
108322
108846
  let handle;
108323
108847
  if (injected) {
108324
- logFilePath = join32(logsDir, `${changeName}.log`);
108848
+ logFilePath = join33(logsDir, `${changeName}.log`);
108325
108849
  handle = injected(buildTaskCmdFor(changeName), cwd2);
108326
108850
  } else {
108327
108851
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, logsDir, onWorkerOutput, `spawn at ${new Date().toISOString()}`);
@@ -108343,9 +108867,9 @@ function createSpawnWorker(input) {
108343
108867
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
108344
108868
  const wrapped = handle.exited.then(async (code) => {
108345
108869
  const workerLayout = projectLayout(cwd2);
108346
- const validateSpecPath = join32(workerLayout.changeDir(changeName), "specs", "validate.md");
108870
+ const validateSpecPath = join33(workerLayout.changeDir(changeName), "specs", "validate.md");
108347
108871
  const hasValidateSpec = await Bun.file(validateSpecPath).exists();
108348
- const wantValidateOnly = hasValidateSpec && !wantPrBase;
108872
+ const wantValidateOnly = computeWantValidateOnly(hasValidateSpec, wantPrBase);
108349
108873
  if (hasValidateSpec) {
108350
108874
  try {
108351
108875
  const stateFile = workerLayout.stateFile(changeName);
@@ -108389,10 +108913,10 @@ function createSpawnWorker(input) {
108389
108913
  } catch (err) {
108390
108914
  diag("tasks", `! tasks.md normalization failed: ${err.message}`, "yellow");
108391
108915
  }
108392
- const wantPr = wantPrBase && !awaitingChangeSet.has(changeName) && !(coordRef.current?.isAwaitingConfirmation(changeName) ?? false);
108393
- const effectiveCode = await runPostTask({
108394
- ...trigger ? { mode: trigger } : {},
108395
- ...prByChange?.get(changeName) ? { prUrl: prByChange.get(changeName) } : {},
108916
+ const wantPr = computeWantPr(wantPrBase, awaitingChangeSet.has(changeName), coordRef.current?.isAwaitingConfirmation(changeName) ?? false);
108917
+ const effectiveCode = await doPostTask(buildPostTaskInput({
108918
+ args,
108919
+ cfg,
108396
108920
  changeName,
108397
108921
  cwd: cwd2,
108398
108922
  projectRoot,
@@ -108406,24 +108930,10 @@ function createSpawnWorker(input) {
108406
108930
  wantFixCi,
108407
108931
  wantAutoMerge,
108408
108932
  wantValidateOnly,
108409
- cfg: {
108410
- teardownScript: cfg.teardownScript ?? null,
108411
- prBaseBranch: cfg.prBaseBranch,
108412
- autoMergeStrategy: cfg.autoMergeStrategy,
108413
- maxCiFixAttempts: cfg.maxCiFixAttempts,
108414
- ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
108415
- cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess,
108416
- ignoreCiChecks: cfg.ignoreCiChecks,
108417
- stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
108418
- neverTouch: cfg.boundaries.never_touch,
108419
- metaOnlyFiles: cfg.boundaries.meta_only_files,
108420
- finalizeNoOpAsDone: cfg.finalizeNoOpAsDone,
108421
- manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
108422
- prDraft: cfg.prDraft,
108423
- validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
108424
- },
108933
+ ...trigger ? { trigger } : {},
108934
+ ...prByChange?.get(changeName) ? { prUrl: prByChange.get(changeName) } : {},
108425
108935
  respawnWorker: respawn
108426
- }, {
108936
+ }), {
108427
108937
  cmd: tracedCmd,
108428
108938
  git: gitRunner,
108429
108939
  log: onLog,
@@ -108462,10 +108972,7 @@ function createSpawnWorker(input) {
108462
108972
  },
108463
108973
  resolveDependencyBaseBranch: (issue2) => resolveDependencyBaseBranchImpl(issue2, tracedCmd, cwd2, { apiKey, onLog })
108464
108974
  });
108465
- cwdByChange.delete(changeName);
108466
- statesDirByChange.delete(changeName);
108467
- branchByChange.delete(changeName);
108468
- issueByChange.delete(changeName);
108975
+ releaseWorkerMaps({ cwdByChange, statesDirByChange, branchByChange, issueByChange }, changeName);
108469
108976
  onWorkerExited(changeName);
108470
108977
  return effectiveCode;
108471
108978
  });
@@ -108827,7 +109334,7 @@ var init_linear_sync = __esm(() => {
108827
109334
  });
108828
109335
 
108829
109336
  // apps/agent/src/agent/linear-sync/comment-sync.ts
108830
- import { dirname as dirname12, join as join33 } from "path";
109337
+ import { dirname as dirname12, join as join34 } from "path";
108831
109338
  async function readInlineLinearComments(statePath) {
108832
109339
  const file2 = Bun.file(statePath);
108833
109340
  if (!await file2.exists())
@@ -108868,7 +109375,7 @@ function isCommentNotFoundError(err) {
108868
109375
  return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
108869
109376
  }
108870
109377
  async function readTasksMd(changeDir, log3) {
108871
- const file2 = Bun.file(join33(changeDir, "tasks.md"));
109378
+ const file2 = Bun.file(join34(changeDir, "tasks.md"));
108872
109379
  if (!await file2.exists()) {
108873
109380
  log3(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
108874
109381
  return null;
@@ -108974,14 +109481,14 @@ async function postPlanCommentOnce(deps) {
108974
109481
  const check2 = parsePlanningSection(tasksMd);
108975
109482
  if (!check2.allChecked)
108976
109483
  return null;
108977
- const proposalPath = join33(deps.changeDir, "proposal.md");
109484
+ const proposalPath = join34(deps.changeDir, "proposal.md");
108978
109485
  const why = await readSection(proposalPath, "Why");
108979
109486
  const whatChanges = await readSection(proposalPath, "What Changes");
108980
109487
  if (!why && !whatChanges) {
108981
109488
  deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
108982
109489
  return null;
108983
109490
  }
108984
- const designSummary = await readFirstParagraph(join33(deps.changeDir, "design.md"));
109491
+ const designSummary = await readFirstParagraph(join34(deps.changeDir, "design.md"));
108985
109492
  const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
108986
109493
  if (why) {
108987
109494
  parts.push("", "**Why**", "", why);
@@ -261257,7 +261764,7 @@ var init_render_pdf = __esm(() => {
261257
261764
  });
261258
261765
 
261259
261766
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
261260
- import { dirname as dirname13, join as join34 } from "path";
261767
+ import { dirname as dirname13, join as join35 } from "path";
261261
261768
  function describeLinearError(err) {
261262
261769
  const e = err;
261263
261770
  const parts = [e.message ?? String(err)];
@@ -261293,6 +261800,25 @@ async function readSpecAttachmentsSubtree(statePath) {
261293
261800
  const sidecar = await readSlotSidecar(dirname13(statePath), "specAttachments");
261294
261801
  return sidecar ?? await readInlineSpecAttachments(statePath);
261295
261802
  }
261803
+ function asRevisions(value) {
261804
+ if (!Array.isArray(value))
261805
+ return [];
261806
+ const out = [];
261807
+ for (const entry of value) {
261808
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
261809
+ const e = entry;
261810
+ if (typeof e.version === "number" && typeof e.attachmentId === "string" && typeof e.sha256 === "string" && typeof e.trigger === "string") {
261811
+ out.push({
261812
+ version: e.version,
261813
+ attachmentId: e.attachmentId,
261814
+ sha256: e.sha256,
261815
+ trigger: e.trigger
261816
+ });
261817
+ }
261818
+ }
261819
+ }
261820
+ return out;
261821
+ }
261296
261822
  async function readSpecAttachments(statePath) {
261297
261823
  const sa = await readSpecAttachmentsSubtree(statePath);
261298
261824
  return {
@@ -261311,12 +261837,45 @@ async function readSpecAttachments(statePath) {
261311
261837
  designPdf: {
261312
261838
  attachmentId: sa.designPdf?.attachmentId ?? null,
261313
261839
  sha256: sa.designPdf?.sha256 ?? null
261314
- }
261840
+ },
261841
+ designRevisions: asRevisions(sa.designRevisions),
261842
+ designPdfRevisions: asRevisions(sa.designPdfRevisions)
261315
261843
  };
261316
261844
  }
261317
261845
  async function persistSlot(statePath, slot, value) {
261318
261846
  await writeField(stateDirOf(statePath), "linear-attachments", `specAttachments.${slot}`, value);
261319
261847
  }
261848
+ async function persistRevision(statePath, slot, revisions) {
261849
+ await writeField(stateDirOf(statePath), "linear-attachments", `specAttachments.${REVISIONS_KEY[slot]}`, revisions);
261850
+ }
261851
+ async function isDesignSealed(stateDir) {
261852
+ try {
261853
+ const pr2 = await readSlotSidecar(stateDir, "pr");
261854
+ const url2 = pr2?.url;
261855
+ if (typeof url2 === "string" && url2.length > 0)
261856
+ return true;
261857
+ } catch {}
261858
+ try {
261859
+ const confirmation = await readSlotSidecar(stateDir, "confirmation");
261860
+ if (confirmation?.earlyDraftPrAt != null)
261861
+ return true;
261862
+ } catch {}
261863
+ return false;
261864
+ }
261865
+ async function resolveTriggerLabel(stateDir) {
261866
+ try {
261867
+ const flow2 = await readSlotSidecar(stateDir, "flow");
261868
+ const snapshot = flow2?.actorSnapshot;
261869
+ const value = snapshot?.value;
261870
+ if (typeof value === "string" && TRIGGER_LABELS[value])
261871
+ return TRIGGER_LABELS[value];
261872
+ } catch {}
261873
+ return "revision";
261874
+ }
261875
+ function versionedTitle(slot, n, label) {
261876
+ const base2 = `Ralph design #${n} (${label})`;
261877
+ return slot === "designPdf" ? `${base2} (PDF)` : base2;
261878
+ }
261320
261879
  async function adopt(deps, slot) {
261321
261880
  const spec = SLOT_SPECS[slot];
261322
261881
  try {
@@ -261361,12 +261920,71 @@ function extractImplementationSection(tasksMarkdown) {
261361
261920
  return captured.join(`
261362
261921
  `).trim();
261363
261922
  }
261923
+ async function syncSlotSealed(deps, slot, sourceBytes, hash2, state) {
261924
+ const spec = SLOT_SPECS[slot];
261925
+ const revisions = state[REVISIONS_KEY[slot]];
261926
+ const v1Sha = state[slot]?.sha256 ?? null;
261927
+ if (hash2 === v1Sha || revisions.some((r) => r.sha256 === hash2)) {
261928
+ deps.log(` spec-attachments: ${spec.uploadFilename} unchanged (sealed), skipping`, "gray");
261929
+ return;
261930
+ }
261931
+ const n = 2 + revisions.length;
261932
+ const label = await resolveTriggerLabel(stateDirOf(deps.statePath));
261933
+ const title = versionedTitle(slot, n, label);
261934
+ let uploadBytes;
261935
+ try {
261936
+ uploadBytes = await spec.renderBytes(sourceBytes);
261937
+ } catch (err) {
261938
+ deps.log(`! spec-attachments: render ${spec.uploadFilename} (sealed) failed: ${err.message}`, "yellow");
261939
+ return;
261940
+ }
261941
+ let assetUrl;
261942
+ try {
261943
+ const uploaded = await deps.mutations.uploadFileToLinear(deps.apiKey, {
261944
+ filename: spec.uploadFilename,
261945
+ contentType: spec.contentType,
261946
+ bytes: uploadBytes
261947
+ });
261948
+ assetUrl = uploaded.assetUrl;
261949
+ } catch (err) {
261950
+ deps.log(`! spec-attachments: upload ${spec.uploadFilename} (sealed) failed: ${describeLinearError(err)}`, "yellow");
261951
+ return;
261952
+ }
261953
+ let attachmentId = null;
261954
+ try {
261955
+ attachmentId = await deps.mutations.findIssueAttachmentByTitle(deps.apiKey, deps.issueId, title);
261956
+ if (attachmentId) {
261957
+ deps.log(` spec-attachments: adopted existing ${title} attachment ${attachmentId}`, "gray");
261958
+ }
261959
+ } catch (err) {
261960
+ deps.log(`! spec-attachments: findIssueAttachmentByTitle ${title} failed (treating as no match): ${describeLinearError(err)}`, "yellow");
261961
+ attachmentId = null;
261962
+ }
261963
+ if (!attachmentId) {
261964
+ try {
261965
+ attachmentId = await deps.mutations.createAttachmentForUrl(deps.apiKey, {
261966
+ issueId: deps.issueId,
261967
+ url: assetUrl,
261968
+ title,
261969
+ subtitle: `iteration ${deps.iteration}`
261970
+ });
261971
+ } catch (err) {
261972
+ deps.log(`! spec-attachments: createAttachmentForUrl ${title} failed: ${describeLinearError(err)}`, "yellow");
261973
+ return;
261974
+ }
261975
+ deps.log(` spec-attachments: created ${title} attachment`, "gray");
261976
+ }
261977
+ await persistRevision(deps.statePath, slot, [
261978
+ ...revisions,
261979
+ { version: n, attachmentId, sha256: hash2, trigger: label }
261980
+ ]);
261981
+ }
261364
261982
  async function syncSlot(deps, slot) {
261365
261983
  const spec = SLOT_SPECS[slot];
261366
261984
  const [primaryName, ...trailingNames] = spec.sourceFiles;
261367
261985
  if (!primaryName)
261368
261986
  return;
261369
- const primary = Bun.file(join34(deps.changeDir, primaryName));
261987
+ const primary = Bun.file(join35(deps.changeDir, primaryName));
261370
261988
  if (!await primary.exists()) {
261371
261989
  deps.log(` spec-attachments: ${primaryName} missing, skipping`, "gray");
261372
261990
  return;
@@ -261385,7 +262003,7 @@ async function syncSlot(deps, slot) {
261385
262003
  const parts = [primaryBytes];
261386
262004
  const enc = new TextEncoder;
261387
262005
  for (const name of trailingNames) {
261388
- const f2 = Bun.file(join34(deps.changeDir, name));
262006
+ const f2 = Bun.file(join35(deps.changeDir, name));
261389
262007
  if (!await f2.exists())
261390
262008
  continue;
261391
262009
  let raw;
@@ -261416,7 +262034,12 @@ ${body}
261416
262034
  offset += p.length;
261417
262035
  }
261418
262036
  const hash2 = sha256Hex(sourceBytes);
261419
- let current = (await readSpecAttachments(deps.statePath))[slot] ?? EMPTY_SLOT;
262037
+ const state = await readSpecAttachments(deps.statePath);
262038
+ if (await isDesignSealed(stateDirOf(deps.statePath))) {
262039
+ await syncSlotSealed(deps, slot, sourceBytes, hash2, state);
262040
+ return;
262041
+ }
262042
+ let current = state[slot] ?? EMPTY_SLOT;
261420
262043
  if (!current.attachmentId) {
261421
262044
  const { adoptedId } = await adopt(deps, slot);
261422
262045
  if (adoptedId) {
@@ -261518,7 +262141,7 @@ async function syncSpecAttachments(deps) {
261518
262141
  await syncSlot(deps, slot);
261519
262142
  }
261520
262143
  }
261521
- var identityRender = async (b2) => b2, pdfRender = (title) => async (b2) => renderMarkdownToPdf(new TextDecoder().decode(b2), title), SLOT_SPECS, LEGACY_SLOT_TITLES, EMPTY_SLOT;
262144
+ var identityRender = async (b2) => b2, pdfRender = (title) => async (b2) => renderMarkdownToPdf(new TextDecoder().decode(b2), title), SLOT_SPECS, LEGACY_SLOT_TITLES, REVISIONS_KEY, EMPTY_SLOT, TRIGGER_LABELS;
261522
262145
  var init_spec_attachments = __esm(() => {
261523
262146
  init_store();
261524
262147
  init_comment_sync();
@@ -261545,7 +262168,16 @@ var init_spec_attachments = __esm(() => {
261545
262168
  proposal: "Ralph proposal",
261546
262169
  proposalPdf: "Ralph proposal (PDF)"
261547
262170
  };
262171
+ REVISIONS_KEY = {
262172
+ design: "designRevisions",
262173
+ designPdf: "designPdfRevisions"
262174
+ };
261548
262175
  EMPTY_SLOT = { attachmentId: null, sha256: null };
262176
+ TRIGGER_LABELS = {
262177
+ review: "review follow-up",
262178
+ "ci-fix": "CI fix",
262179
+ "conflict-fix": "conflict fix"
262180
+ };
261549
262181
  });
261550
262182
 
261551
262183
  // apps/agent/src/agent/wire/comment-sync.ts
@@ -261653,9 +262285,9 @@ var init_comment_sync2 = __esm(() => {
261653
262285
  });
261654
262286
 
261655
262287
  // apps/agent/src/features/pr-tracker/state.ts
261656
- import { join as join35 } from "path";
262288
+ import { join as join36 } from "path";
261657
262289
  async function readState2(projectRoot) {
261658
- const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
262290
+ const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
261659
262291
  const file2 = Bun.file(path);
261660
262292
  if (!await file2.exists())
261661
262293
  return {};
@@ -261671,7 +262303,7 @@ async function readState2(projectRoot) {
261671
262303
  }
261672
262304
  }
261673
262305
  async function writeState2(projectRoot, state) {
261674
- const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
262306
+ const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
261675
262307
  await Bun.write(path, JSON.stringify(state, null, 2));
261676
262308
  }
261677
262309
  var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
@@ -261750,7 +262382,7 @@ var init_pr_tracker = __esm(() => {
261750
262382
  });
261751
262383
 
261752
262384
  // apps/agent/src/agent/wire.ts
261753
- import { join as join36 } from "path";
262385
+ import { join as join37 } from "path";
261754
262386
  function buildAgentCoordinator(input) {
261755
262387
  const {
261756
262388
  args,
@@ -261769,7 +262401,7 @@ function buildAgentCoordinator(input) {
261769
262401
  onWorkerCmd,
261770
262402
  onAwaitingTicket
261771
262403
  } = input;
261772
- const logsDir = join36(projectRoot, ".ralph", "logs");
262404
+ const logsDir = join37(projectRoot, ".ralph", "logs");
261773
262405
  const bus = createBus();
261774
262406
  subscribeAgentDiag(bus, onLog);
261775
262407
  const diag = (area, message, color) => {
@@ -261795,7 +262427,11 @@ function buildAgentCoordinator(input) {
261795
262427
  const awaitingChangeSet = new Set;
261796
262428
  const coordRef = { current: null };
261797
262429
  let pollContext = new PollContext;
261798
- const useWorktree = args.worktree || cfg.useWorktree;
262430
+ let useWorktree = args.worktree || cfg.useWorktree;
262431
+ if (concurrency > 1 && !useWorktree) {
262432
+ diag("config", `! concurrency is ${concurrency} but useWorktree is off \u2014 forcing worktrees on so parallel tasks get isolated working copies`, "yellow");
262433
+ useWorktree = true;
262434
+ }
261799
262435
  const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
261800
262436
  const proc = Bun.spawn({
261801
262437
  cmd: ["sh", "-c", cmd],
@@ -262000,7 +262636,7 @@ function buildAgentCoordinator(input) {
262000
262636
  const changeDir = projectLayout(root).changeDir(changeName);
262001
262637
  const parts = [];
262002
262638
  for (const name of ["tasks.md", "proposal.md", "design.md"]) {
262003
- const file2 = Bun.file(join36(changeDir, name));
262639
+ const file2 = Bun.file(join37(changeDir, name));
262004
262640
  if (!await file2.exists())
262005
262641
  continue;
262006
262642
  parts.push(`${name}:${file2.lastModified}:${file2.size}`);
@@ -262045,7 +262681,7 @@ function buildAgentCoordinator(input) {
262045
262681
  getGaveUpTotal: async () => {
262046
262682
  let total = 0;
262047
262683
  for (const [changeName, root] of cwdByChange) {
262048
- const file2 = Bun.file(join36(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
262684
+ const file2 = Bun.file(join37(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
262049
262685
  if (!await file2.exists())
262050
262686
  continue;
262051
262687
  try {
@@ -262333,7 +262969,7 @@ var init_output_utils = __esm(() => {
262333
262969
  });
262334
262970
 
262335
262971
  // apps/agent/src/agent/state/worker-state-poll.ts
262336
- import { join as join37 } from "path";
262972
+ import { join as join38 } from "path";
262337
262973
  function parseSubtasks(tasksMd) {
262338
262974
  const out = [];
262339
262975
  let skipSection = false;
@@ -262366,7 +263002,7 @@ function initialWorkerSnapshot() {
262366
263002
  async function readWorkerSnapshot(input) {
262367
263003
  const next = { ...input.prev };
262368
263004
  try {
262369
- const file2 = Bun.file(join37(input.statesDir, input.changeName, ".ralph-state.json"));
263005
+ const file2 = Bun.file(join38(input.statesDir, input.changeName, ".ralph-state.json"));
262370
263006
  if (await file2.exists()) {
262371
263007
  const json2 = await file2.json();
262372
263008
  next.iter = json2.iteration ?? next.iter;
@@ -262375,10 +263011,10 @@ async function readWorkerSnapshot(input) {
262375
263011
  } catch {}
262376
263012
  if (input.changeDir) {
262377
263013
  try {
262378
- const tasksFile = Bun.file(join37(input.changeDir, "tasks.md"));
262379
- const proposalFile = Bun.file(join37(input.changeDir, "proposal.md"));
262380
- const designFile = Bun.file(join37(input.changeDir, "design.md"));
262381
- const reviewFindingsFile = Bun.file(join37(input.changeDir, "review-findings.md"));
263014
+ const tasksFile = Bun.file(join38(input.changeDir, "tasks.md"));
263015
+ const proposalFile = Bun.file(join38(input.changeDir, "proposal.md"));
263016
+ const designFile = Bun.file(join38(input.changeDir, "design.md"));
263017
+ const reviewFindingsFile = Bun.file(join38(input.changeDir, "review-findings.md"));
262382
263018
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
262383
263019
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
262384
263020
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -262441,7 +263077,7 @@ var init_worker_state_poll = __esm(() => {
262441
263077
  });
262442
263078
 
262443
263079
  // apps/agent/src/components/AgentMode.tsx
262444
- import { join as join38 } from "path";
263080
+ import { join as join39 } from "path";
262445
263081
  async function appendSteeringImpl(changeDir, message) {
262446
263082
  await runWithContext(createDefaultContext(), async () => {
262447
263083
  appendSteeringMessage(changeDir, message);
@@ -264014,7 +264650,7 @@ function AgentMode({
264014
264650
  },
264015
264651
  onSubmit: async (message) => {
264016
264652
  try {
264017
- await appendSteering2(join38(tasksDir, w2.changeName), message);
264653
+ await appendSteering2(join39(tasksDir, w2.changeName), message);
264018
264654
  fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
264019
264655
  } catch (err) {
264020
264656
  const text = err.message;
@@ -264318,7 +264954,7 @@ __export(exports_list, {
264318
264954
  buildBuckets: () => buildBuckets,
264319
264955
  backlogRankByIssueId: () => backlogRankByIssueId
264320
264956
  });
264321
- import { join as join39 } from "path";
264957
+ import { join as join40 } from "path";
264322
264958
  function countTaskItems(content) {
264323
264959
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
264324
264960
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -264334,13 +264970,13 @@ function buildLocalRows() {
264334
264970
  const sources = [{ dir: statesDir, label: "main" }];
264335
264971
  const worktreesRoot = worktreesDir2(projectRoot);
264336
264972
  for (const wt of storage.list(worktreesRoot)) {
264337
- sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
264973
+ sources.push({ dir: join40(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
264338
264974
  }
264339
264975
  for (const { dir, label } of sources) {
264340
264976
  for (const entry of storage.list(dir)) {
264341
264977
  if (seen.has(entry))
264342
264978
  continue;
264343
- const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
264979
+ const raw = storage.read(join40(dir, entry, ".ralph-state.json"));
264344
264980
  if (raw === null)
264345
264981
  continue;
264346
264982
  let state;
@@ -264355,7 +264991,7 @@ function buildLocalRows() {
264355
264991
  const firstLine = promptRaw.split(`
264356
264992
  `).find((l3) => l3.trim() !== "") ?? "";
264357
264993
  let progress = "\u2014";
264358
- const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
264994
+ const tasksContent = storage.read(join40(dir, entry, "tasks.md"));
264359
264995
  if (tasksContent !== null) {
264360
264996
  const { checked, unchecked } = countTaskItems(tasksContent);
264361
264997
  const total = checked + unchecked;
@@ -264855,7 +265491,7 @@ var exports_json_runner = {};
264855
265491
  __export(exports_json_runner, {
264856
265492
  runAgentJson: () => runAgentJson
264857
265493
  });
264858
- import { join as join40 } from "path";
265494
+ import { join as join41 } from "path";
264859
265495
  import { mkdir as mkdir12 } from "fs/promises";
264860
265496
  import { homedir as homedir8 } from "os";
264861
265497
  function makeEmit(fileSink) {
@@ -264877,7 +265513,7 @@ async function runAgentJson({
264877
265513
  tasksDir,
264878
265514
  runPreflight: runPreflight2 = runPreflight
264879
265515
  }) {
264880
- await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
265516
+ await mkdir12(join41(homedir8(), ".ralph"), { recursive: true }).catch(() => {
264881
265517
  return;
264882
265518
  });
264883
265519
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -265094,7 +265730,7 @@ __export(exports_src3, {
265094
265730
  main: () => main3
265095
265731
  });
265096
265732
  import { mkdir as mkdir13 } from "fs/promises";
265097
- import { join as join41 } from "path";
265733
+ import { join as join42 } from "path";
265098
265734
  async function main3(argv) {
265099
265735
  if (argv.includes("--help") || argv.includes("-h")) {
265100
265736
  printAgentHelp();
@@ -265160,7 +265796,7 @@ async function main3(argv) {
265160
265796
  }
265161
265797
  await mkdir13(statesDir, { recursive: true });
265162
265798
  await mkdir13(tasksDir, { recursive: true });
265163
- await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
265799
+ await mkdir13(join42(projectRoot, ".ralph"), { recursive: true });
265164
265800
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
265165
265801
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
265166
265802
  `);
@@ -265218,6 +265854,7 @@ var init_src8 = __esm(async () => {
265218
265854
  init_src();
265219
265855
  init_src2();
265220
265856
  init_version();
265857
+ init_common_args();
265221
265858
  if (typeof globalThis.Bun === "undefined") {
265222
265859
  process.stderr.write(`ralphy requires the Bun runtime (https://bun.sh/). It is not compatible with plain Node.js.
265223
265860
  ` + "Install Bun and re-run with `bun` or `bunx ralphy`.\n");
@@ -265302,7 +265939,8 @@ ${HELP}
265302
265939
  if (shouldOfferSetup(subcommand, argv.slice(1))) {
265303
265940
  try {
265304
265941
  const { maybeRunSetupWizard: maybeRunSetupWizard2 } = await init_src4().then(() => exports_src);
265305
- await maybeRunSetupWizard2();
265942
+ const { projectRoot, workflowFile } = parseWorkflowPathArgs(argv.slice(1));
265943
+ await maybeRunSetupWizard2(projectRoot, workflowFile);
265306
265944
  } catch (setupErr) {
265307
265945
  captureError("setup_wizard_error", setupErr, { subcommand });
265308
265946
  }