@h-rig/planning-plugin 0.0.6-alpha.133

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/README.md ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/planning-plugin
@@ -0,0 +1,2 @@
1
+ export * from "./planning";
2
+ export * from "./plugin";
@@ -0,0 +1,144 @@
1
+ // @bun
2
+ // packages/planning-plugin/src/planning.ts
3
+ async function spec(input, ports) {
4
+ return ports.spec(input);
5
+ }
6
+ async function clarify(planSpec, context, ports) {
7
+ return ports.clarify(planSpec, context);
8
+ }
9
+ async function plan(planSpec, ports) {
10
+ return ports.plan(planSpec);
11
+ }
12
+ async function specClarifyPlan(input, context, ports) {
13
+ return plan(await clarify(await spec(input, ports), context, ports), ports);
14
+ }
15
+ function taskByLocalId(tasks) {
16
+ return new Map(tasks.map((task) => [task.localId, task]));
17
+ }
18
+ function topologicalTasks(tasks) {
19
+ const byLocalId = taskByLocalId(tasks);
20
+ const state = new Map;
21
+ const ordered = [];
22
+ const visit = (task, trail) => {
23
+ const current = state.get(task.localId);
24
+ if (current?.status === "visited")
25
+ return;
26
+ if (current?.status === "visiting")
27
+ throw new Error(`Planning task dependency cycle: ${[...trail, task.localId].join(" -> ")}`);
28
+ state.set(task.localId, { status: "visiting" });
29
+ const orderingRefs = task.parent ? [...task.dependsOn, task.parent] : task.dependsOn;
30
+ for (const dep of orderingRefs) {
31
+ const dependency = byLocalId.get(dep);
32
+ if (!dependency)
33
+ throw new Error(`Planning task ${task.localId} depends on missing task ${dep}`);
34
+ visit(dependency, [...trail, task.localId]);
35
+ }
36
+ state.set(task.localId, { status: "visited" });
37
+ ordered.push(task);
38
+ };
39
+ for (const task of tasks)
40
+ visit(task, []);
41
+ return ordered;
42
+ }
43
+ function recordValue(value, key) {
44
+ if (typeof value !== "object" || value === null || !(key in value))
45
+ return;
46
+ return value[key];
47
+ }
48
+ function planningMetadata(record) {
49
+ const metadata = recordValue(record, "metadata");
50
+ const rigPlanning = recordValue(metadata, "rigPlanning");
51
+ const localId = recordValue(rigPlanning, "localId");
52
+ const planId = recordValue(rigPlanning, "planId");
53
+ return {
54
+ ...typeof planId === "string" ? { planId } : {},
55
+ ...typeof localId === "string" ? { localId } : {}
56
+ };
57
+ }
58
+ function existingTasksByLocalId(records, planId) {
59
+ const entries = [];
60
+ for (const record of records) {
61
+ const metadata = planningMetadata(record);
62
+ if (metadata.localId && metadata.planId === planId)
63
+ entries.push([metadata.localId, record]);
64
+ }
65
+ return new Map(entries);
66
+ }
67
+ function taskBody(task) {
68
+ const acceptance = task.acceptance.length === 0 ? "- Complete the task as described." : task.acceptance.map((item) => `- ${item}`).join(`
69
+ `);
70
+ const validation = task.validationKeys.length === 0 ? "- Project-specific validation." : task.validationKeys.map((item) => `- ${item}`).join(`
71
+ `);
72
+ return [`${task.description}`, "", "Acceptance:", acceptance, "", "Validation:", validation].join(`
73
+ `);
74
+ }
75
+ function createInputForTask(task, planId, localIdToTaskId) {
76
+ const deps = task.dependsOn.map((dep) => localIdToTaskId.get(dep) ?? dep);
77
+ const parents = task.parent ? [localIdToTaskId.get(task.parent) ?? task.parent] : [];
78
+ return {
79
+ title: task.title,
80
+ body: taskBody(task),
81
+ deps,
82
+ parents,
83
+ metadata: {
84
+ rigPlanning: {
85
+ planId,
86
+ localId: task.localId,
87
+ dependsOn: task.dependsOn,
88
+ parent: task.parent,
89
+ parallelizable: task.parallelizable,
90
+ scope: task.scope,
91
+ validationKeys: task.validationKeys
92
+ }
93
+ }
94
+ };
95
+ }
96
+ async function materialize(planSpec, source, options = {}) {
97
+ if (!source.create)
98
+ throw new Error("Task source does not support idempotent task creation.");
99
+ const planId = options.planId ?? planSpec.prdTitle;
100
+ const existingRecords = await source.list();
101
+ const existingByLocalId = existingTasksByLocalId(existingRecords, planId);
102
+ const localIdToTaskId = new Map;
103
+ const existing = [];
104
+ const created = [];
105
+ for (const task of topologicalTasks(planSpec.tasks)) {
106
+ const existingRecord = existingByLocalId.get(task.localId);
107
+ if (existingRecord) {
108
+ localIdToTaskId.set(task.localId, existingRecord.id);
109
+ existing.push({ localId: task.localId, taskId: existingRecord.id, created: false });
110
+ continue;
111
+ }
112
+ const record = await source.create(createInputForTask(task, planId, localIdToTaskId));
113
+ localIdToTaskId.set(task.localId, record.id);
114
+ created.push({ localId: task.localId, taskId: record.id, created: true });
115
+ }
116
+ return { planId, created, existing, localIdToTaskId: Object.fromEntries(localIdToTaskId) };
117
+ }
118
+ // packages/planning-plugin/src/plugin.ts
119
+ import { definePlugin } from "@rig/core";
120
+ var PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
121
+ var planningPlugin = definePlugin({
122
+ name: PLANNING_PLUGIN_NAME,
123
+ version: "0.0.0-alpha.1",
124
+ provides: [],
125
+ requires: [],
126
+ contributes: {
127
+ cliCommands: [
128
+ {
129
+ id: "planning.materialize",
130
+ command: "rig plan materialize",
131
+ description: "Generate a plan from a PRD and materialize idempotent task-source tasks."
132
+ }
133
+ ]
134
+ }
135
+ });
136
+ export {
137
+ specClarifyPlan,
138
+ spec,
139
+ planningPlugin,
140
+ plan,
141
+ materialize,
142
+ clarify,
143
+ PLANNING_PLUGIN_NAME
144
+ };
@@ -0,0 +1,33 @@
1
+ import type { PlanSpec, RegisteredTaskSource } from "@rig/contracts";
2
+ export interface PrdInput {
3
+ readonly title: string;
4
+ readonly body: string;
5
+ readonly sourceRef?: string;
6
+ }
7
+ export interface PlanningProviderPorts {
8
+ spec(input: PrdInput): Promise<PlanSpec> | PlanSpec;
9
+ clarify(planSpec: PlanSpec, context: ClarificationContext): Promise<PlanSpec> | PlanSpec;
10
+ plan(planSpec: PlanSpec): Promise<PlanSpec> | PlanSpec;
11
+ }
12
+ export interface ClarificationContext {
13
+ readonly answers?: Readonly<Record<string, string>>;
14
+ readonly codeFacts?: readonly string[];
15
+ }
16
+ export interface MaterializedPlanTask {
17
+ readonly localId: string;
18
+ readonly taskId: string;
19
+ readonly created: boolean;
20
+ }
21
+ export interface MaterializePlanResult {
22
+ readonly planId: string;
23
+ readonly created: readonly MaterializedPlanTask[];
24
+ readonly existing: readonly MaterializedPlanTask[];
25
+ readonly localIdToTaskId: Readonly<Record<string, string>>;
26
+ }
27
+ export declare function spec(input: PrdInput, ports: Pick<PlanningProviderPorts, "spec">): Promise<PlanSpec>;
28
+ export declare function clarify(planSpec: PlanSpec, context: ClarificationContext, ports: Pick<PlanningProviderPorts, "clarify">): Promise<PlanSpec>;
29
+ export declare function plan(planSpec: PlanSpec, ports: Pick<PlanningProviderPorts, "plan">): Promise<PlanSpec>;
30
+ export declare function specClarifyPlan(input: PrdInput, context: ClarificationContext, ports: PlanningProviderPorts): Promise<PlanSpec>;
31
+ export declare function materialize(planSpec: PlanSpec, source: Pick<RegisteredTaskSource, "list" | "create">, options?: {
32
+ readonly planId?: string;
33
+ }): Promise<MaterializePlanResult>;
@@ -0,0 +1,124 @@
1
+ // @bun
2
+ // packages/planning-plugin/src/planning.ts
3
+ async function spec(input, ports) {
4
+ return ports.spec(input);
5
+ }
6
+ async function clarify(planSpec, context, ports) {
7
+ return ports.clarify(planSpec, context);
8
+ }
9
+ async function plan(planSpec, ports) {
10
+ return ports.plan(planSpec);
11
+ }
12
+ async function specClarifyPlan(input, context, ports) {
13
+ return plan(await clarify(await spec(input, ports), context, ports), ports);
14
+ }
15
+ function taskByLocalId(tasks) {
16
+ return new Map(tasks.map((task) => [task.localId, task]));
17
+ }
18
+ function topologicalTasks(tasks) {
19
+ const byLocalId = taskByLocalId(tasks);
20
+ const state = new Map;
21
+ const ordered = [];
22
+ const visit = (task, trail) => {
23
+ const current = state.get(task.localId);
24
+ if (current?.status === "visited")
25
+ return;
26
+ if (current?.status === "visiting")
27
+ throw new Error(`Planning task dependency cycle: ${[...trail, task.localId].join(" -> ")}`);
28
+ state.set(task.localId, { status: "visiting" });
29
+ const orderingRefs = task.parent ? [...task.dependsOn, task.parent] : task.dependsOn;
30
+ for (const dep of orderingRefs) {
31
+ const dependency = byLocalId.get(dep);
32
+ if (!dependency)
33
+ throw new Error(`Planning task ${task.localId} depends on missing task ${dep}`);
34
+ visit(dependency, [...trail, task.localId]);
35
+ }
36
+ state.set(task.localId, { status: "visited" });
37
+ ordered.push(task);
38
+ };
39
+ for (const task of tasks)
40
+ visit(task, []);
41
+ return ordered;
42
+ }
43
+ function recordValue(value, key) {
44
+ if (typeof value !== "object" || value === null || !(key in value))
45
+ return;
46
+ return value[key];
47
+ }
48
+ function planningMetadata(record) {
49
+ const metadata = recordValue(record, "metadata");
50
+ const rigPlanning = recordValue(metadata, "rigPlanning");
51
+ const localId = recordValue(rigPlanning, "localId");
52
+ const planId = recordValue(rigPlanning, "planId");
53
+ return {
54
+ ...typeof planId === "string" ? { planId } : {},
55
+ ...typeof localId === "string" ? { localId } : {}
56
+ };
57
+ }
58
+ function existingTasksByLocalId(records, planId) {
59
+ const entries = [];
60
+ for (const record of records) {
61
+ const metadata = planningMetadata(record);
62
+ if (metadata.localId && metadata.planId === planId)
63
+ entries.push([metadata.localId, record]);
64
+ }
65
+ return new Map(entries);
66
+ }
67
+ function taskBody(task) {
68
+ const acceptance = task.acceptance.length === 0 ? "- Complete the task as described." : task.acceptance.map((item) => `- ${item}`).join(`
69
+ `);
70
+ const validation = task.validationKeys.length === 0 ? "- Project-specific validation." : task.validationKeys.map((item) => `- ${item}`).join(`
71
+ `);
72
+ return [`${task.description}`, "", "Acceptance:", acceptance, "", "Validation:", validation].join(`
73
+ `);
74
+ }
75
+ function createInputForTask(task, planId, localIdToTaskId) {
76
+ const deps = task.dependsOn.map((dep) => localIdToTaskId.get(dep) ?? dep);
77
+ const parents = task.parent ? [localIdToTaskId.get(task.parent) ?? task.parent] : [];
78
+ return {
79
+ title: task.title,
80
+ body: taskBody(task),
81
+ deps,
82
+ parents,
83
+ metadata: {
84
+ rigPlanning: {
85
+ planId,
86
+ localId: task.localId,
87
+ dependsOn: task.dependsOn,
88
+ parent: task.parent,
89
+ parallelizable: task.parallelizable,
90
+ scope: task.scope,
91
+ validationKeys: task.validationKeys
92
+ }
93
+ }
94
+ };
95
+ }
96
+ async function materialize(planSpec, source, options = {}) {
97
+ if (!source.create)
98
+ throw new Error("Task source does not support idempotent task creation.");
99
+ const planId = options.planId ?? planSpec.prdTitle;
100
+ const existingRecords = await source.list();
101
+ const existingByLocalId = existingTasksByLocalId(existingRecords, planId);
102
+ const localIdToTaskId = new Map;
103
+ const existing = [];
104
+ const created = [];
105
+ for (const task of topologicalTasks(planSpec.tasks)) {
106
+ const existingRecord = existingByLocalId.get(task.localId);
107
+ if (existingRecord) {
108
+ localIdToTaskId.set(task.localId, existingRecord.id);
109
+ existing.push({ localId: task.localId, taskId: existingRecord.id, created: false });
110
+ continue;
111
+ }
112
+ const record = await source.create(createInputForTask(task, planId, localIdToTaskId));
113
+ localIdToTaskId.set(task.localId, record.id);
114
+ created.push({ localId: task.localId, taskId: record.id, created: true });
115
+ }
116
+ return { planId, created, existing, localIdToTaskId: Object.fromEntries(localIdToTaskId) };
117
+ }
118
+ export {
119
+ specClarifyPlan,
120
+ spec,
121
+ plan,
122
+ materialize,
123
+ clarify
124
+ };
@@ -0,0 +1,3 @@
1
+ export declare const PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
2
+ export declare const planningPlugin: import("@rig/core").RigPluginWithRuntime;
3
+ export default planningPlugin;
@@ -0,0 +1,25 @@
1
+ // @bun
2
+ // packages/planning-plugin/src/plugin.ts
3
+ import { definePlugin } from "@rig/core";
4
+ var PLANNING_PLUGIN_NAME = "@rig/planning-plugin";
5
+ var planningPlugin = definePlugin({
6
+ name: PLANNING_PLUGIN_NAME,
7
+ version: "0.0.0-alpha.1",
8
+ provides: [],
9
+ requires: [],
10
+ 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
+ ]
18
+ }
19
+ });
20
+ var plugin_default = planningPlugin;
21
+ export {
22
+ planningPlugin,
23
+ plugin_default as default,
24
+ PLANNING_PLUGIN_NAME
25
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@h-rig/planning-plugin",
3
+ "version": "0.0.6-alpha.133",
4
+ "type": "module",
5
+ "description": "First-party PRD-to-plan plugin for Rig task sources.",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/index.d.ts",
14
+ "import": "./dist/src/index.js"
15
+ },
16
+ "./planning": {
17
+ "types": "./dist/src/planning.d.ts",
18
+ "import": "./dist/src/planning.js"
19
+ },
20
+ "./plugin": {
21
+ "types": "./dist/src/plugin.d.ts",
22
+ "import": "./dist/src/plugin.js"
23
+ }
24
+ },
25
+ "engines": {
26
+ "bun": ">=1.3.11"
27
+ },
28
+ "main": "./dist/src/index.js",
29
+ "module": "./dist/src/index.js",
30
+ "types": "./dist/src/index.d.ts",
31
+ "dependencies": {
32
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.133",
33
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.133"
34
+ }
35
+ }