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
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "./storage";
|
|
42
42
|
import { KernelInvariantError } from "./validation";
|
|
43
43
|
import type {
|
|
44
|
+
ReviewRevisionIdentityV1,
|
|
44
45
|
StoredTaskMutationV3,
|
|
45
46
|
TaskAction,
|
|
46
47
|
TaskApprovalV2,
|
|
@@ -54,13 +55,21 @@ export type CanaryOperation =
|
|
|
54
55
|
| { op: "record_finding"; finding: Omit<TaskFinding, "status" | "source" | "review_round">; actor_id: string }
|
|
55
56
|
| { op: "resolve_finding"; finding_id: string; actor_id: string }
|
|
56
57
|
| { op: "refute_finding"; finding_id: string; attestation_id: string; actor_id: string }
|
|
57
|
-
| {
|
|
58
|
+
| {
|
|
59
|
+
op: "request_rework";
|
|
60
|
+
capability: object;
|
|
61
|
+
findings: TaskFinding[];
|
|
62
|
+
actor_id: string;
|
|
63
|
+
/** Review-authored rework: the immutable revision Review analyzed. */
|
|
64
|
+
review_revision?: ReviewRevisionIdentityV1;
|
|
65
|
+
}
|
|
58
66
|
| { op: "record_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
|
|
59
67
|
| { op: "revise_intent"; next_intent: TaskIntentV1; actor_id: string }
|
|
60
68
|
| { op: "approve_breaking_intent_revision"; capability: object; next_intent: TaskIntentV1; actor_id: string }
|
|
61
69
|
| { op: "complete"; actor_id: string }
|
|
62
70
|
| { op: "stop"; capability: object; reason: string; actor_id: string }
|
|
63
|
-
| { op: "authorize_rework"; capability: object; actor_id: string }
|
|
71
|
+
| { op: "authorize_rework"; capability: object; actor_id: string; rounds?: number }
|
|
72
|
+
| { op: "accept_residual_risk"; capability: object; reason: string; actor_id: string }
|
|
64
73
|
| { op: "resolve_user_decision"; capability: object; finding_id: string; resolution: string; actor_id: string };
|
|
65
74
|
|
|
66
75
|
export interface CanaryExecuteInput {
|
|
@@ -116,6 +125,17 @@ export function beginDrainCapabilityAction(taskId: string, at: string): TaskActi
|
|
|
116
125
|
* capability minted against this action's digest always validates.
|
|
117
126
|
* "begin-drain" delegates to the canonical begin_drain action.
|
|
118
127
|
*/
|
|
128
|
+
/** Fixed key order so the minted digest equals the parsed action's digest. */
|
|
129
|
+
function canonicalReviewRevision(revision: ReviewRevisionIdentityV1): ReviewRevisionIdentityV1 {
|
|
130
|
+
return {
|
|
131
|
+
contract: revision.contract,
|
|
132
|
+
base_head: revision.base_head,
|
|
133
|
+
review_commit: revision.review_commit,
|
|
134
|
+
review_tree: revision.review_tree,
|
|
135
|
+
manifest_digest: revision.manifest_digest,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
export function capabilityActionFor(input: {
|
|
120
140
|
op: string;
|
|
121
141
|
task_id: string;
|
|
@@ -128,6 +148,9 @@ export function capabilityActionFor(input: {
|
|
|
128
148
|
next_intent_ref?: unknown;
|
|
129
149
|
finding_id?: string;
|
|
130
150
|
resolution?: unknown;
|
|
151
|
+
review_revision?: ReviewRevisionIdentityV1;
|
|
152
|
+
/** authorize_rework only: further Review rounds granted, 1-5. */
|
|
153
|
+
rounds?: number;
|
|
131
154
|
}): TaskAction {
|
|
132
155
|
if (input.op === "begin-drain") {
|
|
133
156
|
return beginDrainCapabilityAction(input.task_id, input.at);
|
|
@@ -148,9 +171,21 @@ export function capabilityActionFor(input: {
|
|
|
148
171
|
case "record_approval":
|
|
149
172
|
return { ...base, approval: input.approval } as TaskAction;
|
|
150
173
|
case "request_rework":
|
|
151
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
...base,
|
|
176
|
+
findings: input.findings,
|
|
177
|
+
...(input.review_revision ? { review_revision: canonicalReviewRevision(input.review_revision) } : {}),
|
|
178
|
+
} as TaskAction;
|
|
152
179
|
case "authorize_rework":
|
|
153
|
-
|
|
180
|
+
// `rounds` joins the digest only when present, so a legacy caller
|
|
181
|
+
// keeps the exact digest it always minted.
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
type: "authorize_rework",
|
|
185
|
+
...(input.rounds !== undefined ? { rounds: input.rounds } : {}),
|
|
186
|
+
} as TaskAction;
|
|
187
|
+
case "accept_residual_risk":
|
|
188
|
+
return { ...base, type: "accept_residual_risk", reason: input.reason } as TaskAction;
|
|
154
189
|
case "stop":
|
|
155
190
|
return { ...base, reason: input.reason } as TaskAction;
|
|
156
191
|
case "approve_breaking_intent_revision":
|
|
@@ -216,7 +251,7 @@ export function createCanaryApplication(
|
|
|
216
251
|
throw new KernelInvariantError([`artifact freeze requires active lifecycle, got ${current.record.lifecycle}`]);
|
|
217
252
|
if (workspace.state.current_working !== input.task_id)
|
|
218
253
|
throw new KernelInvariantError([`workspace is not owned by task ${input.task_id}`]);
|
|
219
|
-
const fresh = readTaskIntent(input.root, input.task_id, current.record.intent_ref.path);
|
|
254
|
+
const fresh = readTaskIntent(input.root, input.task_id, current.record.intent_ref.path, current.record.intent_snapshot);
|
|
220
255
|
const pair = inspectIntentTokenPair(input.prior_intent_token, fresh.token);
|
|
221
256
|
if (
|
|
222
257
|
pair.prior.intent_content_hash !== current.record.intent_ref.content_hash
|
|
@@ -376,7 +411,14 @@ export function createCanaryApplication(
|
|
|
376
411
|
break;
|
|
377
412
|
case "request_rework":
|
|
378
413
|
capability = operation.capability;
|
|
379
|
-
action = {
|
|
414
|
+
action = {
|
|
415
|
+
...base,
|
|
416
|
+
type: "request_rework",
|
|
417
|
+
findings: operation.findings,
|
|
418
|
+
...(operation.review_revision
|
|
419
|
+
? { review_revision: canonicalReviewRevision(operation.review_revision) }
|
|
420
|
+
: {}),
|
|
421
|
+
};
|
|
380
422
|
break;
|
|
381
423
|
case "record_approval":
|
|
382
424
|
capability = operation.capability;
|
|
@@ -414,7 +456,15 @@ export function createCanaryApplication(
|
|
|
414
456
|
break;
|
|
415
457
|
case "authorize_rework":
|
|
416
458
|
capability = operation.capability;
|
|
417
|
-
action = {
|
|
459
|
+
action = {
|
|
460
|
+
...base,
|
|
461
|
+
type: "authorize_rework",
|
|
462
|
+
...(operation.rounds !== undefined ? { rounds: operation.rounds } : {}),
|
|
463
|
+
};
|
|
464
|
+
break;
|
|
465
|
+
case "accept_residual_risk":
|
|
466
|
+
capability = operation.capability;
|
|
467
|
+
action = { ...base, type: "accept_residual_risk", reason: operation.reason };
|
|
418
468
|
break;
|
|
419
469
|
case "resolve_user_decision":
|
|
420
470
|
capability = operation.capability;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyTaskRisk } from "./intent";
|
|
2
|
+
import { NO_RISK_FLOOR, raiseRisk, scopeRiskFloor, type RiskFloor } from "./risk_floor";
|
|
2
3
|
import { archivePath, boundSpecPath, inspectSpecBinding } from "./spec_binding";
|
|
3
4
|
import type {
|
|
4
5
|
ApprovalKind,
|
|
@@ -48,13 +49,16 @@ export function asTaskDiffSnapshot(value: TaskDiffInput): {
|
|
|
48
49
|
export function resolveProjectedRisk(
|
|
49
50
|
intent: TaskIntentV1,
|
|
50
51
|
changedPaths: readonly string[] = [],
|
|
52
|
+
floor: RiskFloor = NO_RISK_FLOOR,
|
|
51
53
|
): TaskIntentV1["risk"] {
|
|
54
|
+
const risk = raiseRisk(intent.risk, scopeRiskFloor(intent.scope_hint, floor));
|
|
52
55
|
// Invalid bindings have no trusted planning-path exclusions.
|
|
53
|
-
if (!inspectSpecBinding(intent).ok) return classifyTaskRisk(changedPaths,
|
|
56
|
+
if (!inspectSpecBinding(intent).ok) return classifyTaskRisk(changedPaths, risk, floor);
|
|
54
57
|
const excluded = ownSidecarPaths(intent);
|
|
55
58
|
return classifyTaskRisk(
|
|
56
59
|
changedPaths.filter((path) => !excluded.has(path)),
|
|
57
|
-
|
|
60
|
+
risk,
|
|
61
|
+
floor,
|
|
58
62
|
);
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -80,6 +84,7 @@ export function completionDecision(
|
|
|
80
84
|
currentDiffHash: string,
|
|
81
85
|
currentIntentContentHash: string,
|
|
82
86
|
changedPaths: readonly string[] = [],
|
|
87
|
+
floor: RiskFloor = NO_RISK_FLOOR,
|
|
83
88
|
): CompletionDecision {
|
|
84
89
|
assertKernelInvariantsV3(intent, record);
|
|
85
90
|
const freshAttestations = record.attestations.filter(
|
|
@@ -108,7 +113,22 @@ export function completionDecision(
|
|
|
108
113
|
)
|
|
109
114
|
.map((item) => item.id);
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
// A literal-user residual-risk acceptance stands in for the Review
|
|
117
|
+
// approval only while it names this exact intent revision, intent hash and
|
|
118
|
+
// diff, and every finding it accepted is still accepted. QA, acceptance and
|
|
119
|
+
// every other obligation are unaffected, and any later change makes it stale.
|
|
120
|
+
const acceptance = record.residual_risk_acceptance;
|
|
121
|
+
const residualRiskAccepted =
|
|
122
|
+
acceptance !== undefined &&
|
|
123
|
+
acceptance.intent_revision === intent.revision &&
|
|
124
|
+
acceptance.intent_content_hash === currentIntentContentHash &&
|
|
125
|
+
acceptance.diff_hash === currentDiffHash &&
|
|
126
|
+
acceptance.finding_ids.every((id) =>
|
|
127
|
+
record.findings.some((item) => item.id === id && item.status === "accepted_risk"),
|
|
128
|
+
);
|
|
129
|
+
const requiredKinds = REQUIRED_ATTESTATIONS[
|
|
130
|
+
resolveProjectedRisk(intent, changedPaths, floor)
|
|
131
|
+
].filter((kind) => !(residualRiskAccepted && kind === "review"));
|
|
112
132
|
const candidates = freshAttestations.filter((item) => requiredKinds.includes(item.kind));
|
|
113
133
|
const missingApprovalKinds = requiredKinds.filter(
|
|
114
134
|
(kind) => !candidates.some((attestation) => attestation.kind === kind),
|
|
@@ -160,6 +180,8 @@ export function completionDecision(
|
|
|
160
180
|
|
|
161
181
|
return {
|
|
162
182
|
complete:
|
|
183
|
+
// An invalid Spec binding is never complete, whatever the project's floor says.
|
|
184
|
+
inspectSpecBinding(intent).ok &&
|
|
163
185
|
missingAcceptanceIds.length === 0 &&
|
|
164
186
|
missingApprovalKinds.length === 0 &&
|
|
165
187
|
blockingFindingIds.length === 0 &&
|
|
@@ -183,6 +205,7 @@ export function projectTask(
|
|
|
183
205
|
currentDiffHash: string,
|
|
184
206
|
currentIntentContentHash: string,
|
|
185
207
|
changedPaths: readonly string[] = [],
|
|
208
|
+
floor: RiskFloor = NO_RISK_FLOOR,
|
|
186
209
|
): TaskProjectionV3 {
|
|
187
210
|
if (record.lifecycle === "active" && !inspectSpecBinding(intent).ok) {
|
|
188
211
|
assertKernelInvariantsV3(intent, record);
|
|
@@ -198,7 +221,7 @@ export function projectTask(
|
|
|
198
221
|
fresh_acceptance_ids: [],
|
|
199
222
|
missing_acceptance_ids: intent.acceptance.map((item) => item.id),
|
|
200
223
|
stale_attestation_ids: record.attestations.map((item) => item.id),
|
|
201
|
-
missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)],
|
|
224
|
+
missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths, floor)],
|
|
202
225
|
blocking_finding_ids: record.findings.filter((item) => item.kind === "blocking" && item.status !== "resolved").map((item) => item.id),
|
|
203
226
|
unresolved_user_decision_ids: record.findings.filter((item) => item.kind === "unresolved_user_decision" && item.status === "open").map((item) => item.id),
|
|
204
227
|
replan_required_ids: record.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id),
|
|
@@ -211,6 +234,7 @@ export function projectTask(
|
|
|
211
234
|
currentDiffHash,
|
|
212
235
|
currentIntentContentHash,
|
|
213
236
|
changedPaths,
|
|
237
|
+
floor,
|
|
214
238
|
);
|
|
215
239
|
const blocked =
|
|
216
240
|
decision.blocking_finding_ids.length > 0 ||
|
|
@@ -54,9 +54,35 @@ export interface EnrollCanaryInput {
|
|
|
54
54
|
capability: object;
|
|
55
55
|
capability_binding: EnrollmentCapabilityBinding;
|
|
56
56
|
batch?: EnrollBatchContext;
|
|
57
|
+
/**
|
|
58
|
+
* The in-scope paths the confirmed Enrollment gate listed as already staged,
|
|
59
|
+
* modified or untracked. Only these are claimed as task work; absent means
|
|
60
|
+
* the gate listed none, so any in-scope dirt refuses the Enrollment.
|
|
61
|
+
*/
|
|
62
|
+
disclosed_scope_paths?: readonly string[];
|
|
57
63
|
now: string;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* What an Enrollment gate must show about a scope that is already dirty: the
|
|
68
|
+
* paths, and the one line both Hosts print for them. A scope that cannot be
|
|
69
|
+
* inspected discloses nothing, and the Enrollment itself reports why.
|
|
70
|
+
*/
|
|
71
|
+
export function enrollmentScopeDisclosure(root: string, taskId: string): { paths: string[]; notice: string | null } {
|
|
72
|
+
let paths: string[] = [];
|
|
73
|
+
try {
|
|
74
|
+
paths = dirtyScopePaths(root, readTaskIntent(root, taskId).intent.scope_hint, taskId);
|
|
75
|
+
} catch {
|
|
76
|
+
return { paths: [], notice: null };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
paths,
|
|
80
|
+
notice: paths.length === 0
|
|
81
|
+
? null
|
|
82
|
+
: `In-scope changes that already exist (${paths.length}) and become task work on confirmation: ${paths.join(", ")}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
60
86
|
/**
|
|
61
87
|
* A rehearsal that is not ready rejects through this single typed error so the
|
|
62
88
|
* "Kernel enrollment rehearsal failed" message is produced in exactly one place
|
|
@@ -80,6 +106,8 @@ export interface EnrollmentEntryRequest {
|
|
|
80
106
|
binding: EnrollmentCapabilityBinding;
|
|
81
107
|
/** Present only for a batch-derived child enrollment. */
|
|
82
108
|
batch?: EnrollBatchContext;
|
|
109
|
+
/** The in-scope paths the confirmed gate listed; see `EnrollCanaryInput`. */
|
|
110
|
+
disclosed_scope_paths?: readonly string[];
|
|
83
111
|
now: string;
|
|
84
112
|
/**
|
|
85
113
|
* Optional caller checkpoint run after a ready rehearsal and before the
|
|
@@ -111,6 +139,8 @@ type EnrollmentPreconditionState = {
|
|
|
111
139
|
workspace: ReturnType<typeof readWorkspaceStateRaw> | null;
|
|
112
140
|
intent: Awaited<ReturnType<typeof readTaskIntent>> | null;
|
|
113
141
|
gitBaseHead: string | null;
|
|
142
|
+
/** In-scope dirt the confirmed gate listed; left out of the Enrollment baseline. */
|
|
143
|
+
claimedScopePaths: string[];
|
|
114
144
|
};
|
|
115
145
|
|
|
116
146
|
type EnrollmentPreconditionResult = EnrollmentPreconditionState & {
|
|
@@ -160,6 +190,7 @@ function runEnrollmentPreconditionChecks<T>(
|
|
|
160
190
|
let workspace: EnrollmentPreconditionState["workspace"] = null;
|
|
161
191
|
let intent: EnrollmentPreconditionState["intent"] = null;
|
|
162
192
|
let gitBaseHead: string | null = null;
|
|
193
|
+
let claimedScopePaths: string[] = [];
|
|
163
194
|
const fail = (report: string, error: unknown) => {
|
|
164
195
|
if (mode === "fail_fast")
|
|
165
196
|
throw error instanceof Error ? error : new Error(String(error));
|
|
@@ -210,16 +241,20 @@ function runEnrollmentPreconditionChecks<T>(
|
|
|
210
241
|
} catch (error) {
|
|
211
242
|
fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
212
243
|
}
|
|
213
|
-
// In-scope dirt
|
|
214
|
-
//
|
|
215
|
-
//
|
|
244
|
+
// In-scope dirt the confirmed gate listed is claimed: it is left out of the
|
|
245
|
+
// Enrollment baseline and becomes task work. Any other in-scope dirt would
|
|
246
|
+
// be recorded there as user dirt and withheld from every later task
|
|
247
|
+
// snapshot, so the task could never deliver it. Refuse that before the
|
|
248
|
+
// capability is consumed; nothing is claimed that the gate did not show.
|
|
216
249
|
if (intent && gitBaseHead) {
|
|
217
250
|
try {
|
|
251
|
+
const disclosed = new Set(input.disclosed_scope_paths ?? []);
|
|
218
252
|
const dirty = dirtyScopePaths(root, intent.intent.scope_hint, input.task_id);
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
const unlisted = dirty.filter((path) => !disclosed.has(path));
|
|
254
|
+
if (unlisted.length > 0) {
|
|
255
|
+
const message = `task scope is already dirty before Enrollment: ${unlisted.join(", ")}; the confirmed Enrollment gate did not list these paths, so enroll again from the Host to review them`;
|
|
221
256
|
fail(message, new Error(message));
|
|
222
|
-
}
|
|
257
|
+
} else claimedScopePaths = dirty;
|
|
223
258
|
} catch (error) {
|
|
224
259
|
fail(`scope: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
225
260
|
}
|
|
@@ -231,6 +266,7 @@ function runEnrollmentPreconditionChecks<T>(
|
|
|
231
266
|
workspace,
|
|
232
267
|
intent,
|
|
233
268
|
gitBaseHead,
|
|
269
|
+
claimedScopePaths,
|
|
234
270
|
};
|
|
235
271
|
return onReady ? onReady(state) : { ...state, blockers };
|
|
236
272
|
});
|
|
@@ -276,6 +312,8 @@ export function runEnrollmentRehearsal(
|
|
|
276
312
|
task_id: string;
|
|
277
313
|
outcome: "ready" | "not_ready";
|
|
278
314
|
blockers: string[];
|
|
315
|
+
/** In-scope dirt the gate listed, which this Enrollment would claim as task work. */
|
|
316
|
+
claimed_scope_paths: string[];
|
|
279
317
|
generated_at: string;
|
|
280
318
|
};
|
|
281
319
|
} {
|
|
@@ -295,6 +333,7 @@ export function runEnrollmentRehearsal(
|
|
|
295
333
|
task_id: input.task_id,
|
|
296
334
|
outcome: preconditions.blockers.length === 0 ? "ready" : "not_ready",
|
|
297
335
|
blockers: preconditions.blockers,
|
|
336
|
+
claimed_scope_paths: preconditions.claimedScopePaths,
|
|
298
337
|
generated_at: input.now,
|
|
299
338
|
},
|
|
300
339
|
};
|
|
@@ -357,7 +396,7 @@ export async function enrollTask(
|
|
|
357
396
|
registry: EnrollmentAuthorityRegistry,
|
|
358
397
|
request: EnrollmentEntryRequest,
|
|
359
398
|
): Promise<EnrollCanaryResult | EnrollmentEntryOutcome> {
|
|
360
|
-
const { binding, batch, now, checkpoint } = request;
|
|
399
|
+
const { binding, batch, now, checkpoint, disclosed_scope_paths } = request;
|
|
361
400
|
// (1) Issue the capability for the supplied binding.
|
|
362
401
|
const capability = registry.issue(binding);
|
|
363
402
|
const input: EnrollCanaryInput = {
|
|
@@ -368,6 +407,7 @@ export async function enrollTask(
|
|
|
368
407
|
capability,
|
|
369
408
|
capability_binding: binding,
|
|
370
409
|
...(batch ? { batch } : {}),
|
|
410
|
+
...(disclosed_scope_paths ? { disclosed_scope_paths } : {}),
|
|
371
411
|
now,
|
|
372
412
|
};
|
|
373
413
|
// Replay of a lost Enrollment: the exact request already committed here, so
|
|
@@ -554,7 +594,7 @@ export function enrollCanaryTask(
|
|
|
554
594
|
claim as unknown as Record<string, unknown>,
|
|
555
595
|
digest,
|
|
556
596
|
);
|
|
557
|
-
writeEnrollmentBaseline(root);
|
|
597
|
+
writeEnrollmentBaseline(root, checks.claimedScopePaths);
|
|
558
598
|
return {
|
|
559
599
|
record: mutation.record,
|
|
560
600
|
backend_claim: claim,
|