@flumecode/runner 0.15.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 });
@@ -760,6 +792,9 @@ var planInputSchema = {
760
792
  scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
761
793
  goal: z2.string().min(1).describe("One or two sentences stating the outcome. " + INLINE_CODE_HINT),
762
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
+ ),
763
798
  steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
764
799
  acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
765
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
@@ -783,6 +818,11 @@ function renderPlan(plan) {
783
818
  }
784
819
  }
785
820
  lines2.push("");
821
+ lines2.push("## Requirements");
822
+ for (const requirement of plan.requirements) {
823
+ lines2.push(`- ${requirement}`);
824
+ }
825
+ lines2.push("");
786
826
  lines2.push("## Steps");
787
827
  for (const [i, step] of plan.steps.entries()) {
788
828
  lines2.push("");
@@ -839,7 +879,7 @@ function createPlanTooling() {
839
879
  let renderedPlans = null;
840
880
  const submitPlan = tool2(
841
881
  SUBMIT_PLAN,
842
- "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. ",
843
883
  submitPlanInputSchema,
844
884
  async (args) => {
845
885
  const parsed = submitPlanSchema.parse(args);
@@ -911,8 +951,11 @@ var reportInputSchema = {
911
951
  caveats: z3.string().min(1).describe(
912
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
913
953
  ),
914
- acceptanceCriteria: z3.array(acVerdictSchema).min(1).describe(
915
- "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."
916
959
  )
917
960
  };
918
961
  var reportSchema = z3.object(reportInputSchema);
@@ -920,21 +963,26 @@ function renderReport(report) {
920
963
  const lines2 = [];
921
964
  lines2.push(report.summary.trim());
922
965
  lines2.push("", "## Files changed", "", report.filesChanged.trim());
923
- lines2.push("", "## Acceptance criteria");
924
- for (const ac of report.acceptanceCriteria) {
925
- lines2.push("");
926
- lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
927
- lines2.push("");
928
- lines2.push(ac.rationale.trim());
929
- for (const ev of ac.evidence) {
966
+ if (report.acceptanceCriteria.length > 0) {
967
+ lines2.push("", "## Acceptance criteria");
968
+ for (const ac of report.acceptanceCriteria) {
930
969
  lines2.push("");
931
- lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
970
+ lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
932
971
  lines2.push("");
933
- lines2.push("```diff");
934
- lines2.push(ev.hunk.replace(/\n+$/, ""));
935
- 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
+ }
936
981
  }
937
982
  }
983
+ if (report.conflictResolution?.trim()) {
984
+ lines2.push("", "## Conflict resolution", "", report.conflictResolution.trim());
985
+ }
938
986
  lines2.push("", "## Code quality", "", report.codeQuality.trim());
939
987
  lines2.push("", "## Caveats / follow-ups", "", report.caveats.trim());
940
988
  return lines2.join("\n");
@@ -1114,11 +1162,11 @@ function errorMessage(err) {
1114
1162
 
1115
1163
  // src/rules.ts
1116
1164
  import { readFileSync as readFileSync3 } from "node:fs";
1117
- import { join as join4 } from "node:path";
1165
+ import { join as join5 } from "node:path";
1118
1166
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1119
1167
  var RULES_DIR = fileURLToPath3(new URL("../skills-plugin/rules", import.meta.url));
1120
1168
  function loadRule(name) {
1121
- const raw = readFileSync3(join4(RULES_DIR, `${name}.md`), "utf8");
1169
+ const raw = readFileSync3(join5(RULES_DIR, `${name}.md`), "utf8");
1122
1170
  return stripFrontMatter(raw).trim();
1123
1171
  }
1124
1172
  function stripFrontMatter(raw) {
@@ -1359,6 +1407,7 @@ async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
1359
1407
  const committed = await commitWithRepair(ctx, dir, abort);
1360
1408
  if (!committed) return { outcome: { kind: "none" }, autoMerged: false };
1361
1409
  let autoMerged = false;
1410
+ let conflictResolution;
1362
1411
  if (opts.rebase) {
1363
1412
  try {
1364
1413
  await rebaseOntoMergeBranch(ctx, dir);
@@ -1368,14 +1417,15 @@ async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
1368
1417
  console.warn(
1369
1418
  ` rebase onto ${ctx.repo.mergeBranch} conflicted \u2014 merging it in and resolving with the agent\u2026`
1370
1419
  );
1371
- await mergeAndResolveConflicts(ctx, dir, config, abort);
1420
+ const { report: mergeReport } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1421
+ conflictResolution = mergeReport?.conflictResolution;
1372
1422
  await commitWithRepair(ctx, dir, abort, { skipSocket: true });
1373
1423
  autoMerged = true;
1374
1424
  }
1375
1425
  }
1376
1426
  await pushBranch(ctx, dir);
1377
1427
  const pr = await openPullRequest(ctx);
1378
- return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged };
1428
+ return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged, conflictResolution };
1379
1429
  }
1380
1430
  async function mergeAndResolveConflicts(ctx, dir, config, abort) {
1381
1431
  const { conflicted } = await mergeInMergeBranch(ctx, dir);
@@ -1401,7 +1451,7 @@ async function mergeAndResolveConflicts(ctx, dir, config, abort) {
1401
1451
  `Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still contain conflict markers: ${unresolved.join(", ")}`
1402
1452
  );
1403
1453
  }
1404
- return { resolved: true, text: result.text.trim() || null };
1454
+ return { resolved: true, text: result.text.trim() || null, report: result.report ?? void 0 };
1405
1455
  }
1406
1456
  async function commitWithRepair(ctx, dir, abort, opts = {}) {
1407
1457
  for (let attempt = 1; ; attempt++) {
@@ -1531,7 +1581,7 @@ async function processChatJob(ctx, dir, config, abort) {
1531
1581
  console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1532
1582
  return { text: reply, widgets: result.widgets };
1533
1583
  }
1534
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1584
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1535
1585
  let documented = false;
1536
1586
  if (ctx.permissionMode !== "plan" && wikiExists && await hasChanges(dir)) {
1537
1587
  try {
@@ -1620,7 +1670,7 @@ ${reply}`;
1620
1670
 
1621
1671
  > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
1622
1672
  }
1623
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1673
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1624
1674
  let documented = false;
1625
1675
  if (wikiExists && await hasChanges(dir)) {
1626
1676
  try {
@@ -1640,12 +1690,13 @@ ${reply}`;
1640
1690
  } else if (!wikiExists) {
1641
1691
  console.log(` no .flumecode/wiki \u2014 skipping wiki reconcile for ${ctx.jobId}`);
1642
1692
  }
1643
- const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
1693
+ const { outcome, autoMerged, conflictResolution } = await pushAndOpenPr(ctx, dir, config, abort, {
1644
1694
  rebase: !resumed
1645
1695
  });
1646
1696
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1647
1697
  const lintPlugins = getSocketResults();
1648
- 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;
1649
1700
  return {
1650
1701
  text: reply,
1651
1702
  widgets: [],
@@ -1665,8 +1716,8 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1665
1716
  maxTurns: ORCHESTRATOR_MAX_TURNS,
1666
1717
  abortController: abort
1667
1718
  });
1668
- const summary = result.text.trim();
1669
- 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)";
1670
1721
  if (result.plans?.length) reply = result.plans[0] ?? reply;
1671
1722
  if (installResult.status === "failed") {
1672
1723
  reply += `
@@ -1677,7 +1728,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1677
1728
  console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1678
1729
  return { text: reply, widgets: result.widgets };
1679
1730
  }
1680
- const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
1731
+ const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1681
1732
  let documented = false;
1682
1733
  if (wikiExists && await hasChanges(dir)) {
1683
1734
  try {
@@ -1697,15 +1748,19 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1697
1748
  } else if (!wikiExists) {
1698
1749
  console.log(` no .flumecode/wiki \u2014 skipping wiki reconcile for ${ctx.jobId}`);
1699
1750
  }
1700
- const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
1751
+ const { outcome, autoMerged, conflictResolution } = await pushAndOpenPr(ctx, dir, config, abort, {
1701
1752
  rebase: !resumed
1702
1753
  });
1703
1754
  if (outcome.kind !== "none") {
1704
1755
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1705
1756
  }
1757
+ const lintPlugins = getSocketResults();
1758
+ const reportWithConflict = report && conflictResolution ? { ...report, conflictResolution } : report;
1759
+ const finalReport = reportWithConflict && lintPlugins.length ? { ...reportWithConflict, lint: { plugins: lintPlugins } } : reportWithConflict;
1706
1760
  return {
1707
1761
  text: reply,
1708
1762
  widgets: [],
1763
+ ...finalReport ? { report: finalReport } : {},
1709
1764
  ...outcome.kind === "pr" ? { pr: outcome.pr } : {},
1710
1765
  ...result.plans?.length ? { plans: result.plans } : {}
1711
1766
  };
@@ -1714,8 +1769,13 @@ async function processResolveJob(ctx, dir, config, abort) {
1714
1769
  console.log(`
1715
1770
  \u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
1716
1771
  const installResult = await installDependencies(dir);
1717
- const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1718
- 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
+ }
1719
1779
  if (installResult.status === "failed") {
1720
1780
  reply += `
1721
1781
 
@@ -1727,7 +1787,7 @@ async function processResolveJob(ctx, dir, config, abort) {
1727
1787
  const pr = await openPullRequest(ctx);
1728
1788
  const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
1729
1789
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
1730
- return { text: reply, widgets: [], ...pr ? { pr } : {} };
1790
+ return { text: reply, widgets: [], ...report ? { report } : {}, ...pr ? { pr } : {} };
1731
1791
  }
1732
1792
  async function processReleaseJob(ctx, dir, resumed, config, abort) {
1733
1793
  console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.15.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": {
@@ -66,6 +66,7 @@ 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
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.
@@ -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.