pi-blueprint 0.2.0
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 +57 -0
- package/dist/blueprint-command.d.ts +5 -0
- package/dist/blueprint-command.d.ts.map +1 -0
- package/dist/blueprint-command.js +56 -0
- package/dist/blueprint-command.js.map +1 -0
- package/dist/blueprint-injector.d.ts +3 -0
- package/dist/blueprint-injector.d.ts.map +1 -0
- package/dist/blueprint-injector.js +11 -0
- package/dist/blueprint-injector.js.map +1 -0
- package/dist/blueprint-tools.d.ts +4 -0
- package/dist/blueprint-tools.d.ts.map +1 -0
- package/dist/blueprint-tools.js +302 -0
- package/dist/blueprint-tools.js.map +1 -0
- package/dist/dependency-graph.d.ts +10 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +101 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/plan-next-command.d.ts +5 -0
- package/dist/plan-next-command.d.ts.map +1 -0
- package/dist/plan-next-command.js +36 -0
- package/dist/plan-next-command.js.map +1 -0
- package/dist/plan-renderer.d.ts +3 -0
- package/dist/plan-renderer.d.ts.map +1 -0
- package/dist/plan-renderer.js +61 -0
- package/dist/plan-renderer.js.map +1 -0
- package/dist/plan-status-command.d.ts +5 -0
- package/dist/plan-status-command.d.ts.map +1 -0
- package/dist/plan-status-command.js +20 -0
- package/dist/plan-status-command.js.map +1 -0
- package/dist/plan-verify-command.d.ts +5 -0
- package/dist/plan-verify-command.d.ts.map +1 -0
- package/dist/plan-verify-command.js +65 -0
- package/dist/plan-verify-command.js.map +1 -0
- package/dist/prompts/blueprint-generate.d.ts +2 -0
- package/dist/prompts/blueprint-generate.d.ts.map +1 -0
- package/dist/prompts/blueprint-generate.js +35 -0
- package/dist/prompts/blueprint-generate.js.map +1 -0
- package/dist/prompts/phase-context.d.ts +3 -0
- package/dist/prompts/phase-context.d.ts.map +1 -0
- package/dist/prompts/phase-context.js +62 -0
- package/dist/prompts/phase-context.js.map +1 -0
- package/dist/state-machine.d.ts +11 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +224 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +59 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/verification.d.ts +4 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +55 -0
- package/dist/verification.js.map +1 -0
- package/package.json +72 -0
- package/src/blueprint-command.ts +84 -0
- package/src/blueprint-injector.ts +10 -0
- package/src/blueprint-tools.ts +380 -0
- package/src/dependency-graph.ts +113 -0
- package/src/index.ts +118 -0
- package/src/plan-next-command.ts +56 -0
- package/src/plan-renderer.ts +70 -0
- package/src/plan-status-command.ts +30 -0
- package/src/plan-verify-command.ts +82 -0
- package/src/prompts/blueprint-generate.ts +34 -0
- package/src/prompts/phase-context.ts +76 -0
- package/src/state-machine.ts +278 -0
- package/src/storage.ts +83 -0
- package/src/types.ts +132 -0
- package/src/verification.ts +60 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import type { StateRef } from "./types.js";
|
|
6
|
+
import { getNextTask } from "./state-machine.js";
|
|
7
|
+
|
|
8
|
+
export const COMMAND_NAME = "plan-next";
|
|
9
|
+
|
|
10
|
+
export async function handlePlanNextCommand(
|
|
11
|
+
_args: string,
|
|
12
|
+
ctx: ExtensionCommandContext,
|
|
13
|
+
stateRef: StateRef,
|
|
14
|
+
pi: ExtensionAPI,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const state = stateRef.get();
|
|
17
|
+
if (!state.blueprint) {
|
|
18
|
+
ctx.ui.notify("No active blueprint.", "info");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const next = getNextTask(state.blueprint);
|
|
23
|
+
if (!next) {
|
|
24
|
+
ctx.ui.notify(
|
|
25
|
+
state.blueprint.status === "completed"
|
|
26
|
+
? "Blueprint is complete."
|
|
27
|
+
: "No actionable tasks. Some may be blocked or awaiting verification.",
|
|
28
|
+
"info",
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lines = [
|
|
34
|
+
`Work on blueprint task ${next.id}: ${next.title}`,
|
|
35
|
+
"",
|
|
36
|
+
next.description,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (next.acceptance_criteria.length > 0) {
|
|
40
|
+
lines.push("", "Acceptance criteria:");
|
|
41
|
+
for (const c of next.acceptance_criteria) {
|
|
42
|
+
lines.push(`- ${c}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (next.file_targets.length > 0) {
|
|
47
|
+
lines.push("", "File targets:");
|
|
48
|
+
for (const f of next.file_targets) {
|
|
49
|
+
lines.push(`- ${f}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push("", "When done, call the blueprint_update tool to mark this task as completed.");
|
|
54
|
+
|
|
55
|
+
pi.sendUserMessage(lines.join("\n"), { deliverAs: "followUp" });
|
|
56
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Blueprint, Phase, Task } from "./types.js";
|
|
2
|
+
import { isTaskDone } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function renderPlanMarkdown(blueprint: Blueprint): string {
|
|
5
|
+
const lines: string[] = [];
|
|
6
|
+
lines.push(`# Blueprint: ${blueprint.objective}`);
|
|
7
|
+
lines.push("");
|
|
8
|
+
lines.push(`**Status:** ${blueprint.status} | Created: ${formatDate(blueprint.created_at)} | Updated: ${formatDate(blueprint.updated_at)}`);
|
|
9
|
+
|
|
10
|
+
for (const phase of blueprint.phases) {
|
|
11
|
+
lines.push("");
|
|
12
|
+
lines.push("---");
|
|
13
|
+
lines.push("");
|
|
14
|
+
lines.push(renderPhase(phase, blueprint.active_phase_id));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return lines.join("\n") + "\n";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderPhase(phase: Phase, activePhaseId: string | null): string {
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
const isActive = phase.id === activePhaseId;
|
|
23
|
+
const activeTag = isActive ? " (active)" : "";
|
|
24
|
+
|
|
25
|
+
const completed = phase.tasks.filter(isTaskDone).length;
|
|
26
|
+
const total = phase.tasks.length;
|
|
27
|
+
|
|
28
|
+
lines.push(`## Phase ${phase.id}: ${phase.title}${activeTag}`);
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push(`**Status:** ${phase.status} | ${completed}/${total} tasks completed`);
|
|
31
|
+
|
|
32
|
+
if (phase.description) {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push(phase.description);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (phase.tasks.length > 0) {
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push("### Tasks");
|
|
40
|
+
lines.push("");
|
|
41
|
+
for (const task of phase.tasks) {
|
|
42
|
+
lines.push(renderTask(task));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (phase.verification_gates.length > 0) {
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("### Verification Gates");
|
|
49
|
+
lines.push("");
|
|
50
|
+
for (const gate of phase.verification_gates) {
|
|
51
|
+
const check = gate.passed ? "x" : " ";
|
|
52
|
+
lines.push(`- [${check}] ${gate.description}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderTask(task: Task): string {
|
|
60
|
+
const check = isTaskDone(task) ? "x" : " ";
|
|
61
|
+
let annotation = "";
|
|
62
|
+
if (task.status === "in_progress") annotation = " *(in progress)*";
|
|
63
|
+
else if (task.status === "blocked") annotation = " *(blocked)*";
|
|
64
|
+
else if (task.status === "skipped") annotation = " *(skipped)*";
|
|
65
|
+
return `- [${check}] ${task.id} ${task.title}${annotation}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatDate(iso: string): string {
|
|
69
|
+
return iso.slice(0, 10);
|
|
70
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { StateRef } from "./types.js";
|
|
3
|
+
import { isTaskDone } from "./types.js";
|
|
4
|
+
import { renderPlanMarkdown } from "./plan-renderer.js";
|
|
5
|
+
import { getAllTasks } from "./dependency-graph.js";
|
|
6
|
+
|
|
7
|
+
export const COMMAND_NAME = "plan-status";
|
|
8
|
+
|
|
9
|
+
export async function handlePlanStatusCommand(
|
|
10
|
+
_args: string,
|
|
11
|
+
ctx: ExtensionCommandContext,
|
|
12
|
+
stateRef: StateRef,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const state = stateRef.get();
|
|
15
|
+
if (!state.blueprint) {
|
|
16
|
+
ctx.ui.notify("No active blueprint.", "info");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const bp = state.blueprint;
|
|
21
|
+
const allTasks = getAllTasks(bp.phases);
|
|
22
|
+
const completed = allTasks.filter(isTaskDone).length;
|
|
23
|
+
const total = allTasks.length;
|
|
24
|
+
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
25
|
+
|
|
26
|
+
const header = `Progress: ${completed}/${total} tasks (${pct}%)`;
|
|
27
|
+
const plan = renderPlanMarkdown(bp);
|
|
28
|
+
|
|
29
|
+
ctx.ui.notify(`${header}\n\n${plan}`, "info");
|
|
30
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { StateRef } from "./types.js";
|
|
3
|
+
import { verifyGate, advancePhase } from "./state-machine.js";
|
|
4
|
+
import { runGate } from "./verification.js";
|
|
5
|
+
import { saveBlueprint, appendHistory } from "./storage.js";
|
|
6
|
+
|
|
7
|
+
export const COMMAND_NAME = "plan-verify";
|
|
8
|
+
|
|
9
|
+
export async function handlePlanVerifyCommand(
|
|
10
|
+
_args: string,
|
|
11
|
+
ctx: ExtensionCommandContext,
|
|
12
|
+
stateRef: StateRef,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const state = stateRef.get();
|
|
15
|
+
if (!state.blueprint) {
|
|
16
|
+
ctx.ui.notify("No active blueprint.", "info");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const bp = state.blueprint;
|
|
21
|
+
const phase = bp.phases.find((p) => p.id === bp.active_phase_id);
|
|
22
|
+
if (!phase) {
|
|
23
|
+
ctx.ui.notify("No active phase to verify.", "info");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (phase.verification_gates.length === 0) {
|
|
28
|
+
ctx.ui.notify(`Phase ${phase.id} has no verification gates. Advancing.`, "info");
|
|
29
|
+
const advanced = advancePhase(bp);
|
|
30
|
+
saveBlueprint(advanced);
|
|
31
|
+
stateRef.set({ ...state, blueprint: advanced });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cwd = state.project?.root ?? process.cwd();
|
|
36
|
+
let updated = bp;
|
|
37
|
+
const results: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < phase.verification_gates.length; i++) {
|
|
40
|
+
const gate = phase.verification_gates[i]!;
|
|
41
|
+
|
|
42
|
+
if (gate.type === "user_approval") {
|
|
43
|
+
results.push(`- ${gate.description}: requires manual approval (skipped in automated run)`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ctx.ui.notify(`Running: ${gate.description}...`, "info");
|
|
48
|
+
const result = runGate(gate, cwd);
|
|
49
|
+
updated = verifyGate(updated, phase.id, i, result.passed, result.passed ? undefined : result.output);
|
|
50
|
+
|
|
51
|
+
const status = result.passed ? "PASSED" : "FAILED";
|
|
52
|
+
results.push(`- ${gate.description}: ${status} (${result.duration_ms}ms)`);
|
|
53
|
+
|
|
54
|
+
appendHistory(updated.id, {
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
event: result.passed ? "verification_passed" : "verification_failed",
|
|
57
|
+
phase_id: phase.id,
|
|
58
|
+
task_id: null,
|
|
59
|
+
session_id: state.sessionId,
|
|
60
|
+
details: `${gate.description}: ${status}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const currentPhase = updated.phases.find((p) => p.id === phase.id)!;
|
|
65
|
+
const allPassed = currentPhase.verification_gates
|
|
66
|
+
.filter((g) => g.type !== "user_approval")
|
|
67
|
+
.every((g) => g.passed);
|
|
68
|
+
|
|
69
|
+
if (allPassed) {
|
|
70
|
+
const hasUserApproval = currentPhase.verification_gates.some((g) => g.type === "user_approval" && !g.passed);
|
|
71
|
+
if (!hasUserApproval) {
|
|
72
|
+
updated = advancePhase(updated);
|
|
73
|
+
results.push("\nAll gates passed. Phase advanced.");
|
|
74
|
+
} else {
|
|
75
|
+
results.push("\nAutomated gates passed. User approval still required.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
saveBlueprint(updated);
|
|
80
|
+
stateRef.set({ ...state, blueprint: updated });
|
|
81
|
+
ctx.ui.notify(`Verification results for Phase ${phase.id}:\n${results.join("\n")}`, "info");
|
|
82
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function getBlueprintGeneratePrompt(objective: string): string {
|
|
2
|
+
return `You are a senior software architect planning a complex implementation.
|
|
3
|
+
|
|
4
|
+
## Objective
|
|
5
|
+
${objective}
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
Analyze the codebase and the objective above, then create a phased construction plan by calling the \`blueprint_create\` tool.
|
|
10
|
+
|
|
11
|
+
### Plan structure requirements:
|
|
12
|
+
1. Break the work into 2-6 phases, ordered by dependency (foundations first)
|
|
13
|
+
2. Each phase should have 2-8 concrete, agent-sized tasks
|
|
14
|
+
3. Each task must have:
|
|
15
|
+
- A clear, imperative title (e.g., "Add OAuth2 callback endpoint")
|
|
16
|
+
- A brief description of what to implement
|
|
17
|
+
- Acceptance criteria (testable conditions)
|
|
18
|
+
- File targets (files to create or modify)
|
|
19
|
+
- Dependencies on other tasks (by task ID, e.g., "1.1", "2.3")
|
|
20
|
+
4. Each phase should have verification gates:
|
|
21
|
+
- Phase 1 typically: tests_pass
|
|
22
|
+
- Later phases: tests_pass + typecheck_clean
|
|
23
|
+
- Final phase: tests_pass + typecheck_clean + user_approval (optional)
|
|
24
|
+
5. Tasks within a phase can depend on other tasks (within or across phases)
|
|
25
|
+
6. Task IDs follow the format "phase.task" (e.g., "1.1", "1.2", "2.1")
|
|
26
|
+
|
|
27
|
+
### Quality criteria:
|
|
28
|
+
- Each task should be completable in a single agent session
|
|
29
|
+
- Dependencies should be minimal and acyclic
|
|
30
|
+
- Acceptance criteria should be specific and testable
|
|
31
|
+
- File targets should reference real paths in the codebase when possible
|
|
32
|
+
|
|
33
|
+
Call the \`blueprint_create\` tool with the structured plan.`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Blueprint, Task } from "../types.js";
|
|
2
|
+
import { isTaskDone } from "../types.js";
|
|
3
|
+
import { getBlockingTasks, getAllTasks } from "../dependency-graph.js";
|
|
4
|
+
|
|
5
|
+
export function buildPhaseContext(blueprint: Blueprint): string {
|
|
6
|
+
const lines: string[] = [];
|
|
7
|
+
|
|
8
|
+
lines.push(`## Active Blueprint: "${blueprint.objective}"`);
|
|
9
|
+
lines.push("");
|
|
10
|
+
|
|
11
|
+
const activePhase = blueprint.phases.find((p) => p.id === blueprint.active_phase_id);
|
|
12
|
+
if (!activePhase) {
|
|
13
|
+
lines.push("No active phase. All phases may be complete or verified.");
|
|
14
|
+
return lines.join("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const completed = activePhase.tasks.filter(isTaskDone).length;
|
|
18
|
+
|
|
19
|
+
lines.push(`### Current Phase: Phase ${activePhase.id} - ${activePhase.title}`);
|
|
20
|
+
lines.push(`Status: ${activePhase.status} | ${completed}/${activePhase.tasks.length} tasks completed`);
|
|
21
|
+
|
|
22
|
+
const activeTask = activePhase.tasks.find((t) => t.id === blueprint.active_task_id);
|
|
23
|
+
if (activeTask) {
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(buildTaskContext(activeTask));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const blockedTasks = activePhase.tasks.filter((t) => t.status === "blocked");
|
|
29
|
+
if (blockedTasks.length > 0) {
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("### Blocked Tasks");
|
|
32
|
+
const allTasks = getAllTasks(blueprint.phases);
|
|
33
|
+
for (const task of blockedTasks) {
|
|
34
|
+
const blockers = getBlockingTasks(allTasks, task.id);
|
|
35
|
+
lines.push(`- ${task.id} "${task.title}" - blocked by ${blockers.join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (activePhase.verification_gates.length > 0) {
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`### Phase ${activePhase.id} Verification Gates`);
|
|
42
|
+
for (const gate of activePhase.verification_gates) {
|
|
43
|
+
const check = gate.passed ? "x" : " ";
|
|
44
|
+
lines.push(`- [${check}] ${gate.description}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildTaskContext(task: Task): string {
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
lines.push(`### Current Task: ${task.id} - ${task.title}`);
|
|
54
|
+
|
|
55
|
+
if (task.description) {
|
|
56
|
+
lines.push(task.description);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (task.acceptance_criteria.length > 0) {
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push("**Acceptance criteria:**");
|
|
62
|
+
for (const c of task.acceptance_criteria) {
|
|
63
|
+
lines.push(`- ${c}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (task.file_targets.length > 0) {
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("**File targets:**");
|
|
70
|
+
for (const f of task.file_targets) {
|
|
71
|
+
lines.push(`- ${f}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { Blueprint, Phase, Task, TaskStatus, PhaseStatus } from "./types.js";
|
|
2
|
+
import { isTaskDone } from "./types.js";
|
|
3
|
+
import { findBlockedTasks, isTaskReady, getAllTasks } from "./dependency-graph.js";
|
|
4
|
+
|
|
5
|
+
function updateTask(phase: Phase, taskId: string, patch: Partial<Task>): Phase {
|
|
6
|
+
return {
|
|
7
|
+
...phase,
|
|
8
|
+
tasks: phase.tasks.map((t) => (t.id === taskId ? { ...t, ...patch } : t)),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function updatePhase(blueprint: Blueprint, phaseId: string, patch: Partial<Phase>): Blueprint {
|
|
13
|
+
return {
|
|
14
|
+
...blueprint,
|
|
15
|
+
updated_at: new Date().toISOString(),
|
|
16
|
+
phases: blueprint.phases.map((p) => (p.id === phaseId ? { ...p, ...patch } : p)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findTask(
|
|
21
|
+
blueprint: Blueprint,
|
|
22
|
+
taskId: string,
|
|
23
|
+
): { phase: Phase; task: Task } | null {
|
|
24
|
+
for (const phase of blueprint.phases) {
|
|
25
|
+
const task = phase.tasks.find((t) => t.id === taskId);
|
|
26
|
+
if (task) return { phase, task };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findNextTaskInPhase(phase: Phase, allTasks: readonly Task[]): Task | null {
|
|
32
|
+
for (const task of phase.tasks) {
|
|
33
|
+
if (task.status === "in_progress") return task;
|
|
34
|
+
}
|
|
35
|
+
for (const task of phase.tasks) {
|
|
36
|
+
if (task.status === "pending" && isTaskReady(allTasks, task.id)) return task;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveTaskCompletion(
|
|
42
|
+
blueprint: Blueprint,
|
|
43
|
+
phaseId: string,
|
|
44
|
+
taskId: string,
|
|
45
|
+
): Blueprint {
|
|
46
|
+
let bp = recomputeBlocked(blueprint);
|
|
47
|
+
|
|
48
|
+
if (bp.active_task_id === taskId) {
|
|
49
|
+
const next = getNextTask(bp);
|
|
50
|
+
bp = { ...bp, active_task_id: next?.id ?? null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentPhase = bp.phases.find((p) => p.id === phaseId);
|
|
54
|
+
if (currentPhase && currentPhase.tasks.every(isTaskDone)) {
|
|
55
|
+
bp = updatePhase(bp, phaseId, { status: "completed" as PhaseStatus });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return bp;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startTask(
|
|
62
|
+
blueprint: Blueprint,
|
|
63
|
+
taskId: string,
|
|
64
|
+
sessionId: string,
|
|
65
|
+
): Blueprint {
|
|
66
|
+
const found = findTask(blueprint, taskId);
|
|
67
|
+
if (!found) return blueprint;
|
|
68
|
+
|
|
69
|
+
const { phase, task } = found;
|
|
70
|
+
if (isTaskDone(task)) return blueprint;
|
|
71
|
+
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const updatedPhase = updateTask(phase, taskId, {
|
|
74
|
+
status: "in_progress" as TaskStatus,
|
|
75
|
+
started_at: task.started_at ?? now,
|
|
76
|
+
session_id: sessionId,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let phaseStatus = phase.status;
|
|
80
|
+
if (phaseStatus === "pending") {
|
|
81
|
+
phaseStatus = "active";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let bp = updatePhase(blueprint, phase.id, {
|
|
85
|
+
...updatedPhase,
|
|
86
|
+
status: phaseStatus,
|
|
87
|
+
started_at: phase.started_at ?? now,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
bp = {
|
|
91
|
+
...bp,
|
|
92
|
+
active_phase_id: phase.id,
|
|
93
|
+
active_task_id: taskId,
|
|
94
|
+
status: blueprint.status === "draft" ? "active" : blueprint.status,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return bp;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function completeTask(blueprint: Blueprint, taskId: string): Blueprint {
|
|
101
|
+
const found = findTask(blueprint, taskId);
|
|
102
|
+
if (!found) return blueprint;
|
|
103
|
+
|
|
104
|
+
const { phase, task } = found;
|
|
105
|
+
if (task.status === "completed") return blueprint;
|
|
106
|
+
|
|
107
|
+
const updatedPhase = updateTask(phase, taskId, {
|
|
108
|
+
status: "completed" as TaskStatus,
|
|
109
|
+
completed_at: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let bp = updatePhase(blueprint, phase.id, updatedPhase);
|
|
113
|
+
bp = resolveTaskCompletion(bp, phase.id, taskId);
|
|
114
|
+
|
|
115
|
+
const allTasks = getAllTasks(bp.phases);
|
|
116
|
+
if (allTasks.every(isTaskDone)) {
|
|
117
|
+
const allVerified = bp.phases.every((p) =>
|
|
118
|
+
p.verification_gates.length === 0 || p.verification_gates.every((g) => g.passed),
|
|
119
|
+
);
|
|
120
|
+
if (allVerified) {
|
|
121
|
+
bp = { ...bp, status: "completed" };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return bp;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function skipTask(blueprint: Blueprint, taskId: string): Blueprint {
|
|
129
|
+
const found = findTask(blueprint, taskId);
|
|
130
|
+
if (!found) return blueprint;
|
|
131
|
+
|
|
132
|
+
const { phase } = found;
|
|
133
|
+
const updatedPhase = updateTask(phase, taskId, {
|
|
134
|
+
status: "skipped" as TaskStatus,
|
|
135
|
+
completed_at: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let bp = updatePhase(blueprint, phase.id, updatedPhase);
|
|
139
|
+
bp = resolveTaskCompletion(bp, phase.id, taskId);
|
|
140
|
+
|
|
141
|
+
return bp;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function verifyGate(
|
|
145
|
+
blueprint: Blueprint,
|
|
146
|
+
phaseId: string,
|
|
147
|
+
gateIndex: number,
|
|
148
|
+
passed: boolean,
|
|
149
|
+
errorMsg?: string,
|
|
150
|
+
): Blueprint {
|
|
151
|
+
const phase = blueprint.phases.find((p) => p.id === phaseId);
|
|
152
|
+
if (!phase) return blueprint;
|
|
153
|
+
|
|
154
|
+
const gate = phase.verification_gates[gateIndex];
|
|
155
|
+
if (!gate) return blueprint;
|
|
156
|
+
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
const updatedGates = phase.verification_gates.map((g, i) =>
|
|
159
|
+
i === gateIndex
|
|
160
|
+
? { ...g, passed, last_checked_at: now, error_message: errorMsg ?? null }
|
|
161
|
+
: g,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return updatePhase(blueprint, phaseId, { verification_gates: updatedGates });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function advancePhase(blueprint: Blueprint): Blueprint {
|
|
168
|
+
const currentPhase = blueprint.phases.find((p) => p.id === blueprint.active_phase_id);
|
|
169
|
+
if (!currentPhase) return blueprint;
|
|
170
|
+
|
|
171
|
+
const allGatesPassed =
|
|
172
|
+
currentPhase.verification_gates.length === 0 ||
|
|
173
|
+
currentPhase.verification_gates.every((g) => g.passed);
|
|
174
|
+
|
|
175
|
+
if (!currentPhase.tasks.every(isTaskDone) || !allGatesPassed) return blueprint;
|
|
176
|
+
|
|
177
|
+
let bp = updatePhase(blueprint, currentPhase.id, {
|
|
178
|
+
status: "verified" as PhaseStatus,
|
|
179
|
+
completed_at: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const currentIdx = bp.phases.findIndex((p) => p.id === currentPhase.id);
|
|
183
|
+
const nextPhase = bp.phases[currentIdx + 1];
|
|
184
|
+
|
|
185
|
+
if (nextPhase) {
|
|
186
|
+
bp = {
|
|
187
|
+
...bp,
|
|
188
|
+
active_phase_id: nextPhase.id,
|
|
189
|
+
active_task_id: null,
|
|
190
|
+
};
|
|
191
|
+
const next = getNextTask(bp);
|
|
192
|
+
if (next) {
|
|
193
|
+
bp = { ...bp, active_task_id: next.id };
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
bp = {
|
|
197
|
+
...bp,
|
|
198
|
+
active_phase_id: null,
|
|
199
|
+
active_task_id: null,
|
|
200
|
+
status: "completed",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return bp;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getNextTask(blueprint: Blueprint): Task | null {
|
|
208
|
+
const allTasks = getAllTasks(blueprint.phases);
|
|
209
|
+
|
|
210
|
+
if (blueprint.active_phase_id) {
|
|
211
|
+
const phase = blueprint.phases.find((p) => p.id === blueprint.active_phase_id);
|
|
212
|
+
if (phase) {
|
|
213
|
+
const found = findNextTaskInPhase(phase, allTasks);
|
|
214
|
+
if (found) return found;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const phase of blueprint.phases) {
|
|
219
|
+
if (phase.status === "verified") continue;
|
|
220
|
+
const found = findNextTaskInPhase(phase, allTasks);
|
|
221
|
+
if (found) return found;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function recomputeBlocked(blueprint: Blueprint): Blueprint {
|
|
228
|
+
const allTasks = getAllTasks(blueprint.phases);
|
|
229
|
+
const blockedIds = new Set(findBlockedTasks(allTasks));
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
...blueprint,
|
|
233
|
+
updated_at: new Date().toISOString(),
|
|
234
|
+
phases: blueprint.phases.map((phase) => ({
|
|
235
|
+
...phase,
|
|
236
|
+
tasks: phase.tasks.map((task) => {
|
|
237
|
+
if (isTaskDone(task)) return task;
|
|
238
|
+
if (blockedIds.has(task.id) && task.status !== "blocked") {
|
|
239
|
+
return { ...task, status: "blocked" as TaskStatus };
|
|
240
|
+
}
|
|
241
|
+
if (!blockedIds.has(task.id) && task.status === "blocked") {
|
|
242
|
+
return { ...task, status: "pending" as TaskStatus };
|
|
243
|
+
}
|
|
244
|
+
return task;
|
|
245
|
+
}),
|
|
246
|
+
})),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createBlueprint(
|
|
251
|
+
id: string,
|
|
252
|
+
objective: string,
|
|
253
|
+
projectId: string,
|
|
254
|
+
phases: readonly Phase[],
|
|
255
|
+
): Blueprint {
|
|
256
|
+
const now = new Date().toISOString();
|
|
257
|
+
let bp: Blueprint = {
|
|
258
|
+
id,
|
|
259
|
+
objective,
|
|
260
|
+
project_id: projectId,
|
|
261
|
+
status: "active",
|
|
262
|
+
created_at: now,
|
|
263
|
+
updated_at: now,
|
|
264
|
+
phases,
|
|
265
|
+
active_phase_id: phases[0]?.id ?? null,
|
|
266
|
+
active_task_id: null,
|
|
267
|
+
};
|
|
268
|
+
bp = recomputeBlocked(bp);
|
|
269
|
+
const next = getNextTask(bp);
|
|
270
|
+
if (next) {
|
|
271
|
+
bp = { ...bp, active_task_id: next.id };
|
|
272
|
+
}
|
|
273
|
+
return bp;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function abandonBlueprint(blueprint: Blueprint): Blueprint {
|
|
277
|
+
return { ...blueprint, status: "abandoned", updated_at: new Date().toISOString() };
|
|
278
|
+
}
|