@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.
- package/dist/src/cli.d.ts +19 -0
- package/dist/src/cli.js +125 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +125 -80
- package/dist/src/planning.d.ts +0 -4
- package/dist/src/planning.js +0 -72
- package/dist/src/plugin.d.ts +2 -0
- package/dist/src/plugin.js +141 -8
- package/package.json +4 -3
|
@@ -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 {};
|
package/dist/src/cli.js
ADDED
|
@@ -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
|
+
};
|
package/dist/src/index.d.ts
CHANGED
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
|
-
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/src/planning.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/planning.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
|
}
|
|
@@ -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
|
};
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
33
|
-
"@rig/
|
|
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
|
}
|