@flumecode/runner 0.14.0 → 0.16.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
@@ -27,7 +27,7 @@ function writeConfig(config) {
27
27
 
28
28
  // src/run.ts
29
29
  import { existsSync as existsSync4 } from "node:fs";
30
- import { join as join5 } from "node:path";
30
+ import { join as join6 } from "node:path";
31
31
 
32
32
  // src/version.ts
33
33
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -182,6 +182,8 @@ async function safeText(res) {
182
182
 
183
183
  // src/plugins/socket.ts
184
184
  import { exec as execCb } from "node:child_process";
185
+ import { readFile as readFile2 } from "node:fs/promises";
186
+ import { join as join4 } from "node:path";
185
187
  import { promisify as promisify2 } from "node:util";
186
188
 
187
189
  // src/workspace.ts
@@ -612,7 +614,12 @@ function parseManifest(raw) {
612
614
  if (typeof r.key !== "string" || !r.key) return null;
613
615
  if (r.socket !== "pre-commit") return null;
614
616
  if (typeof r.run !== "string" || !r.run) return null;
615
- return { key: r.key, socket: r.socket, run: r.run };
617
+ let report;
618
+ const rep = r.report;
619
+ if (rep && typeof rep.file === "string" && rep.file && rep.format === "jest") {
620
+ report = { file: rep.file, format: "jest" };
621
+ }
622
+ return { key: r.key, socket: r.socket, run: r.run, ...report ? { report } : {} };
616
623
  }
617
624
 
618
625
  // src/plugins/socket.ts
@@ -633,15 +640,40 @@ async function runSocket(socketName, dir) {
633
640
  const results = [];
634
641
  for (const plugin of plugins) {
635
642
  const result = await runPluginCommand(plugin.run, dir);
643
+ const metrics = await readMetrics(plugin.report, dir);
636
644
  if (result.exitCode !== 0) {
637
- results.push({ key: plugin.key, status: "failed", output: cap(result.output) });
645
+ results.push({
646
+ key: plugin.key,
647
+ status: "failed",
648
+ output: cap(result.output),
649
+ ...metrics ? { metrics } : {}
650
+ });
638
651
  lastSocketResults = results;
639
652
  throw new PreCommitError(`[plugin:${plugin.key}] ${result.output}`);
640
653
  }
641
- results.push({ key: plugin.key, status: "passed", output: cap(result.output) });
654
+ results.push({
655
+ key: plugin.key,
656
+ status: "passed",
657
+ output: cap(result.output),
658
+ ...metrics ? { metrics } : {}
659
+ });
642
660
  }
643
661
  lastSocketResults = results;
644
662
  }
663
+ async function readMetrics(report, dir) {
664
+ if (!report) return void 0;
665
+ try {
666
+ const raw = JSON.parse(await readFile2(join4(dir, report.file), "utf8"));
667
+ if (report.format === "jest") {
668
+ return {
669
+ testsRun: Number(raw.numTotalTests) || 0,
670
+ testsFailed: Number(raw.numFailedTests) || 0
671
+ };
672
+ }
673
+ } catch {
674
+ }
675
+ return void 0;
676
+ }
645
677
  async function runPluginCommand(command2, cwd) {
646
678
  try {
647
679
  const result = await exec2(command2, { cwd, maxBuffer: 1 << 24 });
@@ -746,7 +778,9 @@ var pseudoCodeEntrySchema = z2.object({
746
778
  });
747
779
  var stepSchema = z2.object({
748
780
  title: z2.string().min(1).describe("A concise imperative title for this step."),
749
- description: z2.string().min(1).describe("What changes and why \u2014 the rationale for this step. " + INLINE_CODE_HINT),
781
+ description: z2.array(z2.string().min(1)).min(1).describe(
782
+ "Bullet points that explain this step's change so a reviewer can judge whether the design is correct. Each array item is one short, self-contained bullet \u2014 not a single paragraph, and not a restatement of the pseudo code. " + INLINE_CODE_HINT
783
+ ),
750
784
  pseudoCode: z2.array(pseudoCodeEntrySchema).optional().describe(
751
785
  "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."
752
786
  )
@@ -758,6 +792,9 @@ var planInputSchema = {
758
792
  scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
759
793
  goal: z2.string().min(1).describe("One or two sentences stating the outcome. " + INLINE_CODE_HINT),
760
794
  assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
795
+ requirements: z2.array(z2.string().min(1)).min(1).describe(
796
+ "Required, human-readable statements of what this change must accomplish and why, in plain language a non-technical reader can follow. Distinct from acceptanceCriteria: requirements explain intent/rationale; acceptance criteria are the machine-checkable proof. At least 1 required. " + INLINE_CODE_HINT
797
+ ),
761
798
  steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
762
799
  acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
763
800
  "Concrete, deterministically-checkable conditions that together define done. Each names a trigger/precondition and the exact observable result (run X -> output Y; file Z contains W; f(a) returns b) \u2014 no vague adjectives, not a restatement of a step. The set must collectively cover every step's change. At least 2 required. " + INLINE_CODE_HINT
@@ -781,12 +818,19 @@ function renderPlan(plan) {
781
818
  }
782
819
  }
783
820
  lines2.push("");
821
+ lines2.push("## Requirements");
822
+ for (const requirement of plan.requirements) {
823
+ lines2.push(`- ${requirement}`);
824
+ }
825
+ lines2.push("");
784
826
  lines2.push("## Steps");
785
827
  for (const [i, step] of plan.steps.entries()) {
786
828
  lines2.push("");
787
829
  lines2.push(`### ${i + 1}. ${step.title}`);
788
830
  lines2.push("");
789
- lines2.push(step.description);
831
+ for (const bullet of step.description) {
832
+ lines2.push(`- ${bullet}`);
833
+ }
790
834
  if (step.pseudoCode && step.pseudoCode.length > 0) {
791
835
  for (const entry of step.pseudoCode) {
792
836
  lines2.push("");
@@ -835,7 +879,7 @@ function createPlanTooling() {
835
879
  let renderedPlans = null;
836
880
  const submitPlan = tool2(
837
881
  SUBMIT_PLAN,
838
- "Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. acceptanceCriteria is required in each plan and must contain at least 2 observable, verifiable conditions. The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles.",
882
+ "Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. acceptanceCriteria is required in each plan and must contain at least 2 observable, verifiable conditions. The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles. requirements is required in each plan: at least 1 plain-language statement of what the change must accomplish and why (human-readable intent), separate from the machine-checkable acceptanceCriteria. ",
839
883
  submitPlanInputSchema,
840
884
  async (args) => {
841
885
  const parsed = submitPlanSchema.parse(args);
@@ -907,8 +951,11 @@ var reportInputSchema = {
907
951
  caveats: z3.string().min(1).describe(
908
952
  "Markdown: anything deferred, unmet, or worth a human's eyes, incl. diff hunks that map to no plan AC. Write 'None.' if nothing. Rendered under '## Caveats / follow-ups'. " + INLINE_CODE_HINT
909
953
  ),
910
- acceptanceCriteria: z3.array(acVerdictSchema).min(1).describe(
911
- "One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it."
954
+ acceptanceCriteria: z3.array(acVerdictSchema).describe(
955
+ "One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it. May be empty for resolve runs (no plan to verify)."
956
+ ),
957
+ conflictResolution: z3.string().optional().describe(
958
+ "Markdown: present ONLY when a merge conflict was actually resolved. Explain, per conflicted file, how ours/theirs were integrated. Rendered under '## Conflict resolution'. Omit entirely when no conflict occurred."
912
959
  )
913
960
  };
914
961
  var reportSchema = z3.object(reportInputSchema);
@@ -916,21 +963,26 @@ function renderReport(report) {
916
963
  const lines2 = [];
917
964
  lines2.push(report.summary.trim());
918
965
  lines2.push("", "## Files changed", "", report.filesChanged.trim());
919
- lines2.push("", "## Acceptance criteria");
920
- for (const ac of report.acceptanceCriteria) {
921
- lines2.push("");
922
- lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
923
- lines2.push("");
924
- lines2.push(ac.rationale.trim());
925
- for (const ev of ac.evidence) {
966
+ if (report.acceptanceCriteria.length > 0) {
967
+ lines2.push("", "## Acceptance criteria");
968
+ for (const ac of report.acceptanceCriteria) {
926
969
  lines2.push("");
927
- lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
970
+ lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
928
971
  lines2.push("");
929
- lines2.push("```diff");
930
- lines2.push(ev.hunk.replace(/\n+$/, ""));
931
- lines2.push("```");
972
+ lines2.push(ac.rationale.trim());
973
+ for (const ev of ac.evidence) {
974
+ lines2.push("");
975
+ lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
976
+ lines2.push("");
977
+ lines2.push("```diff");
978
+ lines2.push(ev.hunk.replace(/\n+$/, ""));
979
+ lines2.push("```");
980
+ }
932
981
  }
933
982
  }
983
+ if (report.conflictResolution?.trim()) {
984
+ lines2.push("", "## Conflict resolution", "", report.conflictResolution.trim());
985
+ }
934
986
  lines2.push("", "## Code quality", "", report.codeQuality.trim());
935
987
  lines2.push("", "## Caveats / follow-ups", "", report.caveats.trim());
936
988
  return lines2.join("\n");
@@ -1110,11 +1162,11 @@ function errorMessage(err) {
1110
1162
 
1111
1163
  // src/rules.ts
1112
1164
  import { readFileSync as readFileSync3 } from "node:fs";
1113
- import { join as join4 } from "node:path";
1165
+ import { join as join5 } from "node:path";
1114
1166
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1115
1167
  var RULES_DIR = fileURLToPath3(new URL("../skills-plugin/rules", import.meta.url));
1116
1168
  function loadRule(name) {
1117
- const raw = readFileSync3(join4(RULES_DIR, `${name}.md`), "utf8");
1169
+ const raw = readFileSync3(join5(RULES_DIR, `${name}.md`), "utf8");
1118
1170
  return stripFrontMatter(raw).trim();
1119
1171
  }
1120
1172
  function stripFrontMatter(raw) {
@@ -1355,6 +1407,7 @@ async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
1355
1407
  const committed = await commitWithRepair(ctx, dir, abort);
1356
1408
  if (!committed) return { outcome: { kind: "none" }, autoMerged: false };
1357
1409
  let autoMerged = false;
1410
+ let conflictResolution;
1358
1411
  if (opts.rebase) {
1359
1412
  try {
1360
1413
  await rebaseOntoMergeBranch(ctx, dir);
@@ -1364,14 +1417,15 @@ async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
1364
1417
  console.warn(
1365
1418
  ` rebase onto ${ctx.repo.mergeBranch} conflicted \u2014 merging it in and resolving with the agent\u2026`
1366
1419
  );
1367
- await mergeAndResolveConflicts(ctx, dir, config, abort);
1420
+ const { report: mergeReport } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1421
+ conflictResolution = mergeReport?.conflictResolution;
1368
1422
  await commitWithRepair(ctx, dir, abort, { skipSocket: true });
1369
1423
  autoMerged = true;
1370
1424
  }
1371
1425
  }
1372
1426
  await pushBranch(ctx, dir);
1373
1427
  const pr = await openPullRequest(ctx);
1374
- return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged };
1428
+ return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged, conflictResolution };
1375
1429
  }
1376
1430
  async function mergeAndResolveConflicts(ctx, dir, config, abort) {
1377
1431
  const { conflicted } = await mergeInMergeBranch(ctx, dir);
@@ -1397,7 +1451,7 @@ async function mergeAndResolveConflicts(ctx, dir, config, abort) {
1397
1451
  `Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still contain conflict markers: ${unresolved.join(", ")}`
1398
1452
  );
1399
1453
  }
1400
- return { resolved: true, text: result.text.trim() || null };
1454
+ return { resolved: true, text: result.text.trim() || null, report: result.report ?? void 0 };
1401
1455
  }
1402
1456
  async function commitWithRepair(ctx, dir, abort, opts = {}) {
1403
1457
  for (let attempt = 1; ; attempt++) {
@@ -1527,7 +1581,7 @@ async function processChatJob(ctx, dir, config, abort) {
1527
1581
  console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1528
1582
  return { text: reply, widgets: result.widgets };
1529
1583
  }
1530
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1584
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1531
1585
  let documented = false;
1532
1586
  if (ctx.permissionMode !== "plan" && wikiExists && await hasChanges(dir)) {
1533
1587
  try {
@@ -1616,7 +1670,7 @@ ${reply}`;
1616
1670
 
1617
1671
  > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
1618
1672
  }
1619
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1673
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1620
1674
  let documented = false;
1621
1675
  if (wikiExists && await hasChanges(dir)) {
1622
1676
  try {
@@ -1636,12 +1690,13 @@ ${reply}`;
1636
1690
  } else if (!wikiExists) {
1637
1691
  console.log(` no .flumecode/wiki \u2014 skipping wiki reconcile for ${ctx.jobId}`);
1638
1692
  }
1639
- const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
1693
+ const { outcome, autoMerged, conflictResolution } = await pushAndOpenPr(ctx, dir, config, abort, {
1640
1694
  rebase: !resumed
1641
1695
  });
1642
1696
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1643
1697
  const lintPlugins = getSocketResults();
1644
- const finalReport = report && lintPlugins.length ? { ...report, lint: { plugins: lintPlugins } } : report;
1698
+ const reportWithConflict = report && conflictResolution ? { ...report, conflictResolution } : report;
1699
+ const finalReport = reportWithConflict && lintPlugins.length ? { ...reportWithConflict, lint: { plugins: lintPlugins } } : reportWithConflict;
1645
1700
  return {
1646
1701
  text: reply,
1647
1702
  widgets: [],
@@ -1661,8 +1716,8 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1661
1716
  maxTurns: ORCHESTRATOR_MAX_TURNS,
1662
1717
  abortController: abort
1663
1718
  });
1664
- const summary = result.text.trim();
1665
- let reply = summary || "(the agent produced no reply)";
1719
+ const report = result.report ?? void 0;
1720
+ let reply = (report ? renderReport(report) : result.text.trim()) || "(the agent produced no reply)";
1666
1721
  if (result.plans?.length) reply = result.plans[0] ?? reply;
1667
1722
  if (installResult.status === "failed") {
1668
1723
  reply += `
@@ -1673,7 +1728,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1673
1728
  console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1674
1729
  return { text: reply, widgets: result.widgets };
1675
1730
  }
1676
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1731
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1677
1732
  let documented = false;
1678
1733
  if (wikiExists && await hasChanges(dir)) {
1679
1734
  try {
@@ -1693,15 +1748,19 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1693
1748
  } else if (!wikiExists) {
1694
1749
  console.log(` no .flumecode/wiki \u2014 skipping wiki reconcile for ${ctx.jobId}`);
1695
1750
  }
1696
- const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
1751
+ const { outcome, autoMerged, conflictResolution } = await pushAndOpenPr(ctx, dir, config, abort, {
1697
1752
  rebase: !resumed
1698
1753
  });
1699
1754
  if (outcome.kind !== "none") {
1700
1755
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1701
1756
  }
1757
+ const lintPlugins = getSocketResults();
1758
+ const reportWithConflict = report && conflictResolution ? { ...report, conflictResolution } : report;
1759
+ const finalReport = reportWithConflict && lintPlugins.length ? { ...reportWithConflict, lint: { plugins: lintPlugins } } : reportWithConflict;
1702
1760
  return {
1703
1761
  text: reply,
1704
1762
  widgets: [],
1763
+ ...finalReport ? { report: finalReport } : {},
1705
1764
  ...outcome.kind === "pr" ? { pr: outcome.pr } : {},
1706
1765
  ...result.plans?.length ? { plans: result.plans } : {}
1707
1766
  };
@@ -1710,8 +1769,13 @@ async function processResolveJob(ctx, dir, config, abort) {
1710
1769
  console.log(`
1711
1770
  \u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
1712
1771
  const installResult = await installDependencies(dir);
1713
- const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1714
- 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.`;
1772
+ const { resolved, text, report } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1773
+ let reply;
1774
+ if (resolved) {
1775
+ reply = report ? renderReport(report) : text || "(the agent produced no report)";
1776
+ } else {
1777
+ reply = `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
1778
+ }
1715
1779
  if (installResult.status === "failed") {
1716
1780
  reply += `
1717
1781
 
@@ -1723,7 +1787,7 @@ async function processResolveJob(ctx, dir, config, abort) {
1723
1787
  const pr = await openPullRequest(ctx);
1724
1788
  const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
1725
1789
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
1726
- return { text: reply, widgets: [], ...pr ? { pr } : {} };
1790
+ return { text: reply, widgets: [], ...report ? { report } : {}, ...pr ? { pr } : {} };
1727
1791
  }
1728
1792
  async function processReleaseJob(ctx, dir, resumed, config, abort) {
1729
1793
  console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.14.0",
3
+ "version": "0.16.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,64 @@
1
+ ---
2
+ name: format-code-plugin-generator
3
+ description: >-
4
+ Generate a concrete plan to install the FlumeCode Format plugin for THIS repo —
5
+ a .flumecode/plugins/format-code/ manifest wired to the pre-commit socket that
6
+ auto-formats code (prettier --write) so changes ride into the commit.
7
+ ---
8
+
9
+ # format-code-plugin-generator
10
+
11
+ You generate a concrete, repo-specific plan to install the FlumeCode Format
12
+ plugin. You work **read-only**: inspect the repo and produce a plan via
13
+ `submit_plan`; never edit files.
14
+
15
+ ## Orient yourself first
16
+
17
+ Before producing the plan, inspect:
18
+
19
+ 1. `.flumecode/wiki/README.md` and `components/plugins.md` (if present) for context.
20
+ 2. `package.json` `scripts` — look for `format`, `format:write`, `prettier` references.
21
+ 3. `.prettierrc*` — confirm Prettier is configured.
22
+ 4. `.husky/pre-commit` — find the existing formatting step this plugin replaces.
23
+
24
+ From this, determine the **exact shell command** the `run` script should execute
25
+ (e.g. `pnpm format`). Do not hard-code — derive from the repo.
26
+
27
+ ## Produce the plan
28
+
29
+ Call `submit_plan` **once**, passing a `plans` array with one entry whose steps
30
+ instruct the implementer to create:
31
+
32
+ ### Artifact — `.flumecode/plugins/format-code/plugin.json`
33
+
34
+ ```json
35
+ {
36
+ "key": "format-code",
37
+ "socket": "pre-commit",
38
+ "run": "<detected format write command>"
39
+ }
40
+ ```
41
+
42
+ Derive `run` from the repo's detected commands (e.g. `pnpm format`). Do not hard-code — include the actual commands discovered in the Orient step.
43
+
44
+ ### Manifest shape
45
+
46
+ The manifest `plugin.json` must have exactly these fields:
47
+
48
+ ```
49
+ { key, socket, run }
50
+ ```
51
+
52
+ This is the shape the FlumeCode plugin loader expects.
53
+
54
+ ### Acceptance criteria the plan must include
55
+
56
+ - `.flumecode/plugins/format-code/plugin.json` exists with `key: "format-code"`, `socket: "pre-commit"`, and `run` set to the detected write-format command.
57
+ - The `run` command reformats files in place and exits 0; reformatted files are staged and included in the commit.
58
+
59
+ ## Always
60
+
61
+ - Stay read-only. Produce the plan via `submit_plan`; never edit files.
62
+ - The plan must be specific enough for an `implement-plan` run to execute
63
+ without re-deriving the commands — include the actual detected commands in
64
+ the step descriptions and artifact content.
@@ -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
+ - **`requirements`** — **required; at least 1 item.** Plain-language statements of what this change must accomplish and why, written so a non-technical reader can follow them. Distinct from `acceptanceCriteria`: requirements explain intent and rationale; acceptance criteria are the machine-checkable proof. At least 1 item required.
69
70
  - **`steps`** — an ordered list. For each step provide:
70
71
  - **`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
+ - **`description`** — an array of bullet points that help the reviewer understand the upcoming `pseudoCode` and decide whether the plan and design are correct. Each item is a distinct, self-contained point about what is changing and why not a single paragraph, and not a line-by-line restatement of the pseudo code. Use concrete file references (`path/to/file.ts`) and name the functions/symbols involved. Apply inline-code formatting to all identifiers.
72
73
  - **`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.
73
74
  - **`acceptanceCriteria`** — **required; at least 2 items.** Each criterion must
74
75
  be a concrete, deterministically-checkable condition that a third party can verify
@@ -93,8 +93,16 @@ before you finish. (You don't need to `git add`; the runner stages and commits f
93
93
 
94
94
  ## Your final reply
95
95
 
96
- Your last message **is** the report posted to the session thread. Write it for the
97
- user: list which files conflicted and, briefly, how you resolved each, plus how you
98
- verified (build/tests). Wrap conflicted file names and code identifiers in inline
99
- backticks per the `# Technical Writing` section. The runner appends the pull-request
100
- link, so don't add one.
96
+ Call **`submit_report`** with the structured report. Fields:
97
+
98
+ - `summary`: one or two sentences on what the resolve run did.
99
+ - `filesChanged`: markdown list of files changed (from `git --no-pager diff --stat`).
100
+ - `codeQuality`: markdown: whether build/tests passed, any quality observations.
101
+ - `caveats`: markdown: anything deferred, risky, or worth the user's attention. Write `None.` if nothing.
102
+ - `acceptanceCriteria`: **leave this empty (`[]`)** — there is no plan to verify for a resolve run.
103
+ - `conflictResolution`: **required** — a markdown section, one paragraph or bullet per conflicted
104
+ file, explaining which side you kept and why (or how you merged both intents). Wrap file names
105
+ and code identifiers in inline backticks. This is what the user reads to understand how each
106
+ conflict was integrated.
107
+
108
+ The runner renders the report and appends the pull-request link — do not add one yourself.
@@ -41,7 +41,7 @@ actual code. Pick exactly one:
41
41
  - **Re-plan** — the request meaningfully changes scope or direction, enough that a
42
42
  fresh plan should be agreed before building. Call **`submit_plan`** with a `plans[]` array
43
43
  containing the revised structured fields (same per-plan shape as the request-to-plan skill:
44
- `scope`, `goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
44
+ `scope`, `goal`, `assumptions`, `requirements` — at least 1 —, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
45
45
  `outOfScope`). Include only one entry for a revise turn. The runner posts it as a revision
46
46
  the user can accept; make no code changes this turn.
47
47
  - **Implement** — the request is clear and reasonable. Make the change (via
@@ -80,13 +80,13 @@ essentials:
80
80
  Your last message **is** the comment posted to the plan thread — write it for the
81
81
  user:
82
82
 
83
- - **Implemented:** a short report what you changed and why, which files, and the
84
- verification results: list each build/test command that was run and its final
85
- pass/fail result (or note that no build/test setup was found). Base "what changed"
86
- and "which files" on the actual `git --no-pager diff` (`--stat` for the file
87
- list), not on what a subagent claimed; if the diff is empty, say nothing was
88
- changed rather than describing edits that aren't there. The runner appends the
89
- pull-request link, so don't add one.
83
+ - **Implemented:** call **`submit_report`** with the structured report, exactly as
84
+ `implement-plan` does. Include one `acceptanceCriteria` entry per plan AC (with a
85
+ met / not_met / unclear verdict and the diff hunk(s) that prove it), plus the four
86
+ required markdown sections (`summary`, `filesChanged`, `codeQuality`, `caveats`).
87
+ Base `filesChanged` and evidence on the actual `git --no-pager diff`, not on what
88
+ a subagent claimed; if the diff is empty, say nothing was changed. The runner
89
+ renders the report and appends the pull-request link do not add one yourself.
90
90
  - **Clarify / push back:** your question or reasoning, as prose (plus any widget).
91
91
  - **Re-plan:** you called `submit_plan`; the rendered plan is posted automatically,
92
92
  so keep any extra reply text minimal.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: unit-test-plugin-generator
3
+ description: >-
4
+ Generate a concrete plan to install the FlumeCode Unit Test plugin for THIS repo —
5
+ a .flumecode/plugins/unit-test/ manifest wired to the pre-commit socket that runs
6
+ the repo's unit tests and emits a JSON report the runner parses into pass/fail counts.
7
+ ---
8
+
9
+ # unit-test-plugin-generator
10
+
11
+ You generate a concrete, repo-specific plan to install the FlumeCode Unit Test
12
+ plugin. You work **read-only**: inspect the repo and produce a plan via
13
+ `submit_plan`; never edit files.
14
+
15
+ ## Orient yourself first
16
+
17
+ Before producing the plan, inspect:
18
+
19
+ 1. `.flumecode/wiki/README.md` and `components/plugins.md` (if present) for context.
20
+ 2. `package.json` `scripts` — look for `test`, `test:unit`, or similar test commands.
21
+ 3. `vitest.config.*` — to detect Vitest as the test runner.
22
+ 4. `jest.config.*` — to detect Jest as the test runner.
23
+
24
+ From this, determine:
25
+
26
+ - Which test runner is in use (Vitest or Jest).
27
+ - The **exact shell command** that runs the tests and writes a JSON report file (e.g. `pnpm vitest run --reporter=json --outputFile=.flumecode/tmp/unit-test-report.json`). Do not hard-code — derive from the repo.
28
+
29
+ ## Produce the plan
30
+
31
+ Call `submit_plan` **once**, passing a `plans` array with one entry whose steps
32
+ instruct the implementer to:
33
+
34
+ ### Artifact 1 — `.flumecode/plugins/unit-test/plugin.json`
35
+
36
+ ```json
37
+ {
38
+ "key": "unit-test",
39
+ "socket": "pre-commit",
40
+ "run": "<detected command, e.g. pnpm vitest run --reporter=json --outputFile=.flumecode/tmp/unit-test-report.json>",
41
+ "report": { "file": ".flumecode/tmp/unit-test-report.json", "format": "jest" }
42
+ }
43
+ ```
44
+
45
+ Derive `run` from the repo's detected test runner and scripts. Do not hard-code —
46
+ include the actual commands discovered in the Orient step.
47
+
48
+ ### Artifact 2 — `.gitignore` update
49
+
50
+ Add `.flumecode/tmp/` to `.gitignore` so the transient JSON report file is never committed.
51
+
52
+ ### Manifest shape
53
+
54
+ The manifest `plugin.json` must have exactly these fields:
55
+
56
+ ```
57
+ { key, socket, run, report }
58
+ ```
59
+
60
+ where `report.file` points to the JSON report output path (repo-relative) and
61
+ `report.format` is `"jest"` (Vitest's JSON reporter uses the same schema).
62
+
63
+ ### Acceptance criteria the plan must include
64
+
65
+ - `.flumecode/plugins/unit-test/plugin.json` exists with `key: "unit-test"`, `socket: "pre-commit"`, `run` set to the detected test command that writes a JSON report, and `report` set to `{ "file": ".flumecode/tmp/unit-test-report.json", "format": "jest" }`.
66
+ - The `run` command exits non-zero on any test failure and writes the JSON report file.
67
+ - `.flumecode/tmp/` is present in `.gitignore`.
68
+
69
+ ## Always
70
+
71
+ - Stay read-only. Produce the plan via `submit_plan`; never edit files.
72
+ - The plan must be specific enough for an `implement-plan` run to execute
73
+ without re-deriving the commands — include the actual detected commands in
74
+ the step descriptions and artifact content.