@h-rig/planning-plugin 0.0.6-alpha.141 → 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 +4 -4
- package/dist/src/cli.js +105 -23
- package/dist/src/index.js +106 -24
- package/dist/src/plugin.js +106 -24
- package/package.json +6 -6
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
|
|
54
|
-
const
|
|
118
|
+
const sections = sectionizePrd(input.body);
|
|
119
|
+
const items = extractPlanItems(input);
|
|
55
120
|
return {
|
|
56
121
|
prdTitle: input.title,
|
|
57
|
-
userStories:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
88
|
-
return { title: title.value?.trim() || "Rig generated plan", body, 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
|
|
154
|
-
const
|
|
218
|
+
const sections = sectionizePrd(input.body);
|
|
219
|
+
const items = extractPlanItems(input);
|
|
155
220
|
return {
|
|
156
221
|
prdTitle: input.title,
|
|
157
|
-
userStories:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
188
|
-
return { title: title.value?.trim() || "Rig generated plan", body, 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({
|
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
|
57
|
-
const
|
|
121
|
+
const sections = sectionizePrd(input.body);
|
|
122
|
+
const items = extractPlanItems(input);
|
|
58
123
|
return {
|
|
59
124
|
prdTitle: input.title,
|
|
60
|
-
userStories:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
91
|
-
return { title: title.value?.trim() || "Rig generated plan", body, 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.
|
|
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/
|
|
14
|
-
"import": "./dist/src/
|
|
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.
|
|
33
|
-
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.
|
|
34
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
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
|
}
|