@neriros/ralphy 3.1.0 → 3.3.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/README.md CHANGED
@@ -187,6 +187,8 @@ Example `ralphy.config.json`:
187
187
  "mentionHandle": "@ralphy",
188
188
  "codeReviewTrigger": true,
189
189
  "codeReviewStaleHours": 24,
190
+ "syncTasksToComment": true,
191
+ "syncTasksToDescription": false,
190
192
  "indicators": {
191
193
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
192
194
  "getInProgress": {
@@ -219,7 +221,7 @@ When a Linear issue is in a done state and a reviewer adds the `getReview` marke
219
221
 
220
222
  #### `@ralphy` mention trigger
221
223
 
222
- Set `linear.mentionTrigger: true` to scan done-issue comments on Linear _and_ on the linked GitHub PR for a configurable handle (`linear.mentionHandle`, default `@ralphy`). Each unprocessed mention queues the issue as a review run, with the mention text used **verbatim** as the prepended task. Idempotency: a mention is processed when its `createdAt` is older than Ralph's latest `🔁 picked up` Linear comment, so the same comment never re-fires. Requires `gh` for the GitHub side.
224
+ Set `linear.mentionTrigger: true` to scan Linear issue comments on every non-cancelled issue (Todo, In Progress, Backlog, Triage, Done) _and_ on the linked GitHub PR for a configurable handle (`linear.mentionHandle`, default `@ralphy`). Each unprocessed mention queues the issue as a review run, with the mention text used **verbatim** as the prepended task. Idempotency: a mention is processed when its `createdAt` is older than Ralph's latest `🔁 picked up` Linear comment, so the same comment never re-fires. Requires `gh` for the GitHub side.
223
225
 
224
226
  #### Code-review iteration
225
227
 
@@ -230,6 +232,30 @@ Set `linear.codeReviewTrigger: true` (or pass `--code-review`) to watch open, un
230
232
 
231
233
  The loop exits; the next poll re-checks the PR. The cycle continues until the PR is **approved** or **merged**. If the reviewer is silent for more than `linear.codeReviewStaleHours` (default `24`, `0` disables) while Ralph is the last actor, one `@`-mention ping comment is posted on the GitHub PR.
232
234
 
235
+ #### Sync tasks into a Linear comment
236
+
237
+ `linear.syncTasksToComment` (default `true`) mirrors the active change's
238
+ `tasks.md` into a dedicated Linear **comment** instead of the issue
239
+ description. The same comment is updated in place across iterations so
240
+ the timeline stays clean. When `ralph_append_steering` is invoked the
241
+ existing tasks comment is deleted and re-posted so it always lands at
242
+ the bottom of the timeline, after the new steering comment.
243
+
244
+ The first time planning completes (every `- [ ]` under `## Planning` in
245
+ `tasks.md` becomes `- [x]`), Ralph posts a one-shot "📋 Plan" comment
246
+ summarizing `proposal.md` (`## Why` + `## What Changes`) and the first
247
+ paragraph of `design.md`.
248
+
249
+ ##### Legacy: sync into the issue description
250
+
251
+ Set `linear.syncTasksToDescription: true` to mirror `tasks.md` into the
252
+ linked Linear issue description body instead (the pre-RLF-62 behavior).
253
+ Ralph writes a checklist between sentinel HTML comments
254
+ (`<!-- ralphy:tasks:start -->` / `<!-- ralphy:tasks:end -->`); content
255
+ outside the markers is preserved verbatim. When both
256
+ `syncTasksToComment` and `syncTasksToDescription` are true,
257
+ comment-sync wins and a one-time warning is logged.
258
+
233
259
  #### Conflict re-fix
234
260
 
235
261
  Done issues whose PR `gh pr view --json mergeable` reports as `CONFLICTING` get `setConflicted` applied and a conflict-fix task prepended. The scanner is resilient to:
package/dist/mcp/index.js CHANGED
@@ -24057,7 +24057,12 @@ var StateSchema = exports_external.object({
24057
24057
  createPr: exports_external.boolean().default(false),
24058
24058
  usage: UsageSchema.default({}),
24059
24059
  history: exports_external.array(HistoryEntrySchema).default([]),
24060
- metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
24060
+ metadata: exports_external.object({ branch: exports_external.string().optional() }).default({}),
24061
+ linearComments: exports_external.object({
24062
+ planCommentId: exports_external.string().nullable().default(null),
24063
+ tasksCommentId: exports_external.string().nullable().default(null),
24064
+ planPostedAt: exports_external.string().nullable().default(null)
24065
+ }).default({ planCommentId: null, tasksCommentId: null, planPostedAt: null })
24061
24066
  });
24062
24067
  var PhaseFrontmatterSchema = exports_external.object({
24063
24068
  name: exports_external.string(),
@@ -24132,7 +24137,7 @@ function buildInitialState(options) {
24132
24137
  function safeTool(server, name, config2, callback) {
24133
24138
  server.registerTool(name, config2, callback);
24134
24139
  }
24135
- function registerTools(server, changesDir, changeStore, taskFilesDir = changesDir) {
24140
+ function registerTools(server, changesDir, changeStore, taskFilesDir = changesDir, hooks = {}) {
24136
24141
  safeTool(server, "ralph_list_changes", {
24137
24142
  description: "List all active OpenSpec changes with their status",
24138
24143
  inputSchema: {
@@ -24320,6 +24325,14 @@ function registerTools(server, changesDir, changeStore, taskFilesDir = changesDi
24320
24325
  };
24321
24326
  }
24322
24327
  await changeStore.appendSteering(name, message);
24328
+ if (hooks.onSteeringAppended) {
24329
+ try {
24330
+ await hooks.onSteeringAppended(name, message);
24331
+ } catch (err) {
24332
+ process.stderr.write(`! onSteeringAppended hook failed for ${name}: ${err instanceof Error ? err.message : String(err)}
24333
+ `);
24334
+ }
24335
+ }
24323
24336
  return {
24324
24337
  content: [{ type: "text", text: `Steering appended to change '${name}'` }]
24325
24338
  };
@@ -24982,6 +24995,50 @@ function runOpenspec(args, options = {}) {
24982
24995
  stderr: proc.stderr ? decoder.decode(proc.stderr) : ""
24983
24996
  };
24984
24997
  }
24998
+ function appendSteeringTaskToTasksMd(existing, taskLine) {
24999
+ const SECTION = "## Steering";
25000
+ const trimmed = existing.replace(/\s+$/, "");
25001
+ if (trimmed.length === 0) {
25002
+ return `${SECTION}
25003
+
25004
+ ${taskLine}
25005
+ `;
25006
+ }
25007
+ const lines = trimmed.split(/\r?\n/);
25008
+ let sectionStart = -1;
25009
+ for (let i = 0;i < lines.length; i += 1) {
25010
+ if (/^##\s+Steering\s*$/i.test(lines[i])) {
25011
+ sectionStart = i;
25012
+ break;
25013
+ }
25014
+ }
25015
+ if (sectionStart === -1) {
25016
+ return `${trimmed}
25017
+
25018
+ ${SECTION}
25019
+
25020
+ ${taskLine}
25021
+ `;
25022
+ }
25023
+ let sectionEnd = lines.length;
25024
+ for (let i = sectionStart + 1;i < lines.length; i += 1) {
25025
+ if (/^##\s+/.test(lines[i])) {
25026
+ sectionEnd = i;
25027
+ break;
25028
+ }
25029
+ }
25030
+ let insertAt = sectionEnd;
25031
+ while (insertAt - 1 > sectionStart && (lines[insertAt - 1] ?? "").trim() === "") {
25032
+ insertAt -= 1;
25033
+ }
25034
+ const before = lines.slice(0, insertAt);
25035
+ const after = lines.slice(insertAt);
25036
+ const out = [...before, taskLine, ...after.length ? [""] : [], ...after].join(`
25037
+ `);
25038
+ return out.endsWith(`
25039
+ `) ? out : `${out}
25040
+ `;
25041
+ }
24985
25042
 
24986
25043
  class OpenSpecChangeStore {
24987
25044
  async createChange(name, description) {
@@ -25038,6 +25095,16 @@ ${existing.trimStart()}` : `${message}
25038
25095
  `;
25039
25096
  await mkdir(dirname3(path), { recursive: true });
25040
25097
  await Bun.write(path, updated);
25098
+ const firstLine = message.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? message.trim();
25099
+ if (firstLine.length === 0)
25100
+ return;
25101
+ const tasksPath = join4("openspec", "changes", name, "tasks.md");
25102
+ const tasksFile = Bun.file(tasksPath);
25103
+ const existingTasks = await tasksFile.exists() ? await tasksFile.text() : "";
25104
+ const taskLine = `- [ ] Address steering: ${firstLine}`;
25105
+ const next = appendSteeringTaskToTasksMd(existingTasks, taskLine);
25106
+ await mkdir(dirname3(tasksPath), { recursive: true });
25107
+ await Bun.write(tasksPath, next);
25041
25108
  }
25042
25109
  async readSection(name, artifact, heading) {
25043
25110
  const file = Bun.file(join4("openspec", "changes", name, artifact));