@h-rig/planning-plugin 0.0.6-alpha.155 → 0.0.6-alpha.157
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.js +84 -4
- package/dist/src/index.js +87 -10
- package/dist/src/plugin.d.ts +2 -2
- package/dist/src/plugin.js +85 -10
- package/dist/src/workspace-planning.d.ts +39 -0
- package/dist/src/workspace-planning.js +85 -0
- package/package.json +3 -4
package/dist/src/cli.js
CHANGED
|
@@ -18,6 +18,85 @@ async function specClarifyPlan(input, context, ports) {
|
|
|
18
18
|
return plan(await clarify(await spec(input, ports), context, ports), ports);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// packages/planning-plugin/src/workspace-planning.ts
|
|
22
|
+
function taskByLocalId(spec2) {
|
|
23
|
+
return new Map(spec2.tasks.map((task) => [task.localId, task]));
|
|
24
|
+
}
|
|
25
|
+
function planTaskCreationOrder(spec2) {
|
|
26
|
+
const tasks = taskByLocalId(spec2);
|
|
27
|
+
const visiting = new Set;
|
|
28
|
+
const visited = new Set;
|
|
29
|
+
const ordered = [];
|
|
30
|
+
const visit = (task, stack) => {
|
|
31
|
+
if (visited.has(task.localId))
|
|
32
|
+
return;
|
|
33
|
+
if (visiting.has(task.localId))
|
|
34
|
+
throw new Error(`Plan task cycle detected: ${[...stack, task.localId].join(" -> ")}`);
|
|
35
|
+
visiting.add(task.localId);
|
|
36
|
+
const refs = [...task.dependsOn, ...task.parent ? [task.parent] : []];
|
|
37
|
+
for (const ref of refs) {
|
|
38
|
+
const dependency = tasks.get(ref);
|
|
39
|
+
if (dependency)
|
|
40
|
+
visit(dependency, [...stack, task.localId]);
|
|
41
|
+
}
|
|
42
|
+
visiting.delete(task.localId);
|
|
43
|
+
visited.add(task.localId);
|
|
44
|
+
ordered.push(task);
|
|
45
|
+
};
|
|
46
|
+
for (const task of spec2.tasks)
|
|
47
|
+
visit(task, []);
|
|
48
|
+
return ordered;
|
|
49
|
+
}
|
|
50
|
+
function taskPayload(task, localIdToTaskId) {
|
|
51
|
+
return {
|
|
52
|
+
title: task.title,
|
|
53
|
+
description: task.description,
|
|
54
|
+
acceptance: [...task.acceptance],
|
|
55
|
+
scope: [...task.scope],
|
|
56
|
+
validationKeys: [...task.validationKeys],
|
|
57
|
+
dependencies: task.dependsOn.map((id) => localIdToTaskId.get(id) ?? id),
|
|
58
|
+
parent: task.parent ? localIdToTaskId.get(task.parent) ?? task.parent : null,
|
|
59
|
+
metadata: { localId: task.localId, parallelizable: task.parallelizable }
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function runPlanningDryRun(projectRoot, prd, deps, options = {}) {
|
|
63
|
+
return deps.generatePlan({ projectRoot, prd, ...options.title !== undefined ? { title: options.title } : {} });
|
|
64
|
+
}
|
|
65
|
+
async function materializePlanSpec(projectRoot, spec2, deps, options = {}) {
|
|
66
|
+
if (!deps.createTask)
|
|
67
|
+
throw new Error("materializePlanSpec requires createTask.");
|
|
68
|
+
const localIdToTaskId = new Map(options.existingLocalIdToTaskId ?? []);
|
|
69
|
+
const created = [];
|
|
70
|
+
for (const task of planTaskCreationOrder(spec2)) {
|
|
71
|
+
if (localIdToTaskId.has(task.localId))
|
|
72
|
+
continue;
|
|
73
|
+
const result = await deps.createTask(projectRoot, taskPayload(task, localIdToTaskId));
|
|
74
|
+
if (result.taskId)
|
|
75
|
+
localIdToTaskId.set(task.localId, result.taskId);
|
|
76
|
+
created.push({ localId: task.localId, taskId: result.taskId });
|
|
77
|
+
}
|
|
78
|
+
if (deps.updateTask) {
|
|
79
|
+
for (const task of spec2.tasks) {
|
|
80
|
+
const taskId = localIdToTaskId.get(task.localId);
|
|
81
|
+
if (!taskId)
|
|
82
|
+
continue;
|
|
83
|
+
await deps.updateTask(projectRoot, taskId, taskPayload(task, localIdToTaskId));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
created,
|
|
88
|
+
localIdToTaskId,
|
|
89
|
+
deferredQuestions: spec2.openQuestions.filter((question) => question.status === "deferred" || question.status === "open").map((question) => question.id)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function planWorkspace(projectRoot, prd, deps, options = {}) {
|
|
93
|
+
const spec2 = await runPlanningDryRun(projectRoot, prd, deps, options.title !== undefined ? { title: options.title } : {});
|
|
94
|
+
return {
|
|
95
|
+
spec: spec2,
|
|
96
|
+
materialized: options.materialize ? await materializePlanSpec(projectRoot, spec2, deps, options.existingLocalIdToTaskId !== undefined ? { existingLocalIdToTaskId: options.existingLocalIdToTaskId } : {}) : null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
21
100
|
// packages/planning-plugin/src/cli.ts
|
|
22
101
|
var PLANNING_PLAN_CLI_ID = "planning.plan";
|
|
23
102
|
function printJson(value) {
|
|
@@ -142,8 +221,9 @@ function defaultPlanningProvider(now) {
|
|
|
142
221
|
plan: (planSpec) => planSpec
|
|
143
222
|
};
|
|
144
223
|
}
|
|
145
|
-
async function
|
|
146
|
-
|
|
224
|
+
async function loadPlanningClientIo() {
|
|
225
|
+
const { createTask, getTask } = await import("@rig/core/task-io");
|
|
226
|
+
return { createTask, getTask };
|
|
147
227
|
}
|
|
148
228
|
async function readPrdText(context, args) {
|
|
149
229
|
const title = takeOption(args, "--title");
|
|
@@ -154,7 +234,7 @@ async function readPrdText(context, args) {
|
|
|
154
234
|
if (sources.length !== 1)
|
|
155
235
|
throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
|
|
156
236
|
if (issue.value) {
|
|
157
|
-
const { getTask } = await
|
|
237
|
+
const { getTask } = await loadPlanningClientIo();
|
|
158
238
|
const task = await getTask(context.projectRoot, issue.value);
|
|
159
239
|
if (!task)
|
|
160
240
|
throw new Error(`No task found for issue ${issue.value}.`);
|
|
@@ -180,7 +260,7 @@ async function executePlan(context, args) {
|
|
|
180
260
|
const willMaterialize = materialize.value && !dryRun.value;
|
|
181
261
|
const now = () => new Date().toISOString();
|
|
182
262
|
const provider = defaultPlanningProvider(now);
|
|
183
|
-
const { createTask
|
|
263
|
+
const { createTask } = await loadPlanningClientIo();
|
|
184
264
|
const result = await planWorkspace(context.projectRoot, prd.body, {
|
|
185
265
|
generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
|
|
186
266
|
createTask
|
package/dist/src/index.js
CHANGED
|
@@ -119,6 +119,87 @@ async function materialize(planSpec, source, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
// packages/planning-plugin/src/cli.ts
|
|
121
121
|
import { readFileSync } from "fs";
|
|
122
|
+
|
|
123
|
+
// packages/planning-plugin/src/workspace-planning.ts
|
|
124
|
+
function taskByLocalId2(spec2) {
|
|
125
|
+
return new Map(spec2.tasks.map((task) => [task.localId, task]));
|
|
126
|
+
}
|
|
127
|
+
function planTaskCreationOrder(spec2) {
|
|
128
|
+
const tasks = taskByLocalId2(spec2);
|
|
129
|
+
const visiting = new Set;
|
|
130
|
+
const visited = new Set;
|
|
131
|
+
const ordered = [];
|
|
132
|
+
const visit = (task, stack) => {
|
|
133
|
+
if (visited.has(task.localId))
|
|
134
|
+
return;
|
|
135
|
+
if (visiting.has(task.localId))
|
|
136
|
+
throw new Error(`Plan task cycle detected: ${[...stack, task.localId].join(" -> ")}`);
|
|
137
|
+
visiting.add(task.localId);
|
|
138
|
+
const refs = [...task.dependsOn, ...task.parent ? [task.parent] : []];
|
|
139
|
+
for (const ref of refs) {
|
|
140
|
+
const dependency = tasks.get(ref);
|
|
141
|
+
if (dependency)
|
|
142
|
+
visit(dependency, [...stack, task.localId]);
|
|
143
|
+
}
|
|
144
|
+
visiting.delete(task.localId);
|
|
145
|
+
visited.add(task.localId);
|
|
146
|
+
ordered.push(task);
|
|
147
|
+
};
|
|
148
|
+
for (const task of spec2.tasks)
|
|
149
|
+
visit(task, []);
|
|
150
|
+
return ordered;
|
|
151
|
+
}
|
|
152
|
+
function taskPayload(task, localIdToTaskId) {
|
|
153
|
+
return {
|
|
154
|
+
title: task.title,
|
|
155
|
+
description: task.description,
|
|
156
|
+
acceptance: [...task.acceptance],
|
|
157
|
+
scope: [...task.scope],
|
|
158
|
+
validationKeys: [...task.validationKeys],
|
|
159
|
+
dependencies: task.dependsOn.map((id) => localIdToTaskId.get(id) ?? id),
|
|
160
|
+
parent: task.parent ? localIdToTaskId.get(task.parent) ?? task.parent : null,
|
|
161
|
+
metadata: { localId: task.localId, parallelizable: task.parallelizable }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function runPlanningDryRun(projectRoot, prd, deps, options = {}) {
|
|
165
|
+
return deps.generatePlan({ projectRoot, prd, ...options.title !== undefined ? { title: options.title } : {} });
|
|
166
|
+
}
|
|
167
|
+
async function materializePlanSpec(projectRoot, spec2, deps, options = {}) {
|
|
168
|
+
if (!deps.createTask)
|
|
169
|
+
throw new Error("materializePlanSpec requires createTask.");
|
|
170
|
+
const localIdToTaskId = new Map(options.existingLocalIdToTaskId ?? []);
|
|
171
|
+
const created = [];
|
|
172
|
+
for (const task of planTaskCreationOrder(spec2)) {
|
|
173
|
+
if (localIdToTaskId.has(task.localId))
|
|
174
|
+
continue;
|
|
175
|
+
const result = await deps.createTask(projectRoot, taskPayload(task, localIdToTaskId));
|
|
176
|
+
if (result.taskId)
|
|
177
|
+
localIdToTaskId.set(task.localId, result.taskId);
|
|
178
|
+
created.push({ localId: task.localId, taskId: result.taskId });
|
|
179
|
+
}
|
|
180
|
+
if (deps.updateTask) {
|
|
181
|
+
for (const task of spec2.tasks) {
|
|
182
|
+
const taskId = localIdToTaskId.get(task.localId);
|
|
183
|
+
if (!taskId)
|
|
184
|
+
continue;
|
|
185
|
+
await deps.updateTask(projectRoot, taskId, taskPayload(task, localIdToTaskId));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
created,
|
|
190
|
+
localIdToTaskId,
|
|
191
|
+
deferredQuestions: spec2.openQuestions.filter((question) => question.status === "deferred" || question.status === "open").map((question) => question.id)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function planWorkspace(projectRoot, prd, deps, options = {}) {
|
|
195
|
+
const spec2 = await runPlanningDryRun(projectRoot, prd, deps, options.title !== undefined ? { title: options.title } : {});
|
|
196
|
+
return {
|
|
197
|
+
spec: spec2,
|
|
198
|
+
materialized: options.materialize ? await materializePlanSpec(projectRoot, spec2, deps, options.existingLocalIdToTaskId !== undefined ? { existingLocalIdToTaskId: options.existingLocalIdToTaskId } : {}) : null
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// packages/planning-plugin/src/cli.ts
|
|
122
203
|
var PLANNING_PLAN_CLI_ID = "planning.plan";
|
|
123
204
|
function printJson(value) {
|
|
124
205
|
console.log(JSON.stringify(value, null, 2));
|
|
@@ -242,8 +323,9 @@ function defaultPlanningProvider(now) {
|
|
|
242
323
|
plan: (planSpec) => planSpec
|
|
243
324
|
};
|
|
244
325
|
}
|
|
245
|
-
async function
|
|
246
|
-
|
|
326
|
+
async function loadPlanningClientIo() {
|
|
327
|
+
const { createTask, getTask } = await import("@rig/core/task-io");
|
|
328
|
+
return { createTask, getTask };
|
|
247
329
|
}
|
|
248
330
|
async function readPrdText(context, args) {
|
|
249
331
|
const title = takeOption(args, "--title");
|
|
@@ -254,7 +336,7 @@ async function readPrdText(context, args) {
|
|
|
254
336
|
if (sources.length !== 1)
|
|
255
337
|
throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
|
|
256
338
|
if (issue.value) {
|
|
257
|
-
const { getTask } = await
|
|
339
|
+
const { getTask } = await loadPlanningClientIo();
|
|
258
340
|
const task = await getTask(context.projectRoot, issue.value);
|
|
259
341
|
if (!task)
|
|
260
342
|
throw new Error(`No task found for issue ${issue.value}.`);
|
|
@@ -280,7 +362,7 @@ async function executePlan(context, args) {
|
|
|
280
362
|
const willMaterialize = materialize2.value && !dryRun.value;
|
|
281
363
|
const now = () => new Date().toISOString();
|
|
282
364
|
const provider = defaultPlanningProvider(now);
|
|
283
|
-
const { createTask
|
|
365
|
+
const { createTask } = await loadPlanningClientIo();
|
|
284
366
|
const result = await planWorkspace(context.projectRoot, prd.body, {
|
|
285
367
|
generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
|
|
286
368
|
createTask
|
|
@@ -321,13 +403,8 @@ var planningPlugin = definePlugin({
|
|
|
321
403
|
panels: [
|
|
322
404
|
{ id: PLANNING_PLAN_PANEL_ID, slot: "capability", title: "Plan intake", capabilityId: "planning.plan" }
|
|
323
405
|
],
|
|
324
|
-
cliCommands: planningCliCommands
|
|
406
|
+
cliCommands: planningCliCommands
|
|
325
407
|
}
|
|
326
|
-
}, {
|
|
327
|
-
featureCapabilities: [
|
|
328
|
-
{ id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
|
|
329
|
-
],
|
|
330
|
-
cliCommands: planningCliCommands
|
|
331
408
|
});
|
|
332
409
|
function createPlanningPlugin() {
|
|
333
410
|
return planningPlugin;
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
|
|
2
2
|
export declare const PLANNING_PLAN_PANEL_ID = "plan-intake";
|
|
3
|
-
export declare const planningPlugin: import("@rig/core").
|
|
4
|
-
export declare function createPlanningPlugin(): import("@rig/core").
|
|
3
|
+
export declare const planningPlugin: import("@rig/core/config").RigPlugin;
|
|
4
|
+
export declare function createPlanningPlugin(): import("@rig/core/config").RigPlugin;
|
|
5
5
|
export default planningPlugin;
|
package/dist/src/plugin.js
CHANGED
|
@@ -21,6 +21,85 @@ async function specClarifyPlan(input, context, ports) {
|
|
|
21
21
|
return plan(await clarify(await spec(input, ports), context, ports), ports);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// packages/planning-plugin/src/workspace-planning.ts
|
|
25
|
+
function taskByLocalId(spec2) {
|
|
26
|
+
return new Map(spec2.tasks.map((task) => [task.localId, task]));
|
|
27
|
+
}
|
|
28
|
+
function planTaskCreationOrder(spec2) {
|
|
29
|
+
const tasks = taskByLocalId(spec2);
|
|
30
|
+
const visiting = new Set;
|
|
31
|
+
const visited = new Set;
|
|
32
|
+
const ordered = [];
|
|
33
|
+
const visit = (task, stack) => {
|
|
34
|
+
if (visited.has(task.localId))
|
|
35
|
+
return;
|
|
36
|
+
if (visiting.has(task.localId))
|
|
37
|
+
throw new Error(`Plan task cycle detected: ${[...stack, task.localId].join(" -> ")}`);
|
|
38
|
+
visiting.add(task.localId);
|
|
39
|
+
const refs = [...task.dependsOn, ...task.parent ? [task.parent] : []];
|
|
40
|
+
for (const ref of refs) {
|
|
41
|
+
const dependency = tasks.get(ref);
|
|
42
|
+
if (dependency)
|
|
43
|
+
visit(dependency, [...stack, task.localId]);
|
|
44
|
+
}
|
|
45
|
+
visiting.delete(task.localId);
|
|
46
|
+
visited.add(task.localId);
|
|
47
|
+
ordered.push(task);
|
|
48
|
+
};
|
|
49
|
+
for (const task of spec2.tasks)
|
|
50
|
+
visit(task, []);
|
|
51
|
+
return ordered;
|
|
52
|
+
}
|
|
53
|
+
function taskPayload(task, localIdToTaskId) {
|
|
54
|
+
return {
|
|
55
|
+
title: task.title,
|
|
56
|
+
description: task.description,
|
|
57
|
+
acceptance: [...task.acceptance],
|
|
58
|
+
scope: [...task.scope],
|
|
59
|
+
validationKeys: [...task.validationKeys],
|
|
60
|
+
dependencies: task.dependsOn.map((id) => localIdToTaskId.get(id) ?? id),
|
|
61
|
+
parent: task.parent ? localIdToTaskId.get(task.parent) ?? task.parent : null,
|
|
62
|
+
metadata: { localId: task.localId, parallelizable: task.parallelizable }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function runPlanningDryRun(projectRoot, prd, deps, options = {}) {
|
|
66
|
+
return deps.generatePlan({ projectRoot, prd, ...options.title !== undefined ? { title: options.title } : {} });
|
|
67
|
+
}
|
|
68
|
+
async function materializePlanSpec(projectRoot, spec2, deps, options = {}) {
|
|
69
|
+
if (!deps.createTask)
|
|
70
|
+
throw new Error("materializePlanSpec requires createTask.");
|
|
71
|
+
const localIdToTaskId = new Map(options.existingLocalIdToTaskId ?? []);
|
|
72
|
+
const created = [];
|
|
73
|
+
for (const task of planTaskCreationOrder(spec2)) {
|
|
74
|
+
if (localIdToTaskId.has(task.localId))
|
|
75
|
+
continue;
|
|
76
|
+
const result = await deps.createTask(projectRoot, taskPayload(task, localIdToTaskId));
|
|
77
|
+
if (result.taskId)
|
|
78
|
+
localIdToTaskId.set(task.localId, result.taskId);
|
|
79
|
+
created.push({ localId: task.localId, taskId: result.taskId });
|
|
80
|
+
}
|
|
81
|
+
if (deps.updateTask) {
|
|
82
|
+
for (const task of spec2.tasks) {
|
|
83
|
+
const taskId = localIdToTaskId.get(task.localId);
|
|
84
|
+
if (!taskId)
|
|
85
|
+
continue;
|
|
86
|
+
await deps.updateTask(projectRoot, taskId, taskPayload(task, localIdToTaskId));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
created,
|
|
91
|
+
localIdToTaskId,
|
|
92
|
+
deferredQuestions: spec2.openQuestions.filter((question) => question.status === "deferred" || question.status === "open").map((question) => question.id)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function planWorkspace(projectRoot, prd, deps, options = {}) {
|
|
96
|
+
const spec2 = await runPlanningDryRun(projectRoot, prd, deps, options.title !== undefined ? { title: options.title } : {});
|
|
97
|
+
return {
|
|
98
|
+
spec: spec2,
|
|
99
|
+
materialized: options.materialize ? await materializePlanSpec(projectRoot, spec2, deps, options.existingLocalIdToTaskId !== undefined ? { existingLocalIdToTaskId: options.existingLocalIdToTaskId } : {}) : null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
24
103
|
// packages/planning-plugin/src/cli.ts
|
|
25
104
|
var PLANNING_PLAN_CLI_ID = "planning.plan";
|
|
26
105
|
function printJson(value) {
|
|
@@ -145,8 +224,9 @@ function defaultPlanningProvider(now) {
|
|
|
145
224
|
plan: (planSpec) => planSpec
|
|
146
225
|
};
|
|
147
226
|
}
|
|
148
|
-
async function
|
|
149
|
-
|
|
227
|
+
async function loadPlanningClientIo() {
|
|
228
|
+
const { createTask, getTask } = await import("@rig/core/task-io");
|
|
229
|
+
return { createTask, getTask };
|
|
150
230
|
}
|
|
151
231
|
async function readPrdText(context, args) {
|
|
152
232
|
const title = takeOption(args, "--title");
|
|
@@ -157,7 +237,7 @@ async function readPrdText(context, args) {
|
|
|
157
237
|
if (sources.length !== 1)
|
|
158
238
|
throw new Error("rig plan requires exactly one of --text <prd>, --prd <file>, or --issue <id>.");
|
|
159
239
|
if (issue.value) {
|
|
160
|
-
const { getTask } = await
|
|
240
|
+
const { getTask } = await loadPlanningClientIo();
|
|
161
241
|
const task = await getTask(context.projectRoot, issue.value);
|
|
162
242
|
if (!task)
|
|
163
243
|
throw new Error(`No task found for issue ${issue.value}.`);
|
|
@@ -183,7 +263,7 @@ async function executePlan(context, args) {
|
|
|
183
263
|
const willMaterialize = materialize.value && !dryRun.value;
|
|
184
264
|
const now = () => new Date().toISOString();
|
|
185
265
|
const provider = defaultPlanningProvider(now);
|
|
186
|
-
const { createTask
|
|
266
|
+
const { createTask } = await loadPlanningClientIo();
|
|
187
267
|
const result = await planWorkspace(context.projectRoot, prd.body, {
|
|
188
268
|
generatePlan: (input) => specClarifyPlan({ title: input.title ?? prd.title, body: input.prd }, {}, provider),
|
|
189
269
|
createTask
|
|
@@ -224,13 +304,8 @@ var planningPlugin = definePlugin({
|
|
|
224
304
|
panels: [
|
|
225
305
|
{ id: PLANNING_PLAN_PANEL_ID, slot: "capability", title: "Plan intake", capabilityId: "planning.plan" }
|
|
226
306
|
],
|
|
227
|
-
cliCommands: planningCliCommands
|
|
307
|
+
cliCommands: planningCliCommands
|
|
228
308
|
}
|
|
229
|
-
}, {
|
|
230
|
-
featureCapabilities: [
|
|
231
|
-
{ id: "planning.plan", title: "PRD-to-task planning", commandId: PLANNING_PLAN_CLI_ID, panelId: PLANNING_PLAN_PANEL_ID }
|
|
232
|
-
],
|
|
233
|
-
cliCommands: planningCliCommands
|
|
234
309
|
});
|
|
235
310
|
function createPlanningPlugin() {
|
|
236
311
|
return planningPlugin;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PlanSpec, PlanTask } from "@rig/contracts";
|
|
2
|
+
export interface PlanningDeps {
|
|
3
|
+
readonly generatePlan: (input: PlanningInput) => Promise<PlanSpec> | PlanSpec;
|
|
4
|
+
readonly createTask?: (projectRoot: string, task: Record<string, unknown>) => Promise<{
|
|
5
|
+
readonly taskId: string | null;
|
|
6
|
+
readonly result?: unknown;
|
|
7
|
+
}>;
|
|
8
|
+
readonly updateTask?: (projectRoot: string, taskId: string, patch: Record<string, unknown>) => Promise<unknown>;
|
|
9
|
+
readonly now?: () => string;
|
|
10
|
+
}
|
|
11
|
+
export interface PlanningInput {
|
|
12
|
+
readonly projectRoot: string;
|
|
13
|
+
readonly prd: string;
|
|
14
|
+
readonly title?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkspaceMaterializePlanResult {
|
|
17
|
+
readonly created: readonly {
|
|
18
|
+
readonly localId: string;
|
|
19
|
+
readonly taskId: string | null;
|
|
20
|
+
}[];
|
|
21
|
+
readonly localIdToTaskId: ReadonlyMap<string, string>;
|
|
22
|
+
readonly deferredQuestions: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface PlanWorkspaceResult {
|
|
25
|
+
readonly spec: PlanSpec;
|
|
26
|
+
readonly materialized: WorkspaceMaterializePlanResult | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function planTaskCreationOrder(spec: PlanSpec): readonly PlanTask[];
|
|
29
|
+
export declare function runPlanningDryRun(projectRoot: string, prd: string, deps: Pick<PlanningDeps, "generatePlan">, options?: {
|
|
30
|
+
readonly title?: string;
|
|
31
|
+
}): Promise<PlanSpec>;
|
|
32
|
+
export declare function materializePlanSpec(projectRoot: string, spec: PlanSpec, deps: Pick<PlanningDeps, "createTask" | "updateTask">, options?: {
|
|
33
|
+
readonly existingLocalIdToTaskId?: ReadonlyMap<string, string>;
|
|
34
|
+
}): Promise<WorkspaceMaterializePlanResult>;
|
|
35
|
+
export declare function planWorkspace(projectRoot: string, prd: string, deps: PlanningDeps, options?: {
|
|
36
|
+
readonly title?: string;
|
|
37
|
+
readonly materialize?: boolean;
|
|
38
|
+
readonly existingLocalIdToTaskId?: ReadonlyMap<string, string>;
|
|
39
|
+
}): Promise<PlanWorkspaceResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/planning-plugin/src/workspace-planning.ts
|
|
3
|
+
function taskByLocalId(spec) {
|
|
4
|
+
return new Map(spec.tasks.map((task) => [task.localId, task]));
|
|
5
|
+
}
|
|
6
|
+
function planTaskCreationOrder(spec) {
|
|
7
|
+
const tasks = taskByLocalId(spec);
|
|
8
|
+
const visiting = new Set;
|
|
9
|
+
const visited = new Set;
|
|
10
|
+
const ordered = [];
|
|
11
|
+
const visit = (task, stack) => {
|
|
12
|
+
if (visited.has(task.localId))
|
|
13
|
+
return;
|
|
14
|
+
if (visiting.has(task.localId))
|
|
15
|
+
throw new Error(`Plan task cycle detected: ${[...stack, task.localId].join(" -> ")}`);
|
|
16
|
+
visiting.add(task.localId);
|
|
17
|
+
const refs = [...task.dependsOn, ...task.parent ? [task.parent] : []];
|
|
18
|
+
for (const ref of refs) {
|
|
19
|
+
const dependency = tasks.get(ref);
|
|
20
|
+
if (dependency)
|
|
21
|
+
visit(dependency, [...stack, task.localId]);
|
|
22
|
+
}
|
|
23
|
+
visiting.delete(task.localId);
|
|
24
|
+
visited.add(task.localId);
|
|
25
|
+
ordered.push(task);
|
|
26
|
+
};
|
|
27
|
+
for (const task of spec.tasks)
|
|
28
|
+
visit(task, []);
|
|
29
|
+
return ordered;
|
|
30
|
+
}
|
|
31
|
+
function taskPayload(task, localIdToTaskId) {
|
|
32
|
+
return {
|
|
33
|
+
title: task.title,
|
|
34
|
+
description: task.description,
|
|
35
|
+
acceptance: [...task.acceptance],
|
|
36
|
+
scope: [...task.scope],
|
|
37
|
+
validationKeys: [...task.validationKeys],
|
|
38
|
+
dependencies: task.dependsOn.map((id) => localIdToTaskId.get(id) ?? id),
|
|
39
|
+
parent: task.parent ? localIdToTaskId.get(task.parent) ?? task.parent : null,
|
|
40
|
+
metadata: { localId: task.localId, parallelizable: task.parallelizable }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function runPlanningDryRun(projectRoot, prd, deps, options = {}) {
|
|
44
|
+
return deps.generatePlan({ projectRoot, prd, ...options.title !== undefined ? { title: options.title } : {} });
|
|
45
|
+
}
|
|
46
|
+
async function materializePlanSpec(projectRoot, spec, deps, options = {}) {
|
|
47
|
+
if (!deps.createTask)
|
|
48
|
+
throw new Error("materializePlanSpec requires createTask.");
|
|
49
|
+
const localIdToTaskId = new Map(options.existingLocalIdToTaskId ?? []);
|
|
50
|
+
const created = [];
|
|
51
|
+
for (const task of planTaskCreationOrder(spec)) {
|
|
52
|
+
if (localIdToTaskId.has(task.localId))
|
|
53
|
+
continue;
|
|
54
|
+
const result = await deps.createTask(projectRoot, taskPayload(task, localIdToTaskId));
|
|
55
|
+
if (result.taskId)
|
|
56
|
+
localIdToTaskId.set(task.localId, result.taskId);
|
|
57
|
+
created.push({ localId: task.localId, taskId: result.taskId });
|
|
58
|
+
}
|
|
59
|
+
if (deps.updateTask) {
|
|
60
|
+
for (const task of spec.tasks) {
|
|
61
|
+
const taskId = localIdToTaskId.get(task.localId);
|
|
62
|
+
if (!taskId)
|
|
63
|
+
continue;
|
|
64
|
+
await deps.updateTask(projectRoot, taskId, taskPayload(task, localIdToTaskId));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
created,
|
|
69
|
+
localIdToTaskId,
|
|
70
|
+
deferredQuestions: spec.openQuestions.filter((question) => question.status === "deferred" || question.status === "open").map((question) => question.id)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function planWorkspace(projectRoot, prd, deps, options = {}) {
|
|
74
|
+
const spec = await runPlanningDryRun(projectRoot, prd, deps, options.title !== undefined ? { title: options.title } : {});
|
|
75
|
+
return {
|
|
76
|
+
spec,
|
|
77
|
+
materialized: options.materialize ? await materializePlanSpec(projectRoot, spec, deps, options.existingLocalIdToTaskId !== undefined ? { existingLocalIdToTaskId: options.existingLocalIdToTaskId } : {}) : null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
runPlanningDryRun,
|
|
82
|
+
planWorkspace,
|
|
83
|
+
planTaskCreationOrder,
|
|
84
|
+
materializePlanSpec
|
|
85
|
+
};
|
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.157",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "First-party PRD-to-plan plugin for Rig task sources.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
"module": "./dist/src/index.js",
|
|
30
30
|
"types": "./dist/src/index.d.ts",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@rig/
|
|
33
|
-
"@rig/
|
|
34
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.155"
|
|
32
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.157",
|
|
33
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.157"
|
|
35
34
|
}
|
|
36
35
|
}
|