immune-brain 5.2.0 → 5.5.1
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +17 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -19
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +22 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +17 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// The one owner of every commit the runtime makes on the user's behalf for a
|
|
2
|
+
// workflow precondition. A caller passes the exact repository-relative paths
|
|
3
|
+
// and a subject; this module never derives paths. It commits only the listed
|
|
4
|
+
// paths that are dirty, leaves every other index entry and worktree file
|
|
5
|
+
// byte-identical, runs Git hooks normally, and never pushes, amends, switches
|
|
6
|
+
// branch or writes Git config.
|
|
7
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
|
+
import { accessSync, chmodSync, constants, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
export type WorkflowCommitResult =
|
|
13
|
+
| { committed: false }
|
|
14
|
+
| { committed: true; commit: string; paths: string[] };
|
|
15
|
+
|
|
16
|
+
export interface WorkflowCommitInput {
|
|
17
|
+
/** Repository-relative, slash-separated file paths. Clean paths are skipped. */
|
|
18
|
+
paths: readonly string[];
|
|
19
|
+
subject: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Commit identity for a runtime-made commit: the existing environment, then the
|
|
25
|
+
* repository or global config, then the automation fallback. Never a config write.
|
|
26
|
+
*/
|
|
27
|
+
export function identityEnv(root: string): NodeJS.ProcessEnv {
|
|
28
|
+
const configured = (key: string): string => {
|
|
29
|
+
const result = spawnSync("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
|
|
30
|
+
if (result.status !== 0 && result.status !== 1) throw new Error(`Cannot read Git identity: ${key}`);
|
|
31
|
+
return result.stdout?.trim() ?? "";
|
|
32
|
+
};
|
|
33
|
+
const name = configured("user.name") || "Immune-Brain Enrollment";
|
|
34
|
+
const email = configured("user.email") || "enrollment@immune-brain.local";
|
|
35
|
+
return { ...process.env,
|
|
36
|
+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || name, GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || email,
|
|
37
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || name, GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || email,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function gitFailure(args: readonly string[], error: unknown): Error {
|
|
42
|
+
const output = (key: "stderr" | "stdout"): string => {
|
|
43
|
+
const value = typeof error === "object" && error !== null ? (error as Record<string, unknown>)[key] : undefined;
|
|
44
|
+
return typeof value === "string" ? value.trim() : Buffer.isBuffer(value) ? value.toString("utf8").trim() : "";
|
|
45
|
+
};
|
|
46
|
+
const detail = output("stderr") || output("stdout") || (error instanceof Error ? error.message : String(error));
|
|
47
|
+
return new Error(`git ${args[0] === "-c" ? args[2] : args[0]} failed: ${detail}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Listed paths are literal: no pathspec magic and no glob can widen the list. */
|
|
51
|
+
function git(root: string, args: string[], env: NodeJS.ProcessEnv = process.env): string {
|
|
52
|
+
try {
|
|
53
|
+
return execFileSync("git", args, {
|
|
54
|
+
cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
55
|
+
env: { ...env, GIT_LITERAL_PATHSPECS: "1", GIT_GLOB_PATHSPECS: undefined, GIT_NOGLOB_PATHSPECS: undefined, GIT_ICASE_PATHSPECS: undefined },
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw gitFailure(args, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const splitNull = (output: string): string[] => output.split("\0").filter((entry) => entry.length > 0);
|
|
63
|
+
|
|
64
|
+
function assertListedPath(root: string, path: string): void {
|
|
65
|
+
const parts = path.split("/");
|
|
66
|
+
if (path.length === 0 || path.includes("\0") || path.includes("\\") || isAbsolute(path)
|
|
67
|
+
|| parts.some((part) => part === "" || part === "." || part === ".." || part === ".git"))
|
|
68
|
+
throw new Error(`automatic commit refuses a path outside the repository: ${JSON.stringify(path)}`);
|
|
69
|
+
// A directory component that is a symbolic link would let the path leave the
|
|
70
|
+
// repository. The final component may be a link: Git records the link itself.
|
|
71
|
+
let current = root;
|
|
72
|
+
for (const part of parts.slice(0, -1)) {
|
|
73
|
+
current = join(current, part);
|
|
74
|
+
let stat;
|
|
75
|
+
try { stat = lstatSync(current); } catch { return; }
|
|
76
|
+
if (stat.isSymbolicLink())
|
|
77
|
+
throw new Error(`automatic commit refuses a path through a symbolic link: ${path}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Guard the final commit tree at Git's prepared ref transaction, after all commit hooks. */
|
|
82
|
+
function commitWithPathGuard(root: string, dirty: string[], before: string, subject: string): void {
|
|
83
|
+
// reference-transaction was introduced in Git 2.29; an older Git cannot enforce this boundary.
|
|
84
|
+
const version = git(root, ["--version"]).match(/git version (\d+)\.(\d+)/);
|
|
85
|
+
if (!version || Number(version[1]) < 2 || (Number(version[1]) === 2 && Number(version[2]) < 29))
|
|
86
|
+
throw new Error("automatic commit requires Git 2.29 or newer for its ref transaction guard");
|
|
87
|
+
const targetRef = git(root, ["rev-parse", "--symbolic-full-name", "HEAD"]).trim();
|
|
88
|
+
const originalHooks = resolve(root, git(root, ["rev-parse", "--git-path", "hooks"]).trim());
|
|
89
|
+
const temporary = mkdtempSync(join(tmpdir(), "imm-commit-hooks-"));
|
|
90
|
+
const hooks = join(temporary, "hooks");
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(hooks);
|
|
93
|
+
let entries: string[] = [];
|
|
94
|
+
try { entries = readdirSync(originalHooks); } catch (error) {
|
|
95
|
+
if (!["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
|
96
|
+
}
|
|
97
|
+
const quote = (path: string) => `'${path.replaceAll("'", "'\\''")}'`;
|
|
98
|
+
// Restore the caller's hook environment, excluding our command-local pathspec/config overrides.
|
|
99
|
+
const hookEnvironment = Object.fromEntries([
|
|
100
|
+
"GIT_LITERAL_PATHSPECS", "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_ICASE_PATHSPECS", "GIT_CONFIG_PARAMETERS",
|
|
101
|
+
].map((key) => [key, process.env[key] ?? null]));
|
|
102
|
+
const restoreEnvironment = Object.entries(hookEnvironment).map(([key, value]) =>
|
|
103
|
+
value === null ? `unset ${key}` : `export ${key}=${quote(value)}`,
|
|
104
|
+
).join("\n");
|
|
105
|
+
for (const name of entries) {
|
|
106
|
+
if (name === "reference-transaction") continue;
|
|
107
|
+
const original = join(originalHooks, name);
|
|
108
|
+
try { accessSync(original, constants.X_OK); } catch (error) {
|
|
109
|
+
if (["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) continue;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
const forwarding = join(hooks, name);
|
|
113
|
+
writeFileSync(forwarding, `#!/bin/sh\n${restoreEnvironment}\nexec ${quote(original)} "$@"\n`);
|
|
114
|
+
chmodSync(forwarding, 0o755);
|
|
115
|
+
}
|
|
116
|
+
const originalRefHook = join(originalHooks, "reference-transaction");
|
|
117
|
+
let executableRefHook: string | null = null;
|
|
118
|
+
try { accessSync(originalRefHook, constants.X_OK); executableRefHook = originalRefHook; } catch (error) {
|
|
119
|
+
if (!["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
|
120
|
+
}
|
|
121
|
+
const guard = join(temporary, "guard.cjs");
|
|
122
|
+
writeFileSync(guard, `const {execFileSync, spawnSync} = require("node:child_process");
|
|
123
|
+
const {readFileSync} = require("node:fs");
|
|
124
|
+
const input = readFileSync(0);
|
|
125
|
+
const original = ${JSON.stringify(executableRefHook)};
|
|
126
|
+
const originalEnv = { ...process.env };
|
|
127
|
+
for (const [key, value] of Object.entries(${JSON.stringify(hookEnvironment)})) {
|
|
128
|
+
if (value === null) delete originalEnv[key]; else originalEnv[key] = value;
|
|
129
|
+
}
|
|
130
|
+
if (original) {
|
|
131
|
+
const result = spawnSync(original, process.argv.slice(2), {env: originalEnv, input, stdio: ["pipe", "inherit", "inherit"]});
|
|
132
|
+
if (result.error) throw result.error;
|
|
133
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
134
|
+
}
|
|
135
|
+
if (process.argv[2] === "prepared") {
|
|
136
|
+
const git = (...args) => execFileSync("git", args, {cwd: ${JSON.stringify(root)}, encoding: "utf8"});
|
|
137
|
+
for (const line of input.toString("utf8").trim().split("\\n")) {
|
|
138
|
+
const [old, next, ref] = line.split(" ");
|
|
139
|
+
if (ref !== ${JSON.stringify(targetRef)} && ref !== "HEAD") continue;
|
|
140
|
+
const parents = git("rev-list", "--parents", "-n", "1", next).trim().split(" ");
|
|
141
|
+
const paths = git("diff", "--no-renames", "--name-only", "-z", old, next).split("\\0").filter(Boolean);
|
|
142
|
+
const allowed = new Set(${JSON.stringify(dirty)});
|
|
143
|
+
if (old !== ${JSON.stringify(before)} || parents.length !== 2 || parents[1] !== old
|
|
144
|
+
|| paths.length !== allowed.size || paths.some(path => !allowed.has(path))
|
|
145
|
+
|| git("show", "-s", "--format=%s", next).trimEnd() !== ${JSON.stringify(subject)}) {
|
|
146
|
+
console.error("automatic commit refuses hook changes to the listed paths, parent or subject");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`);
|
|
152
|
+
const hook = join(hooks, "reference-transaction");
|
|
153
|
+
writeFileSync(hook, `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(guard)} "$@"\n`);
|
|
154
|
+
chmodSync(hook, 0o755);
|
|
155
|
+
// Command-local configuration leaves repository/global config unchanged; all original hooks still run.
|
|
156
|
+
git(root, ["-c", `core.hooksPath=${hooks}`, "commit", "--quiet", "--only", "-m", subject, "--", ...dirty], identityEnv(root));
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Commit exactly the listed paths that differ from HEAD, in one commit.
|
|
164
|
+
* Requires a committed HEAD. Throws with Git's own message when the commit is
|
|
165
|
+
* refused (a hook, identity, a lock); nothing is retried and no hook is bypassed.
|
|
166
|
+
*/
|
|
167
|
+
export function commitWorkflowPaths(projectRoot: string, input: WorkflowCommitInput): WorkflowCommitResult {
|
|
168
|
+
const root = realpathSync(resolve(projectRoot));
|
|
169
|
+
const listed = [...new Set(input.paths)].sort();
|
|
170
|
+
if (listed.length === 0) return { committed: false };
|
|
171
|
+
for (const path of listed) assertListedPath(root, path);
|
|
172
|
+
if (!/^[^\r\n]+$/.test(input.subject)) throw new Error("automatic commit requires a one-line subject");
|
|
173
|
+
input.signal?.throwIfAborted();
|
|
174
|
+
|
|
175
|
+
const toplevel = git(root, ["rev-parse", "--show-toplevel"]).trim();
|
|
176
|
+
if (realpathSync(resolve(toplevel)) !== root)
|
|
177
|
+
throw new Error("automatic commit requires the repository root as the project root");
|
|
178
|
+
git(root, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
179
|
+
|
|
180
|
+
if (splitNull(git(root, ["ls-files", "--unmerged", "-z"])).length > 0)
|
|
181
|
+
throw new Error("automatic commit refuses an unmerged index; resolve the merge first");
|
|
182
|
+
// `ls-files -v` tags: lowercase is assume-unchanged, `S` is skip-worktree.
|
|
183
|
+
// Either hides worktree content from the commit, so the commit would not
|
|
184
|
+
// contain what the precondition inspected.
|
|
185
|
+
const flagged = splitNull(git(root, ["ls-files", "-v", "-z", "--", ...listed]))
|
|
186
|
+
.filter((entry) => /^[a-zS] /.test(entry))
|
|
187
|
+
.map((entry) => entry.slice(2));
|
|
188
|
+
if (flagged.length > 0)
|
|
189
|
+
throw new Error(`automatic commit refuses assume-unchanged or skip-worktree paths: ${flagged.join(", ")}`);
|
|
190
|
+
|
|
191
|
+
const status = splitNull(git(root, ["status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--", ...listed]));
|
|
192
|
+
// --only commits worktree content; index-only changes whose worktree equals HEAD stay staged.
|
|
193
|
+
const worktreeChanges = new Set(splitNull(git(root, ["diff", "--no-renames", "--name-only", "-z", "HEAD", "--", ...listed])));
|
|
194
|
+
const wanted = new Set(listed);
|
|
195
|
+
const dirty: string[] = [];
|
|
196
|
+
const untracked: string[] = [];
|
|
197
|
+
for (const entry of status) {
|
|
198
|
+
const path = entry.slice(3);
|
|
199
|
+
// A listed directory would report files beneath it; only exact paths count.
|
|
200
|
+
if (!wanted.has(path)) throw new Error(`automatic commit requires file paths, not a directory: ${path}`);
|
|
201
|
+
if (!entry.startsWith("??") && !worktreeChanges.has(path)) continue;
|
|
202
|
+
if (!dirty.includes(path)) dirty.push(path);
|
|
203
|
+
if (entry.startsWith("??")) untracked.push(path);
|
|
204
|
+
}
|
|
205
|
+
if (dirty.length === 0) return { committed: false };
|
|
206
|
+
dirty.sort();
|
|
207
|
+
|
|
208
|
+
input.signal?.throwIfAborted();
|
|
209
|
+
const before = git(root, ["rev-parse", "HEAD"]).trim();
|
|
210
|
+
if (untracked.length > 0) git(root, ["add", "--", ...untracked]);
|
|
211
|
+
try {
|
|
212
|
+
commitWithPathGuard(root, dirty, before, input.subject);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// Put the paths this call staged back to untracked; a failed commit leaves
|
|
215
|
+
// no trace in the index.
|
|
216
|
+
if (untracked.length > 0 && git(root, ["rev-parse", "HEAD"]).trim() === before) {
|
|
217
|
+
try { git(root, ["reset", "--quiet", "HEAD", "--", ...untracked]); } catch { /* the commit failure is the report */ }
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
return { committed: true, commit: git(root, ["rev-parse", "HEAD"]).trim(), paths: dirty };
|
|
222
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { PHASE_ENTRY_RECOVERY } from "../assurance/phase_entry";
|
|
3
|
+
import type { ReworkChoice, ReworkExitsReadiness } from "../kernel/assurance_projection";
|
|
3
4
|
|
|
4
5
|
export const PRIVILEGED_OPERATIONS = [
|
|
5
6
|
"enroll",
|
|
@@ -81,12 +82,18 @@ export interface NativeConfirmationInput {
|
|
|
81
82
|
bindingDigest?: string;
|
|
82
83
|
gitBaseNotice?: string;
|
|
83
84
|
planDigest?: string;
|
|
85
|
+
/** Present only for a replan boundary: the exits the user chooses between. */
|
|
86
|
+
reworkExits?: ReworkExitsReadiness;
|
|
84
87
|
batchDetails?: {
|
|
85
88
|
initiative_slug: string;
|
|
86
89
|
batch_branch: string;
|
|
87
90
|
children: Array<{ task_id: string; slice_id: string; risk?: string }>;
|
|
88
91
|
excluded: Array<{ task_id: string; slice_id: string; reason: string }>;
|
|
89
|
-
budget
|
|
92
|
+
budget?: { max_children: number; qa_failure_limit: number };
|
|
93
|
+
final_verification?: string[];
|
|
94
|
+
/** Present only for a native Parent: no batch is started, only these commits and final commands run. */
|
|
95
|
+
parent_final_verification?: { integrate_task_ids: string[];
|
|
96
|
+
delivery_consolidation?: import("../unattended/parent_delivery_consolidation").ConsolidationReceipt };
|
|
90
97
|
/** Present only when the run uses Lanes. */
|
|
91
98
|
lane_mode?: {
|
|
92
99
|
max_parallel: number;
|
|
@@ -104,7 +111,7 @@ export interface NativeConfirmationInput {
|
|
|
104
111
|
batch_branch: string;
|
|
105
112
|
plan_digest: string;
|
|
106
113
|
recorded_commits: number;
|
|
107
|
-
children: Array<{ task_id: string; slice_id: string; state: string; commit: string | null; lane_branch: string | null }>;
|
|
114
|
+
children: Array<{ task_id: string; slice_id: string; state: string; commit: string | null; lane_branch: string | null; kernel_lifecycle: "done" | "stopped" | null }>;
|
|
108
115
|
};
|
|
109
116
|
signal?: AbortSignal;
|
|
110
117
|
}
|
|
@@ -114,6 +121,8 @@ export interface NativeConfirmationResult {
|
|
|
114
121
|
requestId: string;
|
|
115
122
|
/** ADR 0018: true only when the user ticked the delegation in a lane batch gate. */
|
|
116
123
|
delegateRevisions?: boolean;
|
|
124
|
+
/** Present only when the request offered `reworkExits` and the user accepted. */
|
|
125
|
+
reworkChoice?: ReworkChoice;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
export type NativeConfirmationPort = (input: NativeConfirmationInput) => Promise<NativeConfirmationResult>;
|