@callumvass/forgeflow-dev 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/index.js +54 -13
- package/package.json +8 -2
- package/src/index.ts +0 -380
- package/src/pipelines/architecture.ts +0 -67
- package/src/pipelines/discover-skills.ts +0 -33
- package/src/pipelines/implement-all.ts +0 -181
- package/src/pipelines/implement.ts +0 -305
- package/src/pipelines/review.ts +0 -183
- package/src/resolve.ts +0 -6
- package/src/utils/exec.ts +0 -13
- package/src/utils/git.ts +0 -132
- package/src/utils/ui.ts +0 -29
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/tsup.config.ts +0 -15
package/extensions/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
+
// ../shared/dist/confluence.js
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
|
|
8
11
|
// ../shared/dist/constants.js
|
|
9
12
|
var TOOLS_ALL = ["read", "write", "edit", "bash", "grep", "find"];
|
|
10
13
|
var TOOLS_READONLY = ["read", "bash", "grep", "find"];
|
|
@@ -16,7 +19,7 @@ var SIGNALS = {
|
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
// ../shared/dist/run-agent.js
|
|
19
|
-
import { spawn } from "child_process";
|
|
22
|
+
import { spawn as spawn2 } from "child_process";
|
|
20
23
|
import * as fs from "fs";
|
|
21
24
|
import * as os from "os";
|
|
22
25
|
import * as path from "path";
|
|
@@ -108,7 +111,7 @@ async function runAgent(agentName, task, options) {
|
|
|
108
111
|
args.push(`Task: ${task}`);
|
|
109
112
|
const exitCode = await new Promise((resolve2) => {
|
|
110
113
|
const invocation = getPiInvocation(args);
|
|
111
|
-
const proc =
|
|
114
|
+
const proc = spawn2(invocation.command, invocation.args, {
|
|
112
115
|
cwd: options.cwd,
|
|
113
116
|
shell: false,
|
|
114
117
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -359,10 +362,10 @@ async function runDiscoverSkills(cwd, query, signal, onUpdate, _ctx) {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
// src/utils/exec.ts
|
|
362
|
-
import { spawn as
|
|
365
|
+
import { spawn as spawn3 } from "child_process";
|
|
363
366
|
function exec(cmd, cwd) {
|
|
364
367
|
return new Promise((resolve2) => {
|
|
365
|
-
const proc =
|
|
368
|
+
const proc = spawn3("bash", ["-c", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
366
369
|
let out = "";
|
|
367
370
|
proc.stdout.on("data", (d) => {
|
|
368
371
|
out += d.toString();
|
|
@@ -373,6 +376,31 @@ function exec(cmd, cwd) {
|
|
|
373
376
|
}
|
|
374
377
|
|
|
375
378
|
// src/utils/git.ts
|
|
379
|
+
import * as fs3 from "fs";
|
|
380
|
+
import * as path4 from "path";
|
|
381
|
+
var PR_TEMPLATE_PATHS = [
|
|
382
|
+
".github/pull_request_template.md",
|
|
383
|
+
".github/PULL_REQUEST_TEMPLATE.md",
|
|
384
|
+
"pull_request_template.md",
|
|
385
|
+
"PULL_REQUEST_TEMPLATE.md",
|
|
386
|
+
".github/PULL_REQUEST_TEMPLATE/pull_request_template.md"
|
|
387
|
+
];
|
|
388
|
+
function buildPrBody(cwd, issue) {
|
|
389
|
+
const isGitHub = issue.source === "github" && issue.number > 0;
|
|
390
|
+
const defaultBody = isGitHub ? `Closes #${issue.number}` : `Jira: ${issue.key}`;
|
|
391
|
+
for (const rel of PR_TEMPLATE_PATHS) {
|
|
392
|
+
const abs = path4.join(cwd, rel);
|
|
393
|
+
try {
|
|
394
|
+
const template = fs3.readFileSync(abs, "utf-8");
|
|
395
|
+
const closeRef = isGitHub ? `Closes #${issue.number}` : `Jira: ${issue.key}`;
|
|
396
|
+
return `${closeRef}
|
|
397
|
+
|
|
398
|
+
${template}`;
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return defaultBody;
|
|
403
|
+
}
|
|
376
404
|
function slugify(text, maxLen = 40) {
|
|
377
405
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLen).replace(/-$/, "");
|
|
378
406
|
}
|
|
@@ -599,6 +627,12 @@ async function runReview(cwd, target, signal, onUpdate, ctx, customPrompt) {
|
|
|
599
627
|
const pr = await exec("gh pr view --json number --jq .number 2>/dev/null", cwd);
|
|
600
628
|
if (pr && pr !== "") prNumber = pr;
|
|
601
629
|
}
|
|
630
|
+
if (ctx.hasUI && !customPrompt) {
|
|
631
|
+
const extra = await ctx.ui.input("Additional instructions?", "Skip");
|
|
632
|
+
if (extra != null && extra.trim() !== "") {
|
|
633
|
+
customPrompt = extra.trim();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
602
636
|
const result = await runReviewInline(cwd, signal, onUpdate, ctx, stages, diffCmd, "review", {
|
|
603
637
|
prNumber,
|
|
604
638
|
interactive: ctx.hasUI,
|
|
@@ -643,16 +677,17 @@ async function resolveQuestions(plan, ctx) {
|
|
|
643
677
|
const sectionMatch = plan.match(/### Unresolved Questions\n([\s\S]*?)(?=\n###|$)/);
|
|
644
678
|
if (!sectionMatch) return plan;
|
|
645
679
|
const section = sectionMatch[1] ?? "";
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
680
|
+
const itemRe = /^(?:[-*]|\d+[.)]+|[a-z][.)]+)\s+(.+(?:\n(?!(?:[-*]|\d+[.)]+|[a-z][.)]+)\s).*)*)/gm;
|
|
681
|
+
const items = [];
|
|
682
|
+
for (const m of section.matchAll(itemRe)) {
|
|
683
|
+
if (m[0] && m[1]) items.push({ full: m[0], text: m[1] });
|
|
649
684
|
}
|
|
650
|
-
if (
|
|
685
|
+
if (items.length === 0) return plan;
|
|
651
686
|
let updatedSection = section;
|
|
652
|
-
for (const
|
|
653
|
-
const answer = await ctx.ui.input(`${
|
|
687
|
+
for (const item of items) {
|
|
688
|
+
const answer = await ctx.ui.input(`${item.text}`, "Skip to use defaults");
|
|
654
689
|
if (answer != null && answer.trim() !== "") {
|
|
655
|
-
updatedSection = updatedSection.replace(
|
|
690
|
+
updatedSection = updatedSection.replace(item.full, `${item.full}
|
|
656
691
|
**Answer:** ${answer.trim()}`);
|
|
657
692
|
}
|
|
658
693
|
}
|
|
@@ -680,6 +715,12 @@ async function runImplement(cwd, issueArg, signal, onUpdate, ctx, flags = {
|
|
|
680
715
|
${resolved.body}` : `Jira ${resolved.key}: ${resolved.title}
|
|
681
716
|
|
|
682
717
|
${resolved.body}`;
|
|
718
|
+
if (interactive && !flags.customPrompt) {
|
|
719
|
+
const extra = await ctx.ui.input("Additional instructions?", "Skip");
|
|
720
|
+
if (extra != null && extra.trim() !== "") {
|
|
721
|
+
flags.customPrompt = extra.trim();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
683
724
|
const customPromptSection = flags.customPrompt ? `
|
|
684
725
|
|
|
685
726
|
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
@@ -704,7 +745,7 @@ ${flags.customPrompt}` : "";
|
|
|
704
745
|
const ahead = await exec(`git rev-list main..${resolved.branch} --count`, cwd);
|
|
705
746
|
if (parseInt(ahead, 10) > 0) {
|
|
706
747
|
await exec(`git push -u origin ${resolved.branch}`, cwd);
|
|
707
|
-
const prBody =
|
|
748
|
+
const prBody = buildPrBody(cwd, resolved);
|
|
708
749
|
await exec(`gh pr create --title "${resolved.title}" --body "${prBody}" --head ${resolved.branch}`, cwd);
|
|
709
750
|
const stages2 = [];
|
|
710
751
|
await refactorAndReview(cwd, signal, onUpdate, ctx, stages2, flags.skipReview);
|
|
@@ -831,7 +872,7 @@ ${reason}` }],
|
|
|
831
872
|
await exec(`git push -u origin ${resolved.branch}`, cwd);
|
|
832
873
|
prNumber = await exec(`gh pr list --head "${resolved.branch}" --json number --jq '.[0].number'`, cwd);
|
|
833
874
|
if (!prNumber || prNumber === "null") {
|
|
834
|
-
const prBody =
|
|
875
|
+
const prBody = buildPrBody(cwd, resolved);
|
|
835
876
|
await exec(`gh pr create --title "${resolved.title}" --body "${prBody}" --head ${resolved.branch}`, cwd);
|
|
836
877
|
prNumber = await exec(`gh pr list --head "${resolved.branch}" --json number --jq '.[0].number'`, cwd);
|
|
837
878
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callumvass/forgeflow-dev",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dev pipeline for Pi — TDD implementation, code review, architecture, and skill discovery.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package"
|
|
8
8
|
],
|
|
9
9
|
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/CallumVass/forgeflow#readme",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/
|
|
13
|
+
"url": "git+https://github.com/CallumVass/forgeflow.git",
|
|
13
14
|
"directory": "packages/dev"
|
|
14
15
|
},
|
|
15
16
|
"publishConfig": {
|
|
16
17
|
"provenance": true
|
|
17
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"extensions",
|
|
21
|
+
"agents",
|
|
22
|
+
"skills"
|
|
23
|
+
],
|
|
18
24
|
"pi": {
|
|
19
25
|
"extensions": [
|
|
20
26
|
"./extensions"
|
package/src/index.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import { type AnyCtx, getFinalOutput, type PipelineDetails, type StageResult } from "@callumvass/forgeflow-shared";
|
|
2
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
4
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import { runArchitecture } from "./pipelines/architecture.js";
|
|
6
|
-
import { runDiscoverSkills } from "./pipelines/discover-skills.js";
|
|
7
|
-
import { runImplement } from "./pipelines/implement.js";
|
|
8
|
-
import { runImplementAll } from "./pipelines/implement-all.js";
|
|
9
|
-
import { runReview } from "./pipelines/review.js";
|
|
10
|
-
|
|
11
|
-
// ─── Display helpers ──────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
14
|
-
|
|
15
|
-
interface ForgeflowDevInput {
|
|
16
|
-
pipeline: string;
|
|
17
|
-
issue?: string;
|
|
18
|
-
target?: string;
|
|
19
|
-
skipPlan?: boolean;
|
|
20
|
-
skipReview?: boolean;
|
|
21
|
-
customPrompt?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseImplFlags(args: string) {
|
|
25
|
-
const skipPlan = args.includes("--skip-plan");
|
|
26
|
-
const skipReview = args.includes("--skip-review");
|
|
27
|
-
const rest = args
|
|
28
|
-
.replace(/--skip-plan/g, "")
|
|
29
|
-
.replace(/--skip-review/g, "")
|
|
30
|
-
.trim();
|
|
31
|
-
|
|
32
|
-
const firstSpace = rest.indexOf(" ");
|
|
33
|
-
const issue = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
|
|
34
|
-
const customPrompt =
|
|
35
|
-
firstSpace === -1
|
|
36
|
-
? ""
|
|
37
|
-
: rest
|
|
38
|
-
.slice(firstSpace + 1)
|
|
39
|
-
.trim()
|
|
40
|
-
.replace(/^"(.*)"$/, "$1");
|
|
41
|
-
|
|
42
|
-
const flags = [skipPlan ? ", skipPlan: true" : "", skipReview ? ", skipReview: true" : ""].join("");
|
|
43
|
-
return { issue, customPrompt, flags };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseReviewArgs(args: string) {
|
|
47
|
-
const trimmed = args.trim();
|
|
48
|
-
if (!trimmed) return { target: "", customPrompt: "" };
|
|
49
|
-
|
|
50
|
-
if (trimmed.startsWith("--branch")) {
|
|
51
|
-
const afterFlag = trimmed.replace(/^--branch\s*/, "").trim();
|
|
52
|
-
const firstSpace = afterFlag.indexOf(" ");
|
|
53
|
-
if (firstSpace === -1) return { target: `--branch ${afterFlag}`, customPrompt: "" };
|
|
54
|
-
return {
|
|
55
|
-
target: `--branch ${afterFlag.slice(0, firstSpace)}`,
|
|
56
|
-
customPrompt: afterFlag
|
|
57
|
-
.slice(firstSpace + 1)
|
|
58
|
-
.trim()
|
|
59
|
-
.replace(/^"(.*)"$/, "$1"),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const firstSpace = trimmed.indexOf(" ");
|
|
64
|
-
if (firstSpace === -1) return { target: trimmed, customPrompt: "" };
|
|
65
|
-
return {
|
|
66
|
-
target: trimmed.slice(0, firstSpace),
|
|
67
|
-
customPrompt: trimmed
|
|
68
|
-
.slice(firstSpace + 1)
|
|
69
|
-
.trim()
|
|
70
|
-
.replace(/^"(.*)"$/, "$1"),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getDisplayItems(messages: AnyCtx[]): DisplayItem[] {
|
|
75
|
-
const items: DisplayItem[] = [];
|
|
76
|
-
for (const msg of messages) {
|
|
77
|
-
if (msg.role === "assistant") {
|
|
78
|
-
for (const part of msg.content) {
|
|
79
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
80
|
-
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return items;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function formatToolCallShort(
|
|
88
|
-
name: string,
|
|
89
|
-
args: Record<string, unknown>,
|
|
90
|
-
fg: (c: string, t: string) => string,
|
|
91
|
-
): string {
|
|
92
|
-
switch (name) {
|
|
93
|
-
case "bash": {
|
|
94
|
-
const cmd = (args.command as string) || "...";
|
|
95
|
-
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
96
|
-
}
|
|
97
|
-
case "read":
|
|
98
|
-
return fg("muted", "read ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
99
|
-
case "write":
|
|
100
|
-
return fg("muted", "write ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
101
|
-
case "edit":
|
|
102
|
-
return fg("muted", "edit ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
103
|
-
case "grep":
|
|
104
|
-
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
105
|
-
case "find":
|
|
106
|
-
return fg("muted", "find ") + fg("accent", (args.pattern || "*") as string);
|
|
107
|
-
default:
|
|
108
|
-
return fg("accent", name);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function formatUsage(usage: { input: number; output: number; cost: number; turns: number }, model?: string): string {
|
|
113
|
-
const parts: string[] = [];
|
|
114
|
-
if (usage.turns) parts.push(`${usage.turns}t`);
|
|
115
|
-
if (usage.input) parts.push(`↑${usage.input < 1000 ? usage.input : `${Math.round(usage.input / 1000)}k`}`);
|
|
116
|
-
if (usage.output) parts.push(`↓${usage.output < 1000 ? usage.output : `${Math.round(usage.output / 1000)}k`}`);
|
|
117
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
118
|
-
if (model) parts.push(model);
|
|
119
|
-
return parts.join(" ");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Tool registration ────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
const ForgeflowDevParams = Type.Object({
|
|
125
|
-
pipeline: Type.String({
|
|
126
|
-
description: 'Which pipeline to run: "implement", "implement-all", "review", "architecture", or "discover-skills"',
|
|
127
|
-
}),
|
|
128
|
-
issue: Type.Optional(
|
|
129
|
-
Type.String({
|
|
130
|
-
description: "Issue number or description for implement pipeline",
|
|
131
|
-
}),
|
|
132
|
-
),
|
|
133
|
-
target: Type.Optional(Type.String({ description: "PR number or --branch for review pipeline" })),
|
|
134
|
-
skipPlan: Type.Optional(Type.Boolean({ description: "Skip planner, implement directly (default false)" })),
|
|
135
|
-
skipReview: Type.Optional(Type.Boolean({ description: "Skip code review after implementation (default false)" })),
|
|
136
|
-
customPrompt: Type.Optional(
|
|
137
|
-
Type.String({ description: "Additional user instructions passed to agents (e.g. 'check the openapi spec')" }),
|
|
138
|
-
),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
function registerForgeflowDevTool(pi: ExtensionAPI) {
|
|
142
|
-
pi.registerTool({
|
|
143
|
-
name: "forgeflow-dev",
|
|
144
|
-
label: "Forgeflow Dev",
|
|
145
|
-
description: [
|
|
146
|
-
"Run forgeflow dev pipelines: implement (plan→TDD→refactor a single issue),",
|
|
147
|
-
"implement-all (loop through all open issues autonomously), review (deterministic checks→code review→judge),",
|
|
148
|
-
"architecture (analyze codebase for structural friction→create RFC issues),",
|
|
149
|
-
"discover-skills (find and install domain-specific plugins).",
|
|
150
|
-
"Each pipeline spawns specialized sub-agents with isolated context.",
|
|
151
|
-
].join(" "),
|
|
152
|
-
parameters: ForgeflowDevParams as AnyCtx,
|
|
153
|
-
|
|
154
|
-
async execute(
|
|
155
|
-
_toolCallId: string,
|
|
156
|
-
_params: unknown,
|
|
157
|
-
signal: AbortSignal | undefined,
|
|
158
|
-
onUpdate: AnyCtx,
|
|
159
|
-
ctx: AnyCtx,
|
|
160
|
-
) {
|
|
161
|
-
const params = _params as ForgeflowDevInput;
|
|
162
|
-
const cwd = ctx.cwd as string;
|
|
163
|
-
const sig = signal ?? new AbortController().signal;
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
switch (params.pipeline) {
|
|
167
|
-
case "implement":
|
|
168
|
-
return await runImplement(cwd, params.issue ?? "", sig, onUpdate, ctx, {
|
|
169
|
-
skipPlan: params.skipPlan ?? false,
|
|
170
|
-
skipReview: params.skipReview ?? false,
|
|
171
|
-
customPrompt: params.customPrompt,
|
|
172
|
-
});
|
|
173
|
-
case "implement-all":
|
|
174
|
-
return await runImplementAll(cwd, sig, onUpdate, ctx, {
|
|
175
|
-
skipPlan: params.skipPlan ?? false,
|
|
176
|
-
skipReview: params.skipReview ?? false,
|
|
177
|
-
});
|
|
178
|
-
case "review":
|
|
179
|
-
return await runReview(cwd, params.target ?? "", sig, onUpdate, ctx, params.customPrompt);
|
|
180
|
-
case "architecture":
|
|
181
|
-
return await runArchitecture(cwd, sig, onUpdate, ctx);
|
|
182
|
-
case "discover-skills":
|
|
183
|
-
return await runDiscoverSkills(cwd, params.issue ?? "", sig, onUpdate, ctx);
|
|
184
|
-
default:
|
|
185
|
-
return {
|
|
186
|
-
content: [
|
|
187
|
-
{
|
|
188
|
-
type: "text",
|
|
189
|
-
text: `Unknown pipeline: ${params.pipeline}. Use: implement, implement-all, review, architecture, discover-skills`,
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
details: { pipeline: params.pipeline, stages: [] } as PipelineDetails,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
} finally {
|
|
196
|
-
if (ctx.hasUI) {
|
|
197
|
-
ctx.ui.setStatus("forgeflow-dev", undefined);
|
|
198
|
-
ctx.ui.setWidget("forgeflow-dev", undefined);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
renderCall(_args: unknown, theme: AnyCtx) {
|
|
204
|
-
const args = _args as ForgeflowDevInput;
|
|
205
|
-
const pipeline = args.pipeline || "?";
|
|
206
|
-
let text = theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", pipeline);
|
|
207
|
-
if (args.issue) {
|
|
208
|
-
const prefix = /^[A-Z]+-\d+$/.test(args.issue) ? " " : " #";
|
|
209
|
-
text += theme.fg("dim", `${prefix}${args.issue}`);
|
|
210
|
-
}
|
|
211
|
-
if (args.target) text += theme.fg("dim", ` ${args.target}`);
|
|
212
|
-
return new Text(text, 0, 0);
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
renderResult(result: AnyCtx, { expanded }: { expanded: boolean }, theme: AnyCtx) {
|
|
216
|
-
const details = result.details as PipelineDetails | undefined;
|
|
217
|
-
if (!details || details.stages.length === 0) {
|
|
218
|
-
const text = result.content[0];
|
|
219
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (expanded) {
|
|
223
|
-
return renderExpanded(details, theme);
|
|
224
|
-
}
|
|
225
|
-
return renderCollapsed(details, theme);
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ─── Rendering ────────────────────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
function renderExpanded(details: PipelineDetails, theme: AnyCtx) {
|
|
233
|
-
const container = new Container();
|
|
234
|
-
container.addChild(
|
|
235
|
-
new Text(theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline), 0, 0),
|
|
236
|
-
);
|
|
237
|
-
container.addChild(new Spacer(1));
|
|
238
|
-
|
|
239
|
-
for (const stage of details.stages) {
|
|
240
|
-
const icon = stageIcon(stage, theme);
|
|
241
|
-
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
242
|
-
|
|
243
|
-
const items = getDisplayItems(stage.messages);
|
|
244
|
-
for (const item of items) {
|
|
245
|
-
if (item.type === "toolCall") {
|
|
246
|
-
container.addChild(
|
|
247
|
-
new Text(
|
|
248
|
-
` ${theme.fg("muted", "→ ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`,
|
|
249
|
-
0,
|
|
250
|
-
0,
|
|
251
|
-
),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const output = getFinalOutput(stage.messages);
|
|
257
|
-
if (output) {
|
|
258
|
-
container.addChild(new Spacer(1));
|
|
259
|
-
try {
|
|
260
|
-
const { getMarkdownTheme } = require("@mariozechner/pi-coding-agent");
|
|
261
|
-
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
262
|
-
} catch {
|
|
263
|
-
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
268
|
-
if (usageStr) container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
269
|
-
container.addChild(new Spacer(1));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return container;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function renderCollapsed(details: PipelineDetails, theme: AnyCtx) {
|
|
276
|
-
let text = theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline);
|
|
277
|
-
for (const stage of details.stages) {
|
|
278
|
-
const icon = stageIcon(stage, theme);
|
|
279
|
-
text += `\n ${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
280
|
-
|
|
281
|
-
if (stage.status === "running") {
|
|
282
|
-
const items = getDisplayItems(stage.messages);
|
|
283
|
-
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
284
|
-
for (const item of last) {
|
|
285
|
-
if (item.type === "toolCall") {
|
|
286
|
-
text += `\n ${theme.fg("muted", "→ ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
} else if (stage.status === "done" || stage.status === "failed") {
|
|
290
|
-
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
291
|
-
text += theme.fg("dim", ` ${preview}`);
|
|
292
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
293
|
-
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return new Text(text, 0, 0);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function stageIcon(stage: StageResult, theme: AnyCtx): string {
|
|
300
|
-
return stage.status === "done"
|
|
301
|
-
? theme.fg("success", "✓")
|
|
302
|
-
: stage.status === "running"
|
|
303
|
-
? theme.fg("warning", "⟳")
|
|
304
|
-
: stage.status === "failed"
|
|
305
|
-
? theme.fg("error", "✗")
|
|
306
|
-
: theme.fg("muted", "○");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ─── Extension entry point ────────────────────────────────────────────
|
|
310
|
-
|
|
311
|
-
const extension: (pi: ExtensionAPI) => void = (pi) => {
|
|
312
|
-
registerForgeflowDevTool(pi);
|
|
313
|
-
|
|
314
|
-
pi.registerCommand("implement", {
|
|
315
|
-
description:
|
|
316
|
-
"Implement a single issue using TDD. Usage: /implement <issue#|JIRA-KEY> [custom prompt] [--skip-plan] [--skip-review]",
|
|
317
|
-
handler: async (args) => {
|
|
318
|
-
const { issue, customPrompt, flags } = parseImplFlags(args);
|
|
319
|
-
const promptPart = customPrompt ? `, customPrompt: "${customPrompt}"` : "";
|
|
320
|
-
|
|
321
|
-
if (issue) {
|
|
322
|
-
pi.sendUserMessage(
|
|
323
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement", issue="${issue}"${promptPart}${flags}. Do not interpret the issue number — pass it as-is.`,
|
|
324
|
-
);
|
|
325
|
-
} else {
|
|
326
|
-
pi.sendUserMessage(
|
|
327
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement"${promptPart}${flags}. No issue number provided — the tool will detect it from the current branch. Do NOT ask for an issue number.`,
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
pi.registerCommand("implement-all", {
|
|
334
|
-
description:
|
|
335
|
-
"Loop through all open auto-generated issues: implement, review, merge. Flags: --skip-plan, --skip-review",
|
|
336
|
-
handler: async (args) => {
|
|
337
|
-
const { flags } = parseImplFlags(args);
|
|
338
|
-
|
|
339
|
-
pi.sendUserMessage(
|
|
340
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement-all"${flags}. Do NOT ask for confirmation — run autonomously.`,
|
|
341
|
-
);
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
pi.registerCommand("review", {
|
|
346
|
-
description: "Run code review: deterministic checks → reviewer → judge. Usage: /review [target] [custom prompt]",
|
|
347
|
-
handler: async (args) => {
|
|
348
|
-
const { target, customPrompt } = parseReviewArgs(args);
|
|
349
|
-
const promptPart = customPrompt ? `, customPrompt: "${customPrompt}"` : "";
|
|
350
|
-
pi.sendUserMessage(
|
|
351
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="review"${target ? `, target="${target}"` : ""}${promptPart}. Do not interpret the target — pass it as-is.`,
|
|
352
|
-
);
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
pi.registerCommand("architecture", {
|
|
357
|
-
description: "Analyze codebase for architectural friction and create RFC issues",
|
|
358
|
-
handler: async () => {
|
|
359
|
-
pi.sendUserMessage(`Call the forgeflow-dev tool now with these exact parameters: pipeline="architecture".`);
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
pi.registerCommand("discover-skills", {
|
|
364
|
-
description: "Find and install domain-specific plugins from skills.sh for this project's tech stack",
|
|
365
|
-
handler: async (args) => {
|
|
366
|
-
const query = args.trim();
|
|
367
|
-
if (query) {
|
|
368
|
-
pi.sendUserMessage(
|
|
369
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="discover-skills", issue="${query}". Present the tool's output verbatim — do not summarize or reformat it.`,
|
|
370
|
-
);
|
|
371
|
-
} else {
|
|
372
|
-
pi.sendUserMessage(
|
|
373
|
-
`Call the forgeflow-dev tool now with these exact parameters: pipeline="discover-skills". Present the tool's output verbatim — do not summarize or reformat it.`,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
});
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
export default extension;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { type AnyCtx, emptyStage, runAgent, TOOLS_READONLY } from "@callumvass/forgeflow-shared";
|
|
2
|
-
import { AGENTS_DIR } from "../resolve.js";
|
|
3
|
-
|
|
4
|
-
export async function runArchitecture(cwd: string, signal: AbortSignal, onUpdate: AnyCtx, ctx: AnyCtx) {
|
|
5
|
-
const stages = [emptyStage("architecture-reviewer")];
|
|
6
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "architecture", onUpdate };
|
|
7
|
-
|
|
8
|
-
// Phase 1: Explore codebase for friction
|
|
9
|
-
const exploreResult = await runAgent(
|
|
10
|
-
"architecture-reviewer",
|
|
11
|
-
"Explore this codebase and identify architectural friction. Present numbered candidates ranked by severity.",
|
|
12
|
-
{ ...opts, tools: TOOLS_READONLY },
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
if (exploreResult.status === "failed") {
|
|
16
|
-
return {
|
|
17
|
-
content: [{ type: "text" as const, text: `Exploration failed: ${exploreResult.output}` }],
|
|
18
|
-
details: { pipeline: "architecture", stages },
|
|
19
|
-
isError: true,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Non-interactive: return candidates
|
|
24
|
-
if (!ctx.hasUI) {
|
|
25
|
-
return {
|
|
26
|
-
content: [{ type: "text" as const, text: exploreResult.output }],
|
|
27
|
-
details: { pipeline: "architecture", stages },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Interactive gate: user reviews candidates, can edit/annotate
|
|
32
|
-
const edited = await ctx.ui.editor(
|
|
33
|
-
"Review architecture candidates (edit to highlight your pick)",
|
|
34
|
-
exploreResult.output,
|
|
35
|
-
);
|
|
36
|
-
const action = await ctx.ui.select("Create RFC issue for a candidate?", ["Yes — generate RFC", "Skip"]);
|
|
37
|
-
|
|
38
|
-
if (action === "Skip" || action == null) {
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: "text" as const, text: "Architecture review complete. No RFC created." }],
|
|
41
|
-
details: { pipeline: "architecture", stages },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Phase 2: Generate RFC and create GitHub issue
|
|
46
|
-
stages.push(emptyStage("architecture-rfc"));
|
|
47
|
-
const candidateContext = edited ?? exploreResult.output;
|
|
48
|
-
|
|
49
|
-
const rfcResult = await runAgent(
|
|
50
|
-
"architecture-reviewer",
|
|
51
|
-
`Based on the following architectural analysis, generate a detailed RFC and create a GitHub issue (with label "architecture") for the highest-priority candidate — or the one the user highlighted/edited.\n\nANALYSIS:\n${candidateContext}`,
|
|
52
|
-
{ ...opts, tools: TOOLS_READONLY },
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
// Extract issue URL/number from agent output
|
|
56
|
-
const issueMatch = rfcResult.output?.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
57
|
-
const issueNum = issueMatch?.[1];
|
|
58
|
-
const issueUrl = issueMatch?.[0];
|
|
59
|
-
const summary = issueUrl
|
|
60
|
-
? `Architecture RFC issue created: ${issueUrl}\n\nRun \`/implement ${issueNum}\` to implement it.`
|
|
61
|
-
: "Architecture RFC issue created.";
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
content: [{ type: "text" as const, text: summary }],
|
|
65
|
-
details: { pipeline: "architecture", stages },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { type AnyCtx, emptyStage, runAgent, TOOLS_ALL, TOOLS_NO_EDIT } from "@callumvass/forgeflow-shared";
|
|
2
|
-
import { AGENTS_DIR } from "../resolve.js";
|
|
3
|
-
|
|
4
|
-
export async function runDiscoverSkills(
|
|
5
|
-
cwd: string,
|
|
6
|
-
query: string,
|
|
7
|
-
signal: AbortSignal,
|
|
8
|
-
onUpdate: AnyCtx,
|
|
9
|
-
_ctx: AnyCtx,
|
|
10
|
-
) {
|
|
11
|
-
// If query looks like specific skill names (contains commas or known skill identifiers),
|
|
12
|
-
// treat as install mode. Otherwise, discover mode.
|
|
13
|
-
const isInstall = query.includes(",") || query.includes("/");
|
|
14
|
-
|
|
15
|
-
const stages = [emptyStage("skill-discoverer")];
|
|
16
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "discover-skills", onUpdate };
|
|
17
|
-
|
|
18
|
-
const task = isInstall
|
|
19
|
-
? `Install these skills as forgeflow plugins: ${query}`
|
|
20
|
-
: query
|
|
21
|
-
? `Discover skills related to "${query}" — recommend only, do NOT install.`
|
|
22
|
-
: "Analyze the project tech stack and discover relevant skills — recommend only, do NOT install.";
|
|
23
|
-
|
|
24
|
-
// Install mode needs write access, discover mode is read-only
|
|
25
|
-
const tools = isInstall ? TOOLS_ALL : TOOLS_NO_EDIT;
|
|
26
|
-
|
|
27
|
-
const result = await runAgent("skill-discoverer", task, { ...opts, tools });
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
content: [{ type: "text" as const, text: result.output || "No skills found." }],
|
|
31
|
-
details: { pipeline: "discover-skills", stages },
|
|
32
|
-
};
|
|
33
|
-
}
|