@h-rig/planning-plugin 0.0.6-alpha.142 → 0.0.6-alpha.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RuntimeCliContext } from "@rig/core";
1
+ import type { RuntimeCliContext } from "@rig/core/config";
2
2
  export declare const PLANNING_PLAN_CLI_ID = "planning.plan";
3
3
  type CommandOutcome = {
4
4
  readonly ok: boolean;
@@ -10,9 +10,9 @@ export declare function executePlan(context: RuntimeCliContext, args: readonly s
10
10
  export declare const planningCliCommands: readonly [{
11
11
  readonly id: "planning.plan";
12
12
  readonly family: "plan";
13
- readonly command: "rig plan --text <prd>|--prd <file>";
14
- readonly description: "Generate a plan from a PRD and optionally materialize task-source tasks.";
15
- readonly usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]";
13
+ readonly command: "rig plan --text <prd>|--prd <file>|--issue <id>";
14
+ readonly description: "Generate a plan from a PRD or task-source issue and optionally materialize task-source tasks.";
15
+ readonly usage: "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]";
16
16
  readonly projectRequired: true;
17
17
  readonly run: typeof executePlan;
18
18
  }];
package/dist/src/cli.js CHANGED
@@ -47,27 +47,94 @@ function requireNoExtraArgs(args, usage) {
47
47
  throw new Error(`Unexpected argument: ${args[0]}
48
48
  Usage: ${usage}`);
49
49
  }
50
+ function stripMarkdownPrefix(line) {
51
+ return line.replace(/^#{1,6}\s+/, "").replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "").replace(/^\[[ xX]\]\s+/, "").replace(/^\*\*(.+)\*\*:?\s*/, "$1: ").trim();
52
+ }
53
+ function sectionizePrd(body) {
54
+ const sections = [{ title: "Overview", lines: [] }];
55
+ for (const rawLine of body.split(/\r?\n/)) {
56
+ const heading = rawLine.match(/^(#{2,4})\s+(.+?)\s*$/);
57
+ if (heading) {
58
+ sections.push({ title: stripMarkdownPrefix(heading[2] ?? "Section"), lines: [] });
59
+ continue;
60
+ }
61
+ const line = stripMarkdownPrefix(rawLine);
62
+ if (!line || line.startsWith("<!--") || line.startsWith("-->") || line.startsWith("```"))
63
+ continue;
64
+ sections[sections.length - 1]?.lines.push(line);
65
+ }
66
+ return sections.filter((section) => section.lines.length > 0 || section.title !== "Overview");
67
+ }
68
+ function checkboxItems(body) {
69
+ return body.split(/\r?\n/).map((line) => line.match(/^\s*[-*]\s+\[[ xX]\]\s+(.+?)\s*$/)?.[1]?.trim()).filter((line) => Boolean(line));
70
+ }
71
+ function slugFor(value, index) {
72
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 42);
73
+ return slug || `task-${index + 1}`;
74
+ }
75
+ function evidenceFor(sections, title) {
76
+ const titleWords = title.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 3);
77
+ const section = sections.find((candidate) => {
78
+ const haystack = `${candidate.title}
79
+ ${candidate.lines.join(`
80
+ `)}`.toLowerCase();
81
+ return titleWords.some((word) => haystack.includes(word));
82
+ }) ?? sections[0] ?? { title: "PRD", lines: [] };
83
+ const evidence = section.lines.find((line) => titleWords.some((word) => line.toLowerCase().includes(word))) ?? section.lines[0] ?? title;
84
+ return { title, evidence, section: section.title };
85
+ }
86
+ function extractPlanItems(input) {
87
+ const sections = sectionizePrd(input.body);
88
+ const checklist = checkboxItems(input.body);
89
+ const titles = checklist.length > 0 ? checklist : sections.filter((section) => /requirement|criteria|deliverable|scope|pain|problem|challenge|objective|task/i.test(section.title)).flatMap((section) => section.lines.filter((line) => line.length >= 18).slice(0, 3));
90
+ const distinctTitles = [...new Set(titles.map(stripMarkdownPrefix).filter((line) => line.length > 0))];
91
+ const fallback = distinctTitles.length > 0 ? distinctTitles : [`Analyze ${input.title}`, `Design implementation plan for ${input.title}`, `Define validation gates for ${input.title}`];
92
+ return fallback.slice(0, 8).map((title) => evidenceFor(sections, title));
93
+ }
94
+ function questionItems(sections) {
95
+ const explicit = sections.flatMap((section) => section.lines.filter((line) => line.endsWith("?")));
96
+ if (explicit.length > 0)
97
+ return explicit.slice(0, 4);
98
+ return ["Which PRD constraints require owner confirmation before materializing implementation issues?"];
99
+ }
100
+ function planTasksForPrd(input) {
101
+ const items = extractPlanItems(input);
102
+ return items.map((item, index) => ({
103
+ localId: slugFor(item.title, index),
104
+ title: item.title,
105
+ description: [`Source section: ${item.section}.`, `PRD evidence: ${item.evidence}`].join(`
106
+ `),
107
+ acceptance: [`Deliver: ${item.title}`, "Trace the implementation choice back to the source PRD evidence.", "Record unresolved assumptions before closeout."],
108
+ scope: [slugFor(item.section, index), slugFor(item.title, index)],
109
+ validationKeys: ["prd-traceability", "implementation-review"],
110
+ dependsOn: index === 0 ? [] : [slugFor(items[0]?.title ?? input.title, 0)],
111
+ parent: null,
112
+ parallelizable: index > 0
113
+ }));
114
+ }
50
115
  function defaultPlanningProvider(now) {
51
116
  return {
52
117
  spec: (input) => {
53
- const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
54
- const titles = headings.length > 0 ? headings : [input.title];
118
+ const sections = sectionizePrd(input.body);
119
+ const items = extractPlanItems(input);
55
120
  return {
56
121
  prdTitle: input.title,
57
- userStories: [],
58
- functionalRequirements: [],
59
- openQuestions: [],
60
- tasks: titles.map((title, index) => ({
61
- localId: `task-${index + 1}`,
62
- title,
63
- description: `Implement: ${title}`,
64
- acceptance: [],
65
- scope: [],
66
- validationKeys: [],
67
- dependsOn: index === 0 ? [] : [`task-${index}`],
68
- parent: null,
69
- parallelizable: index === 0
122
+ userStories: items.slice(0, 5).map((item, index) => ({
123
+ id: `US-${String(index + 1).padStart(3, "0")}`,
124
+ title: `Stakeholders can evaluate: ${item.title}`,
125
+ priority: index + 1
70
126
  })),
127
+ functionalRequirements: items.slice(0, 8).map((item, index) => ({
128
+ id: `FR-${String(index + 1).padStart(3, "0")}`,
129
+ text: item.title
130
+ })),
131
+ openQuestions: questionItems(sections).map((question, index) => ({
132
+ id: `Q-${String(index + 1).padStart(3, "0")}`,
133
+ question,
134
+ status: "deferred",
135
+ resolution: null
136
+ })),
137
+ tasks: [...planTasksForPrd(input)],
71
138
  generatedAt: now()
72
139
  };
73
140
  },
@@ -78,14 +145,29 @@ function defaultPlanningProvider(now) {
78
145
  async function loadPlanningClient() {
79
146
  return await import("@rig/client");
80
147
  }
81
- function readPrdText(args) {
148
+ async function readPrdText(context, args) {
82
149
  const title = takeOption(args, "--title");
83
150
  const text = takeOption(title.rest, "--text");
84
151
  const prd = takeOption(text.rest, "--prd");
152
+ const issue = takeOption(prd.rest, "--issue");
153
+ const sources = [text.value, prd.value, issue.value].filter(Boolean);
154
+ if (sources.length !== 1)
155
+ throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
156
+ if (issue.value) {
157
+ const { getTask } = await loadPlanningClient();
158
+ const task = await getTask(context.projectRoot, issue.value);
159
+ if (!task)
160
+ throw new Error(`No task found for issue ${issue.value}.`);
161
+ const body2 = typeof task.body === "string" ? task.body : "";
162
+ if (!body2.trim())
163
+ throw new Error(`Issue ${issue.value} has no PRD body text.`);
164
+ const taskTitle = typeof task.title === "string" && task.title.trim() ? task.title.trim() : `Issue ${issue.value}`;
165
+ return { title: title.value?.trim() || taskTitle, body: body2, rest: issue.rest };
166
+ }
85
167
  const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
86
168
  if (!body?.trim())
87
- throw new Error("rig plan requires --text <prd> or --prd <file>.");
88
- return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
169
+ throw new Error("rig plan requires non-empty PRD text.");
170
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: issue.rest };
89
171
  }
90
172
  async function executePlan(context, args) {
91
173
  const materialize = takeFlag(args, "--materialize");
@@ -93,8 +175,8 @@ async function executePlan(context, args) {
93
175
  const json = takeFlag(dryRun.rest, "--json");
94
176
  if (materialize.value && dryRun.value)
95
177
  throw new Error("Pass only one of --materialize or --dry-run.");
96
- const prd = readPrdText(json.rest);
97
- requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
178
+ const prd = await readPrdText(context, json.rest);
179
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]");
98
180
  const willMaterialize = materialize.value && !dryRun.value;
99
181
  const now = () => new Date().toISOString();
100
182
  const provider = defaultPlanningProvider(now);
@@ -116,9 +198,9 @@ var planningCliCommands = [
116
198
  {
117
199
  id: PLANNING_PLAN_CLI_ID,
118
200
  family: "plan",
119
- command: "rig plan --text <prd>|--prd <file>",
120
- description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
121
- usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
201
+ command: "rig plan --text <prd>|--prd <file>|--issue <id>",
202
+ description: "Generate a plan from a PRD or task-source issue and optionally materialize task-source tasks.",
203
+ usage: "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]",
122
204
  projectRequired: true,
123
205
  run: executePlan
124
206
  }
package/dist/src/index.js CHANGED
@@ -147,27 +147,94 @@ function requireNoExtraArgs(args, usage) {
147
147
  throw new Error(`Unexpected argument: ${args[0]}
148
148
  Usage: ${usage}`);
149
149
  }
150
+ function stripMarkdownPrefix(line) {
151
+ return line.replace(/^#{1,6}\s+/, "").replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "").replace(/^\[[ xX]\]\s+/, "").replace(/^\*\*(.+)\*\*:?\s*/, "$1: ").trim();
152
+ }
153
+ function sectionizePrd(body) {
154
+ const sections = [{ title: "Overview", lines: [] }];
155
+ for (const rawLine of body.split(/\r?\n/)) {
156
+ const heading = rawLine.match(/^(#{2,4})\s+(.+?)\s*$/);
157
+ if (heading) {
158
+ sections.push({ title: stripMarkdownPrefix(heading[2] ?? "Section"), lines: [] });
159
+ continue;
160
+ }
161
+ const line = stripMarkdownPrefix(rawLine);
162
+ if (!line || line.startsWith("<!--") || line.startsWith("-->") || line.startsWith("```"))
163
+ continue;
164
+ sections[sections.length - 1]?.lines.push(line);
165
+ }
166
+ return sections.filter((section) => section.lines.length > 0 || section.title !== "Overview");
167
+ }
168
+ function checkboxItems(body) {
169
+ return body.split(/\r?\n/).map((line) => line.match(/^\s*[-*]\s+\[[ xX]\]\s+(.+?)\s*$/)?.[1]?.trim()).filter((line) => Boolean(line));
170
+ }
171
+ function slugFor(value, index) {
172
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 42);
173
+ return slug || `task-${index + 1}`;
174
+ }
175
+ function evidenceFor(sections, title) {
176
+ const titleWords = title.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 3);
177
+ const section = sections.find((candidate) => {
178
+ const haystack = `${candidate.title}
179
+ ${candidate.lines.join(`
180
+ `)}`.toLowerCase();
181
+ return titleWords.some((word) => haystack.includes(word));
182
+ }) ?? sections[0] ?? { title: "PRD", lines: [] };
183
+ const evidence = section.lines.find((line) => titleWords.some((word) => line.toLowerCase().includes(word))) ?? section.lines[0] ?? title;
184
+ return { title, evidence, section: section.title };
185
+ }
186
+ function extractPlanItems(input) {
187
+ const sections = sectionizePrd(input.body);
188
+ const checklist = checkboxItems(input.body);
189
+ const titles = checklist.length > 0 ? checklist : sections.filter((section) => /requirement|criteria|deliverable|scope|pain|problem|challenge|objective|task/i.test(section.title)).flatMap((section) => section.lines.filter((line) => line.length >= 18).slice(0, 3));
190
+ const distinctTitles = [...new Set(titles.map(stripMarkdownPrefix).filter((line) => line.length > 0))];
191
+ const fallback = distinctTitles.length > 0 ? distinctTitles : [`Analyze ${input.title}`, `Design implementation plan for ${input.title}`, `Define validation gates for ${input.title}`];
192
+ return fallback.slice(0, 8).map((title) => evidenceFor(sections, title));
193
+ }
194
+ function questionItems(sections) {
195
+ const explicit = sections.flatMap((section) => section.lines.filter((line) => line.endsWith("?")));
196
+ if (explicit.length > 0)
197
+ return explicit.slice(0, 4);
198
+ return ["Which PRD constraints require owner confirmation before materializing implementation issues?"];
199
+ }
200
+ function planTasksForPrd(input) {
201
+ const items = extractPlanItems(input);
202
+ return items.map((item, index) => ({
203
+ localId: slugFor(item.title, index),
204
+ title: item.title,
205
+ description: [`Source section: ${item.section}.`, `PRD evidence: ${item.evidence}`].join(`
206
+ `),
207
+ acceptance: [`Deliver: ${item.title}`, "Trace the implementation choice back to the source PRD evidence.", "Record unresolved assumptions before closeout."],
208
+ scope: [slugFor(item.section, index), slugFor(item.title, index)],
209
+ validationKeys: ["prd-traceability", "implementation-review"],
210
+ dependsOn: index === 0 ? [] : [slugFor(items[0]?.title ?? input.title, 0)],
211
+ parent: null,
212
+ parallelizable: index > 0
213
+ }));
214
+ }
150
215
  function defaultPlanningProvider(now) {
151
216
  return {
152
217
  spec: (input) => {
153
- const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
154
- const titles = headings.length > 0 ? headings : [input.title];
218
+ const sections = sectionizePrd(input.body);
219
+ const items = extractPlanItems(input);
155
220
  return {
156
221
  prdTitle: input.title,
157
- userStories: [],
158
- functionalRequirements: [],
159
- openQuestions: [],
160
- tasks: titles.map((title, index) => ({
161
- localId: `task-${index + 1}`,
162
- title,
163
- description: `Implement: ${title}`,
164
- acceptance: [],
165
- scope: [],
166
- validationKeys: [],
167
- dependsOn: index === 0 ? [] : [`task-${index}`],
168
- parent: null,
169
- parallelizable: index === 0
222
+ userStories: items.slice(0, 5).map((item, index) => ({
223
+ id: `US-${String(index + 1).padStart(3, "0")}`,
224
+ title: `Stakeholders can evaluate: ${item.title}`,
225
+ priority: index + 1
170
226
  })),
227
+ functionalRequirements: items.slice(0, 8).map((item, index) => ({
228
+ id: `FR-${String(index + 1).padStart(3, "0")}`,
229
+ text: item.title
230
+ })),
231
+ openQuestions: questionItems(sections).map((question, index) => ({
232
+ id: `Q-${String(index + 1).padStart(3, "0")}`,
233
+ question,
234
+ status: "deferred",
235
+ resolution: null
236
+ })),
237
+ tasks: [...planTasksForPrd(input)],
171
238
  generatedAt: now()
172
239
  };
173
240
  },
@@ -178,14 +245,29 @@ function defaultPlanningProvider(now) {
178
245
  async function loadPlanningClient() {
179
246
  return await import("@rig/client");
180
247
  }
181
- function readPrdText(args) {
248
+ async function readPrdText(context, args) {
182
249
  const title = takeOption(args, "--title");
183
250
  const text = takeOption(title.rest, "--text");
184
251
  const prd = takeOption(text.rest, "--prd");
252
+ const issue = takeOption(prd.rest, "--issue");
253
+ const sources = [text.value, prd.value, issue.value].filter(Boolean);
254
+ if (sources.length !== 1)
255
+ throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
256
+ if (issue.value) {
257
+ const { getTask } = await loadPlanningClient();
258
+ const task = await getTask(context.projectRoot, issue.value);
259
+ if (!task)
260
+ throw new Error(`No task found for issue ${issue.value}.`);
261
+ const body2 = typeof task.body === "string" ? task.body : "";
262
+ if (!body2.trim())
263
+ throw new Error(`Issue ${issue.value} has no PRD body text.`);
264
+ const taskTitle = typeof task.title === "string" && task.title.trim() ? task.title.trim() : `Issue ${issue.value}`;
265
+ return { title: title.value?.trim() || taskTitle, body: body2, rest: issue.rest };
266
+ }
185
267
  const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
186
268
  if (!body?.trim())
187
- throw new Error("rig plan requires --text <prd> or --prd <file>.");
188
- return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
269
+ throw new Error("rig plan requires non-empty PRD text.");
270
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: issue.rest };
189
271
  }
190
272
  async function executePlan(context, args) {
191
273
  const materialize2 = takeFlag(args, "--materialize");
@@ -193,8 +275,8 @@ async function executePlan(context, args) {
193
275
  const json = takeFlag(dryRun.rest, "--json");
194
276
  if (materialize2.value && dryRun.value)
195
277
  throw new Error("Pass only one of --materialize or --dry-run.");
196
- const prd = readPrdText(json.rest);
197
- requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
278
+ const prd = await readPrdText(context, json.rest);
279
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]");
198
280
  const willMaterialize = materialize2.value && !dryRun.value;
199
281
  const now = () => new Date().toISOString();
200
282
  const provider = defaultPlanningProvider(now);
@@ -216,15 +298,15 @@ var planningCliCommands = [
216
298
  {
217
299
  id: PLANNING_PLAN_CLI_ID,
218
300
  family: "plan",
219
- command: "rig plan --text <prd>|--prd <file>",
220
- description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
221
- usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
301
+ command: "rig plan --text <prd>|--prd <file>|--issue <id>",
302
+ description: "Generate a plan from a PRD or task-source issue and optionally materialize task-source tasks.",
303
+ usage: "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]",
222
304
  projectRequired: true,
223
305
  run: executePlan
224
306
  }
225
307
  ];
226
308
  // packages/planning-plugin/src/plugin.ts
227
- import { definePlugin } from "@rig/core";
309
+ import { definePlugin } from "@rig/core/config";
228
310
  var PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
229
311
  var PLANNING_PLAN_PANEL_ID = "plan-intake";
230
312
  var planningPlugin = definePlugin({
@@ -2,7 +2,7 @@
2
2
  var __require = import.meta.require;
3
3
 
4
4
  // packages/planning-plugin/src/plugin.ts
5
- import { definePlugin } from "@rig/core";
5
+ import { definePlugin } from "@rig/core/config";
6
6
 
7
7
  // packages/planning-plugin/src/cli.ts
8
8
  import { readFileSync } from "fs";
@@ -50,27 +50,94 @@ function requireNoExtraArgs(args, usage) {
50
50
  throw new Error(`Unexpected argument: ${args[0]}
51
51
  Usage: ${usage}`);
52
52
  }
53
+ function stripMarkdownPrefix(line) {
54
+ return line.replace(/^#{1,6}\s+/, "").replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "").replace(/^\[[ xX]\]\s+/, "").replace(/^\*\*(.+)\*\*:?\s*/, "$1: ").trim();
55
+ }
56
+ function sectionizePrd(body) {
57
+ const sections = [{ title: "Overview", lines: [] }];
58
+ for (const rawLine of body.split(/\r?\n/)) {
59
+ const heading = rawLine.match(/^(#{2,4})\s+(.+?)\s*$/);
60
+ if (heading) {
61
+ sections.push({ title: stripMarkdownPrefix(heading[2] ?? "Section"), lines: [] });
62
+ continue;
63
+ }
64
+ const line = stripMarkdownPrefix(rawLine);
65
+ if (!line || line.startsWith("<!--") || line.startsWith("-->") || line.startsWith("```"))
66
+ continue;
67
+ sections[sections.length - 1]?.lines.push(line);
68
+ }
69
+ return sections.filter((section) => section.lines.length > 0 || section.title !== "Overview");
70
+ }
71
+ function checkboxItems(body) {
72
+ return body.split(/\r?\n/).map((line) => line.match(/^\s*[-*]\s+\[[ xX]\]\s+(.+?)\s*$/)?.[1]?.trim()).filter((line) => Boolean(line));
73
+ }
74
+ function slugFor(value, index) {
75
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 42);
76
+ return slug || `task-${index + 1}`;
77
+ }
78
+ function evidenceFor(sections, title) {
79
+ const titleWords = title.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 3);
80
+ const section = sections.find((candidate) => {
81
+ const haystack = `${candidate.title}
82
+ ${candidate.lines.join(`
83
+ `)}`.toLowerCase();
84
+ return titleWords.some((word) => haystack.includes(word));
85
+ }) ?? sections[0] ?? { title: "PRD", lines: [] };
86
+ const evidence = section.lines.find((line) => titleWords.some((word) => line.toLowerCase().includes(word))) ?? section.lines[0] ?? title;
87
+ return { title, evidence, section: section.title };
88
+ }
89
+ function extractPlanItems(input) {
90
+ const sections = sectionizePrd(input.body);
91
+ const checklist = checkboxItems(input.body);
92
+ const titles = checklist.length > 0 ? checklist : sections.filter((section) => /requirement|criteria|deliverable|scope|pain|problem|challenge|objective|task/i.test(section.title)).flatMap((section) => section.lines.filter((line) => line.length >= 18).slice(0, 3));
93
+ const distinctTitles = [...new Set(titles.map(stripMarkdownPrefix).filter((line) => line.length > 0))];
94
+ const fallback = distinctTitles.length > 0 ? distinctTitles : [`Analyze ${input.title}`, `Design implementation plan for ${input.title}`, `Define validation gates for ${input.title}`];
95
+ return fallback.slice(0, 8).map((title) => evidenceFor(sections, title));
96
+ }
97
+ function questionItems(sections) {
98
+ const explicit = sections.flatMap((section) => section.lines.filter((line) => line.endsWith("?")));
99
+ if (explicit.length > 0)
100
+ return explicit.slice(0, 4);
101
+ return ["Which PRD constraints require owner confirmation before materializing implementation issues?"];
102
+ }
103
+ function planTasksForPrd(input) {
104
+ const items = extractPlanItems(input);
105
+ return items.map((item, index) => ({
106
+ localId: slugFor(item.title, index),
107
+ title: item.title,
108
+ description: [`Source section: ${item.section}.`, `PRD evidence: ${item.evidence}`].join(`
109
+ `),
110
+ acceptance: [`Deliver: ${item.title}`, "Trace the implementation choice back to the source PRD evidence.", "Record unresolved assumptions before closeout."],
111
+ scope: [slugFor(item.section, index), slugFor(item.title, index)],
112
+ validationKeys: ["prd-traceability", "implementation-review"],
113
+ dependsOn: index === 0 ? [] : [slugFor(items[0]?.title ?? input.title, 0)],
114
+ parent: null,
115
+ parallelizable: index > 0
116
+ }));
117
+ }
53
118
  function defaultPlanningProvider(now) {
54
119
  return {
55
120
  spec: (input) => {
56
- const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
57
- const titles = headings.length > 0 ? headings : [input.title];
121
+ const sections = sectionizePrd(input.body);
122
+ const items = extractPlanItems(input);
58
123
  return {
59
124
  prdTitle: input.title,
60
- userStories: [],
61
- functionalRequirements: [],
62
- openQuestions: [],
63
- tasks: titles.map((title, index) => ({
64
- localId: `task-${index + 1}`,
65
- title,
66
- description: `Implement: ${title}`,
67
- acceptance: [],
68
- scope: [],
69
- validationKeys: [],
70
- dependsOn: index === 0 ? [] : [`task-${index}`],
71
- parent: null,
72
- parallelizable: index === 0
125
+ userStories: items.slice(0, 5).map((item, index) => ({
126
+ id: `US-${String(index + 1).padStart(3, "0")}`,
127
+ title: `Stakeholders can evaluate: ${item.title}`,
128
+ priority: index + 1
73
129
  })),
130
+ functionalRequirements: items.slice(0, 8).map((item, index) => ({
131
+ id: `FR-${String(index + 1).padStart(3, "0")}`,
132
+ text: item.title
133
+ })),
134
+ openQuestions: questionItems(sections).map((question, index) => ({
135
+ id: `Q-${String(index + 1).padStart(3, "0")}`,
136
+ question,
137
+ status: "deferred",
138
+ resolution: null
139
+ })),
140
+ tasks: [...planTasksForPrd(input)],
74
141
  generatedAt: now()
75
142
  };
76
143
  },
@@ -81,14 +148,29 @@ function defaultPlanningProvider(now) {
81
148
  async function loadPlanningClient() {
82
149
  return await import("@rig/client");
83
150
  }
84
- function readPrdText(args) {
151
+ async function readPrdText(context, args) {
85
152
  const title = takeOption(args, "--title");
86
153
  const text = takeOption(title.rest, "--text");
87
154
  const prd = takeOption(text.rest, "--prd");
155
+ const issue = takeOption(prd.rest, "--issue");
156
+ const sources = [text.value, prd.value, issue.value].filter(Boolean);
157
+ if (sources.length !== 1)
158
+ throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
159
+ if (issue.value) {
160
+ const { getTask } = await loadPlanningClient();
161
+ const task = await getTask(context.projectRoot, issue.value);
162
+ if (!task)
163
+ throw new Error(`No task found for issue ${issue.value}.`);
164
+ const body2 = typeof task.body === "string" ? task.body : "";
165
+ if (!body2.trim())
166
+ throw new Error(`Issue ${issue.value} has no PRD body text.`);
167
+ const taskTitle = typeof task.title === "string" && task.title.trim() ? task.title.trim() : `Issue ${issue.value}`;
168
+ return { title: title.value?.trim() || taskTitle, body: body2, rest: issue.rest };
169
+ }
88
170
  const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
89
171
  if (!body?.trim())
90
- throw new Error("rig plan requires --text <prd> or --prd <file>.");
91
- return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
172
+ throw new Error("rig plan requires non-empty PRD text.");
173
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: issue.rest };
92
174
  }
93
175
  async function executePlan(context, args) {
94
176
  const materialize = takeFlag(args, "--materialize");
@@ -96,8 +178,8 @@ async function executePlan(context, args) {
96
178
  const json = takeFlag(dryRun.rest, "--json");
97
179
  if (materialize.value && dryRun.value)
98
180
  throw new Error("Pass only one of --materialize or --dry-run.");
99
- const prd = readPrdText(json.rest);
100
- requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
181
+ const prd = await readPrdText(context, json.rest);
182
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]");
101
183
  const willMaterialize = materialize.value && !dryRun.value;
102
184
  const now = () => new Date().toISOString();
103
185
  const provider = defaultPlanningProvider(now);
@@ -119,9 +201,9 @@ var planningCliCommands = [
119
201
  {
120
202
  id: PLANNING_PLAN_CLI_ID,
121
203
  family: "plan",
122
- command: "rig plan --text <prd>|--prd <file>",
123
- description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
124
- usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
204
+ command: "rig plan --text <prd>|--prd <file>|--issue <id>",
205
+ description: "Generate a plan from a PRD or task-source issue and optionally materialize task-source tasks.",
206
+ usage: "rig plan --text <prd>|--prd <file>|--issue <id> [--title <title>] [--materialize|--dry-run] [--json]",
125
207
  projectRequired: true,
126
208
  run: executePlan
127
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/planning-plugin",
3
- "version": "0.0.6-alpha.142",
3
+ "version": "0.0.6-alpha.143",
4
4
  "type": "module",
5
5
  "description": "First-party PRD-to-plan plugin for Rig task sources.",
6
6
  "license": "UNLICENSED",
@@ -10,8 +10,8 @@
10
10
  ],
11
11
  "exports": {
12
12
  ".": {
13
- "types": "./dist/src/index.d.ts",
14
- "import": "./dist/src/index.js"
13
+ "types": "./dist/src/plugin.d.ts",
14
+ "import": "./dist/src/plugin.js"
15
15
  },
16
16
  "./planning": {
17
17
  "types": "./dist/src/planning.d.ts",
@@ -29,8 +29,8 @@
29
29
  "module": "./dist/src/index.js",
30
30
  "types": "./dist/src/index.d.ts",
31
31
  "dependencies": {
32
- "@rig/client": "npm:@h-rig/client@0.0.6-alpha.142",
33
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.142",
34
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.142"
32
+ "@rig/client": "npm:@h-rig/client@0.0.6-alpha.143",
33
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.143",
34
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.143"
35
35
  }
36
36
  }