@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,212 @@
|
|
|
1
|
+
export function validateSession(session) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
const checkStringArray = (arr, path, fieldName) => {
|
|
4
|
+
if (!Array.isArray(arr)) {
|
|
5
|
+
issues.push({ severity: "error", code: `missing-${fieldName}`, message: `${fieldName} must be an array`, path });
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
let allStrings = true;
|
|
9
|
+
arr.forEach((item, index) => {
|
|
10
|
+
if (typeof item !== "string") {
|
|
11
|
+
issues.push({
|
|
12
|
+
severity: "error",
|
|
13
|
+
code: `invalid-${fieldName}-element`,
|
|
14
|
+
message: `${fieldName} element at index ${index} must be a string`,
|
|
15
|
+
path: [...path, index.toString()]
|
|
16
|
+
});
|
|
17
|
+
allStrings = false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return allStrings;
|
|
21
|
+
};
|
|
22
|
+
const checkString = (val, path, fieldName, required = true) => {
|
|
23
|
+
if (typeof val !== "string") {
|
|
24
|
+
issues.push({ severity: "error", code: `invalid-${fieldName}`, message: `${fieldName} must be a string`, path });
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (required && !val) {
|
|
28
|
+
issues.push({ severity: "error", code: `missing-${fieldName}`, message: `${fieldName} is required`, path });
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
// Required fields
|
|
34
|
+
checkString(session.title, ["title"], "title");
|
|
35
|
+
checkString(session.summary, ["summary"], "summary");
|
|
36
|
+
checkString(session.recommendedFirstWaveId, ["recommendedFirstWaveId"], "recommended-first-wave");
|
|
37
|
+
checkString(session.suggestedNextAction, ["suggestedNextAction"], "suggested-next-action");
|
|
38
|
+
checkString(session.createdAt, ["createdAt"], "createdAt");
|
|
39
|
+
checkString(session.updatedAt, ["updatedAt"], "updatedAt");
|
|
40
|
+
checkStringArray(session.graphConcerns, ["graphConcerns"], "graphConcerns");
|
|
41
|
+
checkStringArray(session.humanDecisionsNeeded, ["humanDecisionsNeeded"], "humanDecisionsNeeded");
|
|
42
|
+
if (typeof session.tasks !== "object" || session.tasks === null || Array.isArray(session.tasks)) {
|
|
43
|
+
issues.push({ severity: "error", code: "invalid-tasks", message: "tasks must be an object", path: ["tasks"] });
|
|
44
|
+
}
|
|
45
|
+
if (typeof session.waves !== "object" || session.waves === null || Array.isArray(session.waves)) {
|
|
46
|
+
issues.push({ severity: "error", code: "invalid-waves", message: "waves must be an object", path: ["waves"] });
|
|
47
|
+
}
|
|
48
|
+
const taskIds = new Set(Object.keys(session.tasks || {}));
|
|
49
|
+
if (taskIds.size === 0) {
|
|
50
|
+
issues.push({ severity: "error", code: "no-tasks", message: "At least one task is required" });
|
|
51
|
+
}
|
|
52
|
+
const waveIdsInOrder = new Set();
|
|
53
|
+
if (checkStringArray(session.waveOrder, ["waveOrder"], "waveOrder")) {
|
|
54
|
+
for (const waveId of session.waveOrder) {
|
|
55
|
+
if (waveIdsInOrder.has(waveId)) {
|
|
56
|
+
issues.push({ severity: "error", code: "duplicate-wave-order", message: `Wave ID ${waveId} appears multiple times in waveOrder`, path: ["waveOrder"] });
|
|
57
|
+
}
|
|
58
|
+
waveIdsInOrder.add(waveId);
|
|
59
|
+
}
|
|
60
|
+
if (waveIdsInOrder.size === 0) {
|
|
61
|
+
issues.push({ severity: "error", code: "no-waves", message: "At least one wave is required" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const waves = session.waves || {};
|
|
65
|
+
for (const waveId of Object.keys(waves)) {
|
|
66
|
+
if (!waveIdsInOrder.has(waveId)) {
|
|
67
|
+
issues.push({ severity: "error", code: "orphaned-wave", message: `Wave ${waveId} exists but is not in waveOrder`, path: ["waves", waveId] });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Task validation
|
|
71
|
+
for (const [taskId, task] of Object.entries(session.tasks || {})) {
|
|
72
|
+
if (checkString(task.id, ["tasks", taskId, "id"], "task-id")) {
|
|
73
|
+
if (task.id !== taskId) {
|
|
74
|
+
issues.push({ severity: "error", code: "task-id-mismatch", message: `Task entry ${taskId} has mismatched internal ID: ${task.id}`, path: ["tasks", taskId] });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
checkString(task.title, ["tasks", taskId, "title"], "task-title");
|
|
78
|
+
checkString(task.description, ["tasks", taskId, "description"], "task-description");
|
|
79
|
+
if (typeof task.risk !== "string") {
|
|
80
|
+
issues.push({ severity: "error", code: "invalid-risk-type", message: `Task ${taskId} risk must be a string`, path: ["tasks", taskId, "risk"] });
|
|
81
|
+
}
|
|
82
|
+
else if (!["low", "medium", "high"].includes(task.risk)) {
|
|
83
|
+
issues.push({ severity: "error", code: "invalid-risk-value", message: `Task ${taskId} has invalid risk: ${task.risk}`, path: ["tasks", taskId, "risk"] });
|
|
84
|
+
}
|
|
85
|
+
const checkRefs = (refs, name) => {
|
|
86
|
+
if (!checkStringArray(refs, ["tasks", taskId, name], name)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
for (const ref of refs) {
|
|
91
|
+
if (seen.has(ref)) {
|
|
92
|
+
issues.push({ severity: "error", code: "duplicate-ref", message: `Task ${taskId} has duplicate reference in ${name}: ${ref}`, path: ["tasks", taskId, name] });
|
|
93
|
+
}
|
|
94
|
+
seen.add(ref);
|
|
95
|
+
if (ref === taskId) {
|
|
96
|
+
issues.push({ severity: "error", code: "self-ref", message: `Task ${taskId} has a self-reference in ${name}`, path: ["tasks", taskId, name] });
|
|
97
|
+
}
|
|
98
|
+
if (!taskIds.has(ref)) {
|
|
99
|
+
issues.push({ severity: "error", code: "unknown-ref", message: `Task ${taskId} references unknown task ID in ${name}: ${ref}`, path: ["tasks", taskId, name] });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
checkRefs(task.dependsOn, "dependsOn");
|
|
104
|
+
checkRefs(task.blocks, "blocks");
|
|
105
|
+
checkRefs(task.canRunInParallelWith, "canRunInParallelWith");
|
|
106
|
+
// Warnings & More validation
|
|
107
|
+
if (checkStringArray(task.verification, ["tasks", taskId, "verification"], "verification")) {
|
|
108
|
+
if (task.verification.length === 0) {
|
|
109
|
+
issues.push({ severity: "warning", code: "missing-verification", message: `Task ${taskId} has no verification commands`, path: ["tasks", taskId] });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
checkStringArray(task.acceptanceCriteria, ["tasks", taskId, "acceptanceCriteria"], "acceptanceCriteria");
|
|
113
|
+
checkStringArray(task.filesLikelyInvolved, ["tasks", taskId, "filesLikelyInvolved"], "filesLikelyInvolved");
|
|
114
|
+
}
|
|
115
|
+
// Wave validation
|
|
116
|
+
const tasksInWaves = new Set();
|
|
117
|
+
if (Array.isArray(session.waveOrder)) {
|
|
118
|
+
for (const waveId of session.waveOrder) {
|
|
119
|
+
const wave = waves[waveId];
|
|
120
|
+
if (!wave) {
|
|
121
|
+
issues.push({ severity: "error", code: "unknown-wave-order", message: `Wave ID ${waveId} in waveOrder does not exist in waves`, path: ["waveOrder"] });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (checkString(wave.id, ["waves", waveId, "id"], "wave-id")) {
|
|
125
|
+
if (wave.id !== waveId) {
|
|
126
|
+
issues.push({ severity: "error", code: "wave-id-mismatch", message: `Wave entry ${waveId} has mismatched internal ID: ${wave.id}`, path: ["waves", waveId] });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
checkString(wave.rationale, ["waves", waveId, "rationale"], "wave-rationale");
|
|
130
|
+
if (checkStringArray(wave.taskIds, ["waves", waveId, "taskIds"], "wave-taskIds")) {
|
|
131
|
+
if (wave.taskIds.length === 0) {
|
|
132
|
+
issues.push({ severity: "error", code: "empty-wave", message: `Wave ${waveId} has no tasks`, path: ["waves", waveId] });
|
|
133
|
+
}
|
|
134
|
+
for (const taskId of wave.taskIds) {
|
|
135
|
+
if (!taskIds.has(taskId)) {
|
|
136
|
+
issues.push({ severity: "error", code: "unknown-wave-task", message: `Wave ${waveId} references unknown task ID: ${taskId}`, path: ["waves", waveId] });
|
|
137
|
+
}
|
|
138
|
+
if (tasksInWaves.has(taskId)) {
|
|
139
|
+
issues.push({ severity: "error", code: "duplicate-task-wave", message: `Task ${taskId} appears in multiple waves`, path: ["waves", waveId] });
|
|
140
|
+
}
|
|
141
|
+
tasksInWaves.add(taskId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const taskId of taskIds) {
|
|
147
|
+
if (!tasksInWaves.has(taskId)) {
|
|
148
|
+
issues.push({ severity: "error", code: "unassigned-task", message: `Task ${taskId} is not assigned to any wave`, path: ["tasks", taskId] });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (session.recommendedFirstWaveId && !waveIdsInOrder.has(session.recommendedFirstWaveId)) {
|
|
152
|
+
issues.push({ severity: "error", code: "invalid-first-wave", message: `recommendedFirstWaveId ${session.recommendedFirstWaveId} does not exist`, path: ["recommendedFirstWaveId"] });
|
|
153
|
+
}
|
|
154
|
+
// Wave dependency check. A wave is a dispatch batch, so dependencies must
|
|
155
|
+
// be satisfied by earlier waves, never by later or same-wave packets.
|
|
156
|
+
const taskToWaveIndex = new Map();
|
|
157
|
+
if (Array.isArray(session.waveOrder)) {
|
|
158
|
+
session.waveOrder.forEach((waveId, index) => {
|
|
159
|
+
const wave = waves[waveId];
|
|
160
|
+
if (wave && Array.isArray(wave.taskIds)) {
|
|
161
|
+
wave.taskIds.forEach((taskId) => taskToWaveIndex.set(taskId, index));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const [taskId, task] of Object.entries(session.tasks || {})) {
|
|
166
|
+
const currentWaveIndex = taskToWaveIndex.get(taskId);
|
|
167
|
+
if (currentWaveIndex === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
if (Array.isArray(task.dependsOn)) {
|
|
170
|
+
for (const depId of task.dependsOn) {
|
|
171
|
+
const depWaveIndex = taskToWaveIndex.get(depId);
|
|
172
|
+
if (depWaveIndex !== undefined && depWaveIndex === currentWaveIndex) {
|
|
173
|
+
issues.push({
|
|
174
|
+
severity: "error",
|
|
175
|
+
code: "same-wave-dependency",
|
|
176
|
+
message: `Task ${taskId} (Wave ${currentWaveIndex + 1}) depends on ${depId} in the same wave; dependent packets must be split into later waves`,
|
|
177
|
+
path: ["tasks", taskId, "dependsOn"]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else if (depWaveIndex !== undefined && depWaveIndex > currentWaveIndex) {
|
|
181
|
+
issues.push({
|
|
182
|
+
severity: "error",
|
|
183
|
+
code: "forward-dependency",
|
|
184
|
+
message: `Task ${taskId} (Wave ${currentWaveIndex + 1}) depends on ${depId} in a later wave (Wave ${depWaveIndex + 1})`,
|
|
185
|
+
path: ["tasks", taskId, "dependsOn"]
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// File overlap warning
|
|
192
|
+
const fileToTasks = new Map();
|
|
193
|
+
for (const [taskId, task] of Object.entries(session.tasks || {})) {
|
|
194
|
+
if (Array.isArray(task.filesLikelyInvolved)) {
|
|
195
|
+
for (const file of task.filesLikelyInvolved) {
|
|
196
|
+
if (!fileToTasks.has(file))
|
|
197
|
+
fileToTasks.set(file, []);
|
|
198
|
+
fileToTasks.get(file).push(taskId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const [file, overlappingTaskIds] of fileToTasks.entries()) {
|
|
203
|
+
if (overlappingTaskIds.length > 3) {
|
|
204
|
+
issues.push({
|
|
205
|
+
severity: "warning",
|
|
206
|
+
code: "heavy-file-overlap",
|
|
207
|
+
message: `File ${file} is involved in many tasks: ${overlappingTaskIds.join(", ")}`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return issues;
|
|
212
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export const PLAN_BREAKDOWN_JSON_CONTRACT = `Return exactly one JSON object matching this structure:
|
|
2
|
+
{
|
|
3
|
+
"title": "Short descriptive title of the overall plan",
|
|
4
|
+
"summary": "High-level summary of the implementation strategy",
|
|
5
|
+
"tasks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "T1",
|
|
8
|
+
"title": "Task title",
|
|
9
|
+
"description": "Detailed description of what to implement",
|
|
10
|
+
"dependsOn": [],
|
|
11
|
+
"blocks": [],
|
|
12
|
+
"canRunInParallelWith": [],
|
|
13
|
+
"risk": "low | medium | high",
|
|
14
|
+
"filesLikelyInvolved": ["src/file.ts"],
|
|
15
|
+
"acceptanceCriteria": ["Criterion 1"],
|
|
16
|
+
"verification": ["npm test"]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"waves": [
|
|
20
|
+
{
|
|
21
|
+
"id": "W1",
|
|
22
|
+
"taskIds": ["T1"],
|
|
23
|
+
"rationale": "Why these tasks are grouped together"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"recommendedFirstWaveId": "W1",
|
|
27
|
+
"graphConcerns": ["Any potential risks or complexities in the dependency graph"],
|
|
28
|
+
"humanDecisionsNeeded": ["Questions or choices that require human input"],
|
|
29
|
+
"suggestedNextAction": "The next concrete step for the orchestrator (e.g., move to implementation)"
|
|
30
|
+
}`;
|
|
31
|
+
export const PLAN_BREAKDOWN_GRAPH_RULES = [
|
|
32
|
+
"Every task must appear in exactly one wave.",
|
|
33
|
+
"Every task ID referenced in 'dependsOn', 'blocks', or 'canRunInParallelWith' must exist in the 'tasks' array.",
|
|
34
|
+
"Every wave must reference existing task IDs.",
|
|
35
|
+
"The 'recommendedFirstWaveId' must match an existing wave ID.",
|
|
36
|
+
"Dependencies ('dependsOn') must point only to tasks in earlier waves. A wave is a dispatch batch, so dependent tasks must never be grouped in the same wave.",
|
|
37
|
+
"All array fields are required; use empty arrays [] if no items exist.",
|
|
38
|
+
"Do not use null for optional fields; use empty strings or arrays as appropriate."
|
|
39
|
+
];
|
|
40
|
+
export const PLAN_BREAKDOWN_JSON_EXAMPLE_OBJECT = {
|
|
41
|
+
title: "Add XState Support",
|
|
42
|
+
summary: "Introduce XState for workstream phase management to improve reliability and observability.",
|
|
43
|
+
tasks: [
|
|
44
|
+
{
|
|
45
|
+
id: "T1",
|
|
46
|
+
title: "Add xstate dependency",
|
|
47
|
+
description: "Install xstate and @xstate/test in package.json.",
|
|
48
|
+
dependsOn: [],
|
|
49
|
+
blocks: ["T2"],
|
|
50
|
+
canRunInParallelWith: [],
|
|
51
|
+
risk: "low",
|
|
52
|
+
filesLikelyInvolved: ["package.json"],
|
|
53
|
+
acceptanceCriteria: ["xstate is listed in dependencies", "package-lock.json is updated"],
|
|
54
|
+
verification: ["npm list xstate"]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "T2",
|
|
58
|
+
title: "Define workstream machine",
|
|
59
|
+
description: "Create the state machine definition for workstream phases.",
|
|
60
|
+
dependsOn: ["T1"],
|
|
61
|
+
blocks: [],
|
|
62
|
+
canRunInParallelWith: [],
|
|
63
|
+
risk: "medium",
|
|
64
|
+
filesLikelyInvolved: ["src/workflows/workstream-machine.ts"],
|
|
65
|
+
acceptanceCriteria: ["Machine covers all WORKSTREAM_PHASES", "Transitions are correctly mapped"],
|
|
66
|
+
verification: ["npm test test/workstream-machine.test.ts"]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
waves: [
|
|
70
|
+
{
|
|
71
|
+
id: "W1",
|
|
72
|
+
taskIds: ["T1"],
|
|
73
|
+
rationale: "Initial environment setup."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "W2",
|
|
77
|
+
taskIds: ["T2"],
|
|
78
|
+
rationale: "Core logic implementation."
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
recommendedFirstWaveId: "W1",
|
|
82
|
+
graphConcerns: ["Ensure transition logic matches existing manual phase advancement."],
|
|
83
|
+
humanDecisionsNeeded: ["Should we migrate existing workstreams immediately?"],
|
|
84
|
+
suggestedNextAction: "Apply implementation-start label to begin T1."
|
|
85
|
+
};
|
|
86
|
+
export const PLAN_BREAKDOWN_JSON_EXAMPLE = JSON.stringify(PLAN_BREAKDOWN_JSON_EXAMPLE_OBJECT, null, 2);
|
|
87
|
+
export function validatePlanBreakdownResultForPrompt(result) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
const taskIds = new Set();
|
|
90
|
+
for (const task of result.tasks) {
|
|
91
|
+
if (task.id && taskIds.has(task.id)) {
|
|
92
|
+
errors.push(`Duplicate task ID: ${task.id}`);
|
|
93
|
+
}
|
|
94
|
+
taskIds.add(task.id);
|
|
95
|
+
}
|
|
96
|
+
const waveIds = new Set();
|
|
97
|
+
for (const wave of result.waves) {
|
|
98
|
+
if (wave.id && waveIds.has(wave.id)) {
|
|
99
|
+
errors.push(`Duplicate wave ID: ${wave.id}`);
|
|
100
|
+
}
|
|
101
|
+
waveIds.add(wave.id);
|
|
102
|
+
}
|
|
103
|
+
const tasksInWaves = new Set();
|
|
104
|
+
if (result.tasks.length === 0) {
|
|
105
|
+
errors.push("At least one task is required.");
|
|
106
|
+
}
|
|
107
|
+
if (result.waves.length === 0) {
|
|
108
|
+
errors.push("At least one wave is required.");
|
|
109
|
+
}
|
|
110
|
+
for (const task of result.tasks) {
|
|
111
|
+
if (!task.id) {
|
|
112
|
+
errors.push(`Task has missing ID: ${task.title}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const checkArray = (refs, name) => {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const ref of refs) {
|
|
118
|
+
if (seen.has(ref)) {
|
|
119
|
+
errors.push(`Task ${task.id} has duplicate reference in ${name}: ${ref}`);
|
|
120
|
+
}
|
|
121
|
+
seen.add(ref);
|
|
122
|
+
if (ref === task.id) {
|
|
123
|
+
errors.push(`Task ${task.id} has a self-reference in ${name}.`);
|
|
124
|
+
}
|
|
125
|
+
if (!taskIds.has(ref)) {
|
|
126
|
+
errors.push(`Task ${task.id} references unknown task ID in ${name}: ${ref}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
checkArray(task.dependsOn, "dependsOn");
|
|
131
|
+
checkArray(task.blocks, "blocks");
|
|
132
|
+
checkArray(task.canRunInParallelWith, "canRunInParallelWith");
|
|
133
|
+
}
|
|
134
|
+
for (const wave of result.waves) {
|
|
135
|
+
if (!wave.id)
|
|
136
|
+
errors.push(`Wave has missing ID: ${wave.rationale}`);
|
|
137
|
+
for (const taskId of wave.taskIds) {
|
|
138
|
+
if (!taskIds.has(taskId)) {
|
|
139
|
+
errors.push(`Wave ${wave.id} references unknown task ID: ${taskId}`);
|
|
140
|
+
}
|
|
141
|
+
if (tasksInWaves.has(taskId)) {
|
|
142
|
+
errors.push(`Task ${taskId} appears in multiple waves.`);
|
|
143
|
+
}
|
|
144
|
+
tasksInWaves.add(taskId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const taskId of taskIds) {
|
|
148
|
+
if (taskId && !tasksInWaves.has(taskId)) {
|
|
149
|
+
errors.push(`Task ${taskId} is not assigned to any wave.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (result.recommendedFirstWaveId && !waveIds.has(result.recommendedFirstWaveId)) {
|
|
153
|
+
errors.push(`recommendedFirstWaveId ${result.recommendedFirstWaveId} does not exist.`);
|
|
154
|
+
}
|
|
155
|
+
// Wave-based dependency check: dependsOn must be in previous waves.
|
|
156
|
+
const taskToWaveIndex = new Map();
|
|
157
|
+
result.waves.forEach((wave, index) => {
|
|
158
|
+
wave.taskIds.forEach((taskId) => taskToWaveIndex.set(taskId, index));
|
|
159
|
+
});
|
|
160
|
+
for (const task of result.tasks) {
|
|
161
|
+
const currentWaveIndex = taskToWaveIndex.get(task.id);
|
|
162
|
+
if (currentWaveIndex === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
for (const depId of task.dependsOn) {
|
|
165
|
+
const depWaveIndex = taskToWaveIndex.get(depId);
|
|
166
|
+
if (depWaveIndex !== undefined && depWaveIndex === currentWaveIndex) {
|
|
167
|
+
errors.push(`Task ${task.id} (Wave ${currentWaveIndex + 1}) depends on ${depId} in the same wave; dependent tasks must be split into later waves.`);
|
|
168
|
+
}
|
|
169
|
+
else if (depWaveIndex !== undefined && depWaveIndex > currentWaveIndex) {
|
|
170
|
+
errors.push(`Task ${task.id} (Wave ${currentWaveIndex + 1}) depends on ${depId} in a later wave (Wave ${depWaveIndex + 1}).`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return errors;
|
|
175
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { slugify } from "../utils/slug.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns a URL-safe, filesystem-safe, and git-ref-safe slug for a packet ID.
|
|
5
|
+
* Packet IDs are agent-produced and may contain characters like spaces, slashes, or colons.
|
|
6
|
+
*
|
|
7
|
+
* To prevent collisions between different IDs that slugify to the same string
|
|
8
|
+
* (e.g. "foo/bar" and "foo bar") or IDs that are truncated, a short stable hash
|
|
9
|
+
* is appended to the slug.
|
|
10
|
+
*/
|
|
11
|
+
export function getPacketSlug(packetId) {
|
|
12
|
+
const slug = slugify(packetId);
|
|
13
|
+
const hash = createHash("sha256").update(packetId).digest("hex").slice(0, 8);
|
|
14
|
+
return `${slug}-${hash}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function buildWaveAssessmentCheckpointIdentity(preflight, extra) {
|
|
3
|
+
return {
|
|
4
|
+
repoKey: preflight.repoKey,
|
|
5
|
+
workstreamId: preflight.workstreamId,
|
|
6
|
+
waveId: `wave-${preflight.currentWaveIndex}`,
|
|
7
|
+
parentPrNumber: preflight.parentPrNumber ?? 0,
|
|
8
|
+
parentHeadSha: extra?.parentHeadSha ?? preflight.parentHeadSha ?? "unknown",
|
|
9
|
+
parentIntegrationBranch: preflight.parentIntegrationBranch,
|
|
10
|
+
defaultBranch: preflight.defaultBranch,
|
|
11
|
+
planRevision: extra?.planRevision ?? preflight.planRevision ?? "unknown",
|
|
12
|
+
originalPlanningSessionId: extra?.originalPlanningSessionId ?? preflight.originalPlanningSessionId ?? "unknown",
|
|
13
|
+
currentWaveIndex: preflight.currentWaveIndex,
|
|
14
|
+
nextWaveIndex: preflight.nextWaveIndex,
|
|
15
|
+
completedPacketIds: [...preflight.completedPacketIds],
|
|
16
|
+
pendingFuturePacketIds: [...preflight.pendingFuturePacketIds],
|
|
17
|
+
planNodeIds: [
|
|
18
|
+
...(extra?.planNodeIds ??
|
|
19
|
+
preflight.planNodeIds ?? [...preflight.completedPacketIds, ...preflight.pendingFuturePacketIds])
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function buildWaveAssessmentCheckpoint(identity) {
|
|
24
|
+
const identityHash = createHash("sha256")
|
|
25
|
+
.update(stableStringifyIdentity(identity))
|
|
26
|
+
.digest("hex")
|
|
27
|
+
.slice(0, 16);
|
|
28
|
+
const id = [
|
|
29
|
+
"wave-assessment",
|
|
30
|
+
truncateIdPart(sanitizeIdPart(identity.repoKey), 40),
|
|
31
|
+
truncateIdPart(sanitizeIdPart(identity.workstreamId), 60),
|
|
32
|
+
`wave-${identity.currentWaveIndex}-to-${identity.nextWaveIndex}`,
|
|
33
|
+
`pr-${identity.parentPrNumber}`,
|
|
34
|
+
identityHash
|
|
35
|
+
].join(":");
|
|
36
|
+
return {
|
|
37
|
+
id,
|
|
38
|
+
sessionId: id,
|
|
39
|
+
identity
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function buildWaveAssessmentCheckpointForPreflight(preflight) {
|
|
43
|
+
return buildWaveAssessmentCheckpoint(buildWaveAssessmentCheckpointIdentity(preflight));
|
|
44
|
+
}
|
|
45
|
+
export function waveAssessmentCheckpointIdentityMatches(left, right) {
|
|
46
|
+
return (left.repoKey === right.repoKey &&
|
|
47
|
+
left.workstreamId === right.workstreamId &&
|
|
48
|
+
left.waveId === right.waveId &&
|
|
49
|
+
left.parentPrNumber === right.parentPrNumber &&
|
|
50
|
+
left.parentHeadSha === right.parentHeadSha &&
|
|
51
|
+
left.parentIntegrationBranch === right.parentIntegrationBranch &&
|
|
52
|
+
left.defaultBranch === right.defaultBranch &&
|
|
53
|
+
left.planRevision === right.planRevision &&
|
|
54
|
+
left.originalPlanningSessionId === right.originalPlanningSessionId &&
|
|
55
|
+
left.currentWaveIndex === right.currentWaveIndex &&
|
|
56
|
+
left.nextWaveIndex === right.nextWaveIndex &&
|
|
57
|
+
arraysEqual(left.completedPacketIds, right.completedPacketIds) &&
|
|
58
|
+
arraysEqual(left.pendingFuturePacketIds, right.pendingFuturePacketIds) &&
|
|
59
|
+
arraysEqual(left.planNodeIds, right.planNodeIds));
|
|
60
|
+
}
|
|
61
|
+
export function isWaveAssessmentStaleForPreflight(assessment, preflight) {
|
|
62
|
+
const checkpoint = buildWaveAssessmentCheckpointForPreflight(preflight);
|
|
63
|
+
return !waveAssessmentCheckpointIdentityMatches(assessment.checkpoint.identity, checkpoint.identity);
|
|
64
|
+
}
|
|
65
|
+
export function decideWaveAssessmentGate(input) {
|
|
66
|
+
const checkpoint = buildWaveAssessmentCheckpointForPreflight(input.preflight);
|
|
67
|
+
const { assessment } = input;
|
|
68
|
+
if (!assessment) {
|
|
69
|
+
return { status: "gated", reason: "assessment_missing", checkpoint };
|
|
70
|
+
}
|
|
71
|
+
if (!waveAssessmentCheckpointIdentityMatches(assessment.checkpoint.identity, checkpoint.identity)) {
|
|
72
|
+
return {
|
|
73
|
+
status: "stale",
|
|
74
|
+
reason: "assessment_checkpoint_stale",
|
|
75
|
+
checkpoint,
|
|
76
|
+
assessment
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (assessment.status !== "finalized") {
|
|
80
|
+
return {
|
|
81
|
+
status: "gated",
|
|
82
|
+
reason: "assessment_not_finalized",
|
|
83
|
+
checkpoint,
|
|
84
|
+
assessment
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (assessment.decision === "replan") {
|
|
88
|
+
return {
|
|
89
|
+
status: "gated",
|
|
90
|
+
reason: "assessment_requires_replan",
|
|
91
|
+
checkpoint,
|
|
92
|
+
assessment
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (assessment.decision === "blocked") {
|
|
96
|
+
return {
|
|
97
|
+
status: "gated",
|
|
98
|
+
reason: "assessment_blocked",
|
|
99
|
+
checkpoint,
|
|
100
|
+
assessment
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!assessment.postedCommentId) {
|
|
104
|
+
return {
|
|
105
|
+
status: "gated",
|
|
106
|
+
reason: "assessment_comment_missing",
|
|
107
|
+
checkpoint,
|
|
108
|
+
assessment
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status: "allowed",
|
|
113
|
+
reason: "assessment_continue_comment_posted",
|
|
114
|
+
checkpoint,
|
|
115
|
+
assessment
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function arraysEqual(left, right) {
|
|
119
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
120
|
+
}
|
|
121
|
+
function sanitizeIdPart(value) {
|
|
122
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "none";
|
|
123
|
+
}
|
|
124
|
+
function truncateIdPart(value, maxLength) {
|
|
125
|
+
return value.length <= maxLength ? value : value.slice(0, maxLength);
|
|
126
|
+
}
|
|
127
|
+
function stableStringifyIdentity(identity) {
|
|
128
|
+
return JSON.stringify({
|
|
129
|
+
repoKey: identity.repoKey,
|
|
130
|
+
workstreamId: identity.workstreamId,
|
|
131
|
+
waveId: identity.waveId,
|
|
132
|
+
parentPrNumber: identity.parentPrNumber,
|
|
133
|
+
parentHeadSha: identity.parentHeadSha,
|
|
134
|
+
parentIntegrationBranch: identity.parentIntegrationBranch,
|
|
135
|
+
defaultBranch: identity.defaultBranch,
|
|
136
|
+
planRevision: identity.planRevision,
|
|
137
|
+
originalPlanningSessionId: identity.originalPlanningSessionId,
|
|
138
|
+
currentWaveIndex: identity.currentWaveIndex,
|
|
139
|
+
nextWaveIndex: identity.nextWaveIndex,
|
|
140
|
+
completedPacketIds: identity.completedPacketIds,
|
|
141
|
+
pendingFuturePacketIds: identity.pendingFuturePacketIds,
|
|
142
|
+
planNodeIds: identity.planNodeIds
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export function applyWaveAssessmentResultsToWorkstreams(input) {
|
|
146
|
+
if (input.assessments.length === 0) {
|
|
147
|
+
return input.workstreams;
|
|
148
|
+
}
|
|
149
|
+
const assessmentsByWorkstreamId = new Map();
|
|
150
|
+
for (const assessment of input.assessments) {
|
|
151
|
+
const list = assessmentsByWorkstreamId.get(assessment.checkpoint.identity.workstreamId) ?? [];
|
|
152
|
+
list.push(assessment);
|
|
153
|
+
assessmentsByWorkstreamId.set(assessment.checkpoint.identity.workstreamId, list);
|
|
154
|
+
}
|
|
155
|
+
return input.workstreams.map((workstream) => {
|
|
156
|
+
const assessments = assessmentsByWorkstreamId.get(workstream.id);
|
|
157
|
+
if (!workstream.packetPlan || !assessments || assessments.length === 0) {
|
|
158
|
+
return workstream;
|
|
159
|
+
}
|
|
160
|
+
const byId = new Map((workstream.packetPlan.waveAssessments ?? []).map((assessment) => [assessment.id, assessment]));
|
|
161
|
+
for (const assessment of assessments) {
|
|
162
|
+
byId.set(assessment.id, assessment);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...workstream,
|
|
166
|
+
packetPlan: {
|
|
167
|
+
...workstream.packetPlan,
|
|
168
|
+
waveAssessments: [...byId.values()].sort((left, right) => left.id.localeCompare(right.id))
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const WAVE_ASSESSMENT_DECISIONS = ["continue", "replan", "blocked"];
|
|
3
|
+
export const WAVE_ASSESSMENT_FINDING_CATEGORIES = [
|
|
4
|
+
"correctness",
|
|
5
|
+
"completeness",
|
|
6
|
+
"drift",
|
|
7
|
+
"invalid_assumption"
|
|
8
|
+
];
|
|
9
|
+
export function createWaveAssessmentSession(input) {
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
return {
|
|
12
|
+
...input,
|
|
13
|
+
planNodeIds: [...input.planNodeIds],
|
|
14
|
+
sessionId: input.sessionId,
|
|
15
|
+
status: "open",
|
|
16
|
+
summary: "",
|
|
17
|
+
findings: [],
|
|
18
|
+
validationIssues: [],
|
|
19
|
+
timestamps: {
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function addWaveAssessmentFinding(session, input) {
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
return {
|
|
28
|
+
...session,
|
|
29
|
+
status: "open",
|
|
30
|
+
findings: [
|
|
31
|
+
...session.findings,
|
|
32
|
+
{
|
|
33
|
+
id: randomUUID(),
|
|
34
|
+
category: input.category,
|
|
35
|
+
summary: input.summary,
|
|
36
|
+
details: input.details,
|
|
37
|
+
affectedPlanNodeIds: [...input.affectedPlanNodeIds],
|
|
38
|
+
createdAt: now
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
validationIssues: [],
|
|
42
|
+
timestamps: {
|
|
43
|
+
...session.timestamps,
|
|
44
|
+
updatedAt: now
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function completeWaveAssessmentSession(session, input) {
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
return {
|
|
51
|
+
...session,
|
|
52
|
+
status: "completed",
|
|
53
|
+
decision: input.decision,
|
|
54
|
+
summary: input.summary,
|
|
55
|
+
validationIssues: [],
|
|
56
|
+
timestamps: {
|
|
57
|
+
...session.timestamps,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
finalizedAt: now
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function exportWaveAssessmentSession(session) {
|
|
64
|
+
if (session.status !== "completed" || !session.decision || !session.timestamps.finalizedAt) {
|
|
65
|
+
throw new Error(`Wave assessment session ${session.sessionId} is not completed`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
sessionId: session.sessionId,
|
|
69
|
+
workstreamId: session.workstreamId,
|
|
70
|
+
waveId: session.waveId,
|
|
71
|
+
parentPrNumber: session.parentPrNumber,
|
|
72
|
+
assessedParentHeadSha: session.assessedParentHeadSha,
|
|
73
|
+
originalPlanningSessionId: session.originalPlanningSessionId,
|
|
74
|
+
currentPlanRevision: session.currentPlanRevision,
|
|
75
|
+
planNodeIds: [...session.planNodeIds],
|
|
76
|
+
decision: session.decision,
|
|
77
|
+
summary: session.summary,
|
|
78
|
+
findings: session.findings,
|
|
79
|
+
timestamps: {
|
|
80
|
+
createdAt: session.timestamps.createdAt,
|
|
81
|
+
finalizedAt: session.timestamps.finalizedAt
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|