@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 +94 -34
- package/package.json +1 -1
- package/skills-plugin/skills/request-to-plan/SKILL.md +1 -0
- package/skills-plugin/skills/resolve-merge-conflict/SKILL.md +13 -5
- package/skills-plugin/skills/revise-implementation/SKILL.md +8 -8
- package/skills-plugin/skills/unit-test-plugin-generator/SKILL.md +74 -0
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
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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).
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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(
|
|
970
|
+
lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
|
|
932
971
|
lines2.push("");
|
|
933
|
-
lines2.push(
|
|
934
|
-
|
|
935
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1669
|
-
let 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(
|
|
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
|
|
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
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
pull-request link
|
|
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.
|