@h-rig/planning-plugin 0.0.6-alpha.136 → 0.0.6-alpha.137

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.
@@ -0,0 +1,19 @@
1
+ import type { RuntimeCliContext } from "@rig/core";
2
+ export declare const PLANNING_PLAN_CLI_ID = "planning.plan";
3
+ type CommandOutcome = {
4
+ readonly ok: boolean;
5
+ readonly group: string;
6
+ readonly command: string;
7
+ readonly details?: Record<string, unknown>;
8
+ };
9
+ export declare function executePlan(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
10
+ export declare const planningCliCommands: readonly [{
11
+ readonly id: "planning.plan";
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]";
16
+ readonly projectRequired: true;
17
+ readonly run: typeof executePlan;
18
+ }];
19
+ export {};
@@ -0,0 +1,125 @@
1
+ // @bun
2
+ // packages/planning-plugin/src/cli.ts
3
+ import { readFileSync } from "fs";
4
+ import { createTask, planWorkspace } from "@rig/client";
5
+
6
+ // packages/planning-plugin/src/planning.ts
7
+ async function spec(input, ports) {
8
+ return ports.spec(input);
9
+ }
10
+ async function clarify(planSpec, context, ports) {
11
+ return ports.clarify(planSpec, context);
12
+ }
13
+ async function plan(planSpec, ports) {
14
+ return ports.plan(planSpec);
15
+ }
16
+ async function specClarifyPlan(input, context, ports) {
17
+ return plan(await clarify(await spec(input, ports), context, ports), ports);
18
+ }
19
+
20
+ // packages/planning-plugin/src/cli.ts
21
+ var PLANNING_PLAN_CLI_ID = "planning.plan";
22
+ function printJson(value) {
23
+ console.log(JSON.stringify(value, null, 2));
24
+ }
25
+ function takeFlag(args, flag) {
26
+ const rest = [...args];
27
+ const index = rest.indexOf(flag);
28
+ if (index < 0)
29
+ return { value: false, rest };
30
+ rest.splice(index, 1);
31
+ return { value: true, rest };
32
+ }
33
+ function takeOption(args, flag) {
34
+ const rest = [...args];
35
+ const index = rest.indexOf(flag);
36
+ if (index < 0)
37
+ return { rest };
38
+ const value = rest[index + 1];
39
+ if (!value || value.startsWith("-"))
40
+ throw new Error(`${flag} requires a value.`);
41
+ rest.splice(index, 2);
42
+ return { value, rest };
43
+ }
44
+ function requireNoExtraArgs(args, usage) {
45
+ if (args.length > 0)
46
+ throw new Error(`Unexpected argument: ${args[0]}
47
+ Usage: ${usage}`);
48
+ }
49
+ function defaultPlanningProvider(now) {
50
+ return {
51
+ spec: (input) => {
52
+ const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
53
+ const titles = headings.length > 0 ? headings : [input.title];
54
+ return {
55
+ prdTitle: input.title,
56
+ userStories: [],
57
+ functionalRequirements: [],
58
+ openQuestions: [],
59
+ tasks: titles.map((title, index) => ({
60
+ localId: `task-${index + 1}`,
61
+ title,
62
+ description: `Implement: ${title}`,
63
+ acceptance: [],
64
+ scope: [],
65
+ validationKeys: [],
66
+ dependsOn: index === 0 ? [] : [`task-${index}`],
67
+ parent: null,
68
+ parallelizable: index === 0
69
+ })),
70
+ generatedAt: now()
71
+ };
72
+ },
73
+ clarify: (planSpec) => planSpec,
74
+ plan: (planSpec) => planSpec
75
+ };
76
+ }
77
+ function readPrdText(args) {
78
+ const title = takeOption(args, "--title");
79
+ const text = takeOption(title.rest, "--text");
80
+ const prd = takeOption(text.rest, "--prd");
81
+ const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
82
+ if (!body?.trim())
83
+ throw new Error("rig plan requires --text <prd> or --prd <file>.");
84
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
85
+ }
86
+ async function executePlan(context, args) {
87
+ const materialize = takeFlag(args, "--materialize");
88
+ const dryRun = takeFlag(materialize.rest, "--dry-run");
89
+ const json = takeFlag(dryRun.rest, "--json");
90
+ if (materialize.value && dryRun.value)
91
+ throw new Error("Pass only one of --materialize or --dry-run.");
92
+ const prd = readPrdText(json.rest);
93
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
94
+ const willMaterialize = materialize.value && !dryRun.value;
95
+ const now = () => new Date().toISOString();
96
+ const provider = defaultPlanningProvider(now);
97
+ const result = await planWorkspace(context.projectRoot, prd.body, {
98
+ generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
99
+ createTask
100
+ }, { title: prd.title, materialize: willMaterialize });
101
+ if (context.outputMode === "text") {
102
+ if (json.value)
103
+ printJson(result);
104
+ else
105
+ console.log(result.spec.tasks.map((task) => `${task.localId} ${task.title}`).join(`
106
+ `) || "No plan tasks.");
107
+ }
108
+ return { ok: true, group: "plan", command: willMaterialize ? "materialize" : "show", details: result };
109
+ }
110
+ var planningCliCommands = [
111
+ {
112
+ id: PLANNING_PLAN_CLI_ID,
113
+ family: "plan",
114
+ command: "rig plan --text <prd>|--prd <file>",
115
+ description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
116
+ usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
117
+ projectRequired: true,
118
+ run: executePlan
119
+ }
120
+ ];
121
+ export {
122
+ planningCliCommands,
123
+ executePlan,
124
+ PLANNING_PLAN_CLI_ID
125
+ };
@@ -1,2 +1,3 @@
1
1
  export * from "./planning";
2
+ export * from "./cli";
2
3
  export * from "./plugin";
package/dist/src/index.js CHANGED
@@ -12,76 +12,6 @@ async function plan(planSpec, ports) {
12
12
  async function specClarifyPlan(input, context, ports) {
13
13
  return plan(await clarify(await spec(input, ports), context, ports), ports);
14
14
  }
15
- var PLAN_SPEC_SYSTEM = "You are an engineering architect. Convert a PRD into a structured spec. Capture WHAT and WHY, not the tech stack. Surface every ambiguity as an open question. Decompose into ordered, dependency-aware tasks. Reply ONLY with a single JSON object.";
16
- function planSpecUser(input) {
17
- return [
18
- `PRD title: ${input.title}`,
19
- `PRD:
20
- """${input.body}"""`,
21
- "",
22
- 'Reply with JSON of exactly this shape: { "userStories": [{"id":"US1","title":"...","priority":1}], "functionalRequirements": [{"id":"FR-001","text":"..."}], "openQuestions": [{"id":"Q1","question":"..."}], "tasks": [{"localId":"task-1","title":"...","description":"...","acceptance":["..."],"scope":["path"],"validationKeys":[],"dependsOn":[],"parent":null,"parallelizable":true}] }',
23
- "Mark as open any underspecified data shape, edge case, non-functional limit, or integration point. Use dependsOn (localIds) to order tasks. The task graph must be acyclic."
24
- ].join(`
25
- `);
26
- }
27
- function extractJsonObject(text) {
28
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
29
- const raw = fenced?.[1] ?? text;
30
- const start = raw.indexOf("{");
31
- const end = raw.lastIndexOf("}");
32
- if (start < 0 || end <= start)
33
- throw new Error("model output contained no JSON object");
34
- return JSON.parse(raw.slice(start, end + 1));
35
- }
36
- function createLlmPlanningProvider(chat, now) {
37
- return {
38
- spec: async (input) => {
39
- const parsed = extractJsonObject(await chat(PLAN_SPEC_SYSTEM, planSpecUser(input)));
40
- const stories = Array.isArray(parsed.userStories) ? parsed.userStories : [];
41
- const reqs = Array.isArray(parsed.functionalRequirements) ? parsed.functionalRequirements : [];
42
- const questions = Array.isArray(parsed.openQuestions) ? parsed.openQuestions : [];
43
- const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
44
- return {
45
- prdTitle: input.title,
46
- userStories: stories.filter((s) => s.title).map((s, i) => ({ id: s.id ?? `US${i + 1}`, title: String(s.title), priority: typeof s.priority === "number" ? s.priority : i + 1 })),
47
- functionalRequirements: reqs.filter((r) => r.text).map((r, i) => ({ id: r.id ?? `FR-${String(i + 1).padStart(3, "0")}`, text: String(r.text) })),
48
- openQuestions: questions.filter((q) => q.question).map((q, i) => ({ id: q.id ?? `Q${i + 1}`, question: String(q.question), status: "open", resolution: null })),
49
- tasks: tasks.filter((t) => typeof t.title === "string").map((t, i) => ({
50
- localId: typeof t.localId === "string" ? t.localId : `task-${i + 1}`,
51
- title: String(t.title),
52
- description: typeof t.description === "string" ? t.description : "",
53
- acceptance: Array.isArray(t.acceptance) ? t.acceptance.map(String) : [],
54
- scope: Array.isArray(t.scope) ? t.scope.map(String) : [],
55
- validationKeys: Array.isArray(t.validationKeys) ? t.validationKeys.map(String) : [],
56
- dependsOn: Array.isArray(t.dependsOn) ? t.dependsOn.map(String) : [],
57
- parent: typeof t.parent === "string" ? t.parent : null,
58
- parallelizable: t.parallelizable !== false
59
- })),
60
- generatedAt: now()
61
- };
62
- },
63
- clarify: (planSpec) => planSpec,
64
- plan: (planSpec) => planSpec
65
- };
66
- }
67
- function openAiChat(apiKey, model = "gpt-4o-mini") {
68
- return async (system, user) => {
69
- const res = await fetch("https://api.openai.com/v1/chat/completions", {
70
- method: "POST",
71
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
72
- body: JSON.stringify({
73
- model,
74
- messages: [{ role: "system", content: system }, { role: "user", content: user }],
75
- temperature: 0.2,
76
- response_format: { type: "json_object" }
77
- })
78
- });
79
- if (!res.ok)
80
- throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 300)}`);
81
- const data = await res.json();
82
- return data.choices?.[0]?.message?.content ?? "{}";
83
- };
84
- }
85
15
  function taskByLocalId(tasks) {
86
16
  return new Map(tasks.map((task) => [task.localId, task]));
87
17
  }
@@ -185,32 +115,147 @@ async function materialize(planSpec, source, options = {}) {
185
115
  }
186
116
  return { planId, created, existing, localIdToTaskId: Object.fromEntries(localIdToTaskId) };
187
117
  }
118
+ // packages/planning-plugin/src/cli.ts
119
+ import { readFileSync } from "fs";
120
+ import { createTask, planWorkspace } from "@rig/client";
121
+ var PLANNING_PLAN_CLI_ID = "planning.plan";
122
+ function printJson(value) {
123
+ console.log(JSON.stringify(value, null, 2));
124
+ }
125
+ function takeFlag(args, flag) {
126
+ const rest = [...args];
127
+ const index = rest.indexOf(flag);
128
+ if (index < 0)
129
+ return { value: false, rest };
130
+ rest.splice(index, 1);
131
+ return { value: true, rest };
132
+ }
133
+ function takeOption(args, flag) {
134
+ const rest = [...args];
135
+ const index = rest.indexOf(flag);
136
+ if (index < 0)
137
+ return { rest };
138
+ const value = rest[index + 1];
139
+ if (!value || value.startsWith("-"))
140
+ throw new Error(`${flag} requires a value.`);
141
+ rest.splice(index, 2);
142
+ return { value, rest };
143
+ }
144
+ function requireNoExtraArgs(args, usage) {
145
+ if (args.length > 0)
146
+ throw new Error(`Unexpected argument: ${args[0]}
147
+ Usage: ${usage}`);
148
+ }
149
+ function defaultPlanningProvider(now) {
150
+ return {
151
+ spec: (input) => {
152
+ const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
153
+ const titles = headings.length > 0 ? headings : [input.title];
154
+ return {
155
+ prdTitle: input.title,
156
+ userStories: [],
157
+ functionalRequirements: [],
158
+ openQuestions: [],
159
+ tasks: titles.map((title, index) => ({
160
+ localId: `task-${index + 1}`,
161
+ title,
162
+ description: `Implement: ${title}`,
163
+ acceptance: [],
164
+ scope: [],
165
+ validationKeys: [],
166
+ dependsOn: index === 0 ? [] : [`task-${index}`],
167
+ parent: null,
168
+ parallelizable: index === 0
169
+ })),
170
+ generatedAt: now()
171
+ };
172
+ },
173
+ clarify: (planSpec) => planSpec,
174
+ plan: (planSpec) => planSpec
175
+ };
176
+ }
177
+ function readPrdText(args) {
178
+ const title = takeOption(args, "--title");
179
+ const text = takeOption(title.rest, "--text");
180
+ const prd = takeOption(text.rest, "--prd");
181
+ const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
182
+ if (!body?.trim())
183
+ throw new Error("rig plan requires --text <prd> or --prd <file>.");
184
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
185
+ }
186
+ async function executePlan(context, args) {
187
+ const materialize2 = takeFlag(args, "--materialize");
188
+ const dryRun = takeFlag(materialize2.rest, "--dry-run");
189
+ const json = takeFlag(dryRun.rest, "--json");
190
+ if (materialize2.value && dryRun.value)
191
+ throw new Error("Pass only one of --materialize or --dry-run.");
192
+ const prd = readPrdText(json.rest);
193
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
194
+ const willMaterialize = materialize2.value && !dryRun.value;
195
+ const now = () => new Date().toISOString();
196
+ const provider = defaultPlanningProvider(now);
197
+ const result = await planWorkspace(context.projectRoot, prd.body, {
198
+ generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
199
+ createTask
200
+ }, { title: prd.title, materialize: willMaterialize });
201
+ if (context.outputMode === "text") {
202
+ if (json.value)
203
+ printJson(result);
204
+ else
205
+ console.log(result.spec.tasks.map((task) => `${task.localId} ${task.title}`).join(`
206
+ `) || "No plan tasks.");
207
+ }
208
+ return { ok: true, group: "plan", command: willMaterialize ? "materialize" : "show", details: result };
209
+ }
210
+ var planningCliCommands = [
211
+ {
212
+ id: PLANNING_PLAN_CLI_ID,
213
+ family: "plan",
214
+ command: "rig plan --text <prd>|--prd <file>",
215
+ description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
216
+ usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
217
+ projectRequired: true,
218
+ run: executePlan
219
+ }
220
+ ];
188
221
  // packages/planning-plugin/src/plugin.ts
189
222
  import { definePlugin } from "@rig/core";
190
223
  var PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
224
+ var PLANNING_PLAN_PANEL_ID = "plan-intake";
191
225
  var planningPlugin = definePlugin({
192
226
  name: PLANNING_PLUGIN_NAME,
193
227
  version: "0.0.0-alpha.1",
194
228
  provides: [],
195
229
  requires: [],
196
230
  contributes: {
197
- cliCommands: [
198
- {
199
- id: "planning.materialize",
200
- command: "rig plan materialize",
201
- description: "Generate a plan from a PRD and materialize idempotent task-source tasks."
202
- }
203
- ]
231
+ capabilities: [
232
+ { id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
233
+ ],
234
+ panels: [
235
+ { id: PLANNING_PLAN_PANEL_ID, slot: "capability", title: "Plan intake", capabilityId: "planning.plan" }
236
+ ],
237
+ cliCommands: planningCliCommands.map(({ run: _run, ...metadata }) => metadata)
204
238
  }
239
+ }, {
240
+ featureCapabilities: [
241
+ { id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
242
+ ],
243
+ cliCommands: planningCliCommands
205
244
  });
245
+ function createPlanningPlugin() {
246
+ return planningPlugin;
247
+ }
206
248
  export {
207
249
  specClarifyPlan,
208
250
  spec,
209
251
  planningPlugin,
252
+ planningCliCommands,
210
253
  plan,
211
- openAiChat,
212
254
  materialize,
213
- createLlmPlanningProvider,
255
+ executePlan,
256
+ createPlanningPlugin,
214
257
  clarify,
215
- PLANNING_PLUGIN_NAME
258
+ PLANNING_PLUGIN_NAME,
259
+ PLANNING_PLAN_PANEL_ID,
260
+ PLANNING_PLAN_CLI_ID
216
261
  };
@@ -28,10 +28,6 @@ export declare function spec(input: PrdInput, ports: Pick<PlanningProviderPorts,
28
28
  export declare function clarify(planSpec: PlanSpec, context: ClarificationContext, ports: Pick<PlanningProviderPorts, "clarify">): Promise<PlanSpec>;
29
29
  export declare function plan(planSpec: PlanSpec, ports: Pick<PlanningProviderPorts, "plan">): Promise<PlanSpec>;
30
30
  export declare function specClarifyPlan(input: PrdInput, context: ClarificationContext, ports: PlanningProviderPorts): Promise<PlanSpec>;
31
- export type ChatComplete = (system: string, user: string) => Promise<string>;
32
- export declare function createLlmPlanningProvider(chat: ChatComplete, now: () => string): PlanningProviderPorts;
33
- /** OpenAI chat-completions port via fetch (no SDK). */
34
- export declare function openAiChat(apiKey: string, model?: string): ChatComplete;
35
31
  export declare function materialize(planSpec: PlanSpec, source: Pick<RegisteredTaskSource, "list" | "create">, options?: {
36
32
  readonly planId?: string;
37
33
  }): Promise<MaterializePlanResult>;
@@ -12,76 +12,6 @@ async function plan(planSpec, ports) {
12
12
  async function specClarifyPlan(input, context, ports) {
13
13
  return plan(await clarify(await spec(input, ports), context, ports), ports);
14
14
  }
15
- var PLAN_SPEC_SYSTEM = "You are an engineering architect. Convert a PRD into a structured spec. Capture WHAT and WHY, not the tech stack. Surface every ambiguity as an open question. Decompose into ordered, dependency-aware tasks. Reply ONLY with a single JSON object.";
16
- function planSpecUser(input) {
17
- return [
18
- `PRD title: ${input.title}`,
19
- `PRD:
20
- """${input.body}"""`,
21
- "",
22
- 'Reply with JSON of exactly this shape: { "userStories": [{"id":"US1","title":"...","priority":1}], "functionalRequirements": [{"id":"FR-001","text":"..."}], "openQuestions": [{"id":"Q1","question":"..."}], "tasks": [{"localId":"task-1","title":"...","description":"...","acceptance":["..."],"scope":["path"],"validationKeys":[],"dependsOn":[],"parent":null,"parallelizable":true}] }',
23
- "Mark as open any underspecified data shape, edge case, non-functional limit, or integration point. Use dependsOn (localIds) to order tasks. The task graph must be acyclic."
24
- ].join(`
25
- `);
26
- }
27
- function extractJsonObject(text) {
28
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
29
- const raw = fenced?.[1] ?? text;
30
- const start = raw.indexOf("{");
31
- const end = raw.lastIndexOf("}");
32
- if (start < 0 || end <= start)
33
- throw new Error("model output contained no JSON object");
34
- return JSON.parse(raw.slice(start, end + 1));
35
- }
36
- function createLlmPlanningProvider(chat, now) {
37
- return {
38
- spec: async (input) => {
39
- const parsed = extractJsonObject(await chat(PLAN_SPEC_SYSTEM, planSpecUser(input)));
40
- const stories = Array.isArray(parsed.userStories) ? parsed.userStories : [];
41
- const reqs = Array.isArray(parsed.functionalRequirements) ? parsed.functionalRequirements : [];
42
- const questions = Array.isArray(parsed.openQuestions) ? parsed.openQuestions : [];
43
- const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
44
- return {
45
- prdTitle: input.title,
46
- userStories: stories.filter((s) => s.title).map((s, i) => ({ id: s.id ?? `US${i + 1}`, title: String(s.title), priority: typeof s.priority === "number" ? s.priority : i + 1 })),
47
- functionalRequirements: reqs.filter((r) => r.text).map((r, i) => ({ id: r.id ?? `FR-${String(i + 1).padStart(3, "0")}`, text: String(r.text) })),
48
- openQuestions: questions.filter((q) => q.question).map((q, i) => ({ id: q.id ?? `Q${i + 1}`, question: String(q.question), status: "open", resolution: null })),
49
- tasks: tasks.filter((t) => typeof t.title === "string").map((t, i) => ({
50
- localId: typeof t.localId === "string" ? t.localId : `task-${i + 1}`,
51
- title: String(t.title),
52
- description: typeof t.description === "string" ? t.description : "",
53
- acceptance: Array.isArray(t.acceptance) ? t.acceptance.map(String) : [],
54
- scope: Array.isArray(t.scope) ? t.scope.map(String) : [],
55
- validationKeys: Array.isArray(t.validationKeys) ? t.validationKeys.map(String) : [],
56
- dependsOn: Array.isArray(t.dependsOn) ? t.dependsOn.map(String) : [],
57
- parent: typeof t.parent === "string" ? t.parent : null,
58
- parallelizable: t.parallelizable !== false
59
- })),
60
- generatedAt: now()
61
- };
62
- },
63
- clarify: (planSpec) => planSpec,
64
- plan: (planSpec) => planSpec
65
- };
66
- }
67
- function openAiChat(apiKey, model = "gpt-4o-mini") {
68
- return async (system, user) => {
69
- const res = await fetch("https://api.openai.com/v1/chat/completions", {
70
- method: "POST",
71
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
72
- body: JSON.stringify({
73
- model,
74
- messages: [{ role: "system", content: system }, { role: "user", content: user }],
75
- temperature: 0.2,
76
- response_format: { type: "json_object" }
77
- })
78
- });
79
- if (!res.ok)
80
- throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 300)}`);
81
- const data = await res.json();
82
- return data.choices?.[0]?.message?.content ?? "{}";
83
- };
84
- }
85
15
  function taskByLocalId(tasks) {
86
16
  return new Map(tasks.map((task) => [task.localId, task]));
87
17
  }
@@ -189,8 +119,6 @@ export {
189
119
  specClarifyPlan,
190
120
  spec,
191
121
  plan,
192
- openAiChat,
193
122
  materialize,
194
- createLlmPlanningProvider,
195
123
  clarify
196
124
  };
@@ -1,3 +1,5 @@
1
1
  export declare const PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
2
+ export declare const PLANNING_PLAN_PANEL_ID = "plan-intake";
2
3
  export declare const planningPlugin: import("@rig/core").RigPluginWithRuntime;
4
+ export declare function createPlanningPlugin(): import("@rig/core").RigPluginWithRuntime;
3
5
  export default planningPlugin;
@@ -1,25 +1,158 @@
1
1
  // @bun
2
2
  // packages/planning-plugin/src/plugin.ts
3
3
  import { definePlugin } from "@rig/core";
4
+
5
+ // packages/planning-plugin/src/cli.ts
6
+ import { readFileSync } from "fs";
7
+ import { createTask, planWorkspace } from "@rig/client";
8
+
9
+ // packages/planning-plugin/src/planning.ts
10
+ async function spec(input, ports) {
11
+ return ports.spec(input);
12
+ }
13
+ async function clarify(planSpec, context, ports) {
14
+ return ports.clarify(planSpec, context);
15
+ }
16
+ async function plan(planSpec, ports) {
17
+ return ports.plan(planSpec);
18
+ }
19
+ async function specClarifyPlan(input, context, ports) {
20
+ return plan(await clarify(await spec(input, ports), context, ports), ports);
21
+ }
22
+
23
+ // packages/planning-plugin/src/cli.ts
24
+ var PLANNING_PLAN_CLI_ID = "planning.plan";
25
+ function printJson(value) {
26
+ console.log(JSON.stringify(value, null, 2));
27
+ }
28
+ function takeFlag(args, flag) {
29
+ const rest = [...args];
30
+ const index = rest.indexOf(flag);
31
+ if (index < 0)
32
+ return { value: false, rest };
33
+ rest.splice(index, 1);
34
+ return { value: true, rest };
35
+ }
36
+ function takeOption(args, flag) {
37
+ const rest = [...args];
38
+ const index = rest.indexOf(flag);
39
+ if (index < 0)
40
+ return { rest };
41
+ const value = rest[index + 1];
42
+ if (!value || value.startsWith("-"))
43
+ throw new Error(`${flag} requires a value.`);
44
+ rest.splice(index, 2);
45
+ return { value, rest };
46
+ }
47
+ function requireNoExtraArgs(args, usage) {
48
+ if (args.length > 0)
49
+ throw new Error(`Unexpected argument: ${args[0]}
50
+ Usage: ${usage}`);
51
+ }
52
+ function defaultPlanningProvider(now) {
53
+ return {
54
+ spec: (input) => {
55
+ const headings = input.body.split(/\r?\n/).map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((line) => Boolean(line));
56
+ const titles = headings.length > 0 ? headings : [input.title];
57
+ return {
58
+ prdTitle: input.title,
59
+ userStories: [],
60
+ functionalRequirements: [],
61
+ openQuestions: [],
62
+ tasks: titles.map((title, index) => ({
63
+ localId: `task-${index + 1}`,
64
+ title,
65
+ description: `Implement: ${title}`,
66
+ acceptance: [],
67
+ scope: [],
68
+ validationKeys: [],
69
+ dependsOn: index === 0 ? [] : [`task-${index}`],
70
+ parent: null,
71
+ parallelizable: index === 0
72
+ })),
73
+ generatedAt: now()
74
+ };
75
+ },
76
+ clarify: (planSpec) => planSpec,
77
+ plan: (planSpec) => planSpec
78
+ };
79
+ }
80
+ function readPrdText(args) {
81
+ const title = takeOption(args, "--title");
82
+ const text = takeOption(title.rest, "--text");
83
+ const prd = takeOption(text.rest, "--prd");
84
+ const body = text.value ?? (prd.value ? readFileSync(prd.value, "utf8") : null);
85
+ if (!body?.trim())
86
+ throw new Error("rig plan requires --text <prd> or --prd <file>.");
87
+ return { title: title.value?.trim() || "Rig generated plan", body, rest: prd.rest };
88
+ }
89
+ async function executePlan(context, args) {
90
+ const materialize = takeFlag(args, "--materialize");
91
+ const dryRun = takeFlag(materialize.rest, "--dry-run");
92
+ const json = takeFlag(dryRun.rest, "--json");
93
+ if (materialize.value && dryRun.value)
94
+ throw new Error("Pass only one of --materialize or --dry-run.");
95
+ const prd = readPrdText(json.rest);
96
+ requireNoExtraArgs(prd.rest, "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]");
97
+ const willMaterialize = materialize.value && !dryRun.value;
98
+ const now = () => new Date().toISOString();
99
+ const provider = defaultPlanningProvider(now);
100
+ const result = await planWorkspace(context.projectRoot, prd.body, {
101
+ generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
102
+ createTask
103
+ }, { title: prd.title, materialize: willMaterialize });
104
+ if (context.outputMode === "text") {
105
+ if (json.value)
106
+ printJson(result);
107
+ else
108
+ console.log(result.spec.tasks.map((task) => `${task.localId} ${task.title}`).join(`
109
+ `) || "No plan tasks.");
110
+ }
111
+ return { ok: true, group: "plan", command: willMaterialize ? "materialize" : "show", details: result };
112
+ }
113
+ var planningCliCommands = [
114
+ {
115
+ id: PLANNING_PLAN_CLI_ID,
116
+ family: "plan",
117
+ command: "rig plan --text <prd>|--prd <file>",
118
+ description: "Generate a plan from a PRD and optionally materialize task-source tasks.",
119
+ usage: "rig plan --text <prd>|--prd <file> [--title <title>] [--materialize|--dry-run] [--json]",
120
+ projectRequired: true,
121
+ run: executePlan
122
+ }
123
+ ];
124
+
125
+ // packages/planning-plugin/src/plugin.ts
4
126
  var PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
127
+ var PLANNING_PLAN_PANEL_ID = "plan-intake";
5
128
  var planningPlugin = definePlugin({
6
129
  name: PLANNING_PLUGIN_NAME,
7
130
  version: "0.0.0-alpha.1",
8
131
  provides: [],
9
132
  requires: [],
10
133
  contributes: {
11
- cliCommands: [
12
- {
13
- id: "planning.materialize",
14
- command: "rig plan materialize",
15
- description: "Generate a plan from a PRD and materialize idempotent task-source tasks."
16
- }
17
- ]
134
+ capabilities: [
135
+ { id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
136
+ ],
137
+ panels: [
138
+ { id: PLANNING_PLAN_PANEL_ID, slot: "capability", title: "Plan intake", capabilityId: "planning.plan" }
139
+ ],
140
+ cliCommands: planningCliCommands.map(({ run: _run, ...metadata }) => metadata)
18
141
  }
142
+ }, {
143
+ featureCapabilities: [
144
+ { id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
145
+ ],
146
+ cliCommands: planningCliCommands
19
147
  });
148
+ function createPlanningPlugin() {
149
+ return planningPlugin;
150
+ }
20
151
  var plugin_default = planningPlugin;
21
152
  export {
22
153
  planningPlugin,
23
154
  plugin_default as default,
24
- PLANNING_PLUGIN_NAME
155
+ createPlanningPlugin,
156
+ PLANNING_PLUGIN_NAME,
157
+ PLANNING_PLAN_PANEL_ID
25
158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/planning-plugin",
3
- "version": "0.0.6-alpha.136",
3
+ "version": "0.0.6-alpha.137",
4
4
  "type": "module",
5
5
  "description": "First-party PRD-to-plan plugin for Rig task sources.",
6
6
  "license": "UNLICENSED",
@@ -29,7 +29,8 @@
29
29
  "module": "./dist/src/index.js",
30
30
  "types": "./dist/src/index.d.ts",
31
31
  "dependencies": {
32
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.136",
33
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.136"
32
+ "@rig/client": "npm:@h-rig/client@0.0.6-alpha.137",
33
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.137",
34
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.137"
34
35
  }
35
36
  }