@neriros/ralphy 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +716 -43
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -50548,8 +50548,8 @@ var require_axios = __commonJS((exports, module) => {
50548
50548
  });
50549
50549
 
50550
50550
  // apps/cli/src/index.ts
50551
- import { resolve, join as join12, dirname as dirname4 } from "path";
50552
- import { exists, mkdir as mkdir2 } from "fs/promises";
50551
+ import { resolve, join as join15, dirname as dirname4 } from "path";
50552
+ import { exists, mkdir as mkdir3 } from "fs/promises";
50553
50553
 
50554
50554
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
50555
50555
  import { Stream } from "stream";
@@ -56091,7 +56091,7 @@ var import_react20 = __toESM(require_react(), 1);
56091
56091
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/hooks/use-focus-manager.js
56092
56092
  var import_react21 = __toESM(require_react(), 1);
56093
56093
  // apps/cli/src/index.ts
56094
- var import_react58 = __toESM(require_react(), 1);
56094
+ var import_react59 = __toESM(require_react(), 1);
56095
56095
 
56096
56096
  // packages/output/src/output.ts
56097
56097
  var formatters = {
@@ -56117,7 +56117,7 @@ function log(msg) {
56117
56117
  }
56118
56118
 
56119
56119
  // apps/cli/src/cli.ts
56120
- var VALID_MODES = new Set(["task", "list", "status", "init"]);
56120
+ var VALID_MODES = new Set(["task", "list", "status", "init", "agent"]);
56121
56121
  var VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
56122
56122
  var HELP_TEXT = [
56123
56123
  "Usage: ralph <command> [options]",
@@ -56127,6 +56127,7 @@ var HELP_TEXT = [
56127
56127
  " list List active changes",
56128
56128
  " status Show detailed change status",
56129
56129
  " init Initialize OpenSpec in current directory",
56130
+ " agent Poll Linear for new tasks and run loops concurrently",
56130
56131
  "",
56131
56132
  "Options:",
56132
56133
  " --name <name> Change name (required for most commands)",
@@ -56143,6 +56144,15 @@ var HELP_TEXT = [
56143
56144
  " --unlimited No iteration limit (default)",
56144
56145
  " --log Log raw engine stream",
56145
56146
  " --verbose Verbose output",
56147
+ "",
56148
+ "Agent mode options (require LINEAR_API_KEY env var):",
56149
+ " --linear-team <key> Linear team key (e.g. ENG)",
56150
+ " --linear-assignee <id> Filter by assignee (user id, email, or 'me')",
56151
+ " --linear-status <name> Filter by status name (repeatable, e.g. Todo, In Progress)",
56152
+ " --linear-label <name> Filter by label name (repeatable, any-of)",
56153
+ " --poll-interval <s> Seconds between Linear polls (default: 60)",
56154
+ " --concurrency <n> Max concurrent task loops (default: 1)",
56155
+ "",
56146
56156
  " --help, -h Show this help message",
56147
56157
  "",
56148
56158
  "Examples:",
@@ -56171,7 +56181,13 @@ async function parseArgs(argv) {
56171
56181
  maxConsecutiveFailures: 5,
56172
56182
  delay: 0,
56173
56183
  log: false,
56174
- verbose: false
56184
+ verbose: false,
56185
+ linearTeam: "",
56186
+ linearAssignee: "",
56187
+ linearStatus: [],
56188
+ linearLabel: [],
56189
+ pollInterval: 60,
56190
+ concurrency: 1
56175
56191
  };
56176
56192
  let expectModel = false;
56177
56193
  let expectModelFlag = false;
@@ -56185,6 +56201,12 @@ async function parseArgs(argv) {
56185
56201
  let expectMaxIterations = false;
56186
56202
  let expectTimeout = false;
56187
56203
  let expectPushInterval = false;
56204
+ let expectLinearTeam = false;
56205
+ let expectLinearAssignee = false;
56206
+ let expectLinearStatus = false;
56207
+ let expectLinearLabel = false;
56208
+ let expectPollInterval = false;
56209
+ let expectConcurrency = false;
56188
56210
  for (const arg of argv) {
56189
56211
  if (expectModel) {
56190
56212
  if (VALID_MODELS.has(arg)) {
@@ -56250,6 +56272,36 @@ async function parseArgs(argv) {
56250
56272
  expectPushInterval = false;
56251
56273
  continue;
56252
56274
  }
56275
+ if (expectLinearTeam) {
56276
+ result2.linearTeam = arg;
56277
+ expectLinearTeam = false;
56278
+ continue;
56279
+ }
56280
+ if (expectLinearAssignee) {
56281
+ result2.linearAssignee = arg;
56282
+ expectLinearAssignee = false;
56283
+ continue;
56284
+ }
56285
+ if (expectLinearStatus) {
56286
+ result2.linearStatus.push(arg);
56287
+ expectLinearStatus = false;
56288
+ continue;
56289
+ }
56290
+ if (expectLinearLabel) {
56291
+ result2.linearLabel.push(arg);
56292
+ expectLinearLabel = false;
56293
+ continue;
56294
+ }
56295
+ if (expectPollInterval) {
56296
+ result2.pollInterval = parseInt(arg, 10);
56297
+ expectPollInterval = false;
56298
+ continue;
56299
+ }
56300
+ if (expectConcurrency) {
56301
+ result2.concurrency = parseInt(arg, 10);
56302
+ expectConcurrency = false;
56303
+ continue;
56304
+ }
56253
56305
  switch (arg) {
56254
56306
  case "--claude":
56255
56307
  if (result2.engineSet && result2.engine !== "claude") {
@@ -56308,6 +56360,24 @@ async function parseArgs(argv) {
56308
56360
  case "--verbose":
56309
56361
  result2.verbose = true;
56310
56362
  break;
56363
+ case "--linear-team":
56364
+ expectLinearTeam = true;
56365
+ break;
56366
+ case "--linear-assignee":
56367
+ expectLinearAssignee = true;
56368
+ break;
56369
+ case "--linear-status":
56370
+ expectLinearStatus = true;
56371
+ break;
56372
+ case "--linear-label":
56373
+ expectLinearLabel = true;
56374
+ break;
56375
+ case "--poll-interval":
56376
+ expectPollInterval = true;
56377
+ break;
56378
+ case "--concurrency":
56379
+ expectConcurrency = true;
56380
+ break;
56311
56381
  default:
56312
56382
  if (VALID_MODES.has(arg)) {
56313
56383
  result2.mode = arg;
@@ -56374,8 +56444,8 @@ function createDefaultContext() {
56374
56444
  }
56375
56445
 
56376
56446
  // apps/cli/src/components/App.tsx
56377
- var import_react57 = __toESM(require_react(), 1);
56378
- import { join as join11 } from "path";
56447
+ var import_react58 = __toESM(require_react(), 1);
56448
+ import { join as join14 } from "path";
56379
56449
 
56380
56450
  // packages/core/src/state.ts
56381
56451
  import { join as join2 } from "path";
@@ -69501,12 +69571,603 @@ function TaskLoop({ opts }) {
69501
69571
  }, undefined, true, undefined, this);
69502
69572
  }
69503
69573
 
69574
+ // apps/cli/src/components/AgentMode.tsx
69575
+ var import_react57 = __toESM(require_react(), 1);
69576
+
69577
+ // apps/cli/src/agent/linear.ts
69578
+ var OPEN_STATE_TYPES = ["unstarted", "started", "backlog"];
69579
+ var LINEAR_API = "https://api.linear.app/graphql";
69580
+ async function fetchOpenIssues(apiKey, filter2) {
69581
+ const where = {};
69582
+ if (filter2.team)
69583
+ where.team = { key: { eq: filter2.team } };
69584
+ if (filter2.assignee) {
69585
+ if (filter2.assignee === "me") {
69586
+ where.assignee = { isMe: { eq: true } };
69587
+ } else if (filter2.assignee.includes("@")) {
69588
+ where.assignee = { email: { eq: filter2.assignee } };
69589
+ } else {
69590
+ where.assignee = { id: { eq: filter2.assignee } };
69591
+ }
69592
+ }
69593
+ if (filter2.statuses && filter2.statuses.length > 0) {
69594
+ where.state = { name: { in: filter2.statuses } };
69595
+ } else {
69596
+ where.state = { type: { in: [...OPEN_STATE_TYPES] } };
69597
+ }
69598
+ if (filter2.labels && filter2.labels.length > 0) {
69599
+ where.labels = { some: { name: { in: filter2.labels } } };
69600
+ }
69601
+ const query = `query Issues($filter: IssueFilter) {
69602
+ issues(filter: $filter, first: 50) {
69603
+ nodes {
69604
+ id identifier title description url
69605
+ state { name type }
69606
+ assignee { id email name }
69607
+ labels { nodes { name } }
69608
+ }
69609
+ }
69610
+ }`;
69611
+ const data = await linearRequest(apiKey, query, {
69612
+ filter: where
69613
+ });
69614
+ return data.issues.nodes.map((n) => ({
69615
+ id: n.id,
69616
+ identifier: n.identifier,
69617
+ title: n.title,
69618
+ description: n.description,
69619
+ url: n.url,
69620
+ state: n.state,
69621
+ assignee: n.assignee,
69622
+ labels: n.labels.nodes.map((l) => l.name)
69623
+ }));
69624
+ }
69625
+ async function linearRequest(apiKey, query, variables) {
69626
+ const res = await fetch(LINEAR_API, {
69627
+ method: "POST",
69628
+ headers: { "Content-Type": "application/json", Authorization: apiKey },
69629
+ body: JSON.stringify({ query, variables })
69630
+ });
69631
+ if (!res.ok) {
69632
+ const err = new Error("Linear API request failed");
69633
+ err.status = res.status;
69634
+ err.body = await res.text();
69635
+ throw err;
69636
+ }
69637
+ const json = await res.json();
69638
+ if (json.errors?.length) {
69639
+ const err = new Error("Linear API returned errors");
69640
+ err.messages = json.errors.map((e) => e.message);
69641
+ throw err;
69642
+ }
69643
+ if (!json.data) {
69644
+ throw new Error("Linear API returned no data");
69645
+ }
69646
+ return json.data;
69647
+ }
69648
+ async function addIssueComment(apiKey, issueId, body) {
69649
+ const mutation = `mutation Comment($issueId: String!, $body: String!) {
69650
+ commentCreate(input: { issueId: $issueId, body: $body }) { success }
69651
+ }`;
69652
+ await linearRequest(apiKey, mutation, {
69653
+ issueId,
69654
+ body
69655
+ });
69656
+ }
69657
+ async function fetchIssueComments(apiKey, issueId) {
69658
+ const query = `query Comments($id: String!) {
69659
+ issue(id: $id) {
69660
+ comments(first: 50) {
69661
+ nodes { id body createdAt user { name email } }
69662
+ }
69663
+ }
69664
+ }`;
69665
+ const data = await linearRequest(apiKey, query, { id: issueId });
69666
+ return data.issue?.comments.nodes ?? [];
69667
+ }
69668
+ async function fetchWorkflowStates(apiKey, teamKey) {
69669
+ const query = `query States($team: String!) {
69670
+ workflowStates(filter: { team: { key: { eq: $team } } }, first: 50) {
69671
+ nodes { id name type }
69672
+ }
69673
+ }`;
69674
+ const data = await linearRequest(apiKey, query, {
69675
+ team: teamKey
69676
+ });
69677
+ return data.workflowStates.nodes;
69678
+ }
69679
+ async function updateIssueState(apiKey, issueId, stateId) {
69680
+ const mutation = `mutation Update($id: String!, $stateId: String!) {
69681
+ issueUpdate(id: $id, input: { stateId: $stateId }) { success }
69682
+ }`;
69683
+ await linearRequest(apiKey, mutation, {
69684
+ id: issueId,
69685
+ stateId
69686
+ });
69687
+ }
69688
+
69689
+ // apps/cli/src/agent/state.ts
69690
+ import { join as join10 } from "path";
69691
+ var AgentStateSchema = exports_external.object({
69692
+ processedIssueIds: exports_external.array(exports_external.string()).default([]),
69693
+ lastPollAt: exports_external.string().nullable().default(null)
69694
+ });
69695
+ function statePath(projectRoot) {
69696
+ return join10(projectRoot, ".ralph", "agent-state.json");
69697
+ }
69698
+ async function readAgentState(projectRoot) {
69699
+ const file = Bun.file(statePath(projectRoot));
69700
+ if (!await file.exists()) {
69701
+ return AgentStateSchema.parse({});
69702
+ }
69703
+ return AgentStateSchema.parse(await file.json());
69704
+ }
69705
+ async function writeAgentState(projectRoot, state) {
69706
+ await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
69707
+ `);
69708
+ }
69709
+
69710
+ // apps/cli/src/agent/scaffold.ts
69711
+ import { join as join11 } from "path";
69712
+ import { mkdir } from "fs/promises";
69713
+ function changeNameForIssue(issue) {
69714
+ const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
69715
+ return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
69716
+ }
69717
+ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = []) {
69718
+ const name = changeNameForIssue(issue);
69719
+ const changeDir = join11(tasksDir, name);
69720
+ const stateDir = join11(statesDir, name);
69721
+ await mkdir(changeDir, { recursive: true });
69722
+ await mkdir(join11(changeDir, "specs"), { recursive: true });
69723
+ await mkdir(stateDir, { recursive: true });
69724
+ const commentsBlock = comments.length > 0 ? [
69725
+ "",
69726
+ "## Linear comments",
69727
+ "",
69728
+ ...comments.flatMap((c) => [
69729
+ `**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
69730
+ "",
69731
+ c.body.trim(),
69732
+ ""
69733
+ ])
69734
+ ] : [];
69735
+ const proposal = [
69736
+ `# ${issue.identifier}: ${issue.title}`,
69737
+ "",
69738
+ `Source: [${issue.identifier}](${issue.url})`,
69739
+ `Status: ${issue.state.name}`,
69740
+ issue.assignee ? `Assignee: ${issue.assignee.name}` : "",
69741
+ issue.labels.length ? `Labels: ${issue.labels.join(", ")}` : "",
69742
+ "",
69743
+ "## Description",
69744
+ "",
69745
+ issue.description?.trim() || "_No description provided in Linear._",
69746
+ ...commentsBlock,
69747
+ "",
69748
+ "## Steering",
69749
+ "",
69750
+ "_Add steering notes here as the loop runs._",
69751
+ ""
69752
+ ].filter((l) => l !== "").join(`
69753
+ `);
69754
+ const tasks = [
69755
+ `# Tasks for ${issue.identifier}`,
69756
+ "",
69757
+ `- [ ] Read the Linear issue at ${issue.url} and break it into concrete subtasks`,
69758
+ `- [ ] Implement the changes described in proposal.md`,
69759
+ `- [ ] Add or update tests covering the new behavior`,
69760
+ `- [ ] Run \`bun run lint\` and \`bun run test\` and fix any failures`,
69761
+ ""
69762
+ ].join(`
69763
+ `);
69764
+ const design = [
69765
+ `# Design for ${issue.identifier}`,
69766
+ "",
69767
+ "_Fill in the technical design as you work through the issue._",
69768
+ ""
69769
+ ].join(`
69770
+ `);
69771
+ await Bun.write(join11(changeDir, "proposal.md"), proposal);
69772
+ await Bun.write(join11(changeDir, "tasks.md"), tasks);
69773
+ await Bun.write(join11(changeDir, "design.md"), design);
69774
+ return name;
69775
+ }
69776
+
69777
+ // apps/cli/src/agent/config.ts
69778
+ import { join as join12 } from "path";
69779
+ var RalphyConfigSchema = exports_external.object({
69780
+ concurrency: exports_external.number().int().positive().default(1),
69781
+ pollIntervalSeconds: exports_external.number().int().positive().default(60),
69782
+ maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
69783
+ maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
69784
+ engine: exports_external.enum(["claude", "codex"]).default("claude"),
69785
+ model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69786
+ linear: exports_external.object({
69787
+ team: exports_external.string().optional(),
69788
+ assignee: exports_external.string().optional(),
69789
+ statuses: exports_external.array(exports_external.string()).default([]),
69790
+ labels: exports_external.union([exports_external.array(exports_external.string()), exports_external.string()]).transform((v) => typeof v === "string" ? [v] : v).default([]),
69791
+ inProgressStatus: exports_external.string().optional(),
69792
+ doneStatus: exports_external.string().optional(),
69793
+ postComments: exports_external.boolean().default(true)
69794
+ }).default({ statuses: [], labels: [], postComments: true })
69795
+ }).default({
69796
+ concurrency: 1,
69797
+ pollIntervalSeconds: 60,
69798
+ maxIterationsPerTask: 0,
69799
+ maxCostUsdPerTask: 0,
69800
+ engine: "claude",
69801
+ model: "opus",
69802
+ linear: { statuses: [], labels: [], postComments: true }
69803
+ });
69804
+ async function loadRalphyConfig(projectRoot) {
69805
+ const path = join12(projectRoot, "ralphy.config.json");
69806
+ const file = Bun.file(path);
69807
+ if (!await file.exists()) {
69808
+ return RalphyConfigSchema.parse({});
69809
+ }
69810
+ const raw = await file.json();
69811
+ return RalphyConfigSchema.parse(raw);
69812
+ }
69813
+ async function ensureRalphyConfig(projectRoot) {
69814
+ const path = join12(projectRoot, "ralphy.config.json");
69815
+ const file = Bun.file(path);
69816
+ if (await file.exists())
69817
+ return path;
69818
+ const defaults2 = RalphyConfigSchema.parse({});
69819
+ await Bun.write(path, JSON.stringify(defaults2, null, 2) + `
69820
+ `);
69821
+ return path;
69822
+ }
69823
+
69824
+ // apps/cli/src/agent/coordinator.ts
69825
+ class AgentCoordinator {
69826
+ deps;
69827
+ opts;
69828
+ workers = [];
69829
+ pendingIds = new Set;
69830
+ queue = [];
69831
+ state = null;
69832
+ stopped = false;
69833
+ constructor(deps, opts) {
69834
+ this.deps = deps;
69835
+ this.opts = opts;
69836
+ }
69837
+ get activeCount() {
69838
+ return this.workers.length;
69839
+ }
69840
+ get queuedCount() {
69841
+ return this.queue.length;
69842
+ }
69843
+ get activeWorkers() {
69844
+ return this.workers;
69845
+ }
69846
+ async init() {
69847
+ this.state = await this.deps.loadState();
69848
+ }
69849
+ async pollOnce() {
69850
+ if (this.stopped)
69851
+ return { found: 0, added: 0 };
69852
+ let issues;
69853
+ try {
69854
+ issues = await this.deps.fetchIssues(this.opts.filter);
69855
+ } catch (err) {
69856
+ this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
69857
+ return { found: 0, added: 0 };
69858
+ }
69859
+ const state = this.state;
69860
+ const seen = new Set(state.processedIssueIds);
69861
+ const queued = new Set(this.queue.map((i) => i.id));
69862
+ const active = new Set(this.workers.map((w) => w.issueId));
69863
+ let added = 0;
69864
+ for (const issue of issues) {
69865
+ if (seen.has(issue.id))
69866
+ continue;
69867
+ if (queued.has(issue.id))
69868
+ continue;
69869
+ if (active.has(issue.id))
69870
+ continue;
69871
+ if (this.pendingIds.has(issue.id))
69872
+ continue;
69873
+ this.queue.push(issue);
69874
+ added += 1;
69875
+ }
69876
+ state.lastPollAt = new Date().toISOString();
69877
+ await this.deps.saveState(state);
69878
+ this.spawnNext();
69879
+ return { found: issues.length, added };
69880
+ }
69881
+ spawnNext() {
69882
+ if (this.stopped || !this.state)
69883
+ return;
69884
+ while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
69885
+ const issue = this.queue.shift();
69886
+ this.pendingIds.add(issue.id);
69887
+ this.launchWorker(issue);
69888
+ }
69889
+ }
69890
+ async launchWorker(issue) {
69891
+ let changeName;
69892
+ try {
69893
+ changeName = await this.deps.scaffold(issue);
69894
+ } catch (err) {
69895
+ this.pendingIds.delete(issue.id);
69896
+ this.deps.onLog(`! scaffold failed for ${issue.identifier}: ${err.message}`, "red");
69897
+ this.spawnNext();
69898
+ return;
69899
+ }
69900
+ if (this.stopped) {
69901
+ this.pendingIds.delete(issue.id);
69902
+ return;
69903
+ }
69904
+ this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
69905
+ const handle = this.deps.spawnWorker(changeName, issue);
69906
+ const worker = {
69907
+ changeName,
69908
+ issueId: issue.id,
69909
+ issueIdentifier: issue.identifier,
69910
+ kill: handle.kill
69911
+ };
69912
+ this.workers.push(worker);
69913
+ this.pendingIds.delete(issue.id);
69914
+ this.deps.onWorkersChanged();
69915
+ this.notifyStarted(issue, changeName);
69916
+ handle.exited.then((code) => {
69917
+ const idx = this.workers.indexOf(worker);
69918
+ if (idx >= 0)
69919
+ this.workers.splice(idx, 1);
69920
+ const ok = code === 0;
69921
+ this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
69922
+ if (ok && this.state && !this.state.processedIssueIds.includes(issue.id)) {
69923
+ this.state.processedIssueIds.push(issue.id);
69924
+ this.deps.saveState(this.state);
69925
+ }
69926
+ this.notifyExited(issue, changeName, code);
69927
+ this.deps.onWorkersChanged();
69928
+ this.spawnNext();
69929
+ });
69930
+ }
69931
+ async notifyStarted(issue, changeName) {
69932
+ const updater = this.deps.updater;
69933
+ if (!updater)
69934
+ return;
69935
+ if (this.opts.postComments !== false) {
69936
+ try {
69937
+ await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
69938
+ } catch (err) {
69939
+ this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
69940
+ }
69941
+ }
69942
+ if (this.opts.inProgressStatus) {
69943
+ await this.moveIssue(issue, this.opts.inProgressStatus);
69944
+ }
69945
+ }
69946
+ async notifyExited(issue, changeName, code) {
69947
+ const updater = this.deps.updater;
69948
+ if (!updater)
69949
+ return;
69950
+ const ok = code === 0;
69951
+ if (this.opts.postComments !== false) {
69952
+ const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\``;
69953
+ try {
69954
+ await updater.postComment(issue, body);
69955
+ } catch (err) {
69956
+ this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
69957
+ }
69958
+ }
69959
+ if (ok && this.opts.doneStatus) {
69960
+ await this.moveIssue(issue, this.opts.doneStatus);
69961
+ }
69962
+ }
69963
+ async moveIssue(issue, stateName) {
69964
+ const updater = this.deps.updater;
69965
+ try {
69966
+ const stateId = await updater.resolveStateId(issue, stateName);
69967
+ if (!stateId) {
69968
+ this.deps.onLog(`! Linear state '${stateName}' not found for ${issue.identifier}`, "yellow");
69969
+ return;
69970
+ }
69971
+ await updater.setState(issue, stateId);
69972
+ this.deps.onLog(` \u2192 ${issue.identifier} moved to '${stateName}'`, "gray");
69973
+ } catch (err) {
69974
+ this.deps.onLog(`! Linear state move failed for ${issue.identifier}: ${err.message}`, "red");
69975
+ }
69976
+ }
69977
+ stop() {
69978
+ this.stopped = true;
69979
+ for (const w of this.workers) {
69980
+ try {
69981
+ w.kill();
69982
+ } catch {}
69983
+ }
69984
+ }
69985
+ }
69986
+
69987
+ // apps/cli/src/components/AgentMode.tsx
69988
+ var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
69989
+ var lineCounter = 0;
69990
+ function nextId() {
69991
+ lineCounter += 1;
69992
+ return `${Date.now()}-${lineCounter}`;
69993
+ }
69994
+ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
69995
+ const { exit } = use_app_default();
69996
+ const [logs, setLogs] = import_react57.useState([]);
69997
+ const [, setTick] = import_react57.useState(0);
69998
+ const coordRef = import_react57.useRef(null);
69999
+ function appendLog(text, color) {
70000
+ setLogs((prev) => [...prev, { id: nextId(), text, color }]);
70001
+ }
70002
+ import_react57.useEffect(() => {
70003
+ let pollTimer = null;
70004
+ let cancelled = false;
70005
+ async function init2() {
70006
+ const cfgPath = await ensureRalphyConfig(projectRoot);
70007
+ const cfg = await loadRalphyConfig(projectRoot);
70008
+ appendLog(`agent mode \u2014 config: ${cfgPath}`, "gray");
70009
+ const concurrency = args.concurrency || cfg.concurrency;
70010
+ const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
70011
+ appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
70012
+ const apiKey = process.env["LINEAR_API_KEY"];
70013
+ if (!apiKey) {
70014
+ appendLog("! LINEAR_API_KEY not set \u2014 cannot poll Linear", "red");
70015
+ exit();
70016
+ return;
70017
+ }
70018
+ const filter2 = {
70019
+ team: args.linearTeam || cfg.linear.team,
70020
+ assignee: args.linearAssignee || cfg.linear.assignee,
70021
+ statuses: args.linearStatus.length ? args.linearStatus : cfg.linear.statuses,
70022
+ labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
70023
+ };
70024
+ const stateCache = new Map;
70025
+ const teamKeyOf = (issue) => issue.identifier.split("-")[0];
70026
+ const coord2 = new AgentCoordinator({
70027
+ fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
70028
+ scaffold: async (issue) => {
70029
+ let comments = [];
70030
+ try {
70031
+ comments = await fetchIssueComments(apiKey, issue.id);
70032
+ } catch (err) {
70033
+ appendLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
70034
+ }
70035
+ return scaffoldChangeForIssue(tasksDir, statesDir, issue, comments);
70036
+ },
70037
+ spawnWorker: (changeName) => {
70038
+ const cmd = [
70039
+ process.execPath,
70040
+ process.argv[1] ?? "",
70041
+ "task",
70042
+ "--name",
70043
+ changeName,
70044
+ "--" + (args.engineSet ? args.engine : cfg.engine),
70045
+ args.engineSet ? args.model : cfg.model
70046
+ ];
70047
+ const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
70048
+ if (maxIter > 0)
70049
+ cmd.push("--max-iterations", String(maxIter));
70050
+ const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70051
+ if (maxCost > 0)
70052
+ cmd.push("--max-cost", String(maxCost));
70053
+ const proc = Bun.spawn({
70054
+ cmd,
70055
+ cwd: projectRoot,
70056
+ stdout: "ignore",
70057
+ stderr: "ignore",
70058
+ stdin: "ignore"
70059
+ });
70060
+ return { exited: proc.exited, kill: () => proc.kill() };
70061
+ },
70062
+ loadState: () => readAgentState(projectRoot),
70063
+ saveState: (s) => writeAgentState(projectRoot, s),
70064
+ onLog: appendLog,
70065
+ onWorkersChanged: () => setTick((t) => t + 1),
70066
+ updater: {
70067
+ postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
70068
+ setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
70069
+ resolveStateId: async (issue, stateName) => {
70070
+ const team = teamKeyOf(issue);
70071
+ let map2 = stateCache.get(team);
70072
+ if (!map2) {
70073
+ const states = await fetchWorkflowStates(apiKey, team);
70074
+ map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
70075
+ stateCache.set(team, map2);
70076
+ }
70077
+ return map2.get(stateName.toLowerCase()) ?? null;
70078
+ }
70079
+ }
70080
+ }, {
70081
+ concurrency,
70082
+ filter: filter2,
70083
+ inProgressStatus: cfg.linear.inProgressStatus,
70084
+ doneStatus: cfg.linear.doneStatus,
70085
+ postComments: cfg.linear.postComments
70086
+ });
70087
+ coordRef.current = coord2;
70088
+ await coord2.init();
70089
+ const tick = async () => {
70090
+ if (cancelled)
70091
+ return;
70092
+ const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
70093
+ appendLog(`\u2026 polling Linear (${filterDesc})`);
70094
+ const { found, added } = await coord2.pollOnce();
70095
+ appendLog(` found ${found} open, ${added} new (queue=${coord2.queuedCount})`);
70096
+ if (cancelled)
70097
+ return;
70098
+ pollTimer = setTimeout(tick, pollInterval * 1000);
70099
+ };
70100
+ tick();
70101
+ }
70102
+ init2();
70103
+ const onSig = () => {
70104
+ cancelled = true;
70105
+ appendLog("stopping agent \u2014 sending SIGTERM to workers", "yellow");
70106
+ coordRef.current?.stop();
70107
+ if (pollTimer)
70108
+ clearTimeout(pollTimer);
70109
+ exit();
70110
+ };
70111
+ process.on("SIGINT", onSig);
70112
+ process.on("SIGTERM", onSig);
70113
+ return () => {
70114
+ cancelled = true;
70115
+ if (pollTimer)
70116
+ clearTimeout(pollTimer);
70117
+ coordRef.current?.stop();
70118
+ process.off("SIGINT", onSig);
70119
+ process.off("SIGTERM", onSig);
70120
+ };
70121
+ }, []);
70122
+ const coord = coordRef.current;
70123
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
70124
+ flexDirection: "column",
70125
+ children: [
70126
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Static, {
70127
+ items: logs,
70128
+ children: (line) => line.color ? /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70129
+ color: line.color,
70130
+ children: line.text
70131
+ }, line.id, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70132
+ children: line.text
70133
+ }, line.id, false, undefined, this)
70134
+ }, undefined, false, undefined, this),
70135
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
70136
+ marginTop: 1,
70137
+ flexDirection: "column",
70138
+ children: [
70139
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70140
+ dimColor: true,
70141
+ children: [
70142
+ "workers active: ",
70143
+ coord?.activeCount ?? 0,
70144
+ " \xB7 queued: ",
70145
+ coord?.queuedCount ?? 0
70146
+ ]
70147
+ }, undefined, true, undefined, this),
70148
+ coord?.activeWorkers.map((w) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70149
+ color: "cyan",
70150
+ children: [
70151
+ " ",
70152
+ "\u25CF ",
70153
+ w.issueIdentifier,
70154
+ " (",
70155
+ w.changeName,
70156
+ ")"
70157
+ ]
70158
+ }, w.changeName, true, undefined, this))
70159
+ ]
70160
+ }, undefined, true, undefined, this)
70161
+ ]
70162
+ }, undefined, true, undefined, this);
70163
+ }
70164
+
69504
70165
  // packages/openspec/src/openspec-change-store.ts
69505
- import { join as join10, dirname as dirname3 } from "path";
69506
- import { readdir, mkdir } from "fs/promises";
70166
+ import { join as join13, dirname as dirname3 } from "path";
70167
+ import { readdir, mkdir as mkdir2 } from "fs/promises";
69507
70168
  function resolveOpenspecBin() {
69508
70169
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
69509
- return join10(dirname3(pkgJsonPath), "bin", "openspec.js");
70170
+ return join13(dirname3(pkgJsonPath), "bin", "openspec.js");
69510
70171
  }
69511
70172
  function runOpenspec(args, options = {}) {
69512
70173
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -69532,7 +70193,7 @@ class OpenSpecChangeStore {
69532
70193
  }
69533
70194
  }
69534
70195
  getChangeDirectory(name) {
69535
- return join10("openspec", "changes", name);
70196
+ return join13("openspec", "changes", name);
69536
70197
  }
69537
70198
  async listChanges() {
69538
70199
  const result2 = runOpenspec(["list", "--json"]);
@@ -69546,7 +70207,7 @@ class OpenSpecChangeStore {
69546
70207
  }
69547
70208
  } catch {}
69548
70209
  }
69549
- const changesDir = join10("openspec", "changes");
70210
+ const changesDir = join13("openspec", "changes");
69550
70211
  if (!await Bun.file(changesDir).exists())
69551
70212
  return [];
69552
70213
  try {
@@ -69557,29 +70218,29 @@ class OpenSpecChangeStore {
69557
70218
  }
69558
70219
  }
69559
70220
  async readTaskList(name) {
69560
- const file = Bun.file(join10("openspec", "changes", name, "tasks.md"));
70221
+ const file = Bun.file(join13("openspec", "changes", name, "tasks.md"));
69561
70222
  if (!await file.exists())
69562
70223
  return "";
69563
70224
  return await file.text();
69564
70225
  }
69565
70226
  async writeTaskList(name, content) {
69566
- const path = join10("openspec", "changes", name, "tasks.md");
69567
- await mkdir(dirname3(path), { recursive: true });
70227
+ const path = join13("openspec", "changes", name, "tasks.md");
70228
+ await mkdir2(dirname3(path), { recursive: true });
69568
70229
  await Bun.write(path, content);
69569
70230
  }
69570
70231
  async appendSteering(name, message) {
69571
- const path = join10("openspec", "changes", name, "steering.md");
70232
+ const path = join13("openspec", "changes", name, "steering.md");
69572
70233
  const file = Bun.file(path);
69573
70234
  const existing = await file.exists() ? await file.text() : null;
69574
70235
  const updated = existing ? `${message}
69575
70236
 
69576
70237
  ${existing.trimStart()}` : `${message}
69577
70238
  `;
69578
- await mkdir(dirname3(path), { recursive: true });
70239
+ await mkdir2(dirname3(path), { recursive: true });
69579
70240
  await Bun.write(path, updated);
69580
70241
  }
69581
70242
  async readSection(name, artifact, heading) {
69582
- const file = Bun.file(join10("openspec", "changes", name, artifact));
70243
+ const file = Bun.file(join13("openspec", "changes", name, artifact));
69583
70244
  if (!await file.exists())
69584
70245
  return "";
69585
70246
  const content = await file.text();
@@ -69620,67 +70281,74 @@ ${existing.trimStart()}` : `${message}
69620
70281
  }
69621
70282
  }
69622
70283
  // apps/cli/src/components/App.tsx
69623
- var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
70284
+ var jsx_dev_runtime10 = __toESM(require_jsx_dev_runtime(), 1);
69624
70285
  function ExitAfterRender({ children }) {
69625
70286
  const { exit } = use_app_default();
69626
- import_react57.useEffect(() => {
70287
+ import_react58.useEffect(() => {
69627
70288
  exit();
69628
70289
  }, [exit]);
69629
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(jsx_dev_runtime9.Fragment, {
70290
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
69630
70291
  children
69631
70292
  }, undefined, false, undefined, this);
69632
70293
  }
69633
70294
  function ErrorMessage({ message }) {
69634
70295
  const { exit } = use_app_default();
69635
- import_react57.useEffect(() => {
70296
+ import_react58.useEffect(() => {
69636
70297
  process.exitCode = 1;
69637
70298
  exit();
69638
70299
  }, [exit]);
69639
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70300
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69640
70301
  color: "red",
69641
70302
  children: message
69642
70303
  }, undefined, false, undefined, this);
69643
70304
  }
69644
- function App2({ args, statesDir, tasksDir }) {
70305
+ function App2({ args, statesDir, tasksDir, projectRoot }) {
69645
70306
  switch (args.mode) {
69646
70307
  case "list":
69647
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(TaskList, {
70308
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskList, {
69648
70309
  statesDir
69649
70310
  }, undefined, false, undefined, this);
70311
+ case "agent":
70312
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(AgentMode, {
70313
+ args,
70314
+ projectRoot,
70315
+ statesDir,
70316
+ tasksDir
70317
+ }, undefined, false, undefined, this);
69650
70318
  case "status": {
69651
70319
  if (!args.name) {
69652
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
70320
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
69653
70321
  message: "Error: --name is required for status mode"
69654
70322
  }, undefined, false, undefined, this);
69655
70323
  }
69656
- const stateDir = join11(statesDir, args.name);
69657
- if (getStorage().read(join11(stateDir, ".ralph-state.json")) === null) {
69658
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
70324
+ const stateDir = join14(statesDir, args.name);
70325
+ if (getStorage().read(join14(stateDir, ".ralph-state.json")) === null) {
70326
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
69659
70327
  message: `Error: change '${args.name}' not found`
69660
70328
  }, undefined, false, undefined, this);
69661
70329
  }
69662
70330
  const state = readState(stateDir);
69663
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ExitAfterRender, {
69664
- children: /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(TaskStatus, {
70331
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
70332
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskStatus, {
69665
70333
  state,
69666
70334
  stateDir
69667
70335
  }, undefined, false, undefined, this)
69668
70336
  }, undefined, false, undefined, this);
69669
70337
  }
69670
70338
  case "init":
69671
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ExitAfterRender, {
69672
- children: /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70339
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
70340
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69673
70341
  color: "green",
69674
70342
  children: "Initialized openspec directory"
69675
70343
  }, undefined, false, undefined, this)
69676
70344
  }, undefined, false, undefined, this);
69677
70345
  case "task": {
69678
70346
  if (!args.name) {
69679
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
70347
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
69680
70348
  message: "Error: --name is required for task mode"
69681
70349
  }, undefined, false, undefined, this);
69682
70350
  }
69683
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(TaskLoop, {
70351
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskLoop, {
69684
70352
  opts: {
69685
70353
  name: args.name,
69686
70354
  prompt: args.prompt,
@@ -69711,7 +70379,7 @@ if (typeof globalThis.Bun === "undefined") {
69711
70379
  async function findProjectRoot() {
69712
70380
  let dir = process.cwd();
69713
70381
  while (dir !== "/") {
69714
- if (await exists(join12(dir, "openspec")))
70382
+ if (await exists(join15(dir, "openspec")))
69715
70383
  return dir;
69716
70384
  dir = resolve(dir, "..");
69717
70385
  }
@@ -69746,11 +70414,11 @@ try {
69746
70414
  capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
69747
70415
  try {
69748
70416
  const projectRoot = await findProjectRoot();
69749
- const statesDir = join12(projectRoot, ".ralph", "tasks");
69750
- const tasksDir = join12(projectRoot, "openspec", "changes");
70417
+ const statesDir = join15(projectRoot, ".ralph", "tasks");
70418
+ const tasksDir = join15(projectRoot, "openspec", "changes");
69751
70419
  if (args.mode === "init") {
69752
- await mkdir2(statesDir, { recursive: true });
69753
- const openspecBin = join12(dirname4(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
70420
+ await mkdir3(statesDir, { recursive: true });
70421
+ const openspecBin = join15(dirname4(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
69754
70422
  Bun.spawnSync({
69755
70423
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
69756
70424
  stdio: ["inherit", "inherit", "inherit"],
@@ -69758,11 +70426,16 @@ try {
69758
70426
  });
69759
70427
  }
69760
70428
  if (args.mode === "task" && args.name) {
69761
- await mkdir2(join12(statesDir, args.name), { recursive: true });
69762
- await mkdir2(join12(tasksDir, args.name), { recursive: true });
70429
+ await mkdir3(join15(statesDir, args.name), { recursive: true });
70430
+ await mkdir3(join15(tasksDir, args.name), { recursive: true });
70431
+ }
70432
+ if (args.mode === "agent") {
70433
+ await mkdir3(statesDir, { recursive: true });
70434
+ await mkdir3(tasksDir, { recursive: true });
70435
+ await mkdir3(join15(projectRoot, ".ralph"), { recursive: true });
69763
70436
  }
69764
70437
  await runWithContext(createDefaultContext(), async () => {
69765
- const { waitUntilExit } = render_default(import_react58.createElement(App2, { args, statesDir, tasksDir }));
70438
+ const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
69766
70439
  await waitUntilExit();
69767
70440
  });
69768
70441
  await shutdown();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
5
5
  "keywords": [
6
6
  "agent",