immune-brain 5.3.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 +13 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
- 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-planner.md +57 -16
- 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 +21 -1
- 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 +1 -7
- 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 +8 -15
- 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 +13 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stagePlanningArtifactTransition, type VerdictAuthority } from "../assurance/verdict_authority";
|
|
2
2
|
export { ensureReviewRevision as ensureClaudeReviewRevision } from "../assurance/verdict_authority";
|
|
3
|
+
import { parseDeliveryConsolidation, type DeliveryConsolidation } from "../unattended/parent_delivery_consolidation";
|
|
3
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -14,11 +15,23 @@ import {
|
|
|
14
15
|
import {
|
|
15
16
|
writeNativeReviewEvidence,
|
|
16
17
|
} from "../assurance/review_evidence";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
projectAssurance,
|
|
20
|
+
type AssuranceProjection,
|
|
21
|
+
type AssuranceProjectionResult,
|
|
22
|
+
type ReworkChoice,
|
|
23
|
+
type ReworkExitsReadiness,
|
|
24
|
+
} from "../kernel/assurance_projection";
|
|
18
25
|
import { type TaskFinding, type TaskRecord } from "../kernel/types";
|
|
19
26
|
import { findingsDigestV2 } from "../kernel/reducer";
|
|
20
27
|
import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
|
|
21
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
assertScopeAdditionsAreFiles,
|
|
30
|
+
canonicalIntentHash,
|
|
31
|
+
parseProjectTaskIntent,
|
|
32
|
+
readTaskIntent,
|
|
33
|
+
withNextScope,
|
|
34
|
+
} from "../kernel/intent";
|
|
22
35
|
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
23
36
|
import {
|
|
24
37
|
createMutationAuthorityRegistry,
|
|
@@ -30,9 +43,9 @@ import {
|
|
|
30
43
|
createEnrollmentAuthorityRegistry,
|
|
31
44
|
type EnrollmentCapabilityBinding,
|
|
32
45
|
} from "../kernel/enrollment_authority";
|
|
33
|
-
import { enrollTask } from "../kernel/enrollment";
|
|
46
|
+
import { enrollmentScopeDisclosure, enrollTask } from "../kernel/enrollment";
|
|
34
47
|
import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
|
|
35
|
-
import { inspectEnrollmentGitBase, enrollmentGitBaseNotice, initializeEnrollmentGitBase } from "../assurance/enrollment_git_base";
|
|
48
|
+
import { commitEnrollmentPreconditions, commitUnbornEnrollmentPreconditions, inspectEnrollmentGitBase, enrollmentGitBaseNotice, initializeEnrollmentGitBase, preconditionCommitNote } from "../assurance/enrollment_git_base";
|
|
36
49
|
import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
|
|
37
50
|
import { runDeterministicQa } from "../assurance/qa";
|
|
38
51
|
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
@@ -43,16 +56,16 @@ import {
|
|
|
43
56
|
captureStagedIntent,
|
|
44
57
|
restoreStagedIntent as restoreStagedIntentShared,
|
|
45
58
|
} from "../staged_intent";
|
|
46
|
-
import { projectTerminalTrackerState, TRACKER_PROJECTION_FAILURE } from "../assurance/coordinator";
|
|
47
|
-
import { runGithubTrackerOperation } from "../github_issue_tracker";
|
|
59
|
+
import { projectTaskStatus, projectTerminalTrackerState, TRACKER_PROJECTION_FAILURE } from "../assurance/coordinator";
|
|
60
|
+
import { runGithubTrackerOperation, assertIsolatedTaskPrerequisites } from "../github_issue_tracker";
|
|
48
61
|
import { readTaskTombstone } from "../kernel/backend_claim";
|
|
49
62
|
import {
|
|
50
63
|
authorizeBatch,
|
|
51
|
-
|
|
64
|
+
prepareBatchPreflight,
|
|
52
65
|
findResumableBatchSlugForTask,
|
|
53
66
|
} from "../unattended/batch_preflight";
|
|
54
67
|
import {
|
|
55
|
-
startBatch,
|
|
68
|
+
startBatch, completedParentReport,
|
|
56
69
|
type BatchRunnerKernelPort,
|
|
57
70
|
type BatchRunReport,
|
|
58
71
|
type BatchTrackerPort,
|
|
@@ -71,6 +84,7 @@ import {
|
|
|
71
84
|
import type {
|
|
72
85
|
InitiativeObservationReader,
|
|
73
86
|
} from "../unattended/types";
|
|
87
|
+
import { integrateSettledCriticalSlices, advanceNativeParent } from "../unattended/batch_git";
|
|
74
88
|
import { observeInitiative } from "../unattended/batch_plan";
|
|
75
89
|
import {
|
|
76
90
|
confirmationRef,
|
|
@@ -82,6 +96,7 @@ import {
|
|
|
82
96
|
type NativeDecision,
|
|
83
97
|
type PrivilegedOperation,
|
|
84
98
|
} from "./interaction";
|
|
99
|
+
import { selectedClaudeParent } from "./run_continuation";
|
|
85
100
|
import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
|
|
86
101
|
import { probeHost, type PermissionMode } from "./capability";
|
|
87
102
|
|
|
@@ -96,6 +111,14 @@ export function diffHashOf(root: string, record: TaskRecord): string {
|
|
|
96
111
|
return diffSnapshotOf(root, record).diff_hash;
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A revision is prepared, checked and bound against the scope it asks for, so
|
|
116
|
+
* the delivery identity of that operation reads the next scope (ADR 0021, D6).
|
|
117
|
+
*/
|
|
118
|
+
function nextScopeDiffProvider(nextScope: readonly string[]): typeof diffSnapshotOf {
|
|
119
|
+
return (root, record) => diffSnapshotOf(root, withNextScope(record, nextScope));
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
/**
|
|
100
123
|
* Read the TaskIntent through the TaskRecord's `intent_ref.path`.
|
|
101
124
|
*
|
|
@@ -107,8 +130,8 @@ export function diffHashOf(root: string, record: TaskRecord): string {
|
|
|
107
130
|
function readTaskIntentForRecord(root: string, taskId: string) {
|
|
108
131
|
// Converge committed follow-ups first, then follow the record.
|
|
109
132
|
recoverKernelStoreFollowUps(root, taskId);
|
|
110
|
-
const
|
|
111
|
-
return readTaskIntent(root, taskId,
|
|
133
|
+
const record = readTaskRecordRaw(root, taskId).record;
|
|
134
|
+
return readTaskIntent(root, taskId, record?.intent_ref.path, record?.intent_snapshot);
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
import {
|
|
@@ -174,6 +197,7 @@ async function mintCapability(
|
|
|
174
197
|
next_intent?: unknown;
|
|
175
198
|
next_intent_ref?: unknown;
|
|
176
199
|
reason?: string;
|
|
200
|
+
rounds?: number;
|
|
177
201
|
finding_id?: string;
|
|
178
202
|
resolution?: string;
|
|
179
203
|
now: string;
|
|
@@ -186,6 +210,7 @@ async function mintCapability(
|
|
|
186
210
|
at: input.now,
|
|
187
211
|
actor_id: input.actor_id,
|
|
188
212
|
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
213
|
+
...(input.rounds !== undefined ? { rounds: input.rounds } : {}),
|
|
189
214
|
...(input.findings !== undefined ? { findings: input.findings } : {}),
|
|
190
215
|
...(input.approval !== undefined ? { approval: input.approval } : {}),
|
|
191
216
|
...(input.next_intent !== undefined ? { next_intent: input.next_intent } : {}),
|
|
@@ -242,6 +267,7 @@ export interface ClaudeRuntimeOptions {
|
|
|
242
267
|
* authorization flow passes them through untouched.
|
|
243
268
|
*/
|
|
244
269
|
type ClaudeBatchStartResult =
|
|
270
|
+
| NonNullable<Awaited<ReturnType<typeof advanceNativeParent>>>
|
|
245
271
|
| { state: "started"; batch_id: string; report: BatchRunReport }
|
|
246
272
|
| { state: "retired"; batch_id: string; report: BatchRunReport | BatchLaneRunReport }
|
|
247
273
|
| { state: "rejected"; reason: string; recovery_action: string }
|
|
@@ -344,7 +370,8 @@ export class ClaudeRuntime {
|
|
|
344
370
|
intentContentHash?: string;
|
|
345
371
|
bindingDigest?: string;
|
|
346
372
|
gitBaseNotice?: string;
|
|
347
|
-
|
|
373
|
+
reworkExits?: ReworkExitsReadiness;
|
|
374
|
+
} = {}): Promise<{ confirmation_ref: string; reworkChoice?: ReworkChoice }> {
|
|
348
375
|
throwIfCancelled(meta.signal);
|
|
349
376
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
350
377
|
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
@@ -359,6 +386,7 @@ export class ClaudeRuntime {
|
|
|
359
386
|
throwIfCancelled(meta.signal);
|
|
360
387
|
const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
|
|
361
388
|
if (!gate.ok) throw gate.error;
|
|
389
|
+
const { reworkExits: _exits, ...refBinding } = binding;
|
|
362
390
|
return {
|
|
363
391
|
confirmation_ref: confirmationRef({
|
|
364
392
|
connectionId: meta.sessionId,
|
|
@@ -366,34 +394,55 @@ export class ClaudeRuntime {
|
|
|
366
394
|
requestId: result.requestId,
|
|
367
395
|
operation,
|
|
368
396
|
taskId: meta.taskId,
|
|
369
|
-
...
|
|
397
|
+
...refBinding,
|
|
370
398
|
}),
|
|
399
|
+
...(result.reworkChoice ? { reworkChoice: result.reworkChoice } : {}),
|
|
371
400
|
};
|
|
372
401
|
}
|
|
373
402
|
|
|
374
|
-
|
|
375
|
-
|
|
403
|
+
/** The status Tool: readable with unstaged in-scope paths, which come back as a warning. */
|
|
404
|
+
async status(taskId: string, nextScope?: readonly string[]) {
|
|
405
|
+
return nextScope ? this.project(taskId, nextScope) : projectTaskStatus(this.cwd, taskId);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The refusing projection every mutation and authorization is bound to. */
|
|
409
|
+
private async project(taskId: string, nextScope?: readonly string[]) {
|
|
410
|
+
return projectAssurance(this.cwd, taskId, nextScope ? nextScopeDiffProvider(nextScope) : diffSnapshotOf);
|
|
376
411
|
}
|
|
377
412
|
|
|
378
413
|
async enroll(taskId: string, meta: ToolMeta) {
|
|
379
414
|
const now = new Date().toISOString();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const gate = await this.gate("enroll", { ...meta, taskId }, {
|
|
384
|
-
risk: intent.intent.risk,
|
|
385
|
-
intentRevision: preparation.intent?.revision,
|
|
386
|
-
intentContentHash: preparation.intent?.content_hash,
|
|
387
|
-
bindingDigest: preparation.digest,
|
|
388
|
-
gitBaseNotice: enrollmentGitBaseNotice(gitBase),
|
|
389
|
-
});
|
|
390
|
-
const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
|
|
391
|
-
if (!unchanged) throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
392
|
-
if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
|
|
393
|
-
throwIfCancelled(meta.signal);
|
|
394
|
-
preparation = initializeEnrollmentGitBase(this.cwd, { task_id: taskId, now }, preparation, gitBase, meta.signal);
|
|
395
|
-
const gitBaseNote = gitBase.state === "unborn" ? `; empty initial commit ${preparation.git_base_head} remains` : "";
|
|
415
|
+
// Commit the task's own dirty preconditions first, so the confirmation
|
|
416
|
+
// below binds the HEAD that results. An owned workspace commits nothing.
|
|
417
|
+
let gitBaseNote = preconditionCommitNote(commitEnrollmentPreconditions(this.cwd, taskId, meta.signal));
|
|
396
418
|
try {
|
|
419
|
+
await assertIsolatedTaskPrerequisites(this.cwd, taskId);
|
|
420
|
+
let preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
|
|
421
|
+
const intent = await readTaskIntentForRecord(this.cwd, taskId);
|
|
422
|
+
const gitBase = inspectEnrollmentGitBase(this.cwd);
|
|
423
|
+
// In-scope changes that already exist are shown in the gate and only the
|
|
424
|
+
// ones shown are claimed as task work on confirmation.
|
|
425
|
+
const disclosure = enrollmentScopeDisclosure(this.cwd, taskId);
|
|
426
|
+
const gate = await this.gate("enroll", { ...meta, taskId }, {
|
|
427
|
+
risk: intent.intent.risk,
|
|
428
|
+
intentRevision: preparation.intent?.revision,
|
|
429
|
+
intentContentHash: preparation.intent?.content_hash,
|
|
430
|
+
bindingDigest: preparation.digest,
|
|
431
|
+
gitBaseNotice: [enrollmentGitBaseNotice(gitBase), disclosure.notice].filter(Boolean).join("\n") || undefined,
|
|
432
|
+
});
|
|
433
|
+
const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
|
|
434
|
+
if (!unchanged) throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
435
|
+
if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
|
|
436
|
+
throwIfCancelled(meta.signal);
|
|
437
|
+
preparation = initializeEnrollmentGitBase(this.cwd, { task_id: taskId, now }, preparation, gitBase, meta.signal);
|
|
438
|
+
if (gitBase.state === "unborn") {
|
|
439
|
+
gitBaseNote += `; empty initial commit ${preparation.git_base_head} remains`;
|
|
440
|
+
// The scope could not be compared with an unborn HEAD; commit it
|
|
441
|
+
// now that a base exists and bind the HEAD that results.
|
|
442
|
+
const late = commitUnbornEnrollmentPreconditions(this.cwd, { task_id: taskId, now }, preparation, meta.signal);
|
|
443
|
+
gitBaseNote += preconditionCommitNote(late.result);
|
|
444
|
+
preparation = late.preparation;
|
|
445
|
+
}
|
|
397
446
|
if (!preparation.intent) throw new Error("Enrollment requires a readable TaskIntent");
|
|
398
447
|
const nonce = enrollmentNonce();
|
|
399
448
|
const binding: EnrollmentCapabilityBinding = {
|
|
@@ -406,10 +455,15 @@ export class ClaudeRuntime {
|
|
|
406
455
|
confirmation_ref: gate.confirmation_ref,
|
|
407
456
|
nonce,
|
|
408
457
|
};
|
|
409
|
-
return await enrollTask(this.cwd, this.enrollmentRegistry, { binding, now });
|
|
458
|
+
return await enrollTask(this.cwd, this.enrollmentRegistry, { binding, now, disclosed_scope_paths: disclosure.paths });
|
|
410
459
|
} catch (error) {
|
|
411
|
-
if (gitBaseNote) throw
|
|
412
|
-
|
|
460
|
+
if (!gitBaseNote) throw error;
|
|
461
|
+
// A native-gate error keeps its type and code; only the text grows.
|
|
462
|
+
if (error instanceof NativeAuthorityError) {
|
|
463
|
+
error.message += gitBaseNote;
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${gitBaseNote}`);
|
|
413
467
|
}
|
|
414
468
|
}
|
|
415
469
|
|
|
@@ -453,13 +507,14 @@ export class ClaudeRuntime {
|
|
|
453
507
|
}
|
|
454
508
|
if (!slug) return result;
|
|
455
509
|
try {
|
|
510
|
+
if (!selectedClaudeParent(meta.toolCallId, this.cwd, slug)) return result;
|
|
456
511
|
// The batch commit takes exactly the scope envelope plus this task's own
|
|
457
512
|
// audit evidence, and its preflight refuses unstaged changes: stage only
|
|
458
513
|
// that evidence, the step an Executor otherwise does by hand.
|
|
459
514
|
if (existsSync(join(this.cwd, ".imm", "audit", taskId)))
|
|
460
515
|
execFileSync("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
461
516
|
const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
|
|
462
|
-
return { ...(result as object), batch } as T;
|
|
517
|
+
return { ...(result as object), batch, ...(batch.state === "started" && batch.report?.tracker ? { tracker: batch.report.tracker } : {}) } as T;
|
|
463
518
|
} catch (error) {
|
|
464
519
|
return {
|
|
465
520
|
...(result as object),
|
|
@@ -489,6 +544,8 @@ export class ClaudeRuntime {
|
|
|
489
544
|
if (result === null || typeof result !== "object") return result;
|
|
490
545
|
if (!settledKernelResult(result)) return result;
|
|
491
546
|
try {
|
|
547
|
+
// An observation after the commit, not an authority binding: it reads
|
|
548
|
+
// through the same status the Tool returns.
|
|
492
549
|
const projection = await this.status(taskId);
|
|
493
550
|
const tracker = await projectTerminalTrackerState({
|
|
494
551
|
root: this.cwd,
|
|
@@ -558,9 +615,16 @@ export class ClaudeRuntime {
|
|
|
558
615
|
return repairKernelAuthority(this.cwd, taskId, authority.revision);
|
|
559
616
|
}
|
|
560
617
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
|
|
561
|
-
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" = operation;
|
|
618
|
+
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" | "accept_residual_risk" = operation;
|
|
562
619
|
let decisionOp: { finding_id: string; resolution: string } | undefined;
|
|
563
|
-
|
|
620
|
+
// Set only at a replan boundary: the exits the literal user chooses between.
|
|
621
|
+
let reworkExits: ReworkExitsReadiness | undefined;
|
|
622
|
+
let reworkChoice: ReworkChoice | undefined;
|
|
623
|
+
const nextIntent = extra.next_intent ? parseProjectTaskIntent(this.cwd, extra.next_intent) : undefined;
|
|
624
|
+
// The whole operation is judged against the scope it asks for (D6).
|
|
625
|
+
const nextScope = nextIntent?.scope_hint;
|
|
626
|
+
const diffProvider = nextScope ? nextScopeDiffProvider(nextScope) : diffSnapshotOf;
|
|
627
|
+
const projection = await this.project(taskId, nextScope);
|
|
564
628
|
if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
|
|
565
629
|
// Kernel projection is the sole source of authorization readiness, and the
|
|
566
630
|
// derivation is the shared export both Hosts use, so it cannot drift:
|
|
@@ -579,12 +643,12 @@ export class ClaudeRuntime {
|
|
|
579
643
|
decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
|
|
580
644
|
} else {
|
|
581
645
|
op = "authorize_rework";
|
|
646
|
+
reworkExits = projection.projection.authorization.exits;
|
|
582
647
|
}
|
|
583
648
|
}
|
|
584
649
|
const priorIntent = await readTaskIntentForRecord(this.cwd, taskId);
|
|
585
650
|
const now = new Date().toISOString();
|
|
586
651
|
const actorId = LITERAL_USER_ACTOR_ID;
|
|
587
|
-
const nextIntent = extra.next_intent ? await parseTaskIntentV1(extra.next_intent) : undefined;
|
|
588
652
|
if (op === "approve_breaking_intent_revision" && !nextIntent) throw new Error("approve_breaking_intent_revision requires next_intent");
|
|
589
653
|
const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
|
|
590
654
|
const nextIntentRef = nextIntent
|
|
@@ -599,7 +663,7 @@ export class ClaudeRuntime {
|
|
|
599
663
|
restoreStagedIntentShared(this.cwd, stagedSnapshot);
|
|
600
664
|
};
|
|
601
665
|
let preparedDiffHash = projection.projection.diff_hash;
|
|
602
|
-
let gate: { confirmation_ref: string };
|
|
666
|
+
let gate: { confirmation_ref: string; reworkChoice?: ReworkChoice };
|
|
603
667
|
try {
|
|
604
668
|
if (sidecar && nextIntent) {
|
|
605
669
|
writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
|
|
@@ -608,9 +672,9 @@ export class ClaudeRuntime {
|
|
|
608
672
|
if (!preparedRecord.record) {
|
|
609
673
|
throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
|
|
610
674
|
}
|
|
611
|
-
preparedDiffHash =
|
|
675
|
+
preparedDiffHash = diffProvider(this.cwd, preparedRecord.record).diff_hash;
|
|
612
676
|
}
|
|
613
|
-
const preparedProjection = await this.
|
|
677
|
+
const preparedProjection = await this.project(taskId, nextScope);
|
|
614
678
|
try {
|
|
615
679
|
assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
|
|
616
680
|
if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
|
|
@@ -624,7 +688,9 @@ export class ClaudeRuntime {
|
|
|
624
688
|
intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
|
|
625
689
|
intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
|
|
626
690
|
bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`,
|
|
691
|
+
...(reworkExits ? { reworkExits } : {}),
|
|
627
692
|
});
|
|
693
|
+
reworkChoice = gate.reworkChoice;
|
|
628
694
|
} catch (error) {
|
|
629
695
|
if (stagedSnapshot) {
|
|
630
696
|
const current = await readTaskRecordRaw(this.cwd, taskId);
|
|
@@ -634,11 +700,24 @@ export class ClaudeRuntime {
|
|
|
634
700
|
}
|
|
635
701
|
throw error;
|
|
636
702
|
}
|
|
703
|
+
if (reworkExits) {
|
|
704
|
+
if (!reworkChoice) throw new NativeAuthorityError("interaction_not_opened", "native interaction returned no exit");
|
|
705
|
+
// A replan is not a mutation of this operation: the user chose to revise
|
|
706
|
+
// the Intent, which has its own gate. Nothing is written.
|
|
707
|
+
if (reworkChoice.exit === "replan") {
|
|
708
|
+
return {
|
|
709
|
+
state: "replan_selected",
|
|
710
|
+
task_id: taskId,
|
|
711
|
+
next: "revise the Intent, then call approve_breaking_intent_revision",
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (reworkChoice.exit === "accept_residual_risk") op = "accept_residual_risk";
|
|
715
|
+
}
|
|
637
716
|
const { registry, app } = this.authority();
|
|
638
717
|
const confirmation = gate.confirmation_ref;
|
|
639
718
|
let committed: ReturnType<typeof app.execute>;
|
|
640
719
|
try {
|
|
641
|
-
const capabilityProjection = await this.
|
|
720
|
+
const capabilityProjection = await this.project(taskId, nextScope);
|
|
642
721
|
try {
|
|
643
722
|
assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
|
|
644
723
|
} catch (error) {
|
|
@@ -664,6 +743,8 @@ export class ClaudeRuntime {
|
|
|
664
743
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
665
744
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
666
745
|
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
746
|
+
...(op === "authorize_rework" && reworkChoice?.exit === "grant_rounds" ? { rounds: reworkChoice.rounds } : {}),
|
|
747
|
+
...(op === "accept_residual_risk" && reworkChoice?.exit === "accept_residual_risk" ? { reason: reworkChoice.reason } : {}),
|
|
667
748
|
});
|
|
668
749
|
throwIfCancelled(meta.signal);
|
|
669
750
|
const result = app.execute({
|
|
@@ -676,15 +757,18 @@ export class ClaudeRuntime {
|
|
|
676
757
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
677
758
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
678
759
|
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
760
|
+
...(op === "authorize_rework" && reworkChoice?.exit === "grant_rounds" ? { rounds: reworkChoice.rounds } : {}),
|
|
761
|
+
...(op === "accept_residual_risk" && reworkChoice?.exit === "accept_residual_risk" ? { reason: reworkChoice.reason } : {}),
|
|
679
762
|
} as never,
|
|
680
763
|
prior_intent_token: priorIntent.token,
|
|
681
|
-
diffProvider
|
|
764
|
+
diffProvider,
|
|
682
765
|
now,
|
|
683
766
|
});
|
|
684
767
|
committed = result;
|
|
685
768
|
if (
|
|
686
769
|
op === "stop" ||
|
|
687
770
|
op === "authorize_rework" ||
|
|
771
|
+
op === "accept_residual_risk" ||
|
|
688
772
|
op === "approve_breaking_intent_revision"
|
|
689
773
|
) stagePlanningArtifactTransition(this.cwd, result.record);
|
|
690
774
|
} catch (error) {
|
|
@@ -706,9 +790,13 @@ export class ClaudeRuntime {
|
|
|
706
790
|
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string; attestation_id?: string } }) {
|
|
707
791
|
const { app } = await this.authority();
|
|
708
792
|
const operation = input.operation.op === "revise_intent"
|
|
709
|
-
? { ...input.operation, next_intent:
|
|
793
|
+
? { ...input.operation, next_intent: parseProjectTaskIntent(ctx.cwd, input.operation.next_intent) }
|
|
710
794
|
: input.operation;
|
|
711
795
|
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
796
|
+
const nextScope = operation.op === "revise_intent"
|
|
797
|
+
? (operation.next_intent as { scope_hint: string[] }).scope_hint
|
|
798
|
+
: undefined;
|
|
799
|
+
if (nextScope) assertScopeAdditionsAreFiles(ctx.cwd, priorIntent.intent.scope_hint, nextScope);
|
|
712
800
|
const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
|
|
713
801
|
const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
|
|
714
802
|
// A content-changing revision writes the sidecar before the kernel's drift
|
|
@@ -728,7 +816,7 @@ export class ClaudeRuntime {
|
|
|
728
816
|
task_id: input.taskId,
|
|
729
817
|
operation: operation as never,
|
|
730
818
|
prior_intent_token: priorIntent.token,
|
|
731
|
-
diffProvider: diffSnapshotOf,
|
|
819
|
+
diffProvider: nextScope ? nextScopeDiffProvider(nextScope) : diffSnapshotOf,
|
|
732
820
|
now: new Date().toISOString(),
|
|
733
821
|
});
|
|
734
822
|
if (operation.op === "freeze_artifacts" || operation.op === "stop") stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
@@ -746,10 +834,27 @@ export class ClaudeRuntime {
|
|
|
746
834
|
async startUnattendedBatch(
|
|
747
835
|
initiativeSlug: string,
|
|
748
836
|
meta: ToolMeta,
|
|
749
|
-
options: { reuseOnly?: boolean; max_parallel?: number; lane_offers?: LaneOffer[]; final_verification?: string[]; lane_instruction?: LaneInstructionRequest; lane_revision?: LaneRevisionRequest } = {},
|
|
837
|
+
options: { reuseOnly?: boolean; max_parallel?: number; lane_offers?: LaneOffer[]; final_verification?: string[]; delivery_consolidation?: DeliveryConsolidation; lane_instruction?: LaneInstructionRequest; lane_revision?: LaneRevisionRequest } = {},
|
|
838
|
+
): Promise<ClaudeBatchStartResult> {
|
|
839
|
+
let commit: string | undefined;
|
|
840
|
+
try {
|
|
841
|
+
const result = await this.startPreparedBatch(initiativeSlug, meta, options, (value) => { commit = value; });
|
|
842
|
+
if (commit && "reason" in result) result.reason += `; batch precondition commit ${commit} remains`;
|
|
843
|
+
return result;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (commit && error instanceof Error) error.message += `; batch precondition commit ${commit} remains`;
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private async startPreparedBatch(
|
|
851
|
+
initiativeSlug: string, meta: ToolMeta,
|
|
852
|
+
options: { reuseOnly?: boolean; max_parallel?: number; lane_offers?: LaneOffer[]; final_verification?: string[]; delivery_consolidation?: DeliveryConsolidation; lane_instruction?: LaneInstructionRequest; lane_revision?: LaneRevisionRequest },
|
|
853
|
+
onCommit: (commit: string | undefined) => void,
|
|
750
854
|
): Promise<ClaudeBatchStartResult> {
|
|
751
855
|
throwIfCancelled(meta.signal);
|
|
752
856
|
const reuseOnly = options.reuseOnly === true;
|
|
857
|
+
const consolidation = parseDeliveryConsolidation(options.delivery_consolidation);
|
|
753
858
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
754
859
|
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
755
860
|
// A reuse-only continuation never opens a gate, so it needs no confirmation port.
|
|
@@ -760,17 +865,58 @@ export class ClaudeRuntime {
|
|
|
760
865
|
if (!reuseOnly && !this.requestConfirmation)
|
|
761
866
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
762
867
|
|
|
868
|
+
await integrateSettledCriticalSlices(this.cwd, initiativeSlug, this.readInitiative);
|
|
869
|
+
const completed = await completedParentReport(this.cwd, initiativeSlug, this.readInitiative, this.batchTracker);
|
|
870
|
+
if (completed) return { state: "started", batch_id: completed.batch_id, report: completed };
|
|
871
|
+
const native = await advanceNativeParent(this.cwd, initiativeSlug, options.final_verification, this.readInitiative, this.batchTracker ?? createBatchTrackerPort(this.cwd, initiativeSlug),
|
|
872
|
+
async (effects) => {
|
|
873
|
+
// A continuation never opens a gate; unconfirmed commands wait for a fresh explicit entry.
|
|
874
|
+
if (reuseOnly) return batchReason("confirmation_port_unavailable");
|
|
875
|
+
try {
|
|
876
|
+
const confirmation = await this.requestConfirmation!({
|
|
877
|
+
operation: "start_unattended_batch",
|
|
878
|
+
initiativeSlug,
|
|
879
|
+
toolCallId: meta.toolCallId,
|
|
880
|
+
planDigest: effects.plan_digest,
|
|
881
|
+
batchDetails: {
|
|
882
|
+
initiative_slug: effects.initiative_slug,
|
|
883
|
+
batch_branch: "",
|
|
884
|
+
children: effects.children,
|
|
885
|
+
excluded: [],
|
|
886
|
+
final_verification: effects.final_commands,
|
|
887
|
+
parent_final_verification: { integrate_task_ids: effects.integrate_task_ids,
|
|
888
|
+
...(effects.delivery_consolidation ? { delivery_consolidation: effects.delivery_consolidation } : {}) },
|
|
889
|
+
},
|
|
890
|
+
signal: meta.signal,
|
|
891
|
+
});
|
|
892
|
+
if (meta.signal?.aborted) return batchReason("cancelled_before_execution");
|
|
893
|
+
if (confirmation.decision === "accept") return null;
|
|
894
|
+
if (confirmation.decision === "cancel") return batchReason("confirmation_cancelled");
|
|
895
|
+
if (confirmation.decision === "decline") return batchReason("confirmation_declined");
|
|
896
|
+
return batchReason("confirmation_no_decision");
|
|
897
|
+
} catch (err) {
|
|
898
|
+
if (meta.signal?.aborted) return batchReason("cancelled_before_execution");
|
|
899
|
+
if (err instanceof NativeAuthorityError) {
|
|
900
|
+
if (err.reasonCode === "unsupported_host") throw err;
|
|
901
|
+
return { state: err.reasonCode === "user_cancelled" ? "cancelled" : "rejected", reason: err.message, recovery_action: err.recoveryAction };
|
|
902
|
+
}
|
|
903
|
+
return batchReason("confirmation_failed", err instanceof Error ? err.message : String(err));
|
|
904
|
+
}
|
|
905
|
+
}, consolidation);
|
|
906
|
+
if (native) return native;
|
|
763
907
|
// 1. Host-independent batch preflight: claim ownership, branch
|
|
764
908
|
// availability, working-tree cleanliness against the authorized scope,
|
|
765
909
|
// recovery children, plan digest, and base HEAD are one shared projection,
|
|
766
910
|
// so neither Host re-implements a batch decision.
|
|
767
911
|
let now = new Date().toISOString();
|
|
768
|
-
const preflight = await
|
|
912
|
+
const preflight = await prepareBatchPreflight({
|
|
769
913
|
root: this.cwd,
|
|
770
914
|
initiative_slug: initiativeSlug,
|
|
771
915
|
now,
|
|
772
916
|
readInitiative: this.readInitiative ?? observeInitiative,
|
|
917
|
+
signal: meta.signal, reuseOnly,
|
|
773
918
|
});
|
|
919
|
+
onCommit(preflight.precondition_commit);
|
|
774
920
|
if (!preflight.ok) {
|
|
775
921
|
return {
|
|
776
922
|
state: preflight.state,
|
|
@@ -835,6 +981,7 @@ export class ClaudeRuntime {
|
|
|
835
981
|
reason: child.reason,
|
|
836
982
|
})),
|
|
837
983
|
budget: facts.budget,
|
|
984
|
+
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
838
985
|
...(options.max_parallel !== undefined
|
|
839
986
|
? {
|
|
840
987
|
lane_mode: {
|
|
@@ -932,6 +1079,7 @@ export class ClaudeRuntime {
|
|
|
932
1079
|
};
|
|
933
1080
|
const kernelPort = createBatchKernelPort({
|
|
934
1081
|
root: this.cwd,
|
|
1082
|
+
parent: await (this.readInitiative ?? observeInitiative)(this.cwd, initiativeSlug),
|
|
935
1083
|
enrollmentRegistry: this.enrollmentRegistry,
|
|
936
1084
|
registry: this.batchRegistry,
|
|
937
1085
|
capability,
|
|
@@ -940,7 +1088,7 @@ export class ClaudeRuntime {
|
|
|
940
1088
|
resume: { isResuming, existingBatch, batchBranch },
|
|
941
1089
|
overrides: this.batchKernel,
|
|
942
1090
|
});
|
|
943
|
-
|
|
1091
|
+
let report = await startBatch({
|
|
944
1092
|
root: this.cwd,
|
|
945
1093
|
batch_id: batchId,
|
|
946
1094
|
initiative_slug: initiativeSlug,
|
|
@@ -954,11 +1102,11 @@ export class ClaudeRuntime {
|
|
|
954
1102
|
now,
|
|
955
1103
|
kernel: kernelPort,
|
|
956
1104
|
git: this.batchGit,
|
|
1105
|
+
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
957
1106
|
...(options.max_parallel !== undefined
|
|
958
1107
|
? {
|
|
959
1108
|
max_parallel: options.max_parallel,
|
|
960
1109
|
tracker: this.batchTracker ?? createBatchTrackerPort(this.cwd, initiativeSlug),
|
|
961
|
-
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
962
1110
|
...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
|
|
963
1111
|
...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
|
|
964
1112
|
...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
|
|
@@ -970,6 +1118,8 @@ export class ClaudeRuntime {
|
|
|
970
1118
|
// review-3 & review-batch-preflight-recovery-is-diagnostic: map rejected batch state to rejected result with same-Host recovery action
|
|
971
1119
|
if (report.batch_state === "rejected") return batchReason("batch_run_rejected", report.reason ?? "");
|
|
972
1120
|
|
|
1121
|
+
if (report.batch_state === "completed" && report.final_verification?.passed)
|
|
1122
|
+
report = await completedParentReport(this.cwd, initiativeSlug, this.readInitiative, this.batchTracker) ?? report;
|
|
973
1123
|
return {
|
|
974
1124
|
state: "started",
|
|
975
1125
|
batch_id: batchId,
|
|
@@ -1013,6 +1163,7 @@ export class ClaudeRuntime {
|
|
|
1013
1163
|
state: child.state,
|
|
1014
1164
|
commit: child.commit,
|
|
1015
1165
|
lane_branch: child.lane_branch,
|
|
1166
|
+
kernel_lifecycle: child.kernel_lifecycle,
|
|
1016
1167
|
})),
|
|
1017
1168
|
},
|
|
1018
1169
|
signal: meta.signal,
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
// planning or execution; PreToolUse denies `enroll`, `start_unattended_batch`
|
|
9
9
|
// and the `imm-run` Skill without a live run entry, and otherwise leaves an
|
|
10
10
|
// admission the tool handler consumes by tool-use id. A model Skill call for
|
|
11
|
-
// `imm-planner` or `imm-brainstorm` is never denied
|
|
12
|
-
// revokes the run entry.
|
|
11
|
+
// `imm-planner` or `imm-brainstorm` is never denied and changes no entry state.
|
|
13
12
|
import { lstatSync, rmSync } from "node:fs";
|
|
14
13
|
import { homedir, tmpdir } from "node:os";
|
|
15
14
|
import { join } from "node:path";
|
|
16
15
|
import { laneWriteRefusal } from "../unattended/lane_workspace";
|
|
17
16
|
import { activeTaskNotice } from "../assurance/active_task_notice";
|
|
17
|
+
import { releasesRunBinding } from "../assurance/run_continuation";
|
|
18
18
|
import { readAliasFile } from "../assurance/alias_file";
|
|
19
19
|
import {
|
|
20
20
|
admitPhaseOperation,
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
type PhaseEntryState,
|
|
32
32
|
type PhaseOperation,
|
|
33
33
|
} from "../assurance/phase_entry";
|
|
34
|
-
import { readActiveClaimTaskId } from "../unattended/batch_preflight";
|
|
35
34
|
import { appendPrivate, cacheDir, ensurePrivateDir, ownedByUs, readPrivate, sessionHash } from "./review_host";
|
|
36
35
|
|
|
37
36
|
const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
@@ -95,7 +94,7 @@ function cwdOf(payload: Record<string, unknown>): string {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
/** Project aliases first, then the user's, as Claude Code resolves commands. */
|
|
98
|
-
function claudeAliasReader(cwd: string, home: string): AliasReader {
|
|
97
|
+
export function claudeAliasReader(cwd: string, home: string): AliasReader {
|
|
99
98
|
return (name) => {
|
|
100
99
|
for (const dir of [join(cwd, ".claude", "commands"), join(home, ".claude", "commands")]) {
|
|
101
100
|
const body = readAliasFile(join(dir, `${name}.md`));
|
|
@@ -150,7 +149,8 @@ export function recordPhaseEntryPrompt(payload: Record<string, unknown>, locatio
|
|
|
150
149
|
const sessionId = sessionIdOf(payload);
|
|
151
150
|
if (!sessionId) return false;
|
|
152
151
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
153
|
-
const
|
|
152
|
+
const recognized = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
|
|
153
|
+
const entry = releasesRunBinding(prompt, recognized === "plan" ? "planner" : null) ? "plan" : recognized;
|
|
154
154
|
return appendPhaseEntry(sessionId, entry, locations.root ?? tmpdir(), locations);
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -168,22 +168,15 @@ function gatedOperation(payload: Record<string, unknown>): PhaseOperation | null
|
|
|
168
168
|
/**
|
|
169
169
|
* PreToolUse for a gated tool: deny without a live run entry; for the two MCP
|
|
170
170
|
* tools, otherwise write the admission the handler consumes. A Planner or
|
|
171
|
-
* Brainstorm Skill call is never denied
|
|
172
|
-
*
|
|
173
|
-
* runs as configured.
|
|
171
|
+
* Brainstorm Skill call is never denied and leaves the entry store untouched.
|
|
172
|
+
* Any other tool answers null and runs as configured.
|
|
174
173
|
*/
|
|
175
174
|
export function phaseEntryGateHookOutput(payload: Record<string, unknown>, locations: PhaseEntryLocations = {}): string | null {
|
|
176
175
|
if (payload.hook_event_name !== "PreToolUse") return null;
|
|
177
176
|
const operation = gatedOperation(payload);
|
|
178
177
|
if (!operation) return null;
|
|
178
|
+
if (operation.kind === "skill" && operation.skill !== "imm-run") return null;
|
|
179
179
|
const root = locations.root ?? tmpdir();
|
|
180
|
-
if (operation.kind === "skill" && operation.skill !== "imm-run") {
|
|
181
|
-
let activeClaim = false;
|
|
182
|
-
try { activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null; } catch { /* unreadable authority is no claim */ }
|
|
183
|
-
const sessionId = sessionIdOf(payload);
|
|
184
|
-
if (!activeClaim && sessionId) appendPhaseEntry(sessionId, "plan", root, locations);
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
180
|
let state: PhaseEntryState | null = null;
|
|
188
181
|
try { state = readPhaseEntryState(sessionIdOf(payload), root, locations.read); } catch { /* an unreadable store refuses */ }
|
|
189
182
|
const admission = admitPhaseOperation(operation, state);
|