@neriros/ralphy 3.2.0 → 3.3.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
@@ -187,6 +187,7 @@ Example `ralphy.config.json`:
187
187
  "mentionHandle": "@ralphy",
188
188
  "codeReviewTrigger": true,
189
189
  "codeReviewStaleHours": 24,
190
+ "syncTasksToComment": true,
190
191
  "syncTasksToDescription": false,
191
192
  "indicators": {
192
193
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
@@ -231,9 +232,29 @@ Set `linear.codeReviewTrigger: true` (or pass `--code-review`) to watch open, un
231
232
 
232
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.
233
234
 
234
- #### Sync tasks into Linear description
235
-
236
- Set `linear.syncTasksToDescription: true` to mirror the active change's `tasks.md` into the linked Linear issue description. Ralph writes a checklist between sentinel HTML comments (`<!-- ralphy:tasks:start -->` / `<!-- ralphy:tasks:end -->`); any content outside the markers is preserved verbatim. The block is refreshed when the worker launches, on the same cadence as `updateEveryIterations`, and on done-transition. Sync failures are logged but never abort the loop.
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.
237
258
 
238
259
  #### Conflict re-fix
239
260
 
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));