@neriros/ralphy 2.21.3 → 2.22.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.
package/README.md CHANGED
@@ -142,21 +142,29 @@ The cycle repeats every poll. For code-review-iteration in particular, `setDone`
142
142
 
143
143
  Linear is the source of truth for which issues Ralph has touched. The `linear.indicators` map declares how Ralph queries and mutates Linear at each lifecycle event. All keys are optional; an unset key means "Ralph doesn't perform that action".
144
144
 
145
- | Key | Type | Purpose |
146
- | ----------------- | ------------------------------- | ------------------------------------------------------------------------------- |
147
- | `getTodo` | `{filter: Marker[]}` | Issues to pick up (fresh) |
148
- | `getInProgress` | `{filter: Marker[]}` | Issues to resume after restart |
149
- | `getConflicted` | `{filter: Marker[]}` | Issues whose PR is conflicted (re-fix run) |
150
- | `getReview` | `{filter: Marker[]}` | Done issues flagged for review follow-up |
151
- | `getAutoMerge` | `{filter: Marker[]}` | Issues whose PR should be auto-merged once required checks pass |
152
- | `setInProgress` | `Marker` or `{apply: Marker[]}` | Applied when a worker spawns (any non-resume mode) |
153
- | `setDone` | `Marker` or `{apply: Marker[]}` | Applied on clean exit |
154
- | `setError` | `Marker` or `{apply: Marker[]}` | Applied on non-zero exit (quarantine signal — issue is _not_ auto-resumed) |
155
- | `setConflicted` | `Marker` or `{apply: Marker[]}` | Applied when a done-PR is detected as conflicted |
156
- | `clearConflicted` | `Marker` or `{apply: Marker[]}` | Label(s) removed when a conflict-fix succeeds (status removal is not supported) |
157
- | `clearReview` | `Marker` or `{apply: Marker[]}` | Label(s) removed when a review pickup happens (status removal is not supported) |
158
-
159
- A `Marker` is `{type: "label", value: "ralph:foo"}` or `{type: "status", value: "In Progress"}`. Combine with `apply` when one event sets multiple — e.g. `setDone` flipping a status _and_ adding a label.
145
+ | Key | Type | Purpose |
146
+ | ----------------- | ---------------------- | ------------------------------------------------------------------------------- |
147
+ | `getTodo` | `{filter: Marker[]}` | Issues to pick up (fresh) |
148
+ | `getInProgress` | `{filter: Marker[]}` | Issues to resume after restart |
149
+ | `getConflicted` | `{filter: Marker[]}` | Issues whose PR is conflicted (re-fix run) |
150
+ | `getReview` | `{filter: Marker[]}` | Done issues flagged for review follow-up |
151
+ | `getAutoMerge` | `{filter: Marker[]}` | Issues whose PR should be auto-merged once required checks pass |
152
+ | `setInProgress` | `Marker` or `Marker[]` | Applied when a worker spawns (any non-resume mode) |
153
+ | `setDone` | `Marker` or `Marker[]` | Applied on clean exit |
154
+ | `setError` | `Marker` or `Marker[]` | Applied on non-zero exit (quarantine signal — issue is _not_ auto-resumed) |
155
+ | `setConflicted` | `Marker` or `Marker[]` | Applied when a done-PR is detected as conflicted |
156
+ | `clearConflicted` | `Marker` or `Marker[]` | Label(s) removed when a conflict-fix succeeds (status removal is not supported) |
157
+ | `clearReview` | `Marker` or `Marker[]` | Label(s) removed when a review pickup happens (status removal is not supported) |
158
+
159
+ A `Marker` is one of three types:
160
+
161
+ | Marker type | Example value | Effect |
162
+ | -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `"label"` | `"ralph:in-progress"` | Adds or removes a Linear label on the issue |
164
+ | `"status"` | `"In Progress"` | Updates the Linear workflow status of the issue |
165
+ | `"attachment"` | `"In Progress"` | Upserts a single **Ralphy** attachment on the issue; `value` becomes the subtitle. The same entry is reused across every lifecycle transition — Ralph creates it on first apply and edits it on subsequent ones, so the issue stays tidy. |
166
+
167
+ Use an array when one event sets multiple — e.g. `setDone` flipping a status _and_ adding a label _and_ updating the attachment subtitle.
160
168
 
161
169
  Example `ralphy.config.json`:
162
170
 
@@ -192,12 +200,10 @@ Example `ralphy.config.json`:
192
200
  "filter": [{ "type": "label", "value": "ralph:auto-merge" }],
193
201
  },
194
202
  "setInProgress": { "type": "status", "value": "In Progress" },
195
- "setDone": {
196
- "apply": [
197
- { "type": "status", "value": "In Review" },
198
- { "type": "label", "value": "ralphy-done" },
199
- ],
200
- },
203
+ "setDone": [
204
+ { "type": "status", "value": "In Review" },
205
+ { "type": "label", "value": "ralphy-done" },
206
+ ],
201
207
  "setError": { "type": "label", "value": "ralph:error" },
202
208
  "setConflicted": { "type": "label", "value": "ralph:conflicted" },
203
209
  "clearConflicted": { "type": "label", "value": "ralph:conflicted" },
package/dist/cli/index.js CHANGED
@@ -35029,8 +35029,8 @@ import { readFileSync as readFileSync2 } from "fs";
35029
35029
  import { resolve } from "path";
35030
35030
  function getVersion() {
35031
35031
  try {
35032
- if ("2.21.3")
35033
- return "2.21.3";
35032
+ if ("2.22.1")
35033
+ return "2.22.1";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -35079,8 +35079,8 @@ function parseIndicatorArg(raw) {
35079
35079
  err.key = key;
35080
35080
  throw err;
35081
35081
  }
35082
- if (type !== "label" && type !== "status") {
35083
- const err = new Error("indicator type must be 'label' or 'status'");
35082
+ if (type !== "label" && type !== "status" && type !== "attachment") {
35083
+ const err = new Error("indicator type must be 'label', 'status', or 'attachment'");
35084
35084
  err.type = type;
35085
35085
  throw err;
35086
35086
  }
@@ -35095,13 +35095,7 @@ function mergeIndicator(bag, key, marker) {
35095
35095
  bag[key] = { filter: filter2 };
35096
35096
  } else {
35097
35097
  const existing = bag[key];
35098
- let next;
35099
- if (!existing)
35100
- next = marker;
35101
- else if ("apply" in existing)
35102
- next = { apply: [...existing.apply, marker] };
35103
- else
35104
- next = { apply: [existing, marker] };
35098
+ const next = existing ? [...Array.isArray(existing) ? existing : [existing], marker] : marker;
35105
35099
  bag[key] = next;
35106
35100
  }
35107
35101
  }
@@ -35420,7 +35414,9 @@ var init_cli = __esm(() => {
35420
35414
  " Keys: getTodo, getInProgress, getConflicted, getReview, getAutoMerge,",
35421
35415
  " setInProgress, setDone, setError, setConflicted,",
35422
35416
  " clearConflicted, clearReview",
35423
- " Types: label, status",
35417
+ " Types: label, status, attachment",
35418
+ " --indicator setInProgress:attachment:In Progress",
35419
+ " (attachment upserts a single 'Ralphy' entry; value = subtitle)",
35424
35420
  " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
35425
35421
  " --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
35426
35422
  " --code-review Watch open tracked PRs for unresolved review comments and prepend a code-review task",
@@ -39408,7 +39404,7 @@ var init_zod = __esm(() => {
39408
39404
 
39409
39405
  // packages/types/src/types.ts
39410
39406
  function markersOf(set2) {
39411
- return "apply" in set2 ? set2.apply : [set2];
39407
+ return Array.isArray(set2) ? set2 : [set2];
39412
39408
  }
39413
39409
  var IterationUsageSchema, UsageSchema, HistoryEntrySchema, StateSchema, PhaseFrontmatterSchema;
39414
39410
  var init_types2 = __esm(() => {
@@ -59536,8 +59532,26 @@ async function loadRalphyConfig(projectRoot) {
59536
59532
  return RalphyConfigSchema.parse({});
59537
59533
  }
59538
59534
  const text = await file.text();
59539
- const raw = JSON.parse(stripJsonComments(text));
59540
- return RalphyConfigSchema.parse(raw);
59535
+ let raw;
59536
+ try {
59537
+ raw = JSON.parse(stripJsonComments(text));
59538
+ } catch (error) {
59539
+ throw new Error(`ralphy.config.json is not valid JSON.
59540
+ ` + ` File: ${path}
59541
+ ` + ` ${error instanceof Error ? error.message : String(error)}
59542
+
59543
+ ` + `Run \`ralph init\` to see the full default config with all available settings.`);
59544
+ }
59545
+ const result2 = RalphyConfigSchema.safeParse(raw);
59546
+ if (!result2.success) {
59547
+ const issues = result2.error.issues.map((issue) => ` \u2022 ${issue.path.join(".") || "(root)"}: ${issue.message}`).join(`
59548
+ `);
59549
+ throw new Error(`ralphy.config.json has invalid settings:
59550
+ ${issues}
59551
+
59552
+ ` + `Run \`ralph init\` to see the full default config with all available settings.`);
59553
+ }
59554
+ return result2.data;
59541
59555
  }
59542
59556
  async function ensureRalphyConfig(projectRoot) {
59543
59557
  const path = join11(projectRoot, "ralphy.config.json");
@@ -59667,14 +59681,18 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59667
59681
  // after opening the PR so the PR merges as soon as required checks pass.
59668
59682
  // "getAutoMerge": { "filter": [{ "type": "label", "value": "ralph:auto-merge" }] },
59669
59683
 
59670
- // Applied when Ralph picks up an issue.
59684
+ // Applied when Ralph picks up an issue. Single marker or array of markers.
59671
59685
  // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
59686
+ // "setInProgress": { "type": "attachment", "value": "In Progress" },
59687
+ // "setInProgress": [{ "type": "status", "value": "In Progress" }, { "type": "attachment", "value": "In Progress" }],
59672
59688
 
59673
59689
  // Applied on clean success.
59674
59690
  // "setDone": { "type": "status", "value": "In Review" },
59691
+ // "setDone": [{ "type": "status", "value": "In Review" }, { "type": "attachment", "value": "Done" }],
59675
59692
 
59676
59693
  // Applied when the task exits with an error (quarantine signal).
59677
59694
  // "setError": { "type": "label", "value": "ralph:error" },
59695
+ // "setError": [{ "type": "label", "value": "ralph:error" }, { "type": "attachment", "value": "Error" }],
59678
59696
 
59679
59697
  // Applied when a PR merge conflict is detected.
59680
59698
  // "setConflicted": { "type": "label", "value": "ralph:conflict" },
@@ -59691,16 +59709,13 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59691
59709
  var init_config = __esm(() => {
59692
59710
  init_zod();
59693
59711
  MarkerSchema = exports_external.object({
59694
- type: exports_external.enum(["label", "status"]),
59712
+ type: exports_external.enum(["label", "status", "attachment"]),
59695
59713
  value: exports_external.string().min(1)
59696
59714
  });
59697
59715
  GetIndicatorSchema = exports_external.object({
59698
59716
  filter: exports_external.array(MarkerSchema).default([])
59699
59717
  });
59700
- SetIndicatorSchema = exports_external.union([
59701
- MarkerSchema,
59702
- exports_external.object({ apply: exports_external.array(MarkerSchema).min(1) })
59703
- ]);
59718
+ SetIndicatorSchema = exports_external.union([exports_external.array(MarkerSchema).min(1), MarkerSchema]);
59704
59719
  IndicatorsSchema = exports_external.object({
59705
59720
  getTodo: GetIndicatorSchema.optional(),
59706
59721
  getInProgress: GetIndicatorSchema.optional(),
@@ -59718,7 +59733,7 @@ var init_config = __esm(() => {
59718
59733
  const clear = value[key];
59719
59734
  if (!clear)
59720
59735
  continue;
59721
- const markers = "apply" in clear ? clear.apply : [clear];
59736
+ const markers = Array.isArray(clear) ? clear : [clear];
59722
59737
  for (const m of markers) {
59723
59738
  if (m.type !== "label") {
59724
59739
  ctx.addIssue({
@@ -60007,6 +60022,44 @@ async function fetchIssueComments(apiKey, issueId) {
60007
60022
  const data = await linearRequest(apiKey, query, { id: issueId });
60008
60023
  return data.issue?.comments.nodes ?? [];
60009
60024
  }
60025
+ async function createRalphyAttachment(apiKey, issueId, issueUrl, subtitle) {
60026
+ const mutation = `mutation CreateAttachment(
60027
+ $issueId: String!, $url: String!, $title: String!, $subtitle: String!
60028
+ ) {
60029
+ attachmentCreate(input: { issueId: $issueId, url: $url, title: $title, subtitle: $subtitle }) {
60030
+ success
60031
+ attachment { id }
60032
+ }
60033
+ }`;
60034
+ const data = await linearRequest(apiKey, mutation, {
60035
+ issueId,
60036
+ url: issueUrl,
60037
+ title: RALPHY_ATTACHMENT_TITLE,
60038
+ subtitle
60039
+ });
60040
+ const attachmentId = data.attachmentCreate.attachment?.id;
60041
+ if (!attachmentId)
60042
+ throw new Error("attachmentCreate returned no attachment id");
60043
+ return attachmentId;
60044
+ }
60045
+ async function updateAttachmentSubtitle(apiKey, attachmentId, subtitle) {
60046
+ const mutation = `mutation UpdateAttachment($id: String!, $subtitle: String!) {
60047
+ attachmentUpdate(id: $id, input: { subtitle: $subtitle }) { success }
60048
+ }`;
60049
+ await linearRequest(apiKey, mutation, {
60050
+ id: attachmentId,
60051
+ subtitle
60052
+ });
60053
+ }
60054
+ async function upsertRalphyAttachment(apiKey, issueId, issueUrl, subtitle) {
60055
+ const attachments = await fetchIssueAttachments(apiKey, issueId);
60056
+ const existing = attachments.find((a) => a.title === RALPHY_ATTACHMENT_TITLE);
60057
+ if (existing) {
60058
+ await updateAttachmentSubtitle(apiKey, existing.id, subtitle);
60059
+ } else {
60060
+ await createRalphyAttachment(apiKey, issueId, issueUrl, subtitle);
60061
+ }
60062
+ }
60010
60063
  async function fetchIssueAttachments(apiKey, issueId) {
60011
60064
  const query = `query IssueAttachments($id: String!) {
60012
60065
  issue(id: $id) {
@@ -60127,7 +60180,7 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
60127
60180
  labelId
60128
60181
  });
60129
60182
  }
60130
- var LINEAR_API = "https://api.linear.app/graphql", BRANCH_LABEL_PREFIX = "ralph:branch:";
60183
+ var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
60131
60184
 
60132
60185
  // apps/cli/src/agent/coordinator.ts
60133
60186
  class AgentCoordinator {
@@ -61404,6 +61457,9 @@ function buildAgentCoordinator(input) {
61404
61457
  }
61405
61458
  await updateIssueState(apiKey, issue.id, id);
61406
61459
  onLog(` \u2192 ${issue.identifier} status='${m.value}'`, "gray");
61460
+ } else if (m.type === "attachment") {
61461
+ await upsertRalphyAttachment(apiKey, issue.id, issue.url, m.value);
61462
+ onLog(` \u2192 ${issue.identifier} attachment='${m.value}'`, "gray");
61407
61463
  } else {
61408
61464
  const id = await resolveLabelId(issue, m.value);
61409
61465
  if (!id) {
@@ -61436,7 +61492,7 @@ function buildAgentCoordinator(input) {
61436
61492
  async function fetchByGet(inc, excl) {
61437
61493
  if (!inc)
61438
61494
  return [];
61439
- const include = "filter" in inc ? inc.filter : [];
61495
+ const include = !Array.isArray(inc) && "filter" in inc ? inc.filter : [];
61440
61496
  if (include.length === 0)
61441
61497
  return [];
61442
61498
  const spec = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.21.3",
3
+ "version": "2.22.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",