@callumvass/forgeflow-pm 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/agents/{issue-creator.md → gh-issue-creator.md} +1 -1
- package/agents/{single-issue-creator.md → gh-single-issue-creator.md} +1 -1
- package/agents/investigator.md +32 -0
- package/agents/jira-issue-creator.md +43 -0
- package/extensions/index.js +251 -25
- package/package.json +8 -2
- package/skills/writing-style/SKILL.md +33 -0
- package/src/index.ts +0 -280
- package/src/pipelines/continue.ts +0 -138
- package/src/pipelines/create-issues.ts +0 -45
- package/src/pipelines/prd-qa.ts +0 -88
- package/src/resolve.ts +0 -6
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/tsup.config.ts +0 -15
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: investigator
|
|
3
|
+
description: Explores codebases and produces spikes or RFCs using a provided template.
|
|
4
|
+
tools: read, write, edit, bash, grep, find
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are an investigator agent. You explore codebases, research approaches, and produce structured technical documents (spikes, RFCs, or similar) using a provided template.
|
|
8
|
+
|
|
9
|
+
## Workflow
|
|
10
|
+
|
|
11
|
+
1. **Read the template** provided in your task. This defines the output structure. Keep every section heading from the template.
|
|
12
|
+
2. **Read the writing-style skill** and follow it exactly.
|
|
13
|
+
3. **Explore the codebase** thoroughly: file structure, key modules, existing patterns, dependencies, tests, config.
|
|
14
|
+
4. **Research externally** if the task involves new libraries, services, or approaches:
|
|
15
|
+
- Check what dependencies already exist in the project (package.json, go.mod, etc.)
|
|
16
|
+
- Use `bash` to search the web via `curl` for library comparisons, docs, or alternatives when needed.
|
|
17
|
+
5. **Fill in the template** with your findings. Every section must have substance or be explicitly marked N/A.
|
|
18
|
+
6. **Write the output** as a markdown file in the project root (e.g. `SPIKE-<topic>.md` or `RFC-<topic>.md`, matching the template type).
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
- Follow the template structure exactly. Do not add or remove sections.
|
|
23
|
+
- If the template has placeholder text or instructions in sections, replace them entirely with your findings.
|
|
24
|
+
- Be specific to this codebase. Reference actual file paths, modules, and patterns you found.
|
|
25
|
+
- When comparing approaches, use a table with clear criteria.
|
|
26
|
+
- When recommending libraries, include: name, what it does, monthly downloads or GitHub stars, last release date, and why it fits (or doesn't).
|
|
27
|
+
- If you cannot determine something from the codebase or public information, say so plainly. Do not speculate.
|
|
28
|
+
- Keep the total document under 200 lines unless the template demands more.
|
|
29
|
+
|
|
30
|
+
## Confluence Pages
|
|
31
|
+
|
|
32
|
+
If your task includes Confluence page content (template or reference docs), it has already been fetched for you. Use it directly.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jira-issue-creator
|
|
3
|
+
description: Decomposes PM documents into Jira issues matching a team's ticket format.
|
|
4
|
+
tools: read, write, bash, grep, find
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a Jira issue creator agent. You read PM documents and decompose them into well-structured Jira issues.
|
|
8
|
+
|
|
9
|
+
## Workflow
|
|
10
|
+
|
|
11
|
+
1. **Read the writing-style skill** and follow it exactly.
|
|
12
|
+
2. **Read the example ticket** provided in your task. This defines the structure, tone, and level of detail your issues must match. Study it carefully: heading style, section names, how acceptance criteria are written, how technical detail is balanced.
|
|
13
|
+
3. **Read all PM documents** provided. Understand the full scope.
|
|
14
|
+
4. **Explore the codebase** to understand what exists, what needs changing, and where the boundaries are.
|
|
15
|
+
5. **Decompose into vertical-slice issues.** Each issue must be a complete user-observable flow, not a layer (see rules below).
|
|
16
|
+
6. **Create the issues** using `jira issue create` CLI.
|
|
17
|
+
|
|
18
|
+
## Vertical Slice Rules
|
|
19
|
+
|
|
20
|
+
Each issue must:
|
|
21
|
+
- Cross all necessary layers (DB, server, client, UI) to deliver one user-observable behaviour.
|
|
22
|
+
- Be independently testable and deployable.
|
|
23
|
+
- Include acceptance criteria describing what the user sees, not what the code does.
|
|
24
|
+
|
|
25
|
+
Do NOT create:
|
|
26
|
+
- Layer-only issues ("build the API", "add the schema", "create the component").
|
|
27
|
+
- Issues that only make sense when combined with another issue.
|
|
28
|
+
|
|
29
|
+
## Issue Format
|
|
30
|
+
|
|
31
|
+
Match the example ticket's format exactly. If the example has sections like Description, Acceptance Criteria, Technical Notes, follow that structure. If it uses a different convention, follow that instead.
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- Order issues by dependency. If issue B depends on A, say so in B's description.
|
|
36
|
+
- Keep issue count reasonable: 3-8 issues for a typical feature. If you're above 10, you're slicing too thin.
|
|
37
|
+
- Title format: match the example ticket. If it uses imperative ("Add filtering to dashboard"), follow that.
|
|
38
|
+
- Do not invent requirements not present in the PM documents. If something is ambiguous, note it in the issue.
|
|
39
|
+
- Use `jira issue create` to create each issue. Use `--type Story` unless the example ticket indicates otherwise.
|
|
40
|
+
|
|
41
|
+
## Confluence Pages
|
|
42
|
+
|
|
43
|
+
If your task includes Confluence page content (PM docs or example tickets), it has already been fetched for you. Use it directly.
|
package/extensions/index.js
CHANGED
|
@@ -5,6 +5,61 @@ 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
|
+
function execCmd(cmd) {
|
|
11
|
+
return new Promise((resolve2) => {
|
|
12
|
+
const proc = spawn("bash", ["-c", cmd], { stdio: ["ignore", "pipe", "pipe"] });
|
|
13
|
+
let out = "";
|
|
14
|
+
proc.stdout.on("data", (d) => {
|
|
15
|
+
out += d.toString();
|
|
16
|
+
});
|
|
17
|
+
proc.on("close", () => resolve2(out.trim()));
|
|
18
|
+
proc.on("error", () => resolve2(""));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function extractPageId(url) {
|
|
22
|
+
const idMatch = url.match(/\/pages\/(\d+)/);
|
|
23
|
+
if (idMatch)
|
|
24
|
+
return idMatch[1] ?? null;
|
|
25
|
+
const paramMatch = url.match(/pageId=(\d+)/);
|
|
26
|
+
if (paramMatch)
|
|
27
|
+
return paramMatch[1] ?? null;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function htmlToPlainText(html) {
|
|
31
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/li>/gi, "\n").replace(/<li[^>]*>/gi, "- ").replace(/<\/h[1-6]>/gi, "\n\n").replace(/<h([1-6])[^>]*>/gi, (_m, level) => "#".repeat(parseInt(level, 10)) + " ").replace(/<\/?strong>/gi, "**").replace(/<\/?em>/gi, "*").replace(/<\/?code>/gi, "`").replace(/<ac:structured-macro[^>]*ac:name="code"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/gi, "\n```\n$1\n```\n").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
32
|
+
}
|
|
33
|
+
async function fetchConfluencePage(pageUrl) {
|
|
34
|
+
const baseUrl = process.env.CONFLUENCE_URL;
|
|
35
|
+
const email = process.env.CONFLUENCE_EMAIL;
|
|
36
|
+
const token = process.env.CONFLUENCE_TOKEN;
|
|
37
|
+
if (!baseUrl || !email || !token) {
|
|
38
|
+
return "Missing Confluence env vars. Set CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_TOKEN.";
|
|
39
|
+
}
|
|
40
|
+
const pageId = extractPageId(pageUrl);
|
|
41
|
+
if (!pageId) {
|
|
42
|
+
return `Could not extract page ID from URL: ${pageUrl}`;
|
|
43
|
+
}
|
|
44
|
+
const auth = Buffer.from(`${email}:${token}`).toString("base64");
|
|
45
|
+
const apiUrl = `${baseUrl.replace(/\/$/, "")}/wiki/api/v2/pages/${pageId}?body-format=storage`;
|
|
46
|
+
const raw = await execCmd(`curl -s -H "Authorization: Basic ${auth}" -H "Accept: application/json" "${apiUrl}"`);
|
|
47
|
+
if (!raw)
|
|
48
|
+
return `Failed to fetch Confluence page ${pageId}.`;
|
|
49
|
+
let data;
|
|
50
|
+
try {
|
|
51
|
+
data = JSON.parse(raw);
|
|
52
|
+
} catch {
|
|
53
|
+
return `Could not parse Confluence response for page ${pageId}.`;
|
|
54
|
+
}
|
|
55
|
+
const html = data.body?.storage?.value ?? "";
|
|
56
|
+
return {
|
|
57
|
+
id: data.id ?? pageId,
|
|
58
|
+
title: data.title ?? "Untitled",
|
|
59
|
+
body: htmlToPlainText(html)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
8
63
|
// ../shared/dist/constants.js
|
|
9
64
|
var TOOLS_ALL = ["read", "write", "edit", "bash", "grep", "find"];
|
|
10
65
|
var TOOLS_NO_EDIT = ["read", "write", "bash", "grep", "find"];
|
|
@@ -15,7 +70,7 @@ var SIGNALS = {
|
|
|
15
70
|
};
|
|
16
71
|
|
|
17
72
|
// ../shared/dist/run-agent.js
|
|
18
|
-
import { spawn } from "child_process";
|
|
73
|
+
import { spawn as spawn2 } from "child_process";
|
|
19
74
|
import * as fs from "fs";
|
|
20
75
|
import * as os from "os";
|
|
21
76
|
import * as path from "path";
|
|
@@ -95,7 +150,7 @@ async function runAgent(agentName, task, options) {
|
|
|
95
150
|
args.push(`Task: ${task}`);
|
|
96
151
|
const exitCode = await new Promise((resolve2) => {
|
|
97
152
|
const invocation = getPiInvocation(args);
|
|
98
|
-
const proc =
|
|
153
|
+
const proc = spawn2(invocation.command, invocation.args, {
|
|
99
154
|
cwd: options.cwd,
|
|
100
155
|
shell: false,
|
|
101
156
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -358,9 +413,9 @@ Stderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
|
358
413
|
if (action === "Accept PRD" || action == null) break;
|
|
359
414
|
}
|
|
360
415
|
}
|
|
361
|
-
stages.push(emptyStage("issue-creator"));
|
|
416
|
+
stages.push(emptyStage("gh-issue-creator"));
|
|
362
417
|
await runAgent(
|
|
363
|
-
"issue-creator",
|
|
418
|
+
"gh-issue-creator",
|
|
364
419
|
"Decompose PRD.md into vertical-slice GitHub issues. Focus on the ## Next section \u2014 the ## Done section is context only. Read the issue-template skill for the standard format.",
|
|
365
420
|
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
366
421
|
);
|
|
@@ -372,16 +427,20 @@ Stderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
|
372
427
|
|
|
373
428
|
// src/pipelines/create-issues.ts
|
|
374
429
|
import * as fs4 from "fs";
|
|
375
|
-
async function runCreateIssue(cwd, idea, signal, onUpdate,
|
|
430
|
+
async function runCreateIssue(cwd, idea, signal, onUpdate, ctx) {
|
|
431
|
+
if (!idea && ctx.hasUI) {
|
|
432
|
+
const input = await ctx.ui.input("Feature idea?", "");
|
|
433
|
+
idea = input?.trim() ?? "";
|
|
434
|
+
}
|
|
376
435
|
if (!idea) {
|
|
377
436
|
return {
|
|
378
437
|
content: [{ type: "text", text: "No feature idea provided." }],
|
|
379
438
|
details: { pipeline: "create-issue", stages: [] }
|
|
380
439
|
};
|
|
381
440
|
}
|
|
382
|
-
const stages = [emptyStage("single-issue-creator")];
|
|
441
|
+
const stages = [emptyStage("gh-single-issue-creator")];
|
|
383
442
|
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issue", onUpdate };
|
|
384
|
-
await runAgent("single-issue-creator", idea, { ...opts, tools: TOOLS_NO_EDIT });
|
|
443
|
+
await runAgent("gh-single-issue-creator", idea, { ...opts, tools: TOOLS_NO_EDIT });
|
|
385
444
|
return {
|
|
386
445
|
content: [{ type: "text", text: "Issue created." }],
|
|
387
446
|
details: { pipeline: "create-issue", stages }
|
|
@@ -394,10 +453,10 @@ async function runCreateIssues(cwd, signal, onUpdate, _ctx) {
|
|
|
394
453
|
details: { pipeline: "create-issues", stages: [] }
|
|
395
454
|
};
|
|
396
455
|
}
|
|
397
|
-
const stages = [emptyStage("issue-creator")];
|
|
456
|
+
const stages = [emptyStage("gh-issue-creator")];
|
|
398
457
|
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issues", onUpdate };
|
|
399
458
|
await runAgent(
|
|
400
|
-
"issue-creator",
|
|
459
|
+
"gh-issue-creator",
|
|
401
460
|
"Decompose PRD.md into vertical-slice GitHub issues. Read the issue-template skill for the standard format.",
|
|
402
461
|
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
403
462
|
);
|
|
@@ -407,6 +466,128 @@ async function runCreateIssues(cwd, signal, onUpdate, _ctx) {
|
|
|
407
466
|
};
|
|
408
467
|
}
|
|
409
468
|
|
|
469
|
+
// src/pipelines/investigate.ts
|
|
470
|
+
async function runInvestigate(cwd, description, templateUrl, signal, onUpdate, ctx) {
|
|
471
|
+
const interactive = ctx.hasUI;
|
|
472
|
+
if (!description && interactive) {
|
|
473
|
+
const input = await ctx.ui.input("What should we investigate?", "");
|
|
474
|
+
description = input?.trim() ?? "";
|
|
475
|
+
}
|
|
476
|
+
if (!description) {
|
|
477
|
+
return {
|
|
478
|
+
content: [{ type: "text", text: "No description provided." }],
|
|
479
|
+
details: { pipeline: "investigate", stages: [] }
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (!templateUrl && interactive) {
|
|
483
|
+
const input = await ctx.ui.input("Confluence template URL?", "Skip");
|
|
484
|
+
if (input != null && input.trim() !== "") {
|
|
485
|
+
templateUrl = input.trim();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let templateSection = "";
|
|
489
|
+
if (templateUrl) {
|
|
490
|
+
const result = await fetchConfluencePage(templateUrl);
|
|
491
|
+
if (typeof result === "string") {
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: "text", text: result }],
|
|
494
|
+
details: { pipeline: "investigate", stages: [] },
|
|
495
|
+
isError: true
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const page = result;
|
|
499
|
+
templateSection = `
|
|
500
|
+
|
|
501
|
+
TEMPLATE (from Confluence page "${page.title}"):
|
|
502
|
+
|
|
503
|
+
${page.body}`;
|
|
504
|
+
}
|
|
505
|
+
const stages = [emptyStage("investigator")];
|
|
506
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "investigate", onUpdate };
|
|
507
|
+
const task = `Investigate the following and produce a document using the template provided.
|
|
508
|
+
|
|
509
|
+
TOPIC: ${description}${templateSection}
|
|
510
|
+
|
|
511
|
+
${!templateUrl ? "No template was provided. Structure your output as: Problem, Context, Options (with comparison table), Recommendation, Next Steps." : ""}
|
|
512
|
+
|
|
513
|
+
Read the writing-style skill before writing.`;
|
|
514
|
+
await runAgent("investigator", task, { ...opts, tools: TOOLS_ALL });
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: "text", text: "Investigation complete." }],
|
|
517
|
+
details: { pipeline: "investigate", stages }
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/pipelines/jira-issues.ts
|
|
522
|
+
async function runJiraIssues(cwd, docUrls, exampleUrl, signal, onUpdate, ctx) {
|
|
523
|
+
const interactive = ctx.hasUI;
|
|
524
|
+
if (docUrls.length === 0 && interactive) {
|
|
525
|
+
const input = await ctx.ui.input("Confluence doc URL(s)?", "Space-separated");
|
|
526
|
+
if (input != null && input.trim() !== "") {
|
|
527
|
+
docUrls = input.trim().split(/\s+/).filter(Boolean);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (docUrls.length === 0) {
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text: "No document URLs provided." }],
|
|
533
|
+
details: { pipeline: "jira-issues", stages: [] }
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (!exampleUrl && interactive) {
|
|
537
|
+
const input = await ctx.ui.input("Example ticket URL?", "Skip");
|
|
538
|
+
if (input != null && input.trim() !== "") {
|
|
539
|
+
exampleUrl = input.trim();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const docs = [];
|
|
543
|
+
for (const url of docUrls) {
|
|
544
|
+
const result = await fetchConfluencePage(url);
|
|
545
|
+
if (typeof result === "string") {
|
|
546
|
+
return {
|
|
547
|
+
content: [{ type: "text", text: `Failed to fetch doc: ${result}` }],
|
|
548
|
+
details: { pipeline: "jira-issues", stages: [] },
|
|
549
|
+
isError: true
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
docs.push(result);
|
|
553
|
+
}
|
|
554
|
+
let exampleSection = "";
|
|
555
|
+
if (exampleUrl) {
|
|
556
|
+
const result = await fetchConfluencePage(exampleUrl);
|
|
557
|
+
if (typeof result === "string") {
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: "text", text: `Failed to fetch example: ${result}` }],
|
|
560
|
+
details: { pipeline: "jira-issues", stages: [] },
|
|
561
|
+
isError: true
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const page = result;
|
|
565
|
+
exampleSection = `
|
|
566
|
+
|
|
567
|
+
EXAMPLE TICKET (match this format):
|
|
568
|
+
Title: ${page.title}
|
|
569
|
+
|
|
570
|
+
${page.body}`;
|
|
571
|
+
}
|
|
572
|
+
const docSections = docs.map((d, i) => `DOCUMENT ${i + 1}: "${d.title}"
|
|
573
|
+
|
|
574
|
+
${d.body}`).join("\n\n---\n\n");
|
|
575
|
+
const stages = [emptyStage("jira-issue-creator")];
|
|
576
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "jira-issues", onUpdate };
|
|
577
|
+
const task = `Decompose the following PM documents into vertical-slice Jira issues.
|
|
578
|
+
|
|
579
|
+
${docSections}${exampleSection}
|
|
580
|
+
|
|
581
|
+
${!exampleUrl ? "No example ticket was provided. Use standard format: Summary, Description, Acceptance Criteria." : ""}
|
|
582
|
+
|
|
583
|
+
Read the writing-style skill before writing any issue content.`;
|
|
584
|
+
await runAgent("jira-issue-creator", task, { ...opts, tools: TOOLS_ALL });
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: "text", text: "Jira issue creation complete." }],
|
|
587
|
+
details: { pipeline: "jira-issues", stages }
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
410
591
|
// src/pipelines/prd-qa.ts
|
|
411
592
|
import * as fs5 from "fs";
|
|
412
593
|
async function runPrdQa(cwd, maxIterations, signal, onUpdate, ctx) {
|
|
@@ -435,7 +616,7 @@ Stderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
|
435
616
|
};
|
|
436
617
|
}
|
|
437
618
|
return {
|
|
438
|
-
content: [{ type: "text", text: "PRD refinement complete. Ready for /create-issues." }],
|
|
619
|
+
content: [{ type: "text", text: "PRD refinement complete. Ready for /create-gh-issues." }],
|
|
439
620
|
details: { pipeline: "prd-qa", stages }
|
|
440
621
|
};
|
|
441
622
|
}
|
|
@@ -516,10 +697,15 @@ function formatUsage(usage, model) {
|
|
|
516
697
|
}
|
|
517
698
|
var ForgeflowPmParams = Type.Object({
|
|
518
699
|
pipeline: Type.String({
|
|
519
|
-
description: 'Which pipeline to run: "continue", "prd-qa", "create-issues",
|
|
700
|
+
description: 'Which pipeline to run: "continue", "prd-qa", "create-gh-issues", "create-gh-issue", "investigate", or "jira-issues"'
|
|
520
701
|
}),
|
|
521
702
|
maxIterations: Type.Optional(Type.Number({ description: "Max iterations for prd-qa (default 10)" })),
|
|
522
|
-
issue: Type.Optional(
|
|
703
|
+
issue: Type.Optional(
|
|
704
|
+
Type.String({ description: "Feature idea for create-gh-issue, description for continue/investigate" })
|
|
705
|
+
),
|
|
706
|
+
template: Type.Optional(Type.String({ description: "Confluence URL for a template (investigate)" })),
|
|
707
|
+
docs: Type.Optional(Type.String({ description: "Comma-separated Confluence URLs for PM documents (jira-issues)" })),
|
|
708
|
+
example: Type.Optional(Type.String({ description: "Confluence/Jira URL for an example ticket (jira-issues)" }))
|
|
523
709
|
});
|
|
524
710
|
function registerForgeflowPmTool(pi) {
|
|
525
711
|
pi.registerTool({
|
|
@@ -527,8 +713,10 @@ function registerForgeflowPmTool(pi) {
|
|
|
527
713
|
label: "Forgeflow PM",
|
|
528
714
|
description: [
|
|
529
715
|
"Run forgeflow PM pipelines: continue (update PRD Done/Next\u2192QA\u2192create issues for next phase),",
|
|
530
|
-
"prd-qa (refine PRD), create-issues (decompose PRD into GitHub issues),",
|
|
531
|
-
"create-issue (single issue from a feature idea)
|
|
716
|
+
"prd-qa (refine PRD), create-gh-issues (decompose PRD into GitHub issues),",
|
|
717
|
+
"create-gh-issue (single issue from a feature idea),",
|
|
718
|
+
"investigate (spike/RFC using codebase exploration + optional Confluence template),",
|
|
719
|
+
"jira-issues (decompose Confluence PM docs into Jira issues).",
|
|
532
720
|
"Each pipeline spawns specialized sub-agents with isolated context."
|
|
533
721
|
].join(" "),
|
|
534
722
|
parameters: ForgeflowPmParams,
|
|
@@ -542,16 +730,22 @@ function registerForgeflowPmTool(pi) {
|
|
|
542
730
|
return await runContinue(cwd, params.issue ?? "", params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
543
731
|
case "prd-qa":
|
|
544
732
|
return await runPrdQa(cwd, params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
545
|
-
case "create-issues":
|
|
733
|
+
case "create-gh-issues":
|
|
546
734
|
return await runCreateIssues(cwd, sig, onUpdate, ctx);
|
|
547
|
-
case "create-issue":
|
|
735
|
+
case "create-gh-issue":
|
|
548
736
|
return await runCreateIssue(cwd, params.issue ?? "", sig, onUpdate, ctx);
|
|
737
|
+
case "investigate":
|
|
738
|
+
return await runInvestigate(cwd, params.issue ?? "", params.template ?? "", sig, onUpdate, ctx);
|
|
739
|
+
case "jira-issues": {
|
|
740
|
+
const docUrls = (params.docs ?? "").split(",").map((u) => u.trim()).filter(Boolean);
|
|
741
|
+
return await runJiraIssues(cwd, docUrls, params.example ?? "", sig, onUpdate, ctx);
|
|
742
|
+
}
|
|
549
743
|
default:
|
|
550
744
|
return {
|
|
551
745
|
content: [
|
|
552
746
|
{
|
|
553
747
|
type: "text",
|
|
554
|
-
text: `Unknown pipeline: ${params.pipeline}. Use: continue, prd-qa, create-issues, create-issue`
|
|
748
|
+
text: `Unknown pipeline: ${params.pipeline}. Use: continue, prd-qa, create-gh-issues, create-gh-issue, investigate, jira-issues`
|
|
555
749
|
}
|
|
556
750
|
],
|
|
557
751
|
details: { pipeline: params.pipeline, stages: [] }
|
|
@@ -649,6 +843,19 @@ function renderCollapsed(details, theme) {
|
|
|
649
843
|
function stageIcon(stage, theme) {
|
|
650
844
|
return stage.status === "done" ? theme.fg("success", "\u2713") : stage.status === "running" ? theme.fg("warning", "\u27F3") : stage.status === "failed" ? theme.fg("error", "\u2717") : theme.fg("muted", "\u25CB");
|
|
651
845
|
}
|
|
846
|
+
function parseInvestigateArgs(args) {
|
|
847
|
+
const templateMatch = args.match(/--template\s+(\S+)/);
|
|
848
|
+
const template = templateMatch ? templateMatch[1] ?? "" : "";
|
|
849
|
+
const description = args.replace(/--template\s+\S+/, "").trim().replace(/^"(.*)"$/, "$1");
|
|
850
|
+
return { description, template };
|
|
851
|
+
}
|
|
852
|
+
function parseJiraIssuesArgs(args) {
|
|
853
|
+
const exampleMatch = args.match(/--example\s+(\S+)/);
|
|
854
|
+
const example = exampleMatch ? exampleMatch[1] ?? "" : "";
|
|
855
|
+
const rest = args.replace(/--example\s+\S+/, "").trim();
|
|
856
|
+
const docs = rest.split(/\s+/).filter(Boolean);
|
|
857
|
+
return { docs, example };
|
|
858
|
+
}
|
|
652
859
|
var extension = (pi) => {
|
|
653
860
|
registerForgeflowPmTool(pi);
|
|
654
861
|
pi.registerCommand("continue", {
|
|
@@ -670,21 +877,40 @@ var extension = (pi) => {
|
|
|
670
877
|
);
|
|
671
878
|
}
|
|
672
879
|
});
|
|
673
|
-
pi.registerCommand("create-issues", {
|
|
880
|
+
pi.registerCommand("create-gh-issues", {
|
|
674
881
|
description: "Decompose PRD.md into vertical-slice GitHub issues",
|
|
675
882
|
handler: async () => {
|
|
676
|
-
pi.sendUserMessage(`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-issues".`);
|
|
883
|
+
pi.sendUserMessage(`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-gh-issues".`);
|
|
677
884
|
}
|
|
678
885
|
});
|
|
679
|
-
pi.registerCommand("create-issue", {
|
|
886
|
+
pi.registerCommand("create-gh-issue", {
|
|
680
887
|
description: "Create a single GitHub issue from a feature idea",
|
|
681
888
|
handler: async (args) => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
889
|
+
const issuePart = args.trim() ? `, issue="${args.trim()}"` : "";
|
|
890
|
+
pi.sendUserMessage(
|
|
891
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-gh-issue"${issuePart}. Do not interpret the issue text \u2014 pass it as-is.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
pi.registerCommand("investigate", {
|
|
896
|
+
description: "Spike or RFC: explore codebase + web, fill a Confluence template. Usage: /investigate [description] [--template <confluence-url>]",
|
|
897
|
+
handler: async (args) => {
|
|
898
|
+
const { description, template } = parseInvestigateArgs(args);
|
|
899
|
+
const issuePart = description ? `, issue="${description}"` : "";
|
|
900
|
+
const templatePart = template ? `, template="${template}"` : "";
|
|
901
|
+
pi.sendUserMessage(
|
|
902
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="investigate"${issuePart}${templatePart}. Do not interpret the description \u2014 pass it as-is.`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
pi.registerCommand("jira-issues", {
|
|
907
|
+
description: "Decompose Confluence PM docs into Jira issues. Usage: /jira-issues [confluence-url] [confluence-url...] [--example <confluence-url>]",
|
|
908
|
+
handler: async (args) => {
|
|
909
|
+
const { docs, example } = parseJiraIssuesArgs(args);
|
|
910
|
+
const docsPart = docs.length > 0 ? `, docs="${docs.join(",")}"` : "";
|
|
911
|
+
const examplePart = example ? `, example="${example}"` : "";
|
|
686
912
|
pi.sendUserMessage(
|
|
687
|
-
`Call the forgeflow-pm tool now with these exact parameters: pipeline="
|
|
913
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="jira-issues"${docsPart}${examplePart}. Do not interpret the URLs \u2014 pass them as-is.`
|
|
688
914
|
);
|
|
689
915
|
}
|
|
690
916
|
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callumvass/forgeflow-pm",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PM pipeline for Pi — PRD refinement, issue creation, and continue.",
|
|
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/pm"
|
|
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"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Writing Style
|
|
2
|
+
|
|
3
|
+
All written output must follow these rules. No exceptions.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
- British English throughout (favour, colour, organisation, analyse, behaviour).
|
|
8
|
+
- Spell out acronyms on first use only if the audience wouldn't know them.
|
|
9
|
+
|
|
10
|
+
## Tone
|
|
11
|
+
|
|
12
|
+
- Extremely concise. Every sentence earns its place.
|
|
13
|
+
- Direct and confident. State what is, not what might be.
|
|
14
|
+
- Write like a senior engineer's internal doc — not a blog post, not a sales pitch.
|
|
15
|
+
|
|
16
|
+
## Anti-patterns — never do these
|
|
17
|
+
|
|
18
|
+
- No em dashes (—). Use commas, full stops, or restructure.
|
|
19
|
+
- No "Consider...", "It's worth noting...", "It should be noted...", "Importantly,...".
|
|
20
|
+
- No hedging: "might want to", "could potentially", "it may be beneficial".
|
|
21
|
+
- No filler transitions: "Furthermore", "Additionally", "Moreover", "In conclusion".
|
|
22
|
+
- No rhetorical questions.
|
|
23
|
+
- No bullet points that start with the same word repeatedly.
|
|
24
|
+
- No summarising what was just said. Say it once.
|
|
25
|
+
- No emojis.
|
|
26
|
+
|
|
27
|
+
## Structure
|
|
28
|
+
|
|
29
|
+
- Lead with the answer or recommendation, not the reasoning.
|
|
30
|
+
- Use short paragraphs (2-3 sentences max).
|
|
31
|
+
- Use headings to structure, not to decorate.
|
|
32
|
+
- Tables over prose when comparing options.
|
|
33
|
+
- Code snippets only when they clarify something prose cannot.
|