@callumvass/forgeflow-pm 0.1.0 → 0.3.2

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @callumvass/forgeflow-pm
2
+
3
+ PM pipeline for [Pi](https://github.com/nicholasgriffintn/pi) — PRD refinement, issue creation, and investigation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx pi install @callumvass/forgeflow-pm
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ | Command | Description |
14
+ |---------|-------------|
15
+ | `/prd-qa` | Refine PRD.md via critic → architect → integrator loop |
16
+ | `/continue` | Update PRD with Done/Next, QA, then create issues for next phase |
17
+ | `/create-gh-issues` | Decompose PRD.md into vertical-slice GitHub issues |
18
+ | `/create-gh-issue` | Create a single GitHub issue from a feature idea |
19
+ | `/jira-issues` | Decompose Confluence PM docs into Jira issues |
20
+ | `/investigate` | Spike or RFC: explore codebase + web, fill a Confluence template |
21
+
22
+ ## Agents
23
+
24
+ - **prd-critic** — Reviews PRD.md, outputs questions or signals completion
25
+ - **prd-architect** — Answers questions using PRD and codebase context
26
+ - **prd-integrator** — Incorporates answers back into PRD.md
27
+ - **gh-issue-creator** — Decomposes PRD into vertical-slice GitHub issues
28
+ - **gh-single-issue-creator** — Interactive single issue creation from a feature idea
29
+ - **jira-issue-creator** — Decomposes Confluence docs into Jira issues
30
+ - **investigator** — Spike/RFC research agent
31
+
32
+ ## Skills
33
+
34
+ - **issue-template** — Standard format for GitHub issues
35
+ - **prd-quality** — PRD completeness and quality criteria
36
+ - **writing-style** — Consistent tone and formatting rules
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: issue-creator
2
+ name: gh-issue-creator
3
3
  description: Decomposes a PRD into vertical-slice GitHub issues for autonomous implementation.
4
4
  tools: read, write, bash, grep, find
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: single-issue-creator
2
+ name: gh-single-issue-creator
3
3
  description: Turns a rough feature idea into a well-structured GitHub issue by exploring the codebase.
4
4
  tools: read, write, bash, grep, find
5
5
  ---
@@ -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.
@@ -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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/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 = spawn(invocation.command, invocation.args, {
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, _ctx) {
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", or "create-issue"'
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(Type.String({ description: "Feature idea for create-issue, or description for continue" }))
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
- if (!args.trim()) {
683
- pi.sendUserMessage('I need a feature idea. Usage: /create-issue "Add user authentication"');
684
- return;
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="create-issue", issue="${args.trim()}". Do not interpret the issue text \u2014 pass it as-is.`
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.0",
3
+ "version": "0.3.2",
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/callumvass/forgeflow.git",
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"
@@ -29,7 +35,7 @@
29
35
  "scripts": {
30
36
  "build": "tsup"
31
37
  },
32
- "dependencies": {
38
+ "devDependencies": {
33
39
  "@callumvass/forgeflow-shared": "*"
34
40
  },
35
41
  "peerDependencies": {
@@ -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.