karajan-code 3.14.0 → 3.15.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 +10 -0
- package/package.json +1 -1
- package/packages/hu-board/src/routes/api.js +7 -3
- package/scripts/verify-pack.mjs +5 -1
- package/src/agents/claude-agent.js +9 -4
- package/src/agents/model-registry.js +23 -0
- package/src/config/defaults.js +4 -0
- package/src/git/hu-automation.js +6 -5
- package/src/hu/acceptance-runner.js +8 -4
- package/src/hu/worktree-bootstrap.js +59 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +1 -1
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +1 -1
- package/src/orchestrator/drivers/run-hu-batch.js +82 -52
- package/src/orchestrator/hu-sub-pipeline.js +26 -2
- package/src/orchestrator/stages/coder-stage.js +10 -2
- package/src/orchestrator/stages/reviewer-stage.js +3 -3
- package/src/plan/plan-executor.js +4 -0
- package/src/plan/preflight-hu.js +3 -0
- package/src/rag/embedder.js +2 -81
- package/src/rag/embedders/_cloud-base.js +2 -24
- package/src/rag/embedders/cohere.js +2 -28
- package/src/rag/embedders/factory.js +2 -29
- package/src/rag/embedders/mistral.js +2 -26
- package/src/rag/embedders/onnx.js +2 -63
- package/src/rag/embedders/openai.js +2 -22
- package/src/rag/embedders/voyage.js +2 -18
- package/src/rag/rerank.js +4 -74
- package/src/rag/retriever.js +2 -127
- package/src/rag/where-parser.js +4 -54
- package/src/review/diff-generator.js +2 -2
- package/src/roles/agent-role.js +4 -0
- package/src/roles/coder-role.js +8 -2
- package/src/spec-review/run-spec-review.js +5 -1
- package/src/utils/cli-ask-question.js +9 -0
- package/src/utils/git.js +12 -11
package/README.md
CHANGED
|
@@ -383,6 +383,16 @@ Each AI role is executed by the agent you choose:
|
|
|
383
383
|
>
|
|
384
384
|
> Full per-stage reference: [Pipeline roles](https://karajan-code.web.app/docs/handbook/pipeline-roles/) (handbook).
|
|
385
385
|
|
|
386
|
+
## Step mode and parallel lanes (v3.14+)
|
|
387
|
+
|
|
388
|
+
Two ways to control how a plan executes:
|
|
389
|
+
|
|
390
|
+
**`kj run --step`** — supervise the orchestra iteration by iteration. After every iteration the pipeline pauses with a compact report (what happened, the reviewer's must-fix list, what the next iteration will do, spend vs cap) and asks: press Enter to continue, type `stop` to halt (resumable with `kj resume`), or **type instructions** — free text is injected into the feedback the coder reads next iteration, without clobbering the reviewer's own findings. Also offered as a question in the `kj init` wizard (`session.iteration_gate`).
|
|
391
|
+
|
|
392
|
+
**`kj run --plan <id> --parallel <n>`** — run a plan's independent HUs concurrently, each in its own **git worktree** under `.kj/worktrees/<huId>` on branch `kj-hu-<huId>`. The scheduler walks the `blocked_by` graph and only pairs HUs with disjoint `scope` paths (scopeless HUs run alone); the whole lane — coder, acceptance tests, diffs, sonar, final commit — runs inside its worktree while the main working tree stays parked. Governance is built in: default is `1` (fully sequential), a plan-level budget ceiling (`n × max_budget_usd`) stops the batch loudly when exhausted, and SonarQube serializes across lanes. Each fresh worktree is bootstrapped automatically (submodules + `npm ci`, or your `session.worktree_setup` command) and receives `KJ_LANE_SLOT` / `KJ_PORT_OFFSET` env vars so services started by tests don't collide on ports.
|
|
393
|
+
|
|
394
|
+
Full guide: [`docs/parallel-hus.md`](docs/parallel-hus.md).
|
|
395
|
+
|
|
386
396
|
## 5 AI agents supported
|
|
387
397
|
|
|
388
398
|
| Agent | CLI | Install |
|
package/package.json
CHANGED
|
@@ -76,7 +76,9 @@ router.get('/version', (_req, res) => {
|
|
|
76
76
|
*/
|
|
77
77
|
router.get('/standby', async (_req, res) => {
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
// KJC-TSK-0632: resolve from karajan-core directly — the CLI's
|
|
80
|
+
// src/brain/standby-store.js is just a re-export shim of this.
|
|
81
|
+
const { listPendingStandby } = await import('karajan-core/standby-store');
|
|
80
82
|
const sessions = listPendingStandby();
|
|
81
83
|
res.set('Cache-Control', 'no-store');
|
|
82
84
|
res.json({ sessions });
|
|
@@ -1362,8 +1364,10 @@ router.post('/rag/query', async (req, res) => {
|
|
|
1362
1364
|
}
|
|
1363
1365
|
try {
|
|
1364
1366
|
const { openVecStore, countChunks } = await import('karajan-core/vec-store');
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
+
// KJC-TSK-0632: resolved from karajan-core — the board carries zero
|
|
1368
|
+
// relative imports into the CLI src tree (see no-cli-imports test).
|
|
1369
|
+
const { makeEmbedder } = await import('karajan-core/rag/embedders/factory');
|
|
1370
|
+
const { query } = await import('karajan-core/rag/retriever');
|
|
1367
1371
|
const db = openVecStore({ dim: 768 });
|
|
1368
1372
|
try {
|
|
1369
1373
|
if (countChunks(db) === 0) return res.json({ hits: [], empty: true, topK, scope });
|
package/scripts/verify-pack.mjs
CHANGED
|
@@ -166,7 +166,11 @@ try {
|
|
|
166
166
|
console.log(`verify-pack: installing the tarball with pnpm into ${pnpmTmp}…`);
|
|
167
167
|
// pnpm exits non-zero on ERR_PNPM_IGNORED_BUILDS (it skips native build
|
|
168
168
|
// scripts by default) — expected here, so don't treat the exit as failure.
|
|
169
|
-
|
|
169
|
+
// minimum-release-age=0: modern pnpm quarantines freshly published
|
|
170
|
+
// versions (supply-chain protection), so right after publishing
|
|
171
|
+
// karajan-core it silently resolves an OLD one and this smoke fails on
|
|
172
|
+
// missing subpaths. The gate verifies packaging, not release-age policy.
|
|
173
|
+
spawnSync("pnpm", ["add", tgzPath, "--store-dir", path.join(pnpmTmp, ".store"), "--config.minimum-release-age=0"], {
|
|
170
174
|
encoding: "utf8",
|
|
171
175
|
env: childEnv,
|
|
172
176
|
cwd: pnpmTmp,
|
|
@@ -300,7 +300,10 @@ export function createStreamJsonFilter(onOutput) {
|
|
|
300
300
|
*/
|
|
301
301
|
function cleanExecaOpts(extra = {}) {
|
|
302
302
|
const { CLAUDECODE: _CLAUDECODE, ...env } = process.env;
|
|
303
|
-
|
|
303
|
+
// PAR-H (KJC-TSK-0631): extra.env ADDS to the inherited env (lane slot
|
|
304
|
+
// vars) instead of replacing it wholesale.
|
|
305
|
+
const { env: extraEnv, ...rest } = extra;
|
|
306
|
+
return { env: extraEnv ? { ...env, ...extraEnv } : env, stdin: "ignore", ...rest };
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
/**
|
|
@@ -370,7 +373,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
370
373
|
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
371
374
|
onOutput: streamFilter,
|
|
372
375
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
373
|
-
timeout: task.timeoutMs
|
|
376
|
+
timeout: task.timeoutMs,
|
|
377
|
+
env: task.env
|
|
374
378
|
}));
|
|
375
379
|
const raw = pickOutput(res);
|
|
376
380
|
const output = extractTextFromStreamJson(raw);
|
|
@@ -380,7 +384,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
380
384
|
|
|
381
385
|
// Without streaming, use json output to get structured response via stderr
|
|
382
386
|
args.push("--output-format", "json");
|
|
383
|
-
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts());
|
|
387
|
+
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts({ env: task.env }));
|
|
384
388
|
const raw = pickOutput(res);
|
|
385
389
|
const output = extractTextFromStreamJson(raw);
|
|
386
390
|
const usage = extractUsageFromStreamJson(raw);
|
|
@@ -393,7 +397,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
393
397
|
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
394
398
|
onOutput: task.onOutput,
|
|
395
399
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
396
|
-
timeout: task.timeoutMs
|
|
400
|
+
timeout: task.timeoutMs,
|
|
401
|
+
env: task.env
|
|
397
402
|
}));
|
|
398
403
|
const raw = pickOutput(res);
|
|
399
404
|
const usage = extractUsageFromStreamJson(raw);
|
|
@@ -108,6 +108,29 @@ registerModelAlias("gemini", "gemini-2.5-pro");
|
|
|
108
108
|
registerModel("aider", { provider: "aider", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
109
109
|
registerModel("opencode", { provider: "opencode", pricing: { input_per_million: 0, output_per_million: 0 } });
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Moonshot Kimi Family (KJC-TSK-0633) — consumed through OpenCode as an
|
|
113
|
+
* OpenAI-compatible provider (docs/providers-via-opencode.md).
|
|
114
|
+
* Pricing: https://platform.moonshot.ai/docs/pricing — verify before
|
|
115
|
+
* trusting for billing decisions; ids/prices move fast.
|
|
116
|
+
*/
|
|
117
|
+
registerModel("kimi-k2", { provider: "moonshot", pricing: { input_per_million: 0.6, output_per_million: 2.5 } });
|
|
118
|
+
registerModel("kimi-k2-thinking", { provider: "moonshot", pricing: { input_per_million: 0.6, output_per_million: 2.5 } });
|
|
119
|
+
registerModelAlias("kimi", "kimi-k2");
|
|
120
|
+
// Prefixed ids as the documented opencode.json snippet produces them.
|
|
121
|
+
registerModelAlias("kimi/kimi-k2", "kimi-k2");
|
|
122
|
+
registerModelAlias("kimi/kimi-k2-thinking", "kimi-k2-thinking");
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* DeepSeek Family (KJC-TSK-0633) — same OpenCode route.
|
|
126
|
+
* Pricing: https://api-docs.deepseek.com/quick_start/pricing
|
|
127
|
+
*/
|
|
128
|
+
registerModel("deepseek-chat", { provider: "deepseek", pricing: { input_per_million: 0.28, output_per_million: 0.42 } });
|
|
129
|
+
registerModel("deepseek-reasoner", { provider: "deepseek", pricing: { input_per_million: 0.28, output_per_million: 0.42 } });
|
|
130
|
+
registerModelAlias("deepseek", "deepseek-chat");
|
|
131
|
+
registerModelAlias("deepseek/deepseek-chat", "deepseek-chat");
|
|
132
|
+
registerModelAlias("deepseek/deepseek-reasoner", "deepseek-reasoner");
|
|
133
|
+
|
|
111
134
|
// Common CLI Aliases (with provider overrides)
|
|
112
135
|
registerModelAlias("aider/claude-3-7-sonnet", "claude-sonnet-4.6", { provider: "aider" });
|
|
113
136
|
registerModelAlias("aider/gpt-4o", "gpt-5.4-standard", { provider: "aider" });
|
package/src/config/defaults.js
CHANGED
|
@@ -182,6 +182,10 @@ const DEFAULTS = {
|
|
|
182
182
|
// Concurrent HU lanes per plan run (KJC-TSK-0626). 1 = sequential.
|
|
183
183
|
// Raising it multiplies token burn rate — the plan budget scales with it.
|
|
184
184
|
max_parallel_hus: 1,
|
|
185
|
+
// Command run inside each fresh lane worktree before the coder starts
|
|
186
|
+
// (KJC-TSK-0630). null = auto-detect: `npm ci` when package-lock.json
|
|
187
|
+
// exists, nothing otherwise. Submodules are always initialized first.
|
|
188
|
+
worktree_setup: null,
|
|
185
189
|
max_iteration_minutes: 30,
|
|
186
190
|
max_total_minutes: 120,
|
|
187
191
|
max_planner_minutes: 60,
|
package/src/git/hu-automation.js
CHANGED
|
@@ -113,11 +113,11 @@ export async function prepareHuBranch({ story, huBranches, config, logger }) {
|
|
|
113
113
|
* @param {object} params.logger
|
|
114
114
|
* @returns {Promise<{committed: boolean, pushed: boolean, prUrl: string|null}>}
|
|
115
115
|
*/
|
|
116
|
-
export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
116
|
+
export async function finalizeHuCommit({ story, branchName, config, logger, cwd = null }) {
|
|
117
117
|
const result = { committed: false, pushed: false, prUrl: null };
|
|
118
118
|
if (!branchName) return result;
|
|
119
119
|
|
|
120
|
-
const changed = await hasChanges();
|
|
120
|
+
const changed = await hasChanges(cwd);
|
|
121
121
|
if (!changed) {
|
|
122
122
|
logger.info(`HU ${story.id}: no changes to commit`);
|
|
123
123
|
return result;
|
|
@@ -126,7 +126,7 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
126
126
|
const title = story.title || story.id;
|
|
127
127
|
const commitMsg = `feat(${story.id}): ${title}`;
|
|
128
128
|
if (config.git?.auto_commit) {
|
|
129
|
-
const commitRes = await commitAll(commitMsg);
|
|
129
|
+
const commitRes = await commitAll(commitMsg, cwd);
|
|
130
130
|
if (commitRes) {
|
|
131
131
|
result.committed = true;
|
|
132
132
|
logger.info(`HU ${story.id}: committed on '${branchName}'`);
|
|
@@ -135,7 +135,7 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
135
135
|
|
|
136
136
|
if (config.git?.auto_push && result.committed) {
|
|
137
137
|
try {
|
|
138
|
-
await pushBranch(branchName);
|
|
138
|
+
await pushBranch(branchName, cwd);
|
|
139
139
|
result.pushed = true;
|
|
140
140
|
logger.info(`HU ${story.id}: pushed '${branchName}'`);
|
|
141
141
|
} catch (err) {
|
|
@@ -157,7 +157,8 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
157
157
|
baseBranch: config.base_branch || "main",
|
|
158
158
|
branch: branchName,
|
|
159
159
|
title: commitMsg,
|
|
160
|
-
body: prBody
|
|
160
|
+
body: prBody,
|
|
161
|
+
cwd
|
|
161
162
|
});
|
|
162
163
|
result.prUrl = url;
|
|
163
164
|
logger.info(`HU ${story.id}: PR created ${url}`);
|
|
@@ -12,11 +12,15 @@ import { runCommand } from "../utils/process.js";
|
|
|
12
12
|
* @param {number} [timeoutMs=30000] - Timeout per test
|
|
13
13
|
* @returns {Promise<{cmd: string, passed: boolean, output: string, exitCode: number}>}
|
|
14
14
|
*/
|
|
15
|
-
async function runSingleTest(cmd, cwd, timeoutMs = 30000) {
|
|
15
|
+
async function runSingleTest(cmd, cwd, timeoutMs = 30000, env = null) {
|
|
16
16
|
try {
|
|
17
17
|
const result = await runCommand("bash", ["-c", cmd], {
|
|
18
18
|
timeout: timeoutMs,
|
|
19
|
-
cwd
|
|
19
|
+
cwd,
|
|
20
|
+
// PAR-H (KJC-TSK-0631): lane env (KJ_LANE_SLOT / KJ_PORT_OFFSET) so
|
|
21
|
+
// tests that start services can offset their ports. execa merges
|
|
22
|
+
// this on top of process.env.
|
|
23
|
+
...(env ? { env } : {})
|
|
20
24
|
});
|
|
21
25
|
const output = (result.stdout || "") + (result.stderr || "");
|
|
22
26
|
return {
|
|
@@ -51,7 +55,7 @@ async function runSingleTest(cmd, cwd, timeoutMs = 30000) {
|
|
|
51
55
|
* @param {string} cwd - Working directory
|
|
52
56
|
* @returns {Promise<{allPassed: boolean, results: object[], summary: string, diagnostics: string|null, pending: number}>}
|
|
53
57
|
*/
|
|
54
|
-
export async function runAcceptanceTests(tests, cwd) {
|
|
58
|
+
export async function runAcceptanceTests(tests, cwd, { env = null } = {}) {
|
|
55
59
|
if (!tests || tests.length === 0) {
|
|
56
60
|
return { allPassed: false, results: [], summary: "No acceptance tests defined", diagnostics: null, pending: 0 };
|
|
57
61
|
}
|
|
@@ -86,7 +90,7 @@ export async function runAcceptanceTests(tests, cwd) {
|
|
|
86
90
|
});
|
|
87
91
|
continue;
|
|
88
92
|
}
|
|
89
|
-
const result = await runSingleTest(cmd, cwd);
|
|
93
|
+
const result = await runSingleTest(cmd, cwd, 30000, env);
|
|
90
94
|
results.push({ ...result, type: "shell" });
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAR-G (KJC-TSK-0630): make a freshly created lane worktree operative.
|
|
3
|
+
*
|
|
4
|
+
* `git worktree add` produces a clean checkout: no node_modules, no
|
|
5
|
+
* initialized submodules — and a container/bind-mounted process cannot
|
|
6
|
+
* init them itself because the worktree's real .git lives in the parent
|
|
7
|
+
* repo (gotcha reported by Jorge del Casar's worktree-docker-envs skill).
|
|
8
|
+
* Without this step, --parallel lanes die on the first `npm test` in any
|
|
9
|
+
* real project.
|
|
10
|
+
*
|
|
11
|
+
* Best-effort by contract: a failed or slow bootstrap warns and the lane
|
|
12
|
+
* continues — the acceptance tests deliver the real verdict.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { runCommand } from "../utils/process.js";
|
|
17
|
+
|
|
18
|
+
const STEP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} params
|
|
22
|
+
* @param {string} params.worktreePath Absolute path of the lane worktree.
|
|
23
|
+
* @param {string|null} [params.setupCommand] session.worktree_setup — wins over auto-detect.
|
|
24
|
+
* @param {object|null} [params.logger]
|
|
25
|
+
* @param {number} [params.timeoutMs]
|
|
26
|
+
* @param {Function} [params.run] Injectable runner (tests).
|
|
27
|
+
* @returns {Promise<{ok: boolean, steps: string[], warnings: string[]}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function bootstrapWorktree({ worktreePath, setupCommand = null, logger = null, timeoutMs = STEP_TIMEOUT_MS, run = runCommand }) {
|
|
30
|
+
const warnings = [];
|
|
31
|
+
const steps = [];
|
|
32
|
+
|
|
33
|
+
if (existsSync(join(worktreePath, ".gitmodules"))) {
|
|
34
|
+
steps.push({ name: "submodules", cmd: "git", args: ["submodule", "update", "--init", "--recursive"] });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const explicit = typeof setupCommand === "string" && setupCommand.trim() ? setupCommand.trim() : null;
|
|
38
|
+
if (explicit) {
|
|
39
|
+
steps.push({ name: "setup", cmd: "sh", args: ["-c", explicit] });
|
|
40
|
+
} else if (existsSync(join(worktreePath, "package-lock.json"))) {
|
|
41
|
+
steps.push({ name: "deps", cmd: "npm", args: ["ci", "--no-audit", "--no-fund"] });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const step of steps) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await run(step.cmd, step.args, { cwd: worktreePath, timeout: timeoutMs });
|
|
47
|
+
if (res.exitCode !== 0) {
|
|
48
|
+
warnings.push(`${step.name} failed (exit ${res.exitCode}): ${String(res.stderr || res.stdout || "").slice(0, 200)}`);
|
|
49
|
+
} else {
|
|
50
|
+
logger?.info?.(`worktree bootstrap: ${step.name} ok`);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
warnings.push(`${step.name} threw: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const w of warnings) logger?.warn?.(`worktree bootstrap: ${w} — lane continues`);
|
|
58
|
+
return { ok: warnings.length === 0, steps: steps.map((s) => s.name), warnings };
|
|
59
|
+
}
|
|
@@ -28,7 +28,7 @@ export async function runGuardStages({ config, logger, emitter, eventBase, sessi
|
|
|
28
28
|
let diff;
|
|
29
29
|
try {
|
|
30
30
|
const baseRef = await computeBaseRef({ baseBranch });
|
|
31
|
-
diff = await generateDiff({ baseRef });
|
|
31
|
+
diff = await generateDiff({ baseRef, projectDir: config?.projectDir || null });
|
|
32
32
|
} catch {
|
|
33
33
|
logger.warn("Guards: could not generate diff, skipping");
|
|
34
34
|
return { action: "ok" };
|
|
@@ -77,7 +77,7 @@ export async function runQualityGateStages({ config, logger, emitter, eventBase,
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (pipelineFlags?.impeccableEnabled) {
|
|
80
|
-
const diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
80
|
+
const diff = await generateDiff({ baseRef: session.session_start_sha, projectDir: config?.projectDir || null });
|
|
81
81
|
const impeccableMode = pipelineFlags?.impeccableMode || "audit";
|
|
82
82
|
const impeccableResult = await runImpeccableStage({
|
|
83
83
|
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
@@ -55,7 +55,6 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
57
|
// Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
|
|
58
|
-
const originalMaxIterations = ctx.config.max_iterations;
|
|
59
58
|
const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
|
|
60
59
|
const huBranches = new Map();
|
|
61
60
|
const { prepareHuBranch, finalizeHuCommit } = await import("../../git/hu-automation.js");
|
|
@@ -83,15 +82,7 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
83
82
|
|
|
84
83
|
const subPipelineResult = await runHuSubPipeline({
|
|
85
84
|
huReviewerResult: ctx.stageResults.huReviewer,
|
|
86
|
-
runIterationFn: async (huTask, story) => {
|
|
87
|
-
ctx.config.max_iterations = huMaxIterations;
|
|
88
|
-
if (ctx.brainCtx?.enabled) {
|
|
89
|
-
ctx.brainCtx.extensionCount = 0;
|
|
90
|
-
const { createBrainContext } = await import("../brain-coordinator.js");
|
|
91
|
-
const fresh = createBrainContext({ enabled: true });
|
|
92
|
-
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
93
|
-
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
94
|
-
}
|
|
85
|
+
runIterationFn: async (huTask, story, laneOpts = {}) => {
|
|
95
86
|
// Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd).
|
|
96
87
|
// KJC-TSK-0400: `effectiveTaskType` mira primero story.task_type y, si
|
|
97
88
|
// no es válido, infiere del prefijo del title ([SPIKE], [DOC]…). Así
|
|
@@ -103,15 +94,56 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
103
94
|
logger.info(`HU ${story.id}: task_type inferido del title → ${resolvedTaskType} (era ${story.task_type || "null"})`);
|
|
104
95
|
}
|
|
105
96
|
const huPolicies = applyPolicies({ taskType: resolvedTaskType, policies: ctx.config.policies });
|
|
106
|
-
const savedFlags = { ...ctx.pipelineFlags };
|
|
107
|
-
if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
|
|
108
|
-
if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
|
|
109
|
-
if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
|
|
110
|
-
if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
|
|
111
97
|
logger.info(`HU ${story.id} (${resolvedTaskType}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
// PAR-E2 PR2 (KJC-TSK-0629): every per-HU adjustment lands on LANE
|
|
100
|
+
// copies. ctx.config/pipelineFlags/session/brainCtx are shared by all
|
|
101
|
+
// lanes, so concurrent lanes must never mutate them. This also fixes a
|
|
102
|
+
// latent leak: max_iterations was mutated on ctx.config and only the
|
|
103
|
+
// fallback path restored it — the acceptance path returned early and
|
|
104
|
+
// left it clamped to hu_max_iterations for the rest of the run.
|
|
105
|
+
const worktreePath = laneOpts?.worktreePath || null;
|
|
106
|
+
const projectDir = worktreePath || ctx.config.projectDir || process.cwd();
|
|
107
|
+
// PAR-H (KJC-TSK-0631): the lane's slot travels as env vars so any
|
|
108
|
+
// service the coder or the acceptance tests start can offset its
|
|
109
|
+
// ports (offset = slot × 100; the project applies its own base).
|
|
110
|
+
const laneEnv = Number.isInteger(laneOpts?.laneSlot)
|
|
111
|
+
? { KJ_LANE_SLOT: String(laneOpts.laneSlot), KJ_PORT_OFFSET: String(laneOpts.laneSlot * 100) }
|
|
112
|
+
: null;
|
|
113
|
+
const laneConfig = { ...ctx.config, projectDir, max_iterations: huMaxIterations, lane_env: laneEnv };
|
|
114
|
+
if (!huPolicies.tdd) laneConfig.development = { ...laneConfig.development, methodology: "standard", require_test_changes: false };
|
|
115
|
+
if (!huPolicies.sonar) laneConfig.sonarqube = { ...laneConfig.sonarqube, enabled: false };
|
|
116
|
+
const laneFlags = { ...ctx.pipelineFlags };
|
|
117
|
+
if (!huPolicies.reviewer) laneFlags.reviewerEnabled = false;
|
|
118
|
+
if (!huPolicies.testsRequired) laneFlags.testerEnabled = false;
|
|
119
|
+
// Worktree lanes clone the session so reviewer feedback never leaks
|
|
120
|
+
// across concurrent HUs. Disk saves still share the session id (last
|
|
121
|
+
// writer wins) — journal noise at most; in-memory isolation is what
|
|
122
|
+
// guards correctness.
|
|
123
|
+
const laneSession = worktreePath ? { ...ctx.session } : ctx.session;
|
|
124
|
+
let laneBrain = ctx.brainCtx;
|
|
125
|
+
if (ctx.brainCtx?.enabled) {
|
|
126
|
+
const { createBrainContext } = await import("../brain-coordinator.js");
|
|
127
|
+
const fresh = createBrainContext({ enabled: true });
|
|
128
|
+
if (worktreePath) {
|
|
129
|
+
laneBrain = { ...ctx.brainCtx, extensionCount: 0, feedbackQueue: fresh.feedbackQueue, verificationTracker: fresh.verificationTracker };
|
|
130
|
+
} else {
|
|
131
|
+
ctx.brainCtx.extensionCount = 0;
|
|
132
|
+
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
133
|
+
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let lanePlannedTask = ctx.plannedTask;
|
|
137
|
+
|
|
138
|
+
// A worktree lane skips the main-tree checkout: `git worktree add -b`
|
|
139
|
+
// already left the worktree checked out on kj-hu-<id>.
|
|
140
|
+
let branchName;
|
|
141
|
+
if (worktreePath) {
|
|
142
|
+
branchName = `kj-hu-${story.id}`;
|
|
143
|
+
huBranches.set(story.id, branchName);
|
|
144
|
+
} else {
|
|
145
|
+
branchName = await prepareHuBranch({ story, huBranches, config: laneConfig, logger });
|
|
146
|
+
}
|
|
115
147
|
|
|
116
148
|
// KJC-TSK-0408 step 2: snapshot del workspace ANTES de invocar al
|
|
117
149
|
// coder. Si el usuario hace Undo después, restaura este SHA. El
|
|
@@ -142,11 +174,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
142
174
|
// iterations against an irreparable test (the 2026-04-29 bug).
|
|
143
175
|
const failureTracker = createFailureTracker();
|
|
144
176
|
|
|
145
|
-
for (let attempt = 1; attempt <=
|
|
146
|
-
logger.info(`HU ${story.id}: coder iteration ${attempt}/${
|
|
177
|
+
for (let attempt = 1; attempt <= laneConfig.max_iterations; attempt++) {
|
|
178
|
+
logger.info(`HU ${story.id}: coder iteration ${attempt}/${laneConfig.max_iterations}`);
|
|
147
179
|
emitProgress(emitter, makeEvent("iteration:start", { ...ctx.eventBase, stage: "iteration" }, {
|
|
148
|
-
message: `Iteration ${attempt}/${
|
|
149
|
-
detail: { iteration: attempt, maxIterations:
|
|
180
|
+
message: `Iteration ${attempt}/${laneConfig.max_iterations}`,
|
|
181
|
+
detail: { iteration: attempt, maxIterations: laneConfig.max_iterations }
|
|
150
182
|
}));
|
|
151
183
|
|
|
152
184
|
// Coder runs with the HU task + any diagnostic feedback from previous attempt.
|
|
@@ -155,9 +187,9 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
155
187
|
// not only after the first failed run.
|
|
156
188
|
const coderResult = await runCoderStage({
|
|
157
189
|
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole,
|
|
158
|
-
config:
|
|
159
|
-
session:
|
|
160
|
-
trackBudget: ctx.trackBudget, iteration: attempt, brainCtx:
|
|
190
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
191
|
+
session: laneSession, plannedTask: lanePlannedTask,
|
|
192
|
+
trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: laneBrain,
|
|
161
193
|
acceptanceTests: story.acceptance_tests,
|
|
162
194
|
// PR F (v2.7.5): plan-aware coder context. ADRs are
|
|
163
195
|
// shared across the plan, the rest are scoped to this
|
|
@@ -173,11 +205,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
// Sonar quality gate (sw task_type only, when policies say sonar=true)
|
|
176
|
-
if (huPolicies.sonar &&
|
|
208
|
+
if (huPolicies.sonar && laneConfig.sonarqube?.enabled) {
|
|
177
209
|
try {
|
|
178
210
|
const sonarResult = await runSonarStage({
|
|
179
|
-
config:
|
|
180
|
-
session:
|
|
211
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
212
|
+
session: laneSession, trackBudget: ctx.trackBudget, iteration: attempt,
|
|
181
213
|
// sonarState is required: runSonarStage reads/writes
|
|
182
214
|
// .issuesInitial / .issuesFinal on it. Forgetting this
|
|
183
215
|
// surfaces as `Cannot read properties of undefined
|
|
@@ -188,11 +220,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
188
220
|
sonarState: ctx.sonarState,
|
|
189
221
|
repeatDetector: ctx.repeatDetector,
|
|
190
222
|
budgetSummary: ctx.budgetSummary,
|
|
191
|
-
askQuestion, brainCtx:
|
|
223
|
+
askQuestion, brainCtx: laneBrain
|
|
192
224
|
});
|
|
193
225
|
if (sonarResult?.action === "continue") {
|
|
194
226
|
// Sonar failed — add to feedback for next coder attempt
|
|
195
|
-
|
|
227
|
+
lanePlannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${laneSession.last_reviewer_feedback}`;
|
|
196
228
|
continue;
|
|
197
229
|
}
|
|
198
230
|
} catch (err) {
|
|
@@ -207,7 +239,7 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
207
239
|
detail: { huId: story.id, testCount: story.acceptance_tests.length }
|
|
208
240
|
}));
|
|
209
241
|
|
|
210
|
-
const testResult = await runAcceptanceTests(story.acceptance_tests, projectDir);
|
|
242
|
+
const testResult = await runAcceptanceTests(story.acceptance_tests, projectDir, { env: laneEnv });
|
|
211
243
|
emitProgress(emitter, makeEvent("hu:acceptance-end", { ...ctx.eventBase, stage: "acceptance" }, {
|
|
212
244
|
status: testResult.allPassed ? "ok" : "fail",
|
|
213
245
|
message: testResult.summary,
|
|
@@ -225,8 +257,8 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
225
257
|
);
|
|
226
258
|
if (pendingGherkinTests.length === 0) {
|
|
227
259
|
logger.info(`HU ${story.id}: all shell acceptance tests PASSED, no Gherkin to translate — approved`);
|
|
228
|
-
await finalizeHuCommit({ story, branchName, config:
|
|
229
|
-
return { approved: true, sessionId:
|
|
260
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
261
|
+
return { approved: true, sessionId: laneSession.id, reason: "acceptance_tests_passed" };
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
// Shell tests passed; Gherkin needs translation. Invoke the
|
|
@@ -236,16 +268,16 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
236
268
|
const { runTesterStage } = await import("../post-loop-stages.js");
|
|
237
269
|
const shellResults = testResult.results.filter((r) => r.type !== "gherkin");
|
|
238
270
|
const testerOutcome = await runTesterStage({
|
|
239
|
-
config:
|
|
240
|
-
session:
|
|
271
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
272
|
+
session: laneSession, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
241
273
|
iteration: attempt, task: huTask, diff: null, askQuestion,
|
|
242
274
|
pendingGherkinTests, shellTestResults: shellResults,
|
|
243
275
|
});
|
|
244
276
|
const testerStage = testerOutcome?.stageResult;
|
|
245
277
|
if (testerOutcome?.action !== "continue" && testerStage?.verdict === "pass") {
|
|
246
278
|
logger.info(`HU ${story.id}: tester translated Gherkin and verdict=pass — approved`);
|
|
247
|
-
await finalizeHuCommit({ story, branchName, config:
|
|
248
|
-
return { approved: true, sessionId:
|
|
279
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
280
|
+
return { approved: true, sessionId: laneSession.id, reason: "acceptance_tests_passed" };
|
|
249
281
|
}
|
|
250
282
|
|
|
251
283
|
// Tester rejected (translated tests failed or coverage
|
|
@@ -259,8 +291,8 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
259
291
|
"Fix the implementation so every scenario passes. Do NOT soften the scenarios.",
|
|
260
292
|
].filter(Boolean).join("\n");
|
|
261
293
|
logger.warn(`HU ${story.id}: tester verdict=fail after Gherkin translation — sending back to coder`);
|
|
262
|
-
setReviewerFeedback(
|
|
263
|
-
|
|
294
|
+
setReviewerFeedback(laneSession, diagnostic);
|
|
295
|
+
lanePlannedTask = `${huTask}\n\n--- GHERKIN TRANSLATION FAILURES ---\n${diagnostic}`;
|
|
264
296
|
continue;
|
|
265
297
|
}
|
|
266
298
|
|
|
@@ -314,7 +346,7 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
314
346
|
|
|
315
347
|
if (repairEscalate) {
|
|
316
348
|
logger.warn(`HU ${story.id}: Repairer escalated — ${repairEscalate}`);
|
|
317
|
-
return { approved: false, sessionId:
|
|
349
|
+
return { approved: false, sessionId: laneSession.id, reason: "repair_escalated" };
|
|
318
350
|
}
|
|
319
351
|
if (repairedAny) {
|
|
320
352
|
// Tests were rewritten — restart iteration with the fixed
|
|
@@ -328,26 +360,24 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
328
360
|
// No repair happened — normal coder feedback path.
|
|
329
361
|
const diagnostic = buildDiagnosticPrompt(failed);
|
|
330
362
|
logger.warn(`HU ${story.id}: ${failed.length} acceptance test(s) FAILED — sending diagnostic to coder`);
|
|
331
|
-
setReviewerFeedback(
|
|
332
|
-
|
|
363
|
+
setReviewerFeedback(laneSession, diagnostic);
|
|
364
|
+
lanePlannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
|
|
333
365
|
}
|
|
334
366
|
|
|
335
367
|
// All iterations exhausted
|
|
336
368
|
logger.warn(`HU ${story.id}: max iterations reached with acceptance tests still failing`);
|
|
337
|
-
return { approved: false, sessionId:
|
|
369
|
+
return { approved: false, sessionId: laneSession.id, reason: "acceptance_tests_failed" };
|
|
338
370
|
}
|
|
339
371
|
|
|
340
|
-
// Fallback: no acceptance_tests → standard pipeline (reviewer/tester)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} finally {
|
|
348
|
-
ctx.config.max_iterations = originalMaxIterations;
|
|
349
|
-
Object.assign(ctx.pipelineFlags, savedFlags);
|
|
372
|
+
// Fallback: no acceptance_tests → standard pipeline (reviewer/tester).
|
|
373
|
+
// The loop reads everything through ctx.* — handing it a lane view
|
|
374
|
+
// keeps the shared ctx untouched (no more mutate-and-restore dance).
|
|
375
|
+
const laneCtx = { ...ctx, config: laneConfig, pipelineFlags: laneFlags, session: laneSession, brainCtx: laneBrain, plannedTask: lanePlannedTask };
|
|
376
|
+
const result = await runIterationLoop(laneCtx, { task: huTask, askQuestion, emitter, logger });
|
|
377
|
+
if (result?.approved) {
|
|
378
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
350
379
|
}
|
|
380
|
+
return result;
|
|
351
381
|
},
|
|
352
382
|
emitter,
|
|
353
383
|
eventBase: ctx.eventBase,
|
|
@@ -6,7 +6,11 @@ import { topologicalSort } from "../hu/graph.js";
|
|
|
6
6
|
import { updateStoryStatus, loadHuBatch, saveHuBatch, HU_STATUS } from "../hu/store.js";
|
|
7
7
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
8
8
|
import { refineHuWithContext } from "../hu/lazy-planner.js";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { acquireSlot, releaseSlot } from "karajan-core/slot-registry";
|
|
11
|
+
import { getKarajanHome } from "karajan-core/paths";
|
|
9
12
|
import { findParallelGroups, createWorktree, mergeWorktree, removeWorktree } from "../hu/parallel-executor.js";
|
|
13
|
+
import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
|
|
10
14
|
import { partitionConflictFree } from "./hu-scheduler.js";
|
|
11
15
|
import { createParallelLimiter, planBudgetUsd } from "./parallel-limiter.js";
|
|
12
16
|
|
|
@@ -200,7 +204,7 @@ function buildHuOutcome({ story: _story, iterResult, status, startedAt, huBudget
|
|
|
200
204
|
* @param {object} params
|
|
201
205
|
* @returns {Promise<{huId: string, approved: boolean, result?: object, error?: string, blockedDependents?: string[]}>}
|
|
202
206
|
*/
|
|
203
|
-
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
207
|
+
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, laneSlot = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
204
208
|
// PR1 (live HU status): every saveHuBatch should also notify the
|
|
205
209
|
// plan JSON so the board reflects state in real time. Defined as a
|
|
206
210
|
// local helper so we don't repeat the try/null-check boilerplate.
|
|
@@ -285,7 +289,9 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
285
289
|
}));
|
|
286
290
|
|
|
287
291
|
try {
|
|
288
|
-
|
|
292
|
+
// PAR-E2 (KJC-TSK-0629): lanes handed a worktree must aim every git and
|
|
293
|
+
// filesystem touchpoint at it — laneOpts carries that path to the runner.
|
|
294
|
+
const iterResult = await runIterationFn(huTask, story, { worktreePath: worktreePath || null, laneSlot });
|
|
289
295
|
const approved = Boolean(iterResult?.approved);
|
|
290
296
|
|
|
291
297
|
// --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
|
|
@@ -525,12 +531,27 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
525
531
|
// Multiple HUs: create worktrees, run in parallel
|
|
526
532
|
const projectDir = config?.projectDir || process.cwd();
|
|
527
533
|
const worktrees = new Map();
|
|
534
|
+
// PAR-H (KJC-TSK-0631): every lane gets a stable numeric slot so
|
|
535
|
+
// services it starts can offset their ports (KJ_LANE_SLOT /
|
|
536
|
+
// KJ_PORT_OFFSET reach the coder and the acceptance tests).
|
|
537
|
+
const slotRegistryPath = join(getKarajanHome(), "worktree-slots.json");
|
|
538
|
+
const laneSlots = new Map();
|
|
528
539
|
|
|
529
540
|
// Create worktrees for each HU in the batch
|
|
530
541
|
for (const id of runnableIds) {
|
|
531
542
|
try {
|
|
532
543
|
const wtPath = await createWorktree(projectDir, id);
|
|
533
544
|
worktrees.set(id, wtPath);
|
|
545
|
+
// PAR-G (KJC-TSK-0630): a fresh worktree has no node_modules and
|
|
546
|
+
// no initialized submodules — make the lane operative before the
|
|
547
|
+
// coder lands. Best-effort: warnings never block the lane.
|
|
548
|
+
await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null, logger });
|
|
549
|
+
try {
|
|
550
|
+
const { slot } = await acquireSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${id}` });
|
|
551
|
+
laneSlots.set(id, slot);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
logger.warn(`Failed to acquire lane slot for HU ${id}: ${err.message} — lane runs without port offset`);
|
|
554
|
+
}
|
|
534
555
|
} catch (err) {
|
|
535
556
|
logger.warn(`Failed to create worktree for HU ${id}: ${err.message} — will run sequentially`);
|
|
536
557
|
}
|
|
@@ -545,6 +566,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
545
566
|
storyId, batch, batchSessionId, runIterationFn,
|
|
546
567
|
emitter, eventBase, logger, config, results,
|
|
547
568
|
worktreePath: worktrees.get(storyId),
|
|
569
|
+
laneSlot: laneSlots.get(storyId) ?? null,
|
|
548
570
|
onStatusChange, onOutcome, budgetTracker
|
|
549
571
|
});
|
|
550
572
|
} finally {
|
|
@@ -560,6 +582,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
560
582
|
if (res.approved && worktrees.has(res.huId)) {
|
|
561
583
|
try {
|
|
562
584
|
await mergeWorktree(projectDir, res.huId);
|
|
585
|
+
await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
|
|
563
586
|
} catch (err) {
|
|
564
587
|
logger.warn(`Failed to merge worktree for HU ${res.huId}: ${err.message}`);
|
|
565
588
|
}
|
|
@@ -579,6 +602,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
579
602
|
// Clean up failed worktree
|
|
580
603
|
if (worktrees.has(res.huId)) {
|
|
581
604
|
try { await removeWorktree(projectDir, res.huId); } catch { /* ignore */ }
|
|
605
|
+
await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
|
|
582
606
|
}
|
|
583
607
|
}
|
|
584
608
|
}
|