@backburner/cli 0.1.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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- package/package.json +61 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { validateSession } from "./validate.js";
|
|
2
|
+
export function inspectSession(session) {
|
|
3
|
+
const structuralIssues = validateSession(session);
|
|
4
|
+
const validationIssues = [...(session.validationIssues ?? []), ...structuralIssues];
|
|
5
|
+
const isValid = !validationIssues.some((i) => i.severity === "error");
|
|
6
|
+
const waveOrder = Array.isArray(session.waveOrder) ? session.waveOrder : [];
|
|
7
|
+
const sessionTasks = (session.tasks && typeof session.tasks === "object") ? session.tasks : {};
|
|
8
|
+
const taskToWave = new Map();
|
|
9
|
+
for (const waveId of waveOrder) {
|
|
10
|
+
const wave = session.waves?.[waveId];
|
|
11
|
+
if (wave) {
|
|
12
|
+
const taskIds = Array.isArray(wave.taskIds) ? wave.taskIds : [];
|
|
13
|
+
for (const taskId of taskIds) {
|
|
14
|
+
taskToWave.set(taskId, waveId);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
sessionId: session.id ?? "",
|
|
20
|
+
title: session.title ?? "",
|
|
21
|
+
summary: session.summary ?? "",
|
|
22
|
+
taskCount: Object.keys(sessionTasks).length,
|
|
23
|
+
waveCount: waveOrder.length,
|
|
24
|
+
tasks: Object.entries(sessionTasks).map(([id, task]) => ({
|
|
25
|
+
id,
|
|
26
|
+
title: task?.title,
|
|
27
|
+
waveId: taskToWave.get(id),
|
|
28
|
+
dependencies: Array.isArray(task?.dependsOn) ? task.dependsOn : [],
|
|
29
|
+
validationIssues: validationIssues.filter((i) => i.path?.[0] === "tasks" && i.path?.[1] === id)
|
|
30
|
+
})),
|
|
31
|
+
waves: waveOrder.map((id) => {
|
|
32
|
+
const wave = session.waves?.[id];
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
taskIds: Array.isArray(wave?.taskIds) ? wave.taskIds : [],
|
|
36
|
+
validationIssues: validationIssues.filter((i) => i.path?.[0] === "waves" && i.path?.[1] === id)
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
validationIssues: validationIssues.filter((i) => {
|
|
40
|
+
if (!i.path)
|
|
41
|
+
return true;
|
|
42
|
+
if (i.path[0] === "tasks" && i.path[1])
|
|
43
|
+
return false;
|
|
44
|
+
if (i.path[0] === "waves" && i.path[1])
|
|
45
|
+
return false;
|
|
46
|
+
return true;
|
|
47
|
+
}),
|
|
48
|
+
isValid
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { PatchError } from "./types.js";
|
|
2
|
+
export function applyPatches(session, patches) {
|
|
3
|
+
let current = { ...session, validationIssues: [...(session.validationIssues || [])] };
|
|
4
|
+
const now = new Date().toISOString();
|
|
5
|
+
for (const patch of patches) {
|
|
6
|
+
switch (patch.op) {
|
|
7
|
+
case "set-title":
|
|
8
|
+
current.title = patch.value;
|
|
9
|
+
break;
|
|
10
|
+
case "set-summary":
|
|
11
|
+
current.summary = patch.value;
|
|
12
|
+
break;
|
|
13
|
+
case "add-task":
|
|
14
|
+
if (current.tasks[patch.task.id]) {
|
|
15
|
+
throw new PatchError(`Task ID ${patch.task.id} already exists`);
|
|
16
|
+
}
|
|
17
|
+
current.tasks = { ...current.tasks, [patch.task.id]: patch.task };
|
|
18
|
+
break;
|
|
19
|
+
case "update-task":
|
|
20
|
+
if (!current.tasks[patch.id]) {
|
|
21
|
+
throw new PatchError(`Task ${patch.id} not found`);
|
|
22
|
+
}
|
|
23
|
+
current.tasks = {
|
|
24
|
+
...current.tasks,
|
|
25
|
+
[patch.id]: { ...current.tasks[patch.id], ...patch.patch }
|
|
26
|
+
};
|
|
27
|
+
break;
|
|
28
|
+
case "remove-task": {
|
|
29
|
+
if (!current.tasks[patch.id]) {
|
|
30
|
+
throw new PatchError(`Task ${patch.id} not found`);
|
|
31
|
+
}
|
|
32
|
+
const { [patch.id]: _, ...remainingTasks } = current.tasks;
|
|
33
|
+
current.tasks = remainingTasks;
|
|
34
|
+
// Also remove from waves
|
|
35
|
+
const updatedWaves = {};
|
|
36
|
+
for (const [waveId, wave] of Object.entries(current.waves)) {
|
|
37
|
+
if (wave.taskIds.includes(patch.id)) {
|
|
38
|
+
updatedWaves[waveId] = {
|
|
39
|
+
...wave,
|
|
40
|
+
taskIds: wave.taskIds.filter((id) => id !== patch.id)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
updatedWaves[waveId] = wave;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
current.waves = updatedWaves;
|
|
48
|
+
// Clear validation issues for this task
|
|
49
|
+
current.validationIssues = current.validationIssues.filter(i => !(i.path?.[0] === "tasks" && i.path?.[1] === patch.id));
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "add-wave":
|
|
53
|
+
if (current.waves[patch.wave.id]) {
|
|
54
|
+
throw new PatchError(`Wave ID ${patch.wave.id} already exists`);
|
|
55
|
+
}
|
|
56
|
+
current.waves = { ...current.waves, [patch.wave.id]: patch.wave };
|
|
57
|
+
if (patch.index !== undefined) {
|
|
58
|
+
const newOrder = current.waveOrder.filter(id => id !== patch.wave.id);
|
|
59
|
+
newOrder.splice(patch.index, 0, patch.wave.id);
|
|
60
|
+
current.waveOrder = newOrder;
|
|
61
|
+
}
|
|
62
|
+
else if (!current.waveOrder.includes(patch.wave.id)) {
|
|
63
|
+
current.waveOrder = [...current.waveOrder, patch.wave.id];
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "update-wave":
|
|
67
|
+
if (!current.waves[patch.id]) {
|
|
68
|
+
throw new PatchError(`Wave ${patch.id} not found`);
|
|
69
|
+
}
|
|
70
|
+
current.waves = {
|
|
71
|
+
...current.waves,
|
|
72
|
+
[patch.id]: { ...current.waves[patch.id], ...patch.patch }
|
|
73
|
+
};
|
|
74
|
+
break;
|
|
75
|
+
case "remove-wave": {
|
|
76
|
+
if (!current.waves[patch.id]) {
|
|
77
|
+
throw new PatchError(`Wave ${patch.id} not found`);
|
|
78
|
+
}
|
|
79
|
+
const { [patch.id]: __, ...remainingWaves } = current.waves;
|
|
80
|
+
current.waves = remainingWaves;
|
|
81
|
+
current.waveOrder = current.waveOrder.filter((id) => id !== patch.id);
|
|
82
|
+
if (current.recommendedFirstWaveId === patch.id) {
|
|
83
|
+
current.recommendedFirstWaveId = "";
|
|
84
|
+
}
|
|
85
|
+
// Clear validation issues for this wave
|
|
86
|
+
current.validationIssues = current.validationIssues.filter(i => !(i.path?.[0] === "waves" && i.path?.[1] === patch.id));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "reorder-waves": {
|
|
90
|
+
const existingWaveIds = Object.keys(current.waves);
|
|
91
|
+
if (patch.waveIds.length !== existingWaveIds.length) {
|
|
92
|
+
throw new PatchError(`reorder-waves must include all existing waves. Expected ${existingWaveIds.length} waves, got ${patch.waveIds.length}.`);
|
|
93
|
+
}
|
|
94
|
+
// Validate all wave IDs exist and there are no duplicates
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const waveId of patch.waveIds) {
|
|
97
|
+
if (!current.waves[waveId]) {
|
|
98
|
+
throw new PatchError(`Wave ${waveId} not found for reorder`);
|
|
99
|
+
}
|
|
100
|
+
if (seen.has(waveId)) {
|
|
101
|
+
throw new PatchError(`Duplicate wave ID ${waveId} in reorder`);
|
|
102
|
+
}
|
|
103
|
+
seen.add(waveId);
|
|
104
|
+
}
|
|
105
|
+
current.waveOrder = patch.waveIds;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "set-recommended-first-wave":
|
|
109
|
+
if (!current.waves[patch.id]) {
|
|
110
|
+
throw new PatchError(`Wave ${patch.id} not found for recommended first wave`);
|
|
111
|
+
}
|
|
112
|
+
current.recommendedFirstWaveId = patch.id;
|
|
113
|
+
break;
|
|
114
|
+
case "add-graph-concern":
|
|
115
|
+
current.graphConcerns = [...current.graphConcerns, patch.value];
|
|
116
|
+
break;
|
|
117
|
+
case "remove-graph-concern":
|
|
118
|
+
if (patch.index < 0 || patch.index >= current.graphConcerns.length) {
|
|
119
|
+
throw new PatchError(`Graph concern index ${patch.index} out of bounds`);
|
|
120
|
+
}
|
|
121
|
+
current.graphConcerns = current.graphConcerns.filter((_, i) => i !== patch.index);
|
|
122
|
+
break;
|
|
123
|
+
case "add-human-decision":
|
|
124
|
+
current.humanDecisionsNeeded = [...current.humanDecisionsNeeded, patch.value];
|
|
125
|
+
break;
|
|
126
|
+
case "remove-human-decision":
|
|
127
|
+
if (patch.index < 0 || patch.index >= current.humanDecisionsNeeded.length) {
|
|
128
|
+
throw new PatchError(`Human decision index ${patch.index} out of bounds`);
|
|
129
|
+
}
|
|
130
|
+
current.humanDecisionsNeeded = current.humanDecisionsNeeded.filter((_, i) => i !== patch.index);
|
|
131
|
+
break;
|
|
132
|
+
case "set-suggested-next-action":
|
|
133
|
+
current.suggestedNextAction = patch.value;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
current.updatedAt = now;
|
|
138
|
+
return current;
|
|
139
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function createSession(options) {
|
|
3
|
+
const now = new Date().toISOString();
|
|
4
|
+
return {
|
|
5
|
+
id: options?.id ?? randomUUID(),
|
|
6
|
+
title: options?.title ?? "",
|
|
7
|
+
summary: options?.summary ?? "",
|
|
8
|
+
tasks: {},
|
|
9
|
+
waves: {},
|
|
10
|
+
waveOrder: [],
|
|
11
|
+
recommendedFirstWaveId: "",
|
|
12
|
+
graphConcerns: [],
|
|
13
|
+
humanDecisionsNeeded: [],
|
|
14
|
+
suggestedNextAction: "",
|
|
15
|
+
createdAt: now,
|
|
16
|
+
updatedAt: now,
|
|
17
|
+
validationIssues: []
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export class PlanBreakdownSessionStore {
|
|
4
|
+
logDir;
|
|
5
|
+
sessions = new Map();
|
|
6
|
+
constructor(logDir) {
|
|
7
|
+
this.logDir = logDir;
|
|
8
|
+
}
|
|
9
|
+
async saveSession(session) {
|
|
10
|
+
this.sessions.set(session.id, session);
|
|
11
|
+
if (this.logDir) {
|
|
12
|
+
const sessionsDir = path.join(this.logDir, "plan-breakdown-sessions");
|
|
13
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
14
|
+
const filePath = path.join(sessionsDir, `${session.id}.json`);
|
|
15
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, 2), "utf8");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async getSession(id) {
|
|
19
|
+
return this.sessions.get(id);
|
|
20
|
+
}
|
|
21
|
+
getSessionSync(id) {
|
|
22
|
+
return this.sessions.get(id);
|
|
23
|
+
}
|
|
24
|
+
listSessions() {
|
|
25
|
+
return Array.from(this.sessions.values());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createSession, applyPatches, inspectSession, validateSession, exportSession, PatchError } from "./index.js";
|
|
2
|
+
export class PlanBreakdownToolService {
|
|
3
|
+
store;
|
|
4
|
+
logger;
|
|
5
|
+
namespace = "plan_breakdown";
|
|
6
|
+
constructor(store, logger) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
listTools() {
|
|
11
|
+
return TOOL_DEFINITIONS;
|
|
12
|
+
}
|
|
13
|
+
async callTool(name, rawArguments) {
|
|
14
|
+
const startedAt = new Date().toISOString();
|
|
15
|
+
try {
|
|
16
|
+
switch (name) {
|
|
17
|
+
case "start_session": {
|
|
18
|
+
const args = validateStartSessionArgs(rawArguments);
|
|
19
|
+
const session = createSession({ title: args.title, summary: args.summary });
|
|
20
|
+
await this.store.saveSession(session);
|
|
21
|
+
this.logInvocation(startedAt, name, { sessionId: session.id }, "ok");
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
tool: name,
|
|
25
|
+
summary: `Started new plan breakdown session: ${session.id}`,
|
|
26
|
+
result: { sessionId: session.id }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
case "apply_patch": {
|
|
30
|
+
const args = validateApplyPatchArgs(rawArguments);
|
|
31
|
+
const session = await this.requireSession(args.sessionId);
|
|
32
|
+
const updatedSession = applyPatches(session, [args.patch]);
|
|
33
|
+
await this.store.saveSession(updatedSession);
|
|
34
|
+
const issues = validateSession(updatedSession);
|
|
35
|
+
const hasErrors = issues.some(issue => issue.severity === "error");
|
|
36
|
+
this.logInvocation(startedAt, name, { sessionId: args.sessionId }, "ok");
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
tool: name,
|
|
40
|
+
summary: `Applied patch to session: ${args.sessionId}${hasErrors ? " (now has validation errors)" : ""}`,
|
|
41
|
+
result: {
|
|
42
|
+
sessionId: args.sessionId,
|
|
43
|
+
isValid: !hasErrors,
|
|
44
|
+
issueCount: issues.length
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case "inspect_session": {
|
|
49
|
+
const args = validateSessionLookupArgs(rawArguments);
|
|
50
|
+
const session = await this.requireSession(args.sessionId);
|
|
51
|
+
const inspection = inspectSession(session);
|
|
52
|
+
this.logInvocation(startedAt, name, { sessionId: args.sessionId }, "ok");
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
tool: name,
|
|
56
|
+
summary: `Inspected session: ${args.sessionId}`,
|
|
57
|
+
result: inspection
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
case "validate_session": {
|
|
61
|
+
const args = validateSessionLookupArgs(rawArguments);
|
|
62
|
+
const session = await this.requireSession(args.sessionId);
|
|
63
|
+
const issues = validateSession(session);
|
|
64
|
+
const hasErrors = issues.some(issue => issue.severity === "error");
|
|
65
|
+
this.logInvocation(startedAt, name, { sessionId: args.sessionId }, "ok");
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
tool: name,
|
|
69
|
+
summary: `Validated session: ${args.sessionId} (${issues.length} issues)`,
|
|
70
|
+
result: { sessionId: args.sessionId, issues, isValid: !hasErrors }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "export_session": {
|
|
74
|
+
const args = validateSessionLookupArgs(rawArguments);
|
|
75
|
+
const session = await this.requireSession(args.sessionId);
|
|
76
|
+
const issues = validateSession(session);
|
|
77
|
+
const hasErrors = issues.some(issue => issue.severity === "error");
|
|
78
|
+
if (hasErrors) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
tool: name,
|
|
82
|
+
error: {
|
|
83
|
+
code: "invalid_session",
|
|
84
|
+
message: `Cannot export invalid session ${args.sessionId}. Call validate_session for details.`
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const result = exportSession(session);
|
|
89
|
+
this.logInvocation(startedAt, name, { sessionId: args.sessionId }, "ok");
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
tool: name,
|
|
93
|
+
summary: `Exported session: ${args.sessionId}`,
|
|
94
|
+
result: result
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
tool: name,
|
|
101
|
+
error: {
|
|
102
|
+
code: "tool_not_found",
|
|
103
|
+
message: `Unknown plan breakdown tool "${name}"`
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const failure = toFailure(name, error);
|
|
110
|
+
this.logInvocation(startedAt, name, rawArguments, "error", failure.error.message);
|
|
111
|
+
return failure;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async requireSession(sessionId) {
|
|
115
|
+
const session = await this.store.getSession(sessionId);
|
|
116
|
+
if (!session) {
|
|
117
|
+
throw new SessionNotFoundError(`Session not found: ${sessionId}`);
|
|
118
|
+
}
|
|
119
|
+
return session;
|
|
120
|
+
}
|
|
121
|
+
logInvocation(timestamp, toolName, args, status, message) {
|
|
122
|
+
this.logger.info(JSON.stringify({
|
|
123
|
+
component: "plan-breakdown-tool-service",
|
|
124
|
+
timestamp,
|
|
125
|
+
toolName,
|
|
126
|
+
resultStatus: status,
|
|
127
|
+
arguments: args,
|
|
128
|
+
...(message ? { message } : {})
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
class SessionNotFoundError extends Error {
|
|
133
|
+
}
|
|
134
|
+
class ValidationError extends Error {
|
|
135
|
+
}
|
|
136
|
+
function toFailure(name, error) {
|
|
137
|
+
if (error instanceof SessionNotFoundError) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
tool: name,
|
|
141
|
+
error: {
|
|
142
|
+
code: "session_not_found",
|
|
143
|
+
message: error.message
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (error instanceof ValidationError) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
tool: name,
|
|
151
|
+
error: {
|
|
152
|
+
code: "invalid_arguments",
|
|
153
|
+
message: error.message
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (error instanceof PatchError) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
tool: name,
|
|
161
|
+
error: {
|
|
162
|
+
code: "invalid_patch",
|
|
163
|
+
message: error.message
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
tool: name,
|
|
170
|
+
error: {
|
|
171
|
+
code: "internal_error",
|
|
172
|
+
message: error instanceof Error ? error.message : String(error)
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function validateStartSessionArgs(args) {
|
|
177
|
+
const obj = expectObject(args);
|
|
178
|
+
return {
|
|
179
|
+
title: expectString(obj.title, "title"),
|
|
180
|
+
summary: expectString(obj.summary, "summary")
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function validateApplyPatchArgs(args) {
|
|
184
|
+
const obj = expectObject(args);
|
|
185
|
+
const patch = expectObject(obj.patch, "patch");
|
|
186
|
+
return {
|
|
187
|
+
sessionId: expectString(obj.sessionId, "sessionId"),
|
|
188
|
+
patch: validatePatchOperation(patch)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function validatePatchOperation(patch) {
|
|
192
|
+
const op = expectString(patch.op, "patch.op");
|
|
193
|
+
switch (op) {
|
|
194
|
+
case "set-title":
|
|
195
|
+
return { op, value: expectString(patch.value, "patch.value") };
|
|
196
|
+
case "set-summary":
|
|
197
|
+
return { op, value: expectString(patch.value, "patch.value") };
|
|
198
|
+
case "add-task":
|
|
199
|
+
return {
|
|
200
|
+
op,
|
|
201
|
+
task: validateTask(expectObject(patch.task, "patch.task"), "patch.task")
|
|
202
|
+
};
|
|
203
|
+
case "update-task":
|
|
204
|
+
return {
|
|
205
|
+
op,
|
|
206
|
+
id: expectString(patch.id, "patch.id"),
|
|
207
|
+
patch: validateTaskPatch(expectObject(patch.patch, "patch.patch"), "patch.patch")
|
|
208
|
+
};
|
|
209
|
+
case "remove-task":
|
|
210
|
+
return { op, id: expectString(patch.id, "patch.id") };
|
|
211
|
+
case "add-wave": {
|
|
212
|
+
const result = {
|
|
213
|
+
op,
|
|
214
|
+
wave: validateWave(expectObject(patch.wave, "patch.wave"), "patch.wave")
|
|
215
|
+
};
|
|
216
|
+
if (patch.index !== undefined) {
|
|
217
|
+
result.index = expectInteger(patch.index, "patch.index");
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
case "update-wave":
|
|
222
|
+
return {
|
|
223
|
+
op,
|
|
224
|
+
id: expectString(patch.id, "patch.id"),
|
|
225
|
+
patch: validateWavePatch(expectObject(patch.patch, "patch.patch"), "patch.patch")
|
|
226
|
+
};
|
|
227
|
+
case "remove-wave":
|
|
228
|
+
return { op, id: expectString(patch.id, "patch.id") };
|
|
229
|
+
case "reorder-waves":
|
|
230
|
+
return { op, waveIds: expectStringArray(patch.waveIds, "patch.waveIds") };
|
|
231
|
+
case "set-recommended-first-wave":
|
|
232
|
+
return { op, id: expectString(patch.id, "patch.id") };
|
|
233
|
+
case "add-graph-concern":
|
|
234
|
+
return { op, value: expectString(patch.value, "patch.value") };
|
|
235
|
+
case "remove-graph-concern":
|
|
236
|
+
return { op, index: expectInteger(patch.index, "patch.index") };
|
|
237
|
+
case "add-human-decision":
|
|
238
|
+
return { op, value: expectString(patch.value, "patch.value") };
|
|
239
|
+
case "remove-human-decision":
|
|
240
|
+
return { op, index: expectInteger(patch.index, "patch.index") };
|
|
241
|
+
case "set-suggested-next-action":
|
|
242
|
+
return { op, value: expectString(patch.value, "patch.value") };
|
|
243
|
+
default:
|
|
244
|
+
throw new ValidationError(`Unknown patch operation: ${op}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function validateTask(task, prefix) {
|
|
248
|
+
return {
|
|
249
|
+
id: expectString(task.id, `${prefix}.id`),
|
|
250
|
+
title: expectString(task.title, `${prefix}.title`),
|
|
251
|
+
description: expectString(task.description, `${prefix}.description`),
|
|
252
|
+
dependsOn: expectStringArray(task.dependsOn || task.dependencies || [], `${prefix}.dependsOn`),
|
|
253
|
+
blocks: expectStringArray(task.blocks || [], `${prefix}.blocks`),
|
|
254
|
+
canRunInParallelWith: expectStringArray(task.canRunInParallelWith || [], `${prefix}.canRunInParallelWith`),
|
|
255
|
+
risk: validateRisk(task.risk || "medium", `${prefix}.risk`),
|
|
256
|
+
filesLikelyInvolved: expectStringArray(task.filesLikelyInvolved || [], `${prefix}.filesLikelyInvolved`),
|
|
257
|
+
acceptanceCriteria: expectStringArray(task.acceptanceCriteria || [], `${prefix}.acceptanceCriteria`),
|
|
258
|
+
verification: expectStringArray(task.verification || [], `${prefix}.verification`)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function validateTaskPatch(patch, prefix) {
|
|
262
|
+
const result = {};
|
|
263
|
+
if (patch.title !== undefined)
|
|
264
|
+
result.title = expectString(patch.title, `${prefix}.title`);
|
|
265
|
+
if (patch.description !== undefined)
|
|
266
|
+
result.description = expectString(patch.description, `${prefix}.description`);
|
|
267
|
+
const dependsOn = patch.dependsOn || patch.dependencies;
|
|
268
|
+
if (dependsOn !== undefined)
|
|
269
|
+
result.dependsOn = expectStringArray(dependsOn, `${prefix}.dependsOn`);
|
|
270
|
+
if (patch.blocks !== undefined)
|
|
271
|
+
result.blocks = expectStringArray(patch.blocks, `${prefix}.blocks`);
|
|
272
|
+
if (patch.canRunInParallelWith !== undefined)
|
|
273
|
+
result.canRunInParallelWith = expectStringArray(patch.canRunInParallelWith, `${prefix}.canRunInParallelWith`);
|
|
274
|
+
if (patch.risk !== undefined)
|
|
275
|
+
result.risk = validateRisk(patch.risk, `${prefix}.risk`);
|
|
276
|
+
if (patch.filesLikelyInvolved !== undefined)
|
|
277
|
+
result.filesLikelyInvolved = expectStringArray(patch.filesLikelyInvolved, `${prefix}.filesLikelyInvolved`);
|
|
278
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
279
|
+
result.acceptanceCriteria = expectStringArray(patch.acceptanceCriteria, `${prefix}.acceptanceCriteria`);
|
|
280
|
+
if (patch.verification !== undefined)
|
|
281
|
+
result.verification = expectStringArray(patch.verification, `${prefix}.verification`);
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
function validateWave(wave, prefix) {
|
|
285
|
+
return {
|
|
286
|
+
id: expectString(wave.id, `${prefix}.id`),
|
|
287
|
+
taskIds: expectStringArray(wave.taskIds, `${prefix}.taskIds`),
|
|
288
|
+
rationale: typeof wave.rationale === "string" ? wave.rationale.trim() : ""
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function validateWavePatch(patch, prefix) {
|
|
292
|
+
const result = {};
|
|
293
|
+
if (patch.taskIds !== undefined)
|
|
294
|
+
result.taskIds = expectStringArray(patch.taskIds, `${prefix}.taskIds`);
|
|
295
|
+
if (patch.rationale !== undefined)
|
|
296
|
+
result.rationale = expectString(patch.rationale, `${prefix}.rationale`);
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
function validateRisk(risk, name) {
|
|
300
|
+
const r = expectString(risk, name);
|
|
301
|
+
if (r !== "low" && r !== "medium" && r !== "high") {
|
|
302
|
+
throw new ValidationError(`Expected ${name} to be one of: low, medium, high`);
|
|
303
|
+
}
|
|
304
|
+
return r;
|
|
305
|
+
}
|
|
306
|
+
function validateSessionLookupArgs(args) {
|
|
307
|
+
const obj = expectObject(args);
|
|
308
|
+
return {
|
|
309
|
+
sessionId: expectString(obj.sessionId, "sessionId")
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function expectObject(val, name = "arguments") {
|
|
313
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) {
|
|
314
|
+
throw new ValidationError(`Expected ${name} to be an object`);
|
|
315
|
+
}
|
|
316
|
+
return val;
|
|
317
|
+
}
|
|
318
|
+
function expectString(val, name) {
|
|
319
|
+
if (typeof val !== "string" || val.trim() === "") {
|
|
320
|
+
throw new ValidationError(`Expected ${name} to be a non-empty string`);
|
|
321
|
+
}
|
|
322
|
+
return val.trim();
|
|
323
|
+
}
|
|
324
|
+
function expectStringArray(val, name) {
|
|
325
|
+
if (!Array.isArray(val)) {
|
|
326
|
+
throw new ValidationError(`Expected ${name} to be an array`);
|
|
327
|
+
}
|
|
328
|
+
return val.map((item, i) => expectString(item, `${name}[${i}]`));
|
|
329
|
+
}
|
|
330
|
+
function expectInteger(val, name) {
|
|
331
|
+
if (typeof val !== "number" || !Number.isInteger(val)) {
|
|
332
|
+
throw new ValidationError(`Expected ${name} to be an integer`);
|
|
333
|
+
}
|
|
334
|
+
return val;
|
|
335
|
+
}
|
|
336
|
+
const TOOL_DEFINITIONS = [
|
|
337
|
+
{
|
|
338
|
+
name: "start_session",
|
|
339
|
+
title: "Start Plan Breakdown Session",
|
|
340
|
+
description: "Start a new plan breakdown session for generating packet plans.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
additionalProperties: false,
|
|
344
|
+
required: ["title", "summary"],
|
|
345
|
+
properties: {
|
|
346
|
+
title: { type: "string", description: "The title of the plan breakdown." },
|
|
347
|
+
summary: { type: "string", description: "A high-level summary of the breakdown." }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "apply_patch",
|
|
353
|
+
title: "Apply Patch to Session",
|
|
354
|
+
description: "Apply a patch operation to an existing plan breakdown session.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
required: ["sessionId", "patch"],
|
|
359
|
+
properties: {
|
|
360
|
+
sessionId: { type: "string" },
|
|
361
|
+
patch: {
|
|
362
|
+
type: "object",
|
|
363
|
+
description: "A single patch operation (e.g., add-task, update-task, add-wave)."
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "inspect_session",
|
|
370
|
+
title: "Inspect Session",
|
|
371
|
+
description: "Return a human-readable overview of the current session state.",
|
|
372
|
+
inputSchema: {
|
|
373
|
+
type: "object",
|
|
374
|
+
additionalProperties: false,
|
|
375
|
+
required: ["sessionId"],
|
|
376
|
+
properties: {
|
|
377
|
+
sessionId: { type: "string" }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "validate_session",
|
|
383
|
+
title: "Validate Session",
|
|
384
|
+
description: "Run validation rules on the session and return any issues.",
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: "object",
|
|
387
|
+
additionalProperties: false,
|
|
388
|
+
required: ["sessionId"],
|
|
389
|
+
properties: {
|
|
390
|
+
sessionId: { type: "string" }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "export_session",
|
|
396
|
+
title: "Export Session",
|
|
397
|
+
description: "Export a valid session to a PlanBreakdownResult artifact.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
type: "object",
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
required: ["sessionId"],
|
|
402
|
+
properties: {
|
|
403
|
+
sessionId: { type: "string" }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
];
|