@flumecode/runner 0.5.0 → 0.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.js CHANGED
@@ -225,10 +225,16 @@ var SERVER_NAME2 = "flume_plan";
225
225
  var SUBMIT_PLAN = "submit_plan";
226
226
  var PLAN_TOOL_NAME = `mcp__${SERVER_NAME2}__${SUBMIT_PLAN}`;
227
227
  var PLAN_MARKER = "<!-- flumecode:end-of-plan -->";
228
+ var pseudoCodeEntrySchema = z2.object({
229
+ file: z2.string().min(1),
230
+ pseudoCode: z2.string().min(1)
231
+ });
228
232
  var stepSchema = z2.object({
229
- change: z2.string().min(1).describe("What changes, with concrete file references."),
230
- why: z2.string().min(1).describe("The reason for this step."),
231
- files: z2.array(z2.string()).optional().describe("Affected file paths.")
233
+ title: z2.string().min(1).describe("A concise imperative title for this step."),
234
+ description: z2.string().min(1).describe("What changes and why \u2014 the rationale for this step."),
235
+ pseudoCode: z2.array(pseudoCodeEntrySchema).optional().describe(
236
+ "Per-file pseudo code. Provide an entry for every non-documentation file this step touches. Each entry contains the file path and pseudo code describing the changes to that file."
237
+ )
232
238
  });
233
239
  var planInputSchema = {
234
240
  title: z2.string().min(1).max(120).describe(
@@ -260,12 +266,20 @@ function renderPlan(plan) {
260
266
  }
261
267
  }
262
268
  lines.push("");
263
- lines.push("**Steps**");
269
+ lines.push("## Steps");
264
270
  for (const [i, step] of plan.steps.entries()) {
265
- lines.push(`${i + 1}. **${step.change}** \u2014 ${step.why}`);
266
- if (step.files && step.files.length > 0) {
267
- for (const file of step.files) {
268
- lines.push(` - \`${file}\``);
271
+ lines.push("");
272
+ lines.push(`### ${i + 1}. ${step.title}`);
273
+ lines.push("");
274
+ lines.push(step.description);
275
+ if (step.pseudoCode && step.pseudoCode.length > 0) {
276
+ for (const entry of step.pseudoCode) {
277
+ lines.push("");
278
+ lines.push(`\`${entry.file}\``);
279
+ lines.push("");
280
+ lines.push("```");
281
+ lines.push(entry.pseudoCode);
282
+ lines.push("```");
269
283
  }
270
284
  }
271
285
  }
@@ -572,6 +586,33 @@ function buildRepairPrompt(ctx, hookLog) {
572
586
  ];
573
587
  return lines.join("\n");
574
588
  }
589
+ function buildReleasePrompt(ctx) {
590
+ const task = `Use the \`flumecode:create-release\` skill to handle this turn. You are driving a release: first analyse commits since the last tag, propose version bumps, and ask the user to confirm via widgets (Phase 1); once the user's widget answers appear in the thread, apply the bumps to package.json files and update CHANGELOG.md (Phase 2). Do NOT commit or push \u2014 the runner handles that and opens the bump PR.`;
591
+ const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to this release. If there is no wiki, work from the code directly.`;
592
+ const widgets = `When you need the user to choose, ask it as a widget rather than writing the options as prose: call \`single_select\` for a one-of-N choice (radio buttons) or \`multi_select\` for a "select all that apply" choice (checkboxes). Don't add your own "Other" option \u2014 the UI always provides one. After calling a widget tool, end your turn \u2014 the user's answer comes back as their next message and starts a fresh run.`;
593
+ const lines = [
594
+ `You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
595
+ `The repository ${ctx.repo.fullName} is checked out in your current working directory on the release bump branch "${ctx.repo.checkoutBranch}".`,
596
+ task,
597
+ orient,
598
+ widgets,
599
+ "",
600
+ "These coding guidelines apply to all code produced in this run:",
601
+ "",
602
+ loadRule("coding-guideline"),
603
+ "",
604
+ `# Release: ${ctx.request?.title ?? ""}`
605
+ ];
606
+ if (ctx.request?.body) {
607
+ lines.push("", ctx.request.body);
608
+ }
609
+ appendThread(lines, ctx);
610
+ lines.push(
611
+ "",
612
+ "Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you applied the bumps (Phase 2), make it the report the skill produced. The runner appends the pull-request link."
613
+ );
614
+ return lines.join("\n");
615
+ }
575
616
  function buildInitPrompt(ctx) {
576
617
  return [
577
618
  `You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
@@ -761,14 +802,15 @@ async function rebaseOntoMergeBranch(ctx, dir) {
761
802
  if (!mergeBranch) return;
762
803
  await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
763
804
  try {
764
- await git(["-C", dir, "rebase", "FETCH_HEAD"]);
765
- } catch {
805
+ await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
806
+ } catch (err) {
766
807
  const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
767
808
  () => ({ stdout: "" })
768
809
  );
769
810
  const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
770
811
  await git(["-C", dir, "rebase", "--abort"]).catch(() => {
771
812
  });
813
+ if (files.length === 0) throw err;
772
814
  throw new RebaseConflictError(mergeBranch, files);
773
815
  }
774
816
  }
@@ -854,11 +896,44 @@ var HEARTBEAT_MS = 5 * 6e4;
854
896
  async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
855
897
  if (abort.signal.aborted) throw new Error("Run canceled by user");
856
898
  const committed = await commitWithRepair(ctx, dir, abort);
857
- if (!committed) return { kind: "none" };
858
- if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
899
+ if (!committed) return { outcome: { kind: "none" }, autoMerged: false };
900
+ let autoMerged = false;
901
+ if (opts.rebase) {
902
+ try {
903
+ await rebaseOntoMergeBranch(ctx, dir);
904
+ } catch (err) {
905
+ if (!(err instanceof RebaseConflictError)) throw err;
906
+ if (abort.signal.aborted) throw new Error("Run canceled by user");
907
+ console.warn(
908
+ ` rebase onto ${ctx.repo.mergeBranch} conflicted \u2014 merging it in and resolving with the agent\u2026`
909
+ );
910
+ await mergeAndResolveConflicts(ctx, dir, abort);
911
+ await commitWithRepair(ctx, dir, abort);
912
+ autoMerged = true;
913
+ }
914
+ }
859
915
  await pushBranch(ctx, dir);
860
916
  const pr = await openPullRequest(ctx);
861
- return pr ? { kind: "pr", pr } : { kind: "pushed" };
917
+ return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged };
918
+ }
919
+ async function mergeAndResolveConflicts(ctx, dir, abort) {
920
+ const { conflicted } = await mergeInMergeBranch(ctx, dir);
921
+ if (!conflicted) return { resolved: false, text: null };
922
+ const result = await runClaudeCode({
923
+ cwd: dir,
924
+ prompt: buildResolvePrompt(ctx),
925
+ permissionMode: ctx.permissionMode,
926
+ model: ORCHESTRATOR_MODEL,
927
+ maxTurns: ORCHESTRATOR_MAX_TURNS,
928
+ abortController: abort
929
+ });
930
+ const unresolved = await listUnmergedPaths(dir);
931
+ if (unresolved.length > 0) {
932
+ throw new Error(
933
+ `Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still conflict: ${unresolved.join(", ")}`
934
+ );
935
+ }
936
+ return { resolved: true, text: result.text.trim() || null };
862
937
  }
863
938
  async function commitWithRepair(ctx, dir, abort) {
864
939
  for (let attempt = 1; ; attempt++) {
@@ -882,17 +957,18 @@ async function commitWithRepair(ctx, dir, abort) {
882
957
  }
883
958
  function outcomeBanner(outcome, opts) {
884
959
  const wikiNote = opts.documented ? " (with wiki updates)" : "";
960
+ const mergeNote = opts.autoMerged ? "\n\u{1F500} The base branch had advanced with conflicting changes \u2014 merged it in and resolved the conflicts automatically." : "";
885
961
  switch (outcome.kind) {
886
962
  case "pr":
887
963
  return `
888
964
 
889
965
  ---
890
- \u2705 Opened pull request from \`${opts.branch}\`${wikiNote} \xB7 [View pull request](${outcome.pr.url})`;
966
+ \u2705 Opened pull request from \`${opts.branch}\`${wikiNote} \xB7 [View pull request](${outcome.pr.url})${mergeNote}`;
891
967
  case "pushed":
892
968
  return `
893
969
 
894
970
  ---
895
- \u26A0\uFE0F Pushed \`${opts.branch}\`${wikiNote}, but couldn't open a pull request (no diff against the base branch, or one is already open).`;
971
+ \u26A0\uFE0F Pushed \`${opts.branch}\`${wikiNote}, but couldn't open a pull request (no diff against the base branch, or one is already open).${mergeNote}`;
896
972
  case "none":
897
973
  return `
898
974
 
@@ -924,6 +1000,11 @@ async function processJob(ctx, abort = new AbortController()) {
924
1000
  prepared = true;
925
1001
  return await processResolveJob(ctx, dir, abort);
926
1002
  }
1003
+ if (ctx.kind === "release") {
1004
+ const { resumed } = await prepareResumingBranch(ctx, dir, reused);
1005
+ prepared = true;
1006
+ return await processReleaseJob(ctx, dir, resumed, abort);
1007
+ }
927
1008
  await prepareAtSha(ctx, dir, reused);
928
1009
  prepared = true;
929
1010
  return await processChatJob(ctx, dir, abort);
@@ -947,10 +1028,11 @@ async function processInitJob(ctx, dir, abort) {
947
1028
  abortController: abort
948
1029
  })).text.trim();
949
1030
  let reply = summary || "(the agent produced no summary)";
950
- const outcome = await pushAndOpenPr(ctx, dir, abort);
1031
+ const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
951
1032
  reply += outcomeBanner(outcome, {
952
1033
  branch: ctx.repo.checkoutBranch,
953
- noChange: "no files were generated; the wiki may already exist."
1034
+ noChange: "no files were generated; the wiki may already exist.",
1035
+ autoMerged
954
1036
  });
955
1037
  return { text: reply, widgets: [] };
956
1038
  }
@@ -996,8 +1078,8 @@ async function processChatJob(ctx, dir, abort) {
996
1078
  console.warn(` wiki update skipped: ${errorMessage2(err)}`);
997
1079
  }
998
1080
  }
999
- const outcome = await pushAndOpenPr(ctx, dir, abort);
1000
- reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
1081
+ const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
1082
+ reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1001
1083
  return { text: reply, widgets: [] };
1002
1084
  }
1003
1085
  async function processImplementJob(ctx, dir, resumed, abort) {
@@ -1034,8 +1116,8 @@ async function processImplementJob(ctx, dir, resumed, abort) {
1034
1116
  console.warn(` wiki update skipped: ${errorMessage2(err)}`);
1035
1117
  }
1036
1118
  }
1037
- const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
1038
- reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
1119
+ const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
1120
+ reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1039
1121
  return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
1040
1122
  }
1041
1123
  async function processReviseJob(ctx, dir, resumed, abort) {
@@ -1078,9 +1160,9 @@ async function processReviseJob(ctx, dir, resumed, abort) {
1078
1160
  console.warn(` wiki update skipped: ${errorMessage2(err)}`);
1079
1161
  }
1080
1162
  }
1081
- const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
1163
+ const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
1082
1164
  if (outcome.kind !== "none") {
1083
- reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
1165
+ reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1084
1166
  }
1085
1167
  return {
1086
1168
  text: reply,
@@ -1093,27 +1175,8 @@ async function processResolveJob(ctx, dir, abort) {
1093
1175
  console.log(`
1094
1176
  \u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
1095
1177
  const installResult = await installDependencies(dir);
1096
- const { conflicted } = await mergeInMergeBranch(ctx, dir);
1097
- let reply;
1098
- if (conflicted) {
1099
- const result = await runClaudeCode({
1100
- cwd: dir,
1101
- prompt: buildResolvePrompt(ctx),
1102
- permissionMode: ctx.permissionMode,
1103
- model: ORCHESTRATOR_MODEL,
1104
- maxTurns: ORCHESTRATOR_MAX_TURNS,
1105
- abortController: abort
1106
- });
1107
- reply = result.text.trim() || "(the agent produced no report)";
1108
- const unresolved = await listUnmergedPaths(dir);
1109
- if (unresolved.length > 0) {
1110
- throw new Error(
1111
- `Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still conflict: ${unresolved.join(", ")}`
1112
- );
1113
- }
1114
- } else {
1115
- reply = `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
1116
- }
1178
+ const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, abort);
1179
+ let reply = resolved ? text || "(the agent produced no report)" : `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
1117
1180
  if (installResult.status === "failed") {
1118
1181
  reply += `
1119
1182
 
@@ -1127,6 +1190,40 @@ async function processResolveJob(ctx, dir, abort) {
1127
1190
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
1128
1191
  return { text: reply, widgets: [], ...pr ? { pr } : {} };
1129
1192
  }
1193
+ async function processReleaseJob(ctx, dir, resumed, abort) {
1194
+ console.log(`
1195
+ \u25B6 Release ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
1196
+ const installResult = await installDependencies(dir);
1197
+ const result = await runClaudeCode({
1198
+ cwd: dir,
1199
+ prompt: buildReleasePrompt(ctx),
1200
+ permissionMode: ctx.permissionMode,
1201
+ model: ORCHESTRATOR_MODEL,
1202
+ maxTurns: ORCHESTRATOR_MAX_TURNS,
1203
+ abortController: abort
1204
+ });
1205
+ let reply = result.text.trim() || "(the agent produced no reply)";
1206
+ if (installResult.status === "failed") {
1207
+ reply += `
1208
+
1209
+ > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
1210
+ }
1211
+ if (result.widgets.length > 0) {
1212
+ console.log(
1213
+ ` \u2026release ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`
1214
+ );
1215
+ return { text: reply, widgets: result.widgets };
1216
+ }
1217
+ const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
1218
+ if (outcome.kind !== "none") {
1219
+ reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, autoMerged });
1220
+ }
1221
+ return {
1222
+ text: reply,
1223
+ widgets: [],
1224
+ ...outcome.kind === "pr" ? { pr: outcome.pr } : {}
1225
+ };
1226
+ }
1130
1227
  async function heartbeat(config) {
1131
1228
  const health = await checkClaudeCode();
1132
1229
  if (health.ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "FlumeCode local runner — claims jobs and drives your local Claude Code against a real checkout.",
6
6
  "bin": {
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: create-release
3
+ description: >-
4
+ Draft release notes and version suggestions for a release, then (after the
5
+ user confirms) open a bump PR that updates package.json version(s) and
6
+ CHANGELOG.md. Two-turn flow: first turn asks the user to confirm versions via
7
+ widgets; second turn (answers in thread) writes the bumps and opens the PR.
8
+ ---
9
+
10
+ # create-release
11
+
12
+ You are driving a FlumeCode release. This skill has two distinct phases; detect
13
+ which one applies before acting.
14
+
15
+ ## Phase detection
16
+
17
+ Check the thread (`# Conversation so far` in the prompt). If **no widget answers**
18
+ appear in any prior agent turn, this is **Phase 1** — propose versions and ask.
19
+ If a prior turn contains widget-answer selections (the user picked a version), this
20
+ is **Phase 2** — apply the bumps and report.
21
+
22
+ ---
23
+
24
+ ## Phase 1 — Propose versions and ask
25
+
26
+ ### 1. Find the last release tags
27
+
28
+ For `@flumecode/web`, find the most recent `v*` tag:
29
+
30
+ ```
31
+ git tag -l --sort=-version:refname 'v*' | head -1
32
+ ```
33
+
34
+ For `@flumecode/runner`, find the most recent `runner-v*` tag:
35
+
36
+ ```
37
+ git tag -l --sort=-version:refname 'runner-v*' | head -1
38
+ ```
39
+
40
+ If no matching tag exists for a package, treat the current version in that
41
+ package's `package.json` as the baseline.
42
+
43
+ ### 2. List commits since the last tag, split by package
44
+
45
+ Get all commits since the tag (or all commits if no tag):
46
+
47
+ ```
48
+ git log <lastWebTag>..HEAD --oneline
49
+ ```
50
+
51
+ Then split by what they touch:
52
+
53
+ ```
54
+ # web + shared packages
55
+ git log <lastWebTag>..HEAD --oneline -- apps/web/ packages/
56
+
57
+ # runner only
58
+ git log <lastRunnerTag>..HEAD --oneline -- apps/runner/
59
+ ```
60
+
61
+ If a package has no commits, mark it **unchanged** — do not bump it.
62
+
63
+ ### 3. Infer bump level per package
64
+
65
+ For each package that has commits:
66
+
67
+ - `feat:` in a commit subject, or any breaking change → **minor** (or major if
68
+ explicitly breaking).
69
+ - `fix:`, `chore:`, `docs:`, `refactor:`, `test:` → **patch**.
70
+ - When in doubt, default to **patch**.
71
+
72
+ ### 4. Compute suggested next versions
73
+
74
+ Read current versions from `apps/web/package.json` and `apps/runner/package.json`.
75
+ Apply the inferred bump (semver: patch = last digit +1, minor = middle digit +1 +
76
+ reset patch to 0). For unchanged packages, keep the current version.
77
+
78
+ ### 5. Draft concise release notes
79
+
80
+ 3–10 user-readable bullet points summarising what changed. Group by theme; skip
81
+ internal chore commits unless they affect the user.
82
+
83
+ ### 6. Ask the user to confirm
84
+
85
+ For **each package that has commits** (skip unchanged ones), call
86
+ `mcp__flume_widgets__single_select` with:
87
+
88
+ - `question`: e.g. `@flumecode/web version for this release?`
89
+ - `options`: the suggested next version, plus the two sibling semver levels (e.g.
90
+ if patch suggested: also offer minor and major), plus the current version as
91
+ "No change (keep X.Y.Z)".
92
+
93
+ Also present the drafted release notes in your reply text so the user can read
94
+ them, then call `mcp__flume_widgets__single_select` for a confirmatory question:
95
+ `Do the release notes look correct?` with options `Yes, use these notes` and
96
+ `I'll edit them in the PR`.
97
+
98
+ **After calling widgets, end your turn.** Do NOT open a PR in Phase 1.
99
+
100
+ ---
101
+
102
+ ## Phase 2 — Apply the bumps and report
103
+
104
+ ### 1. Read the widget answers
105
+
106
+ The user's confirmed version selections appear in the `# Conversation so far`
107
+ thread as agent messages (the widget-answer turn). Extract the chosen version for
108
+ each package from those selections.
109
+
110
+ ### 2. Update package.json files
111
+
112
+ For each package whose version changed, edit the `"version"` field in:
113
+
114
+ - `apps/web/package.json` — for `@flumecode/web`
115
+ - `apps/runner/package.json` — for `@flumecode/runner`
116
+
117
+ Change only the `"version"` line; do not reformat the file.
118
+
119
+ ### 3. Update CHANGELOG.md
120
+
121
+ Edit (or create) `CHANGELOG.md` at the repo root. Insert a new section at the
122
+ top, below any existing `# Changelog` title line:
123
+
124
+ ```
125
+ ## [X.Y.Z / runner-X.Y.Z] - YYYY-MM-DD
126
+
127
+ - Bullet point from release notes
128
+ - Another bullet point
129
+ ```
130
+
131
+ Use the ISO date format (`YYYY-MM-DD`). Preserve all existing entries — do not
132
+ delete or rewrite prior sections.
133
+
134
+ If both packages are bumped, list both versions in the heading (e.g.
135
+ `## [0.9.0 / runner-0.5.0] - 2026-06-06`). If only one package is bumped, list
136
+ only that version in the heading.
137
+
138
+ ### 4. Stop — do not commit or push
139
+
140
+ Leave the edited files in the working tree. The runner commits them and opens the
141
+ pull request.
142
+
143
+ ### 5. End with this exact report format
144
+
145
+ Your final message must match this shape (adjust versions and files to match what
146
+ actually changed):
147
+
148
+ ```
149
+ **Bumped versions:**
150
+ - `@flumecode/web`: `0.8.0` → `0.9.0`
151
+ - `@flumecode/runner`: `0.4.0` → `0.5.0`
152
+
153
+ **CHANGELOG updated** with release notes.
154
+
155
+ **Files changed:** `apps/web/package.json`, `apps/runner/package.json`, `CHANGELOG.md`
156
+
157
+ <!-- flumecode:versions {"@flumecode/web":"0.9.0","@flumecode/runner":"0.5.0"} -->
158
+ ```
159
+
160
+ The `<!-- flumecode:versions {...} -->` comment is machine-readable and **must be
161
+ the last line** of your message body (before any trailing newline). The runner
162
+ reads it to persist the confirmed versions on the release entity. Use the exact
163
+ JSON key names `@flumecode/web` and `@flumecode/runner`; omit a package if its
164
+ version did not change.
165
+
166
+ ---
167
+
168
+ ## Notes
169
+
170
+ - **Runner-only bump:** if only `apps/runner/` has commits, bump only
171
+ `apps/runner/package.json`. Leave `apps/web/package.json` unchanged.
172
+ - **Clear Phase 1 text:** be explicit about what changed since the last tag so the
173
+ user can confidently confirm or override your suggestions.
174
+ - **Never edit** any file other than `apps/web/package.json`,
175
+ `apps/runner/package.json`, and `CHANGELOG.md`.
176
+ - **Never commit, push, or open a PR** — the runner does that.
@@ -66,9 +66,10 @@ Field-by-field guidance:
66
66
  and nothing more.
67
67
  - **`assumptions`** — anything you decided during investigation (including
68
68
  unanswered defaults from Phase 1).
69
- - **`steps`** — an ordered list. For each step: what changes (`change`), why
70
- (`why`), and optionally which files are affected (`files`). Use concrete file
71
- references (`path/to/file.ts`) and name the functions/symbols involved.
69
+ - **`steps`** — an ordered list. For each step provide:
70
+ - **`title`** a concise imperative phrase naming the step (e.g. "Add submit_plan schema to plan.ts").
71
+ - **`description`** — what changes and why: the concrete change being made and the rationale for it. Use concrete file references (`path/to/file.ts`) and name the functions/symbols involved.
72
+ - **`pseudoCode`** — an array of `{ file, pseudoCode }` entries. Provide an entry for every file the step touches **except** documentation files (SKILL.md, README.md, wiki pages, etc.). `pseudoCode` is optional in the schema but expected for all non-documentation files. Each entry names the file path and contains pseudo code that precisely describes the changes to make in that file.
72
73
  - **`acceptanceCriteria`** — **required; at least 2 items.** Each criterion must
73
74
  be an observable condition you could check after the work is done: a behavior,
74
75
  a test result, or a verifiable state. Together they must fully define "done" —