karajan-code 2.7.4 → 2.8.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 +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +85 -31
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan reviewer — third pass after `kj plan` (PR D of the v2.7.5
|
|
3
|
+
* tests-first roadmap). Asks the planner LLM five concrete
|
|
4
|
+
* questions about the plan it just produced. Returns structured
|
|
5
|
+
* findings, NOT free-form prose.
|
|
6
|
+
*
|
|
7
|
+
* The questions are intentionally closed-ended:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Missing HUs** — sections of the SPEC the plan doesn't cover.
|
|
10
|
+
* (Catches "the planner forgot module X".)
|
|
11
|
+
* 2. **Missing dependencies** — pairs of HUs that should have a
|
|
12
|
+
* `blocked_by` edge but don't.
|
|
13
|
+
* (Catches "HU 4 needs the schema HU 2 produces".)
|
|
14
|
+
* 3. **Scope overlap** — pairs of HUs that touch the same scope.
|
|
15
|
+
* (Catches "the planner split one piece of work into two HUs
|
|
16
|
+
* that step on each other".)
|
|
17
|
+
* 4. **Parallelisable** — sets of HUs that have no inter-deps and
|
|
18
|
+
* could run side-by-side.
|
|
19
|
+
* (Surface for the user, not auto-applied — `kj run --plan`
|
|
20
|
+
* runs serially today.)
|
|
21
|
+
* 5. **Order issues** — HUs whose `blocked_by` chain is wrong or
|
|
22
|
+
* circular.
|
|
23
|
+
*
|
|
24
|
+
* The output is purely advisory. The reviewer NEVER edits the plan.
|
|
25
|
+
* It reports findings; the user decides whether to apply them via
|
|
26
|
+
* the board's edit modal or another `kj plan` pass.
|
|
27
|
+
*
|
|
28
|
+
* Free-form "could we add logging?" suggestions are explicitly out
|
|
29
|
+
* of scope to avoid scope-creep noise — see the prompt rules.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the focused review prompt. Exported for unit-testing the
|
|
36
|
+
* shape without an LLM in the loop.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} args
|
|
39
|
+
* @param {string} args.task
|
|
40
|
+
* @param {Array<object>} args.hus
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function buildReviewerPrompt({ task, hus }) {
|
|
44
|
+
const huSummary = hus.map((h, i) => {
|
|
45
|
+
const deps = Array.isArray(h.blocked_by) && h.blocked_by.length > 0
|
|
46
|
+
? ` ← blocked_by: ${h.blocked_by.join(", ")}`
|
|
47
|
+
: "";
|
|
48
|
+
const spec = h.spec_section ? ` [§${h.spec_section}]` : "";
|
|
49
|
+
return `${i + 1}. ${h.id}${spec}: ${h.title || "(untitled)"}${deps}\n scope: ${h.scope || "(none)"}`;
|
|
50
|
+
}).join("\n\n");
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
"You previously generated an implementation plan for the task below.",
|
|
54
|
+
"Your ONLY job in this call is to review it against five specific questions.",
|
|
55
|
+
"DO NOT propose new features, logging, monitoring, refactors, or anything",
|
|
56
|
+
"beyond what the SPEC explicitly requires. We're checking the plan for",
|
|
57
|
+
"completeness and correctness — NOT improving its scope.",
|
|
58
|
+
"",
|
|
59
|
+
"Output MUST be a JSON object with this exact shape:",
|
|
60
|
+
"",
|
|
61
|
+
"```json",
|
|
62
|
+
"{",
|
|
63
|
+
' "missing_hus": [',
|
|
64
|
+
' { "spec_section": "<section ref or short description>", "rationale": "<why it should exist>" }',
|
|
65
|
+
" ],",
|
|
66
|
+
' "missing_dependencies": [',
|
|
67
|
+
' { "from": "<huId that should depend on>", "on": "<huId>", "rationale": "<why>" }',
|
|
68
|
+
" ],",
|
|
69
|
+
' "scope_overlaps": [',
|
|
70
|
+
' { "between": ["<huId>", "<huId>"], "rationale": "<why they overlap>" }',
|
|
71
|
+
" ],",
|
|
72
|
+
' "parallelisable_groups": [',
|
|
73
|
+
' { "his": ["<huId>", "<huId>"], "rationale": "<why these can run side-by-side>" }',
|
|
74
|
+
" ],",
|
|
75
|
+
' "order_issues": [',
|
|
76
|
+
' { "hus": ["<huId>"], "issue": "<wrong order | circular | other>", "rationale": "<why>" }',
|
|
77
|
+
" ],",
|
|
78
|
+
' "summary": "<one sentence overall verdict>"',
|
|
79
|
+
"}",
|
|
80
|
+
"```",
|
|
81
|
+
"",
|
|
82
|
+
"Rules:",
|
|
83
|
+
"- Each array can be empty if there's nothing to flag for that question.",
|
|
84
|
+
"- Reference HUs by their EXACT id from the list below.",
|
|
85
|
+
"- Suggest at MOST 5 entries per array — only the most important ones.",
|
|
86
|
+
"- DO NOT propose tests / acceptance criteria — those are handled by other roles.",
|
|
87
|
+
"- DO NOT suggest renames / cosmetic changes — only structural issues.",
|
|
88
|
+
"- Return ONLY the JSON — no prose, no markdown fences around it.",
|
|
89
|
+
"",
|
|
90
|
+
"## Original Task",
|
|
91
|
+
task,
|
|
92
|
+
"",
|
|
93
|
+
"## Plan to review",
|
|
94
|
+
"",
|
|
95
|
+
huSummary,
|
|
96
|
+
"",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run the focused review pass.
|
|
102
|
+
*
|
|
103
|
+
* Pure-function side: parses the LLM JSON and normalises the shape
|
|
104
|
+
* (missing arrays default to `[]`, summary defaults to `""`). Junk
|
|
105
|
+
* entries (missing required fields) are dropped silently.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} args
|
|
108
|
+
* @param {object} args.agent
|
|
109
|
+
* @param {string} args.task
|
|
110
|
+
* @param {Array<object>} args.hus
|
|
111
|
+
* @param {Function} [args.onOutput]
|
|
112
|
+
* @param {number} [args.silenceTimeoutMs]
|
|
113
|
+
* @param {number} [args.timeoutMs]
|
|
114
|
+
* @returns {Promise<{ ok: boolean, findings?: object, error?: string }>}
|
|
115
|
+
*/
|
|
116
|
+
export async function reviewPlan({ agent, task, hus, onOutput, silenceTimeoutMs, timeoutMs }) {
|
|
117
|
+
if (!Array.isArray(hus) || hus.length === 0) {
|
|
118
|
+
return { ok: true, findings: emptyFindings("Plan has no HUs to review.") };
|
|
119
|
+
}
|
|
120
|
+
const prompt = buildReviewerPrompt({ task, hus });
|
|
121
|
+
const runArgs = { prompt, role: "planner" };
|
|
122
|
+
if (onOutput) runArgs.onOutput = onOutput;
|
|
123
|
+
if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
|
|
124
|
+
if (timeoutMs) runArgs.timeoutMs = timeoutMs;
|
|
125
|
+
|
|
126
|
+
const result = await agent.runTask(runArgs);
|
|
127
|
+
if (!result.ok) {
|
|
128
|
+
return { ok: false, error: result.error || "reviewer agent failed" };
|
|
129
|
+
}
|
|
130
|
+
const parsed = extractFirstJson(result.output || "");
|
|
131
|
+
if (!parsed || typeof parsed !== "object") {
|
|
132
|
+
return { ok: false, error: "reviewer returned no JSON object" };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, findings: normaliseFindings(parsed) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function emptyFindings(summary) {
|
|
138
|
+
return {
|
|
139
|
+
missing_hus: [],
|
|
140
|
+
missing_dependencies: [],
|
|
141
|
+
scope_overlaps: [],
|
|
142
|
+
parallelisable_groups: [],
|
|
143
|
+
order_issues: [],
|
|
144
|
+
summary: summary || "",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Coerce the raw LLM payload into the canonical shape. Each list is
|
|
150
|
+
* filtered to keep only entries with the required fields populated.
|
|
151
|
+
* @param {object} raw
|
|
152
|
+
*/
|
|
153
|
+
function normaliseFindings(raw) {
|
|
154
|
+
const f = emptyFindings(typeof raw.summary === "string" ? raw.summary : "");
|
|
155
|
+
if (Array.isArray(raw.missing_hus)) {
|
|
156
|
+
f.missing_hus = raw.missing_hus
|
|
157
|
+
.filter((m) => m && (m.spec_section || m.section || m.title) && m.rationale)
|
|
158
|
+
.map((m) => ({
|
|
159
|
+
spec_section: String(m.spec_section || m.section || m.title),
|
|
160
|
+
rationale: String(m.rationale),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(raw.missing_dependencies)) {
|
|
164
|
+
f.missing_dependencies = raw.missing_dependencies
|
|
165
|
+
.filter((d) => d && d.from && d.on && d.rationale)
|
|
166
|
+
.map((d) => ({ from: String(d.from), on: String(d.on), rationale: String(d.rationale) }));
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(raw.scope_overlaps)) {
|
|
169
|
+
f.scope_overlaps = raw.scope_overlaps
|
|
170
|
+
.filter((o) => o && Array.isArray(o.between) && o.between.length >= 2 && o.rationale)
|
|
171
|
+
.map((o) => ({ between: o.between.map(String), rationale: String(o.rationale) }));
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(raw.parallelisable_groups)) {
|
|
174
|
+
f.parallelisable_groups = raw.parallelisable_groups
|
|
175
|
+
.filter((g) => g && Array.isArray(g.hus) && g.hus.length >= 2)
|
|
176
|
+
.map((g) => ({ hus: g.hus.map(String), rationale: g.rationale ? String(g.rationale) : "" }));
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(raw.order_issues)) {
|
|
179
|
+
f.order_issues = raw.order_issues
|
|
180
|
+
.filter((o) => o && Array.isArray(o.hus) && o.hus.length >= 1 && o.issue)
|
|
181
|
+
.map((o) => ({ hus: o.hus.map(String), issue: String(o.issue), rationale: o.rationale ? String(o.rationale) : "" }));
|
|
182
|
+
}
|
|
183
|
+
return f;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Flatten findings into a count of actionable items (for the CLI
|
|
188
|
+
* banner: "Reviewer found 3 issues.").
|
|
189
|
+
*/
|
|
190
|
+
export function findingsCount(findings) {
|
|
191
|
+
if (!findings) return 0;
|
|
192
|
+
return (
|
|
193
|
+
(findings.missing_hus?.length || 0)
|
|
194
|
+
+ (findings.missing_dependencies?.length || 0)
|
|
195
|
+
+ (findings.scope_overlaps?.length || 0)
|
|
196
|
+
+ (findings.order_issues?.length || 0)
|
|
197
|
+
// parallelisable_groups is informational, not an issue.
|
|
198
|
+
);
|
|
199
|
+
}
|
package/src/plan/plan-schema.js
CHANGED
|
@@ -4,11 +4,58 @@
|
|
|
4
4
|
* v2 = plans + executable HUs with acceptance tests
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { generatePlanId
|
|
7
|
+
import { generatePlanId } from "./plan-id.js";
|
|
8
8
|
|
|
9
9
|
const VALID_PLAN_STATUSES = new Set(["draft", "ready", "running", "done", "failed"]);
|
|
10
10
|
const VALID_HU_STATUSES = new Set(["pending", "certified", "coding", "reviewing", "done", "failed", "blocked"]);
|
|
11
11
|
const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor", "nocode"]);
|
|
12
|
+
const VALID_TEST_TYPES = new Set(["shell", "gherkin"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Structured acceptance-test shape introduced in v2.7.5 (tests-first):
|
|
16
|
+
*
|
|
17
|
+
* { type: "shell", content: "<bash command>", file?: "<optional path>" }
|
|
18
|
+
* { type: "gherkin", content: "Given …\nWhen …\nThen …", file?: "<optional path>" }
|
|
19
|
+
*
|
|
20
|
+
* Legacy entries (plain strings) are accepted by validatePlan and get
|
|
21
|
+
* normalised to `{ type: "shell", content: <string> }` at read time by
|
|
22
|
+
* `normaliseAcceptanceTest`. New plans MUST emit the structured form.
|
|
23
|
+
*
|
|
24
|
+
* Why two types: shell tests are executable as-is (a bash command that
|
|
25
|
+
* exits 0 on pass); gherkin tests are human-readable specs that the
|
|
26
|
+
* tester role translates into real code tests during `kj run` — they
|
|
27
|
+
* are the contract the coder is expected to satisfy.
|
|
28
|
+
*
|
|
29
|
+
* @param {unknown} entry
|
|
30
|
+
* @returns {{ type: string, content: string, file?: string } | null}
|
|
31
|
+
*/
|
|
32
|
+
export function normaliseAcceptanceTest(entry) {
|
|
33
|
+
if (entry == null) return null;
|
|
34
|
+
if (typeof entry === "string") {
|
|
35
|
+
const trimmed = entry.trim();
|
|
36
|
+
if (!trimmed) return null;
|
|
37
|
+
return { type: "shell", content: trimmed };
|
|
38
|
+
}
|
|
39
|
+
if (typeof entry === "object" && typeof entry.content === "string") {
|
|
40
|
+
const content = entry.content.trim();
|
|
41
|
+
if (!content) return null;
|
|
42
|
+
const type = VALID_TEST_TYPES.has(entry.type) ? entry.type : "shell";
|
|
43
|
+
const out = { type, content };
|
|
44
|
+
if (typeof entry.file === "string" && entry.file.trim()) out.file = entry.file.trim();
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalise an array of raw acceptance_tests, dropping empties.
|
|
52
|
+
* @param {unknown[]} list
|
|
53
|
+
* @returns {{ type: string, content: string, file?: string }[]}
|
|
54
|
+
*/
|
|
55
|
+
export function normaliseAcceptanceTests(list) {
|
|
56
|
+
if (!Array.isArray(list)) return [];
|
|
57
|
+
return list.map(normaliseAcceptanceTest).filter(Boolean);
|
|
58
|
+
}
|
|
12
59
|
|
|
13
60
|
/**
|
|
14
61
|
* Check if a plan uses v2 schema (has version field + hus array).
|
|
@@ -98,6 +145,27 @@ export function validatePlan(plan) {
|
|
|
98
145
|
if (!hu.title) errors.push(`HU ${hu.id}: missing title`);
|
|
99
146
|
if (!VALID_HU_STATUSES.has(hu.status)) errors.push(`HU ${hu.id}: invalid status ${hu.status}`);
|
|
100
147
|
if (hu.task_type && !VALID_TASK_TYPES.has(hu.task_type)) errors.push(`HU ${hu.id}: invalid task_type ${hu.task_type}`);
|
|
148
|
+
// acceptance_tests: may be absent (legacy) or an array of either
|
|
149
|
+
// plain strings (legacy shell) or { type, content, file? } objects.
|
|
150
|
+
if (hu.acceptance_tests !== undefined) {
|
|
151
|
+
if (!Array.isArray(hu.acceptance_tests)) {
|
|
152
|
+
errors.push(`HU ${hu.id}: acceptance_tests must be an array`);
|
|
153
|
+
} else {
|
|
154
|
+
hu.acceptance_tests.forEach((entry, idx) => {
|
|
155
|
+
if (typeof entry === "string") return; // legacy form — OK
|
|
156
|
+
if (!entry || typeof entry !== "object") {
|
|
157
|
+
errors.push(`HU ${hu.id}: acceptance_tests[${idx}] must be a string or { type, content } object`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (entry.type && !VALID_TEST_TYPES.has(entry.type)) {
|
|
161
|
+
errors.push(`HU ${hu.id}: acceptance_tests[${idx}].type must be one of ${[...VALID_TEST_TYPES].join("|")}`);
|
|
162
|
+
}
|
|
163
|
+
if (typeof entry.content !== "string" || !entry.content.trim()) {
|
|
164
|
+
errors.push(`HU ${hu.id}: acceptance_tests[${idx}].content must be a non-empty string`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
101
169
|
// Check blocked_by references exist
|
|
102
170
|
for (const dep of hu.blocked_by || []) {
|
|
103
171
|
if (!huIds.has(dep) && !(plan.hus || []).some(h => h.id === dep)) {
|
package/src/plan/plan-store.js
CHANGED
|
@@ -10,11 +10,43 @@ import os from "node:os";
|
|
|
10
10
|
import { generatePlanId } from "./plan-id.js";
|
|
11
11
|
import { isPlanV2, migratePlanV1toV2 } from "./plan-schema.js";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Per-process tmp dir for VITEST runs that forget to set KJ_HOME. We
|
|
14
|
+
// memoise across calls so every helper in the same test process sees
|
|
15
|
+
// the same root (mirrors what setting KJ_HOME explicitly would do)
|
|
16
|
+
// without racing. The trailing `.kj` segment keeps semantic parity
|
|
17
|
+
// with non-VITEST defaults so existing path assertions don't break.
|
|
18
|
+
let _vitestKjHome = null;
|
|
19
|
+
function vitestTmpKjHome() {
|
|
20
|
+
if (_vitestKjHome) return _vitestKjHome;
|
|
21
|
+
_vitestKjHome = path.join(
|
|
22
|
+
os.tmpdir(),
|
|
23
|
+
`kj-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`,
|
|
24
|
+
".kj"
|
|
25
|
+
);
|
|
26
|
+
return _vitestKjHome;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
function
|
|
29
|
+
export function getKjHome() {
|
|
30
|
+
if (process.env.KJ_HOME) return process.env.KJ_HOME;
|
|
31
|
+
// VITEST guard — without this, a test that calls savePlan() drops
|
|
32
|
+
// fixture files into the developer's real `~/.kj/plans/` and the
|
|
33
|
+
// running HU Board's chokidar watcher ingests them as "real" plans
|
|
34
|
+
// (observed: 49 fake "Add auth" plans contaminating the user's
|
|
35
|
+
// board). Auto-isolate to a per-process tmp dir so accidental
|
|
36
|
+
// writes can't leak. Tests that NEED to assert against a known
|
|
37
|
+
// path should still set KJ_HOME explicitly.
|
|
38
|
+
if (process.env.VITEST) return vitestTmpKjHome();
|
|
39
|
+
return path.join(os.homedir(), ".kj");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deterministic slug for a project path — stable across plan-store writes,
|
|
44
|
+
* HU-board syncs and board URL routing. Keep in lockstep with
|
|
45
|
+
* `packages/hu-board/src/sync.js::deriveProjectIdFromDir`.
|
|
46
|
+
* @param {string} projectDir - absolute project path
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function projectSlug(projectDir) {
|
|
18
50
|
return projectDir
|
|
19
51
|
.replace(/^\//, "")
|
|
20
52
|
.replace(/[/\\]/g, "_")
|
|
@@ -40,6 +72,11 @@ export async function savePlan(projectDir, planResult) {
|
|
|
40
72
|
if (isPlanV2(planResult)) {
|
|
41
73
|
const filePath = path.join(dir, `${planResult.planId}.json`);
|
|
42
74
|
planResult.updatedAt = new Date().toISOString();
|
|
75
|
+
// TSK-0341 / garbage-collector: stamp the absolute projectDir on every
|
|
76
|
+
// plan so the GC can check `fs.exists(projectDir)` reliably. Reverse-
|
|
77
|
+
// engineering it from the slug is lossy (underscores in the original
|
|
78
|
+
// name collide with underscores introduced by slash-substitution).
|
|
79
|
+
if (!planResult.projectDir && projectDir) planResult.projectDir = projectDir;
|
|
43
80
|
await fs.writeFile(filePath, JSON.stringify(planResult, null, 2), "utf8");
|
|
44
81
|
return planResult.planId;
|
|
45
82
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect numbered/sectioned headings in a task description so the
|
|
3
|
+
* planner prompt can demand `spec_section` per HU instead of leaving
|
|
4
|
+
* it as a soft "preferred" hint that LLMs ignore.
|
|
5
|
+
*
|
|
6
|
+
* Recognises three common shapes (kept intentionally narrow — false
|
|
7
|
+
* positives turn the planner prompt into noise):
|
|
8
|
+
*
|
|
9
|
+
* ## 1. Overview → section "1", title "Overview"
|
|
10
|
+
* ### 2.1 add → section "2.1", title "add"
|
|
11
|
+
* ## §5 Initial Scope → section "5", title "Initial Scope"
|
|
12
|
+
*
|
|
13
|
+
* The matcher is anchored to markdown heading lines (`^#+`) so prose
|
|
14
|
+
* mentions of a number ("see version 4.2") never trigger detection.
|
|
15
|
+
*
|
|
16
|
+
* Returns `{ hasSpec, sections, sectionList }`:
|
|
17
|
+
* - `hasSpec` boolean — true when at least one heading matched.
|
|
18
|
+
* Prompt builders use this to switch language from
|
|
19
|
+
* "preferred" to "MUST".
|
|
20
|
+
* - `sections` string[] — bare section refs, dedup, in document
|
|
21
|
+
* order. e.g. ["1", "2", "2.1", "2.2", "3"].
|
|
22
|
+
* - `sectionList` Array<{section, title}> — full list with titles
|
|
23
|
+
* so the prompt can echo the checklist back at the
|
|
24
|
+
* LLM. Keeps order, dedup by section ref.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} text
|
|
27
|
+
* @returns {{ hasSpec: boolean, sections: string[], sectionList: Array<{section: string, title: string}> }}
|
|
28
|
+
*/
|
|
29
|
+
export function detectSpecSections(text) {
|
|
30
|
+
const empty = { hasSpec: false, sections: [], sectionList: [] };
|
|
31
|
+
if (!text || typeof text !== "string") return empty;
|
|
32
|
+
|
|
33
|
+
// Anchored to heading lines. Two regex shapes — numbered sections
|
|
34
|
+
// and §-prefixed sections — are both common in the wild.
|
|
35
|
+
//
|
|
36
|
+
// ^#{1,6}\s* ← markdown heading marker (any depth)
|
|
37
|
+
// §?\s* ← optional "§"
|
|
38
|
+
// (\d+(?:\.\d+){0,4}) ← section number, up to 5 levels deep
|
|
39
|
+
// [.)]?\s+ ← optional trailing "." or ")", then space
|
|
40
|
+
// (.+?)\s*$ ← title (lazy, trim trailing whitespace)
|
|
41
|
+
const headingRe = /^#{1,6}\s*§?\s*(\d+(?:\.\d+){0,4})[.)]?\s+(.+?)\s*$/;
|
|
42
|
+
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const sectionList = [];
|
|
45
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
46
|
+
const line = rawLine.trimEnd();
|
|
47
|
+
const m = headingRe.exec(line);
|
|
48
|
+
if (!m) continue;
|
|
49
|
+
const section = m[1];
|
|
50
|
+
if (seen.has(section)) continue;
|
|
51
|
+
seen.add(section);
|
|
52
|
+
sectionList.push({ section, title: m[2].trim() });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sectionList.length === 0) return empty;
|
|
56
|
+
return {
|
|
57
|
+
hasSpec: true,
|
|
58
|
+
sections: sectionList.map((s) => s.section),
|
|
59
|
+
sectionList,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests-first second pass for `kj plan` (v2.7.5).
|
|
3
|
+
*
|
|
4
|
+
* The first planner call asks for steps + acceptance_tests in one
|
|
5
|
+
* shot. Modern LLMs (Claude, Codex) reliably produce the steps but
|
|
6
|
+
* frequently skip the acceptance_tests sub-instruction when the
|
|
7
|
+
* surrounding spec is dense — a known weakness of long single-shot
|
|
8
|
+
* prompts. Pre-v2.7.5 we papered over this with a generic `npx
|
|
9
|
+
* vitest run` placeholder that wasn't actually a per-HU contract.
|
|
10
|
+
*
|
|
11
|
+
* This module runs a focused SECOND pass that asks the LLM ONLY to
|
|
12
|
+
* fill in tests for the HUs that came back without them. Because the
|
|
13
|
+
* prompt is shorter and has a single deliverable, the hit rate is
|
|
14
|
+
* effectively 100% on the HUs that slipped through the first time.
|
|
15
|
+
*
|
|
16
|
+
* Returns a map `{ huId → acceptance_tests[] }` so the caller can
|
|
17
|
+
* patch the plan in place. The patch always normalises through
|
|
18
|
+
* `plan-schema::normaliseAcceptanceTests`, so any malformed entries
|
|
19
|
+
* the LLM produces are silently dropped (better than corrupting the
|
|
20
|
+
* plan with junk).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
24
|
+
import { normaliseAcceptanceTests } from "./plan-schema.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the focused-pass prompt asking ONLY for tests for the given
|
|
28
|
+
* HUs. Exported for unit-testing the prompt shape without an LLM.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} args
|
|
31
|
+
* @param {string} args.task
|
|
32
|
+
* @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function buildSynthesizerPrompt({ task, husNeedingTests }) {
|
|
36
|
+
const lines = [
|
|
37
|
+
"You previously generated an implementation plan for the task below, but some HUs were left without acceptance_tests.",
|
|
38
|
+
"Your ONLY job in this call is to fill that gap.",
|
|
39
|
+
"",
|
|
40
|
+
"Output MUST be a JSON object with this exact shape:",
|
|
41
|
+
"",
|
|
42
|
+
"```json",
|
|
43
|
+
"{",
|
|
44
|
+
' "tests": {',
|
|
45
|
+
' "<huId>": [',
|
|
46
|
+
' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
|
|
47
|
+
' { "type": "shell", "content": "<bash command that must exit 0>" }',
|
|
48
|
+
" ]",
|
|
49
|
+
" }",
|
|
50
|
+
"}",
|
|
51
|
+
"```",
|
|
52
|
+
"",
|
|
53
|
+
"Rules:",
|
|
54
|
+
"- The keys of `tests` are the HU ids EXACTLY as listed below.",
|
|
55
|
+
"- 2-4 tests per HU. At least one happy-path and one edge case.",
|
|
56
|
+
"- gherkin = behaviour spec; shell = concrete bash command exiting 0 on pass.",
|
|
57
|
+
"- Tests must be specific to the HU. Do NOT use generic fallbacks like `npx vitest run`.",
|
|
58
|
+
"- Tests must be writable BEFORE the HU is implemented (they should fail until done).",
|
|
59
|
+
"- Return ONLY the JSON — no prose, no markdown fences around it.",
|
|
60
|
+
"",
|
|
61
|
+
"## Task",
|
|
62
|
+
task,
|
|
63
|
+
"",
|
|
64
|
+
"## HUs that need acceptance_tests",
|
|
65
|
+
"",
|
|
66
|
+
];
|
|
67
|
+
for (const hu of husNeedingTests) {
|
|
68
|
+
lines.push(`### ${hu.id}: ${hu.title || ""}`);
|
|
69
|
+
lines.push(hu.scope ? `Scope: ${hu.scope}` : "(no scope)");
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run the focused second pass. Pass an `agent` with a `runTask`
|
|
77
|
+
* method (Claude / Codex / Gemini wrappers all expose it).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} args
|
|
80
|
+
* @param {object} args.agent
|
|
81
|
+
* @param {string} args.task
|
|
82
|
+
* @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
|
|
83
|
+
* @param {Function} [args.onOutput]
|
|
84
|
+
* @param {number} [args.silenceTimeoutMs]
|
|
85
|
+
* @param {number} [args.timeoutMs]
|
|
86
|
+
* @returns {Promise<{ ok: boolean, tests?: Record<string, object[]>, error?: string }>}
|
|
87
|
+
*/
|
|
88
|
+
export async function synthesizeMissingTests({ agent, task, husNeedingTests, onOutput, silenceTimeoutMs, timeoutMs }) {
|
|
89
|
+
if (!Array.isArray(husNeedingTests) || husNeedingTests.length === 0) {
|
|
90
|
+
return { ok: true, tests: {} };
|
|
91
|
+
}
|
|
92
|
+
const prompt = buildSynthesizerPrompt({ task, husNeedingTests });
|
|
93
|
+
const runArgs = { prompt, role: "planner" };
|
|
94
|
+
if (onOutput) runArgs.onOutput = onOutput;
|
|
95
|
+
if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
|
|
96
|
+
if (timeoutMs) runArgs.timeoutMs = timeoutMs;
|
|
97
|
+
|
|
98
|
+
const result = await agent.runTask(runArgs);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
return { ok: false, error: result.error || "synthesizer agent failed" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parsed = extractFirstJson(result.output || "");
|
|
104
|
+
const rawTests = parsed?.tests;
|
|
105
|
+
if (!rawTests || typeof rawTests !== "object") {
|
|
106
|
+
return { ok: false, error: "synthesizer returned no `tests` object" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Normalise per HU so junk entries don't slip into the plan.
|
|
110
|
+
const tests = {};
|
|
111
|
+
for (const [huId, list] of Object.entries(rawTests)) {
|
|
112
|
+
const cleaned = normaliseAcceptanceTests(list);
|
|
113
|
+
if (cleaned.length > 0) tests[huId] = cleaned;
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, tests };
|
|
116
|
+
}
|
|
@@ -37,7 +37,7 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_M
|
|
|
37
37
|
err.httpStatus = 408;
|
|
38
38
|
throw err;
|
|
39
39
|
}
|
|
40
|
-
throw new Error(`Planning Game network error: ${error?.message || "unknown error"}
|
|
40
|
+
throw new Error(`Planning Game network error: ${error?.message || "unknown error"}`, { cause: error });
|
|
41
41
|
} finally {
|
|
42
42
|
clearTimeout(timeoutId);
|
|
43
43
|
}
|
|
@@ -54,7 +54,7 @@ async function parseJsonResponse(response) {
|
|
|
54
54
|
try {
|
|
55
55
|
return await response.json();
|
|
56
56
|
} catch (error) {
|
|
57
|
-
throw new Error(`Planning Game invalid response: ${error?.message || "invalid JSON"}
|
|
57
|
+
throw new Error(`Planning Game invalid response: ${error?.message || "invalid JSON"}`, { cause: error });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -65,18 +65,7 @@ export async function fetchCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT
|
|
|
65
65
|
return data?.card || data;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
async function
|
|
69
|
-
return fetchCard({ projectId, cardId, timeoutMs });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function listCards({ projectId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
73
|
-
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards`;
|
|
74
|
-
const response = await fetchWithRetry(url, {}, timeoutMs);
|
|
75
|
-
const data = await parseJsonResponse(response);
|
|
76
|
-
return data?.cards || data;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function updateCard({ projectId, cardId, firebaseId, updates, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
68
|
+
export async function updateCard({ projectId, cardId: _cardId, firebaseId, updates, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
80
69
|
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/${encodeURIComponent(firebaseId)}`;
|
|
81
70
|
const response = await fetchWithRetry(
|
|
82
71
|
url,
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
15
|
-
import { addCheckpoint
|
|
15
|
+
import { addCheckpoint } from "../session/store.js";
|
|
16
|
+
import { appendPgCommits } from "../session/mutators.js";
|
|
16
17
|
import { getLang } from "../utils/messages.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -28,7 +29,7 @@ import { getLang } from "../utils/messages.js";
|
|
|
28
29
|
* @param {string|null} opts.pgProject - Planning Game project ID
|
|
29
30
|
* @returns {{ pgCard: object|null }} The fetched PG card (or null)
|
|
30
31
|
*/
|
|
31
|
-
export async function initPgAdapter({ session, config, logger, pgTaskId, pgProject }) {
|
|
32
|
+
export async function initPgAdapter({ session: _session, config, logger, pgTaskId, pgProject }) {
|
|
32
33
|
if (!pgTaskId || !pgProject || config.planning_game?.enabled === false) {
|
|
33
34
|
return { pgCard: null };
|
|
34
35
|
}
|
|
@@ -183,13 +184,12 @@ export function buildHuStoriesFromPgCard(pgCard) {
|
|
|
183
184
|
*/
|
|
184
185
|
export function accumulateCommit(session, commitInfo) {
|
|
185
186
|
if (!session || !commitInfo?.hash) return;
|
|
186
|
-
|
|
187
|
-
session.pg_commits.push({
|
|
187
|
+
appendPgCommits(session, [{
|
|
188
188
|
hash: commitInfo.hash,
|
|
189
189
|
message: commitInfo.message || "",
|
|
190
190
|
date: commitInfo.date || new Date().toISOString(),
|
|
191
191
|
author: commitInfo.author || "Karajan"
|
|
192
|
-
});
|
|
192
|
+
}]);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// --- Internal helpers ---
|
|
@@ -218,3 +218,34 @@ async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {
|
|
|
218
218
|
return null;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* TSK-0339: integration adapter shape — the orchestrator consumes tracker
|
|
224
|
+
* hooks via this neutral interface, not via direct imports. `registerPgAdapter`
|
|
225
|
+
* is called once at bootstrap when `config.planning_game.enabled` is true,
|
|
226
|
+
* wiring these hooks into `src/orchestrator/integrations.js` under the
|
|
227
|
+
* "tracker" slot.
|
|
228
|
+
*
|
|
229
|
+
* Adding a new tracker (GitHub Projects, Jira, Linear, …) means creating
|
|
230
|
+
* a sibling `src/adapters/<name>/adapter.js` with the same hook surface,
|
|
231
|
+
* nothing else in the orchestrator changes.
|
|
232
|
+
*/
|
|
233
|
+
export const planningGameAdapter = {
|
|
234
|
+
name: "planning-game",
|
|
235
|
+
async onSessionStart(opts) { return initPgAdapter(opts); },
|
|
236
|
+
onCommit(session, commit) { accumulateCommit(session, commit); },
|
|
237
|
+
async onSessionApproved(opts) { return markPgCardToValidate(opts); },
|
|
238
|
+
buildHuStoriesFromCard(card) { return buildHuStoriesFromPgCard(card); },
|
|
239
|
+
async handleDecomposition(opts) { return handlePgDecomposition(opts); },
|
|
240
|
+
async createAdrsFromTradeoffs({ tradeoffs, pgTaskId, pgProject, taskTitle }) {
|
|
241
|
+
// Lazy-loaded to avoid pulling createArchitectADRs into every import.
|
|
242
|
+
const { createArchitectADRs } = await import("./architect-adrs.js");
|
|
243
|
+
const pgClient = await import("./client.js");
|
|
244
|
+
return createArchitectADRs({ tradeoffs, pgTaskId, pgProject, taskTitle, mcpClient: pgClient });
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export async function registerPgAdapter() {
|
|
249
|
+
const { registerIntegration } = await import("../orchestrator/integrations.js");
|
|
250
|
+
registerIntegration("tracker", planningGameAdapter);
|
|
251
|
+
}
|