@neriros/ralphy 2.6.0 → 2.7.0
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.
- package/dist/cli/index.js +578 -43
- 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
|
|
50552
|
-
import { exists, mkdir as
|
|
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
|
|
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",
|
|
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 = 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
|
|
56378
|
-
import { join as
|
|
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,465 @@ 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.label)
|
|
69599
|
+
where.labels = { some: { name: { eq: filter2.label } } };
|
|
69600
|
+
const query = `query Issues($filter: IssueFilter) {
|
|
69601
|
+
issues(filter: $filter, first: 50) {
|
|
69602
|
+
nodes {
|
|
69603
|
+
id identifier title description url
|
|
69604
|
+
state { name type }
|
|
69605
|
+
assignee { id email name }
|
|
69606
|
+
labels { nodes { name } }
|
|
69607
|
+
}
|
|
69608
|
+
}
|
|
69609
|
+
}`;
|
|
69610
|
+
const res = await fetch(LINEAR_API, {
|
|
69611
|
+
method: "POST",
|
|
69612
|
+
headers: {
|
|
69613
|
+
"Content-Type": "application/json",
|
|
69614
|
+
Authorization: apiKey
|
|
69615
|
+
},
|
|
69616
|
+
body: JSON.stringify({ query, variables: { filter: where } })
|
|
69617
|
+
});
|
|
69618
|
+
if (!res.ok) {
|
|
69619
|
+
const err = new Error("Linear API request failed");
|
|
69620
|
+
err.status = res.status;
|
|
69621
|
+
err.body = await res.text();
|
|
69622
|
+
throw err;
|
|
69623
|
+
}
|
|
69624
|
+
const json = await res.json();
|
|
69625
|
+
if (json.errors?.length) {
|
|
69626
|
+
const err = new Error("Linear API returned errors");
|
|
69627
|
+
err.messages = json.errors.map((e) => e.message);
|
|
69628
|
+
throw err;
|
|
69629
|
+
}
|
|
69630
|
+
if (!json.data)
|
|
69631
|
+
return [];
|
|
69632
|
+
return json.data.issues.nodes.map((n) => ({
|
|
69633
|
+
id: n.id,
|
|
69634
|
+
identifier: n.identifier,
|
|
69635
|
+
title: n.title,
|
|
69636
|
+
description: n.description,
|
|
69637
|
+
url: n.url,
|
|
69638
|
+
state: n.state,
|
|
69639
|
+
assignee: n.assignee,
|
|
69640
|
+
labels: n.labels.nodes.map((l) => l.name)
|
|
69641
|
+
}));
|
|
69642
|
+
}
|
|
69643
|
+
|
|
69644
|
+
// apps/cli/src/agent/state.ts
|
|
69645
|
+
import { join as join10 } from "path";
|
|
69646
|
+
var AgentStateSchema = exports_external.object({
|
|
69647
|
+
processedIssueIds: exports_external.array(exports_external.string()).default([]),
|
|
69648
|
+
lastPollAt: exports_external.string().nullable().default(null)
|
|
69649
|
+
});
|
|
69650
|
+
function statePath(projectRoot) {
|
|
69651
|
+
return join10(projectRoot, ".ralph", "agent-state.json");
|
|
69652
|
+
}
|
|
69653
|
+
async function readAgentState(projectRoot) {
|
|
69654
|
+
const file = Bun.file(statePath(projectRoot));
|
|
69655
|
+
if (!await file.exists()) {
|
|
69656
|
+
return AgentStateSchema.parse({});
|
|
69657
|
+
}
|
|
69658
|
+
return AgentStateSchema.parse(await file.json());
|
|
69659
|
+
}
|
|
69660
|
+
async function writeAgentState(projectRoot, state) {
|
|
69661
|
+
await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
|
|
69662
|
+
`);
|
|
69663
|
+
}
|
|
69664
|
+
|
|
69665
|
+
// apps/cli/src/agent/scaffold.ts
|
|
69666
|
+
import { join as join11 } from "path";
|
|
69667
|
+
import { mkdir } from "fs/promises";
|
|
69668
|
+
function changeNameForIssue(issue) {
|
|
69669
|
+
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
69670
|
+
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
69671
|
+
}
|
|
69672
|
+
async function scaffoldChangeForIssue(tasksDir, statesDir, issue) {
|
|
69673
|
+
const name = changeNameForIssue(issue);
|
|
69674
|
+
const changeDir = join11(tasksDir, name);
|
|
69675
|
+
const stateDir = join11(statesDir, name);
|
|
69676
|
+
await mkdir(changeDir, { recursive: true });
|
|
69677
|
+
await mkdir(join11(changeDir, "specs"), { recursive: true });
|
|
69678
|
+
await mkdir(stateDir, { recursive: true });
|
|
69679
|
+
const proposal = [
|
|
69680
|
+
`# ${issue.identifier}: ${issue.title}`,
|
|
69681
|
+
"",
|
|
69682
|
+
`Source: [${issue.identifier}](${issue.url})`,
|
|
69683
|
+
`Status: ${issue.state.name}`,
|
|
69684
|
+
issue.assignee ? `Assignee: ${issue.assignee.name}` : "",
|
|
69685
|
+
issue.labels.length ? `Labels: ${issue.labels.join(", ")}` : "",
|
|
69686
|
+
"",
|
|
69687
|
+
"## Description",
|
|
69688
|
+
"",
|
|
69689
|
+
issue.description?.trim() || "_No description provided in Linear._",
|
|
69690
|
+
"",
|
|
69691
|
+
"## Steering",
|
|
69692
|
+
"",
|
|
69693
|
+
"_Add steering notes here as the loop runs._",
|
|
69694
|
+
""
|
|
69695
|
+
].filter((l) => l !== "").join(`
|
|
69696
|
+
`);
|
|
69697
|
+
const tasks = [
|
|
69698
|
+
`# Tasks for ${issue.identifier}`,
|
|
69699
|
+
"",
|
|
69700
|
+
`- [ ] Read the Linear issue at ${issue.url} and break it into concrete subtasks`,
|
|
69701
|
+
`- [ ] Implement the changes described in proposal.md`,
|
|
69702
|
+
`- [ ] Add or update tests covering the new behavior`,
|
|
69703
|
+
`- [ ] Run \`bun run lint\` and \`bun run test\` and fix any failures`,
|
|
69704
|
+
""
|
|
69705
|
+
].join(`
|
|
69706
|
+
`);
|
|
69707
|
+
const design = [
|
|
69708
|
+
`# Design for ${issue.identifier}`,
|
|
69709
|
+
"",
|
|
69710
|
+
"_Fill in the technical design as you work through the issue._",
|
|
69711
|
+
""
|
|
69712
|
+
].join(`
|
|
69713
|
+
`);
|
|
69714
|
+
await Bun.write(join11(changeDir, "proposal.md"), proposal);
|
|
69715
|
+
await Bun.write(join11(changeDir, "tasks.md"), tasks);
|
|
69716
|
+
await Bun.write(join11(changeDir, "design.md"), design);
|
|
69717
|
+
return name;
|
|
69718
|
+
}
|
|
69719
|
+
|
|
69720
|
+
// apps/cli/src/agent/config.ts
|
|
69721
|
+
import { join as join12 } from "path";
|
|
69722
|
+
var RalphyConfigSchema = exports_external.object({
|
|
69723
|
+
concurrency: exports_external.number().int().positive().default(1),
|
|
69724
|
+
pollIntervalSeconds: exports_external.number().int().positive().default(60),
|
|
69725
|
+
maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
|
|
69726
|
+
maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
|
|
69727
|
+
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
69728
|
+
model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
|
|
69729
|
+
linear: exports_external.object({
|
|
69730
|
+
team: exports_external.string().optional(),
|
|
69731
|
+
assignee: exports_external.string().optional(),
|
|
69732
|
+
statuses: exports_external.array(exports_external.string()).default([]),
|
|
69733
|
+
label: exports_external.string().optional()
|
|
69734
|
+
}).default({ statuses: [] })
|
|
69735
|
+
}).default({
|
|
69736
|
+
concurrency: 1,
|
|
69737
|
+
pollIntervalSeconds: 60,
|
|
69738
|
+
maxIterationsPerTask: 0,
|
|
69739
|
+
maxCostUsdPerTask: 0,
|
|
69740
|
+
engine: "claude",
|
|
69741
|
+
model: "opus",
|
|
69742
|
+
linear: { statuses: [] }
|
|
69743
|
+
});
|
|
69744
|
+
async function loadRalphyConfig(projectRoot) {
|
|
69745
|
+
const path = join12(projectRoot, "ralphy.config.json");
|
|
69746
|
+
const file = Bun.file(path);
|
|
69747
|
+
if (!await file.exists()) {
|
|
69748
|
+
return RalphyConfigSchema.parse({});
|
|
69749
|
+
}
|
|
69750
|
+
const raw = await file.json();
|
|
69751
|
+
return RalphyConfigSchema.parse(raw);
|
|
69752
|
+
}
|
|
69753
|
+
async function ensureRalphyConfig(projectRoot) {
|
|
69754
|
+
const path = join12(projectRoot, "ralphy.config.json");
|
|
69755
|
+
const file = Bun.file(path);
|
|
69756
|
+
if (await file.exists())
|
|
69757
|
+
return path;
|
|
69758
|
+
const defaults2 = RalphyConfigSchema.parse({});
|
|
69759
|
+
await Bun.write(path, JSON.stringify(defaults2, null, 2) + `
|
|
69760
|
+
`);
|
|
69761
|
+
return path;
|
|
69762
|
+
}
|
|
69763
|
+
|
|
69764
|
+
// apps/cli/src/agent/coordinator.ts
|
|
69765
|
+
class AgentCoordinator {
|
|
69766
|
+
deps;
|
|
69767
|
+
opts;
|
|
69768
|
+
workers = [];
|
|
69769
|
+
pendingIds = new Set;
|
|
69770
|
+
queue = [];
|
|
69771
|
+
state = null;
|
|
69772
|
+
stopped = false;
|
|
69773
|
+
constructor(deps, opts) {
|
|
69774
|
+
this.deps = deps;
|
|
69775
|
+
this.opts = opts;
|
|
69776
|
+
}
|
|
69777
|
+
get activeCount() {
|
|
69778
|
+
return this.workers.length;
|
|
69779
|
+
}
|
|
69780
|
+
get queuedCount() {
|
|
69781
|
+
return this.queue.length;
|
|
69782
|
+
}
|
|
69783
|
+
get activeWorkers() {
|
|
69784
|
+
return this.workers;
|
|
69785
|
+
}
|
|
69786
|
+
async init() {
|
|
69787
|
+
this.state = await this.deps.loadState();
|
|
69788
|
+
}
|
|
69789
|
+
async pollOnce() {
|
|
69790
|
+
if (this.stopped)
|
|
69791
|
+
return { found: 0, added: 0 };
|
|
69792
|
+
let issues;
|
|
69793
|
+
try {
|
|
69794
|
+
issues = await this.deps.fetchIssues(this.opts.filter);
|
|
69795
|
+
} catch (err) {
|
|
69796
|
+
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
69797
|
+
return { found: 0, added: 0 };
|
|
69798
|
+
}
|
|
69799
|
+
const state = this.state;
|
|
69800
|
+
const seen = new Set(state.processedIssueIds);
|
|
69801
|
+
const queued = new Set(this.queue.map((i) => i.id));
|
|
69802
|
+
const active = new Set(this.workers.map((w) => w.issueId));
|
|
69803
|
+
let added = 0;
|
|
69804
|
+
for (const issue of issues) {
|
|
69805
|
+
if (seen.has(issue.id))
|
|
69806
|
+
continue;
|
|
69807
|
+
if (queued.has(issue.id))
|
|
69808
|
+
continue;
|
|
69809
|
+
if (active.has(issue.id))
|
|
69810
|
+
continue;
|
|
69811
|
+
if (this.pendingIds.has(issue.id))
|
|
69812
|
+
continue;
|
|
69813
|
+
this.queue.push(issue);
|
|
69814
|
+
added += 1;
|
|
69815
|
+
}
|
|
69816
|
+
state.lastPollAt = new Date().toISOString();
|
|
69817
|
+
await this.deps.saveState(state);
|
|
69818
|
+
this.spawnNext();
|
|
69819
|
+
return { found: issues.length, added };
|
|
69820
|
+
}
|
|
69821
|
+
spawnNext() {
|
|
69822
|
+
if (this.stopped || !this.state)
|
|
69823
|
+
return;
|
|
69824
|
+
while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
|
|
69825
|
+
const issue = this.queue.shift();
|
|
69826
|
+
this.pendingIds.add(issue.id);
|
|
69827
|
+
this.launchWorker(issue);
|
|
69828
|
+
}
|
|
69829
|
+
}
|
|
69830
|
+
async launchWorker(issue) {
|
|
69831
|
+
let changeName;
|
|
69832
|
+
try {
|
|
69833
|
+
changeName = await this.deps.scaffold(issue);
|
|
69834
|
+
} catch (err) {
|
|
69835
|
+
this.pendingIds.delete(issue.id);
|
|
69836
|
+
this.deps.onLog(`! scaffold failed for ${issue.identifier}: ${err.message}`, "red");
|
|
69837
|
+
this.spawnNext();
|
|
69838
|
+
return;
|
|
69839
|
+
}
|
|
69840
|
+
if (this.stopped) {
|
|
69841
|
+
this.pendingIds.delete(issue.id);
|
|
69842
|
+
return;
|
|
69843
|
+
}
|
|
69844
|
+
this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
|
|
69845
|
+
const handle = this.deps.spawnWorker(changeName, issue);
|
|
69846
|
+
const worker = {
|
|
69847
|
+
changeName,
|
|
69848
|
+
issueId: issue.id,
|
|
69849
|
+
issueIdentifier: issue.identifier,
|
|
69850
|
+
kill: handle.kill
|
|
69851
|
+
};
|
|
69852
|
+
this.workers.push(worker);
|
|
69853
|
+
this.pendingIds.delete(issue.id);
|
|
69854
|
+
this.deps.onWorkersChanged();
|
|
69855
|
+
handle.exited.then((code) => {
|
|
69856
|
+
const idx = this.workers.indexOf(worker);
|
|
69857
|
+
if (idx >= 0)
|
|
69858
|
+
this.workers.splice(idx, 1);
|
|
69859
|
+
const ok = code === 0;
|
|
69860
|
+
this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
|
|
69861
|
+
if (ok && this.state && !this.state.processedIssueIds.includes(issue.id)) {
|
|
69862
|
+
this.state.processedIssueIds.push(issue.id);
|
|
69863
|
+
this.deps.saveState(this.state);
|
|
69864
|
+
}
|
|
69865
|
+
this.deps.onWorkersChanged();
|
|
69866
|
+
this.spawnNext();
|
|
69867
|
+
});
|
|
69868
|
+
}
|
|
69869
|
+
stop() {
|
|
69870
|
+
this.stopped = true;
|
|
69871
|
+
for (const w of this.workers) {
|
|
69872
|
+
try {
|
|
69873
|
+
w.kill();
|
|
69874
|
+
} catch {}
|
|
69875
|
+
}
|
|
69876
|
+
}
|
|
69877
|
+
}
|
|
69878
|
+
|
|
69879
|
+
// apps/cli/src/components/AgentMode.tsx
|
|
69880
|
+
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
69881
|
+
var lineCounter = 0;
|
|
69882
|
+
function nextId() {
|
|
69883
|
+
lineCounter += 1;
|
|
69884
|
+
return `${Date.now()}-${lineCounter}`;
|
|
69885
|
+
}
|
|
69886
|
+
function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
69887
|
+
const { exit } = use_app_default();
|
|
69888
|
+
const [logs, setLogs] = import_react57.useState([]);
|
|
69889
|
+
const [, setTick] = import_react57.useState(0);
|
|
69890
|
+
const coordRef = import_react57.useRef(null);
|
|
69891
|
+
function appendLog(text, color) {
|
|
69892
|
+
setLogs((prev) => [...prev, { id: nextId(), text, color }]);
|
|
69893
|
+
}
|
|
69894
|
+
import_react57.useEffect(() => {
|
|
69895
|
+
let pollTimer = null;
|
|
69896
|
+
let cancelled = false;
|
|
69897
|
+
async function init2() {
|
|
69898
|
+
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
69899
|
+
const cfg = await loadRalphyConfig(projectRoot);
|
|
69900
|
+
appendLog(`agent mode \u2014 config: ${cfgPath}`, "gray");
|
|
69901
|
+
const concurrency = args.concurrency || cfg.concurrency;
|
|
69902
|
+
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
69903
|
+
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
69904
|
+
const apiKey = process.env["LINEAR_API_KEY"];
|
|
69905
|
+
if (!apiKey) {
|
|
69906
|
+
appendLog("! LINEAR_API_KEY not set \u2014 cannot poll Linear", "red");
|
|
69907
|
+
exit();
|
|
69908
|
+
return;
|
|
69909
|
+
}
|
|
69910
|
+
const filter2 = {
|
|
69911
|
+
team: args.linearTeam || cfg.linear.team,
|
|
69912
|
+
assignee: args.linearAssignee || cfg.linear.assignee,
|
|
69913
|
+
statuses: args.linearStatus.length ? args.linearStatus : cfg.linear.statuses,
|
|
69914
|
+
label: args.linearLabel || cfg.linear.label
|
|
69915
|
+
};
|
|
69916
|
+
const coord2 = new AgentCoordinator({
|
|
69917
|
+
fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
|
|
69918
|
+
scaffold: (issue) => scaffoldChangeForIssue(tasksDir, statesDir, issue),
|
|
69919
|
+
spawnWorker: (changeName) => {
|
|
69920
|
+
const cmd = [
|
|
69921
|
+
process.execPath,
|
|
69922
|
+
process.argv[1] ?? "",
|
|
69923
|
+
"task",
|
|
69924
|
+
"--name",
|
|
69925
|
+
changeName,
|
|
69926
|
+
"--" + (args.engineSet ? args.engine : cfg.engine),
|
|
69927
|
+
args.engineSet ? args.model : cfg.model
|
|
69928
|
+
];
|
|
69929
|
+
const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
|
|
69930
|
+
if (maxIter > 0)
|
|
69931
|
+
cmd.push("--max-iterations", String(maxIter));
|
|
69932
|
+
const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
|
|
69933
|
+
if (maxCost > 0)
|
|
69934
|
+
cmd.push("--max-cost", String(maxCost));
|
|
69935
|
+
const proc = Bun.spawn({
|
|
69936
|
+
cmd,
|
|
69937
|
+
cwd: projectRoot,
|
|
69938
|
+
stdout: "ignore",
|
|
69939
|
+
stderr: "ignore",
|
|
69940
|
+
stdin: "ignore"
|
|
69941
|
+
});
|
|
69942
|
+
return { exited: proc.exited, kill: () => proc.kill() };
|
|
69943
|
+
},
|
|
69944
|
+
loadState: () => readAgentState(projectRoot),
|
|
69945
|
+
saveState: (s) => writeAgentState(projectRoot, s),
|
|
69946
|
+
onLog: appendLog,
|
|
69947
|
+
onWorkersChanged: () => setTick((t) => t + 1)
|
|
69948
|
+
}, { concurrency, filter: filter2 });
|
|
69949
|
+
coordRef.current = coord2;
|
|
69950
|
+
await coord2.init();
|
|
69951
|
+
const tick = async () => {
|
|
69952
|
+
if (cancelled)
|
|
69953
|
+
return;
|
|
69954
|
+
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.label ? `, label=${filter2.label}` : ""}`;
|
|
69955
|
+
appendLog(`\u2026 polling Linear (${filterDesc})`);
|
|
69956
|
+
const { found, added } = await coord2.pollOnce();
|
|
69957
|
+
appendLog(` found ${found} open, ${added} new (queue=${coord2.queuedCount})`);
|
|
69958
|
+
if (cancelled)
|
|
69959
|
+
return;
|
|
69960
|
+
pollTimer = setTimeout(tick, pollInterval * 1000);
|
|
69961
|
+
};
|
|
69962
|
+
tick();
|
|
69963
|
+
}
|
|
69964
|
+
init2();
|
|
69965
|
+
const onSig = () => {
|
|
69966
|
+
cancelled = true;
|
|
69967
|
+
appendLog("stopping agent \u2014 sending SIGTERM to workers", "yellow");
|
|
69968
|
+
coordRef.current?.stop();
|
|
69969
|
+
if (pollTimer)
|
|
69970
|
+
clearTimeout(pollTimer);
|
|
69971
|
+
exit();
|
|
69972
|
+
};
|
|
69973
|
+
process.on("SIGINT", onSig);
|
|
69974
|
+
process.on("SIGTERM", onSig);
|
|
69975
|
+
return () => {
|
|
69976
|
+
cancelled = true;
|
|
69977
|
+
if (pollTimer)
|
|
69978
|
+
clearTimeout(pollTimer);
|
|
69979
|
+
coordRef.current?.stop();
|
|
69980
|
+
process.off("SIGINT", onSig);
|
|
69981
|
+
process.off("SIGTERM", onSig);
|
|
69982
|
+
};
|
|
69983
|
+
}, []);
|
|
69984
|
+
const coord = coordRef.current;
|
|
69985
|
+
return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
|
|
69986
|
+
flexDirection: "column",
|
|
69987
|
+
children: [
|
|
69988
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Static, {
|
|
69989
|
+
items: logs,
|
|
69990
|
+
children: (line) => line.color ? /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
69991
|
+
color: line.color,
|
|
69992
|
+
children: line.text
|
|
69993
|
+
}, line.id, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
69994
|
+
children: line.text
|
|
69995
|
+
}, line.id, false, undefined, this)
|
|
69996
|
+
}, undefined, false, undefined, this),
|
|
69997
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
|
|
69998
|
+
marginTop: 1,
|
|
69999
|
+
flexDirection: "column",
|
|
70000
|
+
children: [
|
|
70001
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
70002
|
+
dimColor: true,
|
|
70003
|
+
children: [
|
|
70004
|
+
"workers active: ",
|
|
70005
|
+
coord?.activeCount ?? 0,
|
|
70006
|
+
" \xB7 queued: ",
|
|
70007
|
+
coord?.queuedCount ?? 0
|
|
70008
|
+
]
|
|
70009
|
+
}, undefined, true, undefined, this),
|
|
70010
|
+
coord?.activeWorkers.map((w) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
70011
|
+
color: "cyan",
|
|
70012
|
+
children: [
|
|
70013
|
+
" ",
|
|
70014
|
+
"\u25CF ",
|
|
70015
|
+
w.issueIdentifier,
|
|
70016
|
+
" (",
|
|
70017
|
+
w.changeName,
|
|
70018
|
+
")"
|
|
70019
|
+
]
|
|
70020
|
+
}, w.changeName, true, undefined, this))
|
|
70021
|
+
]
|
|
70022
|
+
}, undefined, true, undefined, this)
|
|
70023
|
+
]
|
|
70024
|
+
}, undefined, true, undefined, this);
|
|
70025
|
+
}
|
|
70026
|
+
|
|
69504
70027
|
// packages/openspec/src/openspec-change-store.ts
|
|
69505
|
-
import { join as
|
|
69506
|
-
import { readdir, mkdir } from "fs/promises";
|
|
70028
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
70029
|
+
import { readdir, mkdir as mkdir2 } from "fs/promises";
|
|
69507
70030
|
function resolveOpenspecBin() {
|
|
69508
70031
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
69509
|
-
return
|
|
70032
|
+
return join13(dirname3(pkgJsonPath), "bin", "openspec.js");
|
|
69510
70033
|
}
|
|
69511
70034
|
function runOpenspec(args, options = {}) {
|
|
69512
70035
|
const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
|
|
@@ -69532,7 +70055,7 @@ class OpenSpecChangeStore {
|
|
|
69532
70055
|
}
|
|
69533
70056
|
}
|
|
69534
70057
|
getChangeDirectory(name) {
|
|
69535
|
-
return
|
|
70058
|
+
return join13("openspec", "changes", name);
|
|
69536
70059
|
}
|
|
69537
70060
|
async listChanges() {
|
|
69538
70061
|
const result2 = runOpenspec(["list", "--json"]);
|
|
@@ -69546,7 +70069,7 @@ class OpenSpecChangeStore {
|
|
|
69546
70069
|
}
|
|
69547
70070
|
} catch {}
|
|
69548
70071
|
}
|
|
69549
|
-
const changesDir =
|
|
70072
|
+
const changesDir = join13("openspec", "changes");
|
|
69550
70073
|
if (!await Bun.file(changesDir).exists())
|
|
69551
70074
|
return [];
|
|
69552
70075
|
try {
|
|
@@ -69557,29 +70080,29 @@ class OpenSpecChangeStore {
|
|
|
69557
70080
|
}
|
|
69558
70081
|
}
|
|
69559
70082
|
async readTaskList(name) {
|
|
69560
|
-
const file = Bun.file(
|
|
70083
|
+
const file = Bun.file(join13("openspec", "changes", name, "tasks.md"));
|
|
69561
70084
|
if (!await file.exists())
|
|
69562
70085
|
return "";
|
|
69563
70086
|
return await file.text();
|
|
69564
70087
|
}
|
|
69565
70088
|
async writeTaskList(name, content) {
|
|
69566
|
-
const path =
|
|
69567
|
-
await
|
|
70089
|
+
const path = join13("openspec", "changes", name, "tasks.md");
|
|
70090
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
69568
70091
|
await Bun.write(path, content);
|
|
69569
70092
|
}
|
|
69570
70093
|
async appendSteering(name, message) {
|
|
69571
|
-
const path =
|
|
70094
|
+
const path = join13("openspec", "changes", name, "steering.md");
|
|
69572
70095
|
const file = Bun.file(path);
|
|
69573
70096
|
const existing = await file.exists() ? await file.text() : null;
|
|
69574
70097
|
const updated = existing ? `${message}
|
|
69575
70098
|
|
|
69576
70099
|
${existing.trimStart()}` : `${message}
|
|
69577
70100
|
`;
|
|
69578
|
-
await
|
|
70101
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
69579
70102
|
await Bun.write(path, updated);
|
|
69580
70103
|
}
|
|
69581
70104
|
async readSection(name, artifact, heading) {
|
|
69582
|
-
const file = Bun.file(
|
|
70105
|
+
const file = Bun.file(join13("openspec", "changes", name, artifact));
|
|
69583
70106
|
if (!await file.exists())
|
|
69584
70107
|
return "";
|
|
69585
70108
|
const content = await file.text();
|
|
@@ -69620,67 +70143,74 @@ ${existing.trimStart()}` : `${message}
|
|
|
69620
70143
|
}
|
|
69621
70144
|
}
|
|
69622
70145
|
// apps/cli/src/components/App.tsx
|
|
69623
|
-
var
|
|
70146
|
+
var jsx_dev_runtime10 = __toESM(require_jsx_dev_runtime(), 1);
|
|
69624
70147
|
function ExitAfterRender({ children }) {
|
|
69625
70148
|
const { exit } = use_app_default();
|
|
69626
|
-
|
|
70149
|
+
import_react58.useEffect(() => {
|
|
69627
70150
|
exit();
|
|
69628
70151
|
}, [exit]);
|
|
69629
|
-
return /* @__PURE__ */
|
|
70152
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
|
|
69630
70153
|
children
|
|
69631
70154
|
}, undefined, false, undefined, this);
|
|
69632
70155
|
}
|
|
69633
70156
|
function ErrorMessage({ message }) {
|
|
69634
70157
|
const { exit } = use_app_default();
|
|
69635
|
-
|
|
70158
|
+
import_react58.useEffect(() => {
|
|
69636
70159
|
process.exitCode = 1;
|
|
69637
70160
|
exit();
|
|
69638
70161
|
}, [exit]);
|
|
69639
|
-
return /* @__PURE__ */
|
|
70162
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
69640
70163
|
color: "red",
|
|
69641
70164
|
children: message
|
|
69642
70165
|
}, undefined, false, undefined, this);
|
|
69643
70166
|
}
|
|
69644
|
-
function App2({ args, statesDir, tasksDir }) {
|
|
70167
|
+
function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
69645
70168
|
switch (args.mode) {
|
|
69646
70169
|
case "list":
|
|
69647
|
-
return /* @__PURE__ */
|
|
70170
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskList, {
|
|
69648
70171
|
statesDir
|
|
69649
70172
|
}, undefined, false, undefined, this);
|
|
70173
|
+
case "agent":
|
|
70174
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(AgentMode, {
|
|
70175
|
+
args,
|
|
70176
|
+
projectRoot,
|
|
70177
|
+
statesDir,
|
|
70178
|
+
tasksDir
|
|
70179
|
+
}, undefined, false, undefined, this);
|
|
69650
70180
|
case "status": {
|
|
69651
70181
|
if (!args.name) {
|
|
69652
|
-
return /* @__PURE__ */
|
|
70182
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
69653
70183
|
message: "Error: --name is required for status mode"
|
|
69654
70184
|
}, undefined, false, undefined, this);
|
|
69655
70185
|
}
|
|
69656
|
-
const stateDir =
|
|
69657
|
-
if (getStorage().read(
|
|
69658
|
-
return /* @__PURE__ */
|
|
70186
|
+
const stateDir = join14(statesDir, args.name);
|
|
70187
|
+
if (getStorage().read(join14(stateDir, ".ralph-state.json")) === null) {
|
|
70188
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
69659
70189
|
message: `Error: change '${args.name}' not found`
|
|
69660
70190
|
}, undefined, false, undefined, this);
|
|
69661
70191
|
}
|
|
69662
70192
|
const state = readState(stateDir);
|
|
69663
|
-
return /* @__PURE__ */
|
|
69664
|
-
children: /* @__PURE__ */
|
|
70193
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
|
|
70194
|
+
children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskStatus, {
|
|
69665
70195
|
state,
|
|
69666
70196
|
stateDir
|
|
69667
70197
|
}, undefined, false, undefined, this)
|
|
69668
70198
|
}, undefined, false, undefined, this);
|
|
69669
70199
|
}
|
|
69670
70200
|
case "init":
|
|
69671
|
-
return /* @__PURE__ */
|
|
69672
|
-
children: /* @__PURE__ */
|
|
70201
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
|
|
70202
|
+
children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
69673
70203
|
color: "green",
|
|
69674
70204
|
children: "Initialized openspec directory"
|
|
69675
70205
|
}, undefined, false, undefined, this)
|
|
69676
70206
|
}, undefined, false, undefined, this);
|
|
69677
70207
|
case "task": {
|
|
69678
70208
|
if (!args.name) {
|
|
69679
|
-
return /* @__PURE__ */
|
|
70209
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
69680
70210
|
message: "Error: --name is required for task mode"
|
|
69681
70211
|
}, undefined, false, undefined, this);
|
|
69682
70212
|
}
|
|
69683
|
-
return /* @__PURE__ */
|
|
70213
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskLoop, {
|
|
69684
70214
|
opts: {
|
|
69685
70215
|
name: args.name,
|
|
69686
70216
|
prompt: args.prompt,
|
|
@@ -69711,7 +70241,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
69711
70241
|
async function findProjectRoot() {
|
|
69712
70242
|
let dir = process.cwd();
|
|
69713
70243
|
while (dir !== "/") {
|
|
69714
|
-
if (await exists(
|
|
70244
|
+
if (await exists(join15(dir, "openspec")))
|
|
69715
70245
|
return dir;
|
|
69716
70246
|
dir = resolve(dir, "..");
|
|
69717
70247
|
}
|
|
@@ -69746,11 +70276,11 @@ try {
|
|
|
69746
70276
|
capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
|
|
69747
70277
|
try {
|
|
69748
70278
|
const projectRoot = await findProjectRoot();
|
|
69749
|
-
const statesDir =
|
|
69750
|
-
const tasksDir =
|
|
70279
|
+
const statesDir = join15(projectRoot, ".ralph", "tasks");
|
|
70280
|
+
const tasksDir = join15(projectRoot, "openspec", "changes");
|
|
69751
70281
|
if (args.mode === "init") {
|
|
69752
|
-
await
|
|
69753
|
-
const openspecBin =
|
|
70282
|
+
await mkdir3(statesDir, { recursive: true });
|
|
70283
|
+
const openspecBin = join15(dirname4(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
69754
70284
|
Bun.spawnSync({
|
|
69755
70285
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
69756
70286
|
stdio: ["inherit", "inherit", "inherit"],
|
|
@@ -69758,11 +70288,16 @@ try {
|
|
|
69758
70288
|
});
|
|
69759
70289
|
}
|
|
69760
70290
|
if (args.mode === "task" && args.name) {
|
|
69761
|
-
await
|
|
69762
|
-
await
|
|
70291
|
+
await mkdir3(join15(statesDir, args.name), { recursive: true });
|
|
70292
|
+
await mkdir3(join15(tasksDir, args.name), { recursive: true });
|
|
70293
|
+
}
|
|
70294
|
+
if (args.mode === "agent") {
|
|
70295
|
+
await mkdir3(statesDir, { recursive: true });
|
|
70296
|
+
await mkdir3(tasksDir, { recursive: true });
|
|
70297
|
+
await mkdir3(join15(projectRoot, ".ralph"), { recursive: true });
|
|
69763
70298
|
}
|
|
69764
70299
|
await runWithContext(createDefaultContext(), async () => {
|
|
69765
|
-
const { waitUntilExit } = render_default(
|
|
70300
|
+
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|
|
69766
70301
|
await waitUntilExit();
|
|
69767
70302
|
});
|
|
69768
70303
|
await shutdown();
|