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

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/index.js CHANGED
@@ -12,6 +12,76 @@ 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
+ }
15
85
  function taskByLocalId(tasks) {
16
86
  return new Map(tasks.map((task) => [task.localId, task]));
17
87
  }
@@ -138,7 +208,9 @@ export {
138
208
  spec,
139
209
  planningPlugin,
140
210
  plan,
211
+ openAiChat,
141
212
  materialize,
213
+ createLlmPlanningProvider,
142
214
  clarify,
143
215
  PLANNING_PLUGIN_NAME
144
216
  };
@@ -28,6 +28,10 @@ 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;
31
35
  export declare function materialize(planSpec: PlanSpec, source: Pick<RegisteredTaskSource, "list" | "create">, options?: {
32
36
  readonly planId?: string;
33
37
  }): Promise<MaterializePlanResult>;
@@ -12,6 +12,76 @@ 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
+ }
15
85
  function taskByLocalId(tasks) {
16
86
  return new Map(tasks.map((task) => [task.localId, task]));
17
87
  }
@@ -119,6 +189,8 @@ export {
119
189
  specClarifyPlan,
120
190
  spec,
121
191
  plan,
192
+ openAiChat,
122
193
  materialize,
194
+ createLlmPlanningProvider,
123
195
  clarify
124
196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/planning-plugin",
3
- "version": "0.0.6-alpha.134",
3
+ "version": "0.0.6-alpha.136",
4
4
  "type": "module",
5
5
  "description": "First-party PRD-to-plan plugin for Rig task sources.",
6
6
  "license": "UNLICENSED",
@@ -29,7 +29,7 @@
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.134",
33
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.134"
32
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.136",
33
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.136"
34
34
  }
35
35
  }