@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.
@@ -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 = spawn(invocation.command, invocation.args, {
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 spawn2 } from "child_process";
365
+ import { spawn as spawn3 } from "child_process";
363
366
  function exec(cmd, cwd) {
364
367
  return new Promise((resolve2) => {
365
- const proc = spawn2("bash", ["-c", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"] });
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 questions = [];
647
- for (const m of section.matchAll(/^- (.+)$/gm)) {
648
- if (m[1]) questions.push(m[1]);
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 (questions.length === 0) return plan;
685
+ if (items.length === 0) return plan;
651
686
  let updatedSection = section;
652
- for (const q of questions) {
653
- const answer = await ctx.ui.input(`${q}`, "Skip to use defaults");
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(`- ${q}`, `- ${q}
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 = isGitHub ? `Closes #${resolved.number}` : `Jira: ${resolved.key}`;
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 = isGitHub ? `Closes #${resolved.number}` : `Jira: ${resolved.key}`;
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.0",
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/callumvass/forgeflow.git",
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
- }