immune-brain 5.0.0 → 5.1.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 +223 -12
- package/README.zh-CN.md +228 -11
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +18 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +189 -3
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1885 -149
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-planner.md +4 -1
- package/plugins/immune-brain/dist/imm-run.md +198 -3
- package/plugins/immune-brain/dist/role-prompts/code-review.md +10 -0
- package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +22 -4
- package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +105 -58
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +89 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
- package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
- package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +10 -0
- package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +946 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +48 -33
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +13 -1
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +22 -1
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +398 -12
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +4 -0
|
@@ -57,6 +57,9 @@ import {
|
|
|
57
57
|
type BatchRunReport,
|
|
58
58
|
} from "../unattended/batch_runner";
|
|
59
59
|
import { createBatchKernelPort } from "../unattended/batch_kernel_port";
|
|
60
|
+
import type { LaneOffer } from "../unattended/batch_lanes";
|
|
61
|
+
import { retireStaleBatch } from "../unattended/batch_disposition";
|
|
62
|
+
import type { BatchLaneRunReport } from "../unattended/batch_state";
|
|
60
63
|
import {
|
|
61
64
|
createBatchAuthorityRegistry,
|
|
62
65
|
} from "../kernel/batch_authority";
|
|
@@ -66,7 +69,7 @@ import {
|
|
|
66
69
|
import type {
|
|
67
70
|
InitiativeObservationReader,
|
|
68
71
|
} from "../unattended/types";
|
|
69
|
-
import {
|
|
72
|
+
import { observeInitiative } from "../unattended/batch_plan";
|
|
70
73
|
import {
|
|
71
74
|
confirmationRef,
|
|
72
75
|
enrollmentNonce,
|
|
@@ -106,42 +109,11 @@ function readTaskIntentForRecord(root: string, taskId: string) {
|
|
|
106
109
|
return readTaskIntent(root, taskId, currentPath);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
if (typeof input === "object" && input !== null && !Array.isArray(input)) return input as Record<string, unknown>;
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function verdictFingerprint(raw: Record<string, unknown>): string {
|
|
120
|
-
return JSON.stringify({
|
|
121
|
-
contract: raw.contract ?? null,
|
|
122
|
-
role: raw.role ?? null,
|
|
123
|
-
task_id: raw.task_id ?? null,
|
|
124
|
-
snapshot_digest: raw.snapshot_digest ?? null,
|
|
125
|
-
decision: raw.decision ?? null,
|
|
126
|
-
approval: raw.approval ?? null,
|
|
127
|
-
findings: raw.findings ?? null,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const RELEASED_REVIEW_RECOVERY =
|
|
132
|
-
"Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
|
|
133
|
-
const RETAINED_REVIEW_RECOVERY =
|
|
134
|
-
"Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
|
|
135
|
-
const MISMATCH_REVIEW_RECOVERY =
|
|
136
|
-
"Resubmit the reviewer's verdict exactly as the reviewer returned it";
|
|
137
|
-
|
|
138
|
-
function withReviewRecovery(
|
|
139
|
-
result: AssuranceSubmitReviewResult,
|
|
140
|
-
recovery_action: string,
|
|
141
|
-
): AssuranceSubmitReviewResult {
|
|
142
|
-
if (result.state !== "blocked" || result.code === "verdict_invalid") return result;
|
|
143
|
-
return { ...result, recovery_action };
|
|
144
|
-
}
|
|
112
|
+
import {
|
|
113
|
+
extractVerdictJson,
|
|
114
|
+
submitMediatedReview,
|
|
115
|
+
} from "../assurance/review_mediation";
|
|
116
|
+
export { extractVerdictJson };
|
|
145
117
|
|
|
146
118
|
export async function submitClaudeReview(
|
|
147
119
|
host: ClaudeReviewHost,
|
|
@@ -150,24 +122,8 @@ export async function submitClaudeReview(
|
|
|
150
122
|
taskId: string,
|
|
151
123
|
verdictInput: unknown,
|
|
152
124
|
): Promise<AssuranceSubmitReviewResult> {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!observed.ok) {
|
|
156
|
-
if (observed.release) return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
|
|
157
|
-
return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
|
|
158
|
-
}
|
|
159
|
-
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
160
|
-
if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
161
|
-
const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
|
|
162
|
-
if (!receiptValid) {
|
|
163
|
-
return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
|
|
164
|
-
}
|
|
165
|
-
const parentJson = extractVerdictJson(verdictInput)!;
|
|
166
|
-
const receiptJson = extractVerdictJson(observed.receipt.result)!;
|
|
167
|
-
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
168
|
-
return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
|
|
169
|
-
}
|
|
170
|
-
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
125
|
+
// ADR 0017: an omitted verdict applies the hook-observed reviewer receipt.
|
|
126
|
+
return submitMediatedReview(coordinator, ctx, taskId, verdictInput, () => host.inspectReviewForTask(taskId));
|
|
171
127
|
}
|
|
172
128
|
|
|
173
129
|
/**
|
|
@@ -284,6 +240,7 @@ export interface ClaudeRuntimeOptions {
|
|
|
284
240
|
*/
|
|
285
241
|
type ClaudeBatchStartResult =
|
|
286
242
|
| { state: "started"; batch_id: string; report: BatchRunReport }
|
|
243
|
+
| { state: "retired"; batch_id: string; report: BatchRunReport | BatchLaneRunReport }
|
|
287
244
|
| { state: "rejected"; reason: string; recovery_action: string }
|
|
288
245
|
| { state: "cancelled"; reason: string; recovery_action: string }
|
|
289
246
|
| { state: "blocked"; reason: string; recovery_action: string };
|
|
@@ -784,7 +741,7 @@ export class ClaudeRuntime {
|
|
|
784
741
|
async startUnattendedBatch(
|
|
785
742
|
initiativeSlug: string,
|
|
786
743
|
meta: ToolMeta,
|
|
787
|
-
options: { reuseOnly?: boolean } = {},
|
|
744
|
+
options: { reuseOnly?: boolean; max_parallel?: number; lane_offers?: LaneOffer[] } = {},
|
|
788
745
|
): Promise<ClaudeBatchStartResult> {
|
|
789
746
|
throwIfCancelled(meta.signal);
|
|
790
747
|
const reuseOnly = options.reuseOnly === true;
|
|
@@ -807,7 +764,7 @@ export class ClaudeRuntime {
|
|
|
807
764
|
root: this.cwd,
|
|
808
765
|
initiative_slug: initiativeSlug,
|
|
809
766
|
now,
|
|
810
|
-
readInitiative: this.readInitiative ??
|
|
767
|
+
readInitiative: this.readInitiative ?? observeInitiative,
|
|
811
768
|
});
|
|
812
769
|
if (!preflight.ok) {
|
|
813
770
|
return {
|
|
@@ -830,7 +787,7 @@ export class ClaudeRuntime {
|
|
|
830
787
|
initiative_slug: initiativeSlug,
|
|
831
788
|
now,
|
|
832
789
|
projection: preflight.projection,
|
|
833
|
-
readInitiative: this.readInitiative ??
|
|
790
|
+
readInitiative: this.readInitiative ?? observeInitiative,
|
|
834
791
|
nonce: enrollmentNonce(),
|
|
835
792
|
gate: async (facts) => {
|
|
836
793
|
// A continuation after a foreground child never opens a gate: when the
|
|
@@ -867,6 +824,15 @@ export class ClaudeRuntime {
|
|
|
867
824
|
reason: child.reason,
|
|
868
825
|
})),
|
|
869
826
|
budget: facts.budget,
|
|
827
|
+
...(options.max_parallel !== undefined
|
|
828
|
+
? {
|
|
829
|
+
lane_mode: {
|
|
830
|
+
max_parallel: options.max_parallel,
|
|
831
|
+
parallel_groups: facts.parallel_groups,
|
|
832
|
+
serialized: facts.scope_conflicts,
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
: {}),
|
|
870
836
|
...(facts.reuse_blockers.length > 0
|
|
871
837
|
? {
|
|
872
838
|
re_confirmation_required: facts.reuse_blockers,
|
|
@@ -971,6 +937,8 @@ export class ClaudeRuntime {
|
|
|
971
937
|
now,
|
|
972
938
|
kernel: kernelPort,
|
|
973
939
|
git: this.batchGit,
|
|
940
|
+
...(options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {}),
|
|
941
|
+
...(options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}),
|
|
974
942
|
});
|
|
975
943
|
|
|
976
944
|
// review-3 & review-batch-preflight-recovery-is-diagnostic: map rejected batch state to rejected result with same-Host recovery action
|
|
@@ -982,6 +950,85 @@ export class ClaudeRuntime {
|
|
|
982
950
|
report,
|
|
983
951
|
};
|
|
984
952
|
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The batch disposition. One explicit literal-user decision retires a record
|
|
956
|
+
* the plan moved past: the terminal superseded state and its report preserve
|
|
957
|
+
* every child state, recorded commit and the batch branch, and no child is
|
|
958
|
+
* re-enrolled, handed off, or credited with delivery it never had.
|
|
959
|
+
*/
|
|
960
|
+
async retireStaleBatch(initiativeSlug: string, meta: ToolMeta): Promise<ClaudeBatchStartResult> {
|
|
961
|
+
throwIfCancelled(meta.signal);
|
|
962
|
+
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
963
|
+
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
964
|
+
// The disposition always opens a gate, so it needs a confirmation port.
|
|
965
|
+
if (!this.requestConfirmation)
|
|
966
|
+
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
967
|
+
const outcome = await retireStaleBatch<ClaudeBatchStartResult>({
|
|
968
|
+
root: this.cwd,
|
|
969
|
+
initiative_slug: initiativeSlug,
|
|
970
|
+
now: new Date().toISOString(),
|
|
971
|
+
gate: async (facts) => {
|
|
972
|
+
let confirmationResult: { decision: NativeDecision; requestId: string };
|
|
973
|
+
try {
|
|
974
|
+
confirmationResult = await this.requestConfirmation!({
|
|
975
|
+
operation: "retire_stale_batch",
|
|
976
|
+
initiativeSlug,
|
|
977
|
+
toolCallId: meta.toolCallId,
|
|
978
|
+
batchDisposition: {
|
|
979
|
+
batch_id: facts.batch_id,
|
|
980
|
+
batch_state: facts.batch_state,
|
|
981
|
+
batch_branch: facts.branch,
|
|
982
|
+
plan_digest: facts.plan_digest,
|
|
983
|
+
recorded_commits: facts.recorded_commits.length,
|
|
984
|
+
children: facts.children.map((child) => ({
|
|
985
|
+
task_id: child.task_id,
|
|
986
|
+
slice_id: child.slice_id,
|
|
987
|
+
state: child.state,
|
|
988
|
+
commit: child.commit,
|
|
989
|
+
lane_branch: child.lane_branch,
|
|
990
|
+
})),
|
|
991
|
+
},
|
|
992
|
+
signal: meta.signal,
|
|
993
|
+
});
|
|
994
|
+
} catch (err) {
|
|
995
|
+
if (meta.signal?.aborted)
|
|
996
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
997
|
+
if (err instanceof NativeAuthorityError) {
|
|
998
|
+
if (err.reasonCode === "unsupported_host") throw err;
|
|
999
|
+
if (err.reasonCode === "user_cancelled") {
|
|
1000
|
+
return {
|
|
1001
|
+
kind: "host_rejection",
|
|
1002
|
+
value: { state: "cancelled", reason: err.message, recovery_action: err.recoveryAction },
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
kind: "host_rejection",
|
|
1007
|
+
value: { state: "rejected", reason: err.message, recovery_action: err.recoveryAction },
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
kind: "host_rejection",
|
|
1012
|
+
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err)),
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
1016
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
1017
|
+
if (meta.signal?.aborted)
|
|
1018
|
+
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
1019
|
+
if (confirmationResult.decision === "decline")
|
|
1020
|
+
return { kind: "host_rejection", value: batchReason("confirmation_declined") };
|
|
1021
|
+
if (confirmationResult.decision === "cancel")
|
|
1022
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
1023
|
+
if (confirmationResult.decision !== "accept")
|
|
1024
|
+
return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
|
|
1025
|
+
return { kind: "confirmed", request_id: confirmationResult.requestId };
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
if (outcome.outcome === "host_rejection") return outcome.value;
|
|
1029
|
+
if (outcome.outcome === "rejected") return outcome.rejection;
|
|
1030
|
+
return { state: "retired", batch_id: outcome.batch_id, report: outcome.report };
|
|
1031
|
+
}
|
|
985
1032
|
}
|
|
986
1033
|
|
|
987
1034
|
export interface ToolMeta {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./interaction";
|
|
14
14
|
import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
|
|
15
15
|
import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
|
|
16
|
+
import { parseLaneOffers, parseMaxParallel, resolveLaneParallel } from "../unattended/batch_lanes";
|
|
16
17
|
import type { AssuranceCoordinatorPorts } from "../assurance/coordinator";
|
|
17
18
|
|
|
18
19
|
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
@@ -21,12 +22,13 @@ export const TOOLS = [
|
|
|
21
22
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
22
23
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
23
24
|
{ name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
|
|
24
|
-
{ name: "submit_review", description: "Submit the
|
|
25
|
+
{ name: "submit_review", description: "Submit the Review verdict; omit the verdict to apply the correlated reviewer receipt, or relay the reviewer's structured verdict for fingerprint comparison.", privileged: false },
|
|
25
26
|
{ name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
|
|
26
27
|
{ name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
|
|
27
28
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
28
29
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
29
30
|
{ name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
|
|
31
|
+
{ name: "retire_stale_batch", description: "Retire an Initiative's parked or drifted batch record to the terminal superseded state after native confirmation.", privileged: true },
|
|
30
32
|
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
31
33
|
{ name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
|
|
32
34
|
{ name: "refute_finding", description: "Refute one open finding by binding the fresh passing QA attestation that contradicts it.", privileged: false },
|
|
@@ -39,8 +41,24 @@ export function listMcpTools() {
|
|
|
39
41
|
inputSchema: {
|
|
40
42
|
type: "object",
|
|
41
43
|
properties: {
|
|
42
|
-
...(tool.name === "start_unattended_batch"
|
|
43
|
-
? {
|
|
44
|
+
...(tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch"
|
|
45
|
+
? {
|
|
46
|
+
initiative_slug: { type: "string" },
|
|
47
|
+
...(tool.name === "start_unattended_batch"
|
|
48
|
+
? {
|
|
49
|
+
max_parallel: { type: "integer", minimum: 1 },
|
|
50
|
+
lane_offers: {
|
|
51
|
+
type: "array",
|
|
52
|
+
items: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: { task_id: { type: "string" }, path: { type: "string" } },
|
|
55
|
+
required: ["task_id", "path"],
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
}
|
|
44
62
|
: {
|
|
45
63
|
task_id: { type: "string" },
|
|
46
64
|
...(tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {}),
|
|
@@ -50,17 +68,15 @@ export function listMcpTools() {
|
|
|
50
68
|
...(tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}),
|
|
51
69
|
}),
|
|
52
70
|
},
|
|
53
|
-
required: tool.name === "start_unattended_batch"
|
|
71
|
+
required: tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch"
|
|
54
72
|
? ["initiative_slug"]
|
|
55
|
-
: tool.name === "
|
|
56
|
-
? ["task_id", "
|
|
57
|
-
: tool.name === "
|
|
58
|
-
? ["task_id", "finding_id"]
|
|
59
|
-
: tool.name === "
|
|
60
|
-
? ["task_id", "
|
|
61
|
-
:
|
|
62
|
-
? ["task_id", "next_intent"]
|
|
63
|
-
: ["task_id"],
|
|
73
|
+
: tool.name === "resolve_finding"
|
|
74
|
+
? ["task_id", "finding_id"]
|
|
75
|
+
: tool.name === "refute_finding"
|
|
76
|
+
? ["task_id", "finding_id", "attestation_id"]
|
|
77
|
+
: tool.name === "revise_intent"
|
|
78
|
+
? ["task_id", "next_intent"]
|
|
79
|
+
: ["task_id"],
|
|
64
80
|
},
|
|
65
81
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
|
|
66
82
|
}));
|
|
@@ -135,7 +151,33 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
135
151
|
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
136
152
|
signal: meta.signal,
|
|
137
153
|
};
|
|
138
|
-
|
|
154
|
+
const laneOffers = parseLaneOffers(args.lane_offers);
|
|
155
|
+
// A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
|
|
156
|
+
const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers);
|
|
157
|
+
return runtime.startUnattendedBatch(initiativeSlug, toolMeta, {
|
|
158
|
+
...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
|
|
159
|
+
...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (name === "retire_stale_batch") {
|
|
163
|
+
const initiativeSlug = String(args.initiative_slug ?? "");
|
|
164
|
+
if (!initiativeSlug) throw new Error("initiative_slug is required");
|
|
165
|
+
if ("native_decision" in args) throw new Error("native_decision cannot be supplied in tool arguments");
|
|
166
|
+
if (!negotiatedVersion) throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
167
|
+
if (!negotiatedInteractive) {
|
|
168
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
169
|
+
}
|
|
170
|
+
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
171
|
+
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
172
|
+
const toolMeta: ToolMeta = {
|
|
173
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
174
|
+
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
175
|
+
taskId: initiativeSlug,
|
|
176
|
+
initiativeSlug,
|
|
177
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
178
|
+
signal: meta.signal,
|
|
179
|
+
};
|
|
180
|
+
return runtime.retireStaleBatch(initiativeSlug, toolMeta);
|
|
139
181
|
}
|
|
140
182
|
const taskId = String(args.task_id ?? "");
|
|
141
183
|
if (!taskId) throw new Error("task_id is required");
|
|
@@ -164,8 +206,8 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
164
206
|
if (name === "enroll") return runtime.enroll(taskId, toolMeta);
|
|
165
207
|
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
166
208
|
if (name === "submit_review") {
|
|
167
|
-
|
|
168
|
-
return runtime.submitReview(taskId, args.verdict, toolMeta);
|
|
209
|
+
// ADR 0017: an absent verdict is the receipt-bound submission.
|
|
210
|
+
return runtime.submitReview(taskId, Object.hasOwn(args, "verdict") ? args.verdict : undefined, toolMeta);
|
|
169
211
|
}
|
|
170
212
|
if (name === "revise_intent") {
|
|
171
213
|
if (!Object.hasOwn(args, "next_intent")) throw new Error("next_intent is required");
|
|
@@ -362,6 +404,34 @@ async function writeReply(output: Writable, reply: JsonRpc): Promise<void> {
|
|
|
362
404
|
}
|
|
363
405
|
|
|
364
406
|
export function elicitationParams(input: NativeConfirmationInput) {
|
|
407
|
+
if (input.operation === "retire_stale_batch") {
|
|
408
|
+
const d = input.batchDisposition;
|
|
409
|
+
const details = [
|
|
410
|
+
`Operation: retire_stale_batch`,
|
|
411
|
+
`Initiative: ${input.initiativeSlug}`,
|
|
412
|
+
d ? `Batch: ${d.batch_id}` : null,
|
|
413
|
+
d ? `Batch state: ${d.batch_state}` : null,
|
|
414
|
+
d ? `Batch branch: ${d.batch_branch}` : null,
|
|
415
|
+
d ? `Plan digest: ${d.plan_digest}` : null,
|
|
416
|
+
d ? `Recorded child commits: ${d.recorded_commits}` : null,
|
|
417
|
+
d
|
|
418
|
+
? `Children (${d.children.length}):\n${d.children
|
|
419
|
+
.map(
|
|
420
|
+
(child) =>
|
|
421
|
+
` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`,
|
|
422
|
+
)
|
|
423
|
+
.join("\n")}`
|
|
424
|
+
: null,
|
|
425
|
+
"Retiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.",
|
|
426
|
+
"A child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.",
|
|
427
|
+
"After retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative.",
|
|
428
|
+
].filter(Boolean);
|
|
429
|
+
return {
|
|
430
|
+
mode: "form",
|
|
431
|
+
message: `Retire this stale batch record for ${input.initiativeSlug}?\n\n${details.join("\n")}`,
|
|
432
|
+
requestedSchema: { type: "object", properties: {} },
|
|
433
|
+
};
|
|
434
|
+
}
|
|
365
435
|
if (input.operation === "start_unattended_batch") {
|
|
366
436
|
const b = input.batchDetails;
|
|
367
437
|
const details = [
|
|
@@ -372,6 +442,9 @@ export function elicitationParams(input: NativeConfirmationInput) {
|
|
|
372
442
|
b?.children ? `Ordered children (${b.children.length}):\n${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "unknown"}]`).join("\n")}` : null,
|
|
373
443
|
b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):\n${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join("\n")}` : null,
|
|
374
444
|
b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
|
|
445
|
+
b?.lane_mode ? `Lane mode: max_parallel=${b.lane_mode.max_parallel}` : null,
|
|
446
|
+
b?.lane_mode ? `Parallel groups (${b.lane_mode.parallel_groups.length}):\n${b.lane_mode.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}` : null,
|
|
447
|
+
b?.lane_mode && b.lane_mode.serialized.length > 0 ? `Serialized by overlapping scope (${b.lane_mode.serialized.length}):\n${b.lane_mode.serialized.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : null,
|
|
375
448
|
].filter(Boolean);
|
|
376
449
|
return {
|
|
377
450
|
mode: "form",
|
|
@@ -73,9 +73,10 @@ export const CLAUDE_HOST_PROVIDED_ROLE_AGENTS = {
|
|
|
73
73
|
*
|
|
74
74
|
* `code-review` ships its own definition with a wider allowlist (it must run
|
|
75
75
|
* read-only Git commands), so it is not part of this table. The remaining roles
|
|
76
|
-
* write: `executor`, `test-fixer` and `pr-fix` mutate the workspace,
|
|
77
|
-
* `compounder` writes learning storage
|
|
78
|
-
*
|
|
76
|
+
* write: `executor`, `test-fixer` and `pr-fix` mutate the workspace,
|
|
77
|
+
* `compounder` writes learning storage, and `lane-steward` supplies and
|
|
78
|
+
* releases a separate working tree. A write-capable role stays bounded by its
|
|
79
|
+
* dispatch envelope and the Parent, not by an agent definition.
|
|
79
80
|
*/
|
|
80
81
|
export const CLAUDE_NATIVE_AGENT_ROLES = new Set<InternalRole>([
|
|
81
82
|
...CLAUDE_READONLY_ROLES,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
reconcileKernelAuthority,
|
|
22
22
|
} from "./storage";
|
|
23
23
|
import { projectTask, resolveProjectedRisk } from "./completion";
|
|
24
|
+
import { inspectSpecBinding } from "./spec_binding";
|
|
24
25
|
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
25
26
|
import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV3 } from "./types";
|
|
26
27
|
import type { TaskRecordV2 } from "./legacy_task_record";
|
|
@@ -236,7 +237,9 @@ function projectFromRecord(
|
|
|
236
237
|
independence_violations: decision.independence_violations,
|
|
237
238
|
open_user_decision_count: openUserDecisionCount,
|
|
238
239
|
completion_ready: decision.complete,
|
|
239
|
-
authorization:
|
|
240
|
+
authorization: !inspectSpecBinding(intent).ok
|
|
241
|
+
? { state: "none", blocked: "invalid Spec binding requires a valid breaking intent revision" }
|
|
242
|
+
: deriveAssuranceAuthorization({
|
|
240
243
|
next_obligation: decision.next_obligation,
|
|
241
244
|
open_user_decision_count: openUserDecisionCount,
|
|
242
245
|
}),
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type BackendClaim,
|
|
27
27
|
} from "./backend_claim";
|
|
28
28
|
import { canonicalIntentHash } from "./intent";
|
|
29
|
-
import { boundSpecPath, readBoundActiveSpec } from "./spec_binding";
|
|
29
|
+
import { assertValidSpecBinding, boundSpecPath, readBoundActiveSpec } from "./spec_binding";
|
|
30
30
|
import { parseTaskRecord } from "./validation";
|
|
31
31
|
import type { TaskIntentIdentityToken } from "./intent_token_registry";
|
|
32
32
|
import {
|
|
@@ -208,6 +208,7 @@ export function createCanaryApplication(
|
|
|
208
208
|
const current = readTaskRecordRaw(input.root, input.task_id);
|
|
209
209
|
if (!current.record)
|
|
210
210
|
throw new KernelInvariantError([`task ${input.task_id} has no TaskRecord v3`]);
|
|
211
|
+
assertValidSpecBinding(current.record.intent_snapshot);
|
|
211
212
|
const workspace = readWorkspaceStateRaw(input.root);
|
|
212
213
|
if (current.record.artifact_state === "frozen")
|
|
213
214
|
return { revision: current.revision, record: current.record, workspace };
|
|
@@ -318,7 +319,13 @@ export function createCanaryApplication(
|
|
|
318
319
|
const diffHash = asTaskDiffSnapshot((input.diffProvider ?? taskDeliveryIdentity)(input.root, snapshot.record)).diff_hash;
|
|
319
320
|
if (operation.op === "stop" && !("capability" in operation))
|
|
320
321
|
throw new KernelInvariantError(["stop requires user authority capability"]);
|
|
321
|
-
|
|
322
|
+
// Only a capability-bound breaking revision may recover an invalid prior
|
|
323
|
+
// binding. Validate the replacement before any authority write.
|
|
324
|
+
if (operation.op === "approve_breaking_intent_revision") assertValidSpecBinding(operation.next_intent);
|
|
325
|
+
else assertValidSpecBinding(snapshot.intent_snapshot);
|
|
326
|
+
const hasBoundSpec = operation.op === "approve_breaking_intent_revision"
|
|
327
|
+
? boundSpecPath(operation.next_intent) !== undefined
|
|
328
|
+
: boundSpecPath(snapshot.intent_snapshot) !== undefined;
|
|
322
329
|
if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
|
|
323
330
|
throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
|
|
324
331
|
const artifactTransition =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classifyTaskRisk } from "./intent";
|
|
2
|
-
import { archivePath, boundSpecPath } from "./spec_binding";
|
|
2
|
+
import { archivePath, boundSpecPath, inspectSpecBinding } from "./spec_binding";
|
|
3
3
|
import type {
|
|
4
4
|
ApprovalKind,
|
|
5
5
|
CompletionDecision,
|
|
@@ -49,6 +49,8 @@ export function resolveProjectedRisk(
|
|
|
49
49
|
intent: TaskIntentV1,
|
|
50
50
|
changedPaths: readonly string[] = [],
|
|
51
51
|
): TaskIntentV1["risk"] {
|
|
52
|
+
// Invalid bindings have no trusted planning-path exclusions.
|
|
53
|
+
if (!inspectSpecBinding(intent).ok) return classifyTaskRisk(changedPaths, intent.risk);
|
|
52
54
|
const excluded = ownSidecarPaths(intent);
|
|
53
55
|
return classifyTaskRisk(
|
|
54
56
|
changedPaths.filter((path) => !excluded.has(path)),
|
|
@@ -182,6 +184,27 @@ export function projectTask(
|
|
|
182
184
|
currentIntentContentHash: string,
|
|
183
185
|
changedPaths: readonly string[] = [],
|
|
184
186
|
): TaskProjectionV3 {
|
|
187
|
+
if (record.lifecycle === "active" && !inspectSpecBinding(intent).ok) {
|
|
188
|
+
assertKernelInvariantsV3(intent, record);
|
|
189
|
+
return {
|
|
190
|
+
contract: "assurance_kernel/projection/v3",
|
|
191
|
+
task_id: record.task_id,
|
|
192
|
+
intent_revision: intent.revision,
|
|
193
|
+
lifecycle: record.lifecycle,
|
|
194
|
+
artifact_state: record.artifact_state,
|
|
195
|
+
blocked: true,
|
|
196
|
+
next_obligation: "revise_intent",
|
|
197
|
+
complete: false,
|
|
198
|
+
fresh_acceptance_ids: [],
|
|
199
|
+
missing_acceptance_ids: intent.acceptance.map((item) => item.id),
|
|
200
|
+
stale_attestation_ids: record.attestations.map((item) => item.id),
|
|
201
|
+
missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)],
|
|
202
|
+
blocking_finding_ids: record.findings.filter((item) => item.kind === "blocking" && item.status !== "resolved").map((item) => item.id),
|
|
203
|
+
unresolved_user_decision_ids: record.findings.filter((item) => item.kind === "unresolved_user_decision" && item.status === "open").map((item) => item.id),
|
|
204
|
+
replan_required_ids: record.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id),
|
|
205
|
+
independence_violations: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
185
208
|
const decision = completionDecision(
|
|
186
209
|
intent,
|
|
187
210
|
record,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
parseTaskRecord,
|
|
24
24
|
} from "./validation";
|
|
25
25
|
import { classifyIntentRevision, canonicalIntentHash } from "./intent";
|
|
26
|
+
import { assertValidSpecBinding } from "./spec_binding";
|
|
26
27
|
|
|
27
28
|
const RISK_RANK: Record<TaskIntentV1["risk"], number> = {
|
|
28
29
|
routine: 0,
|
|
@@ -431,15 +432,24 @@ export function reduceTask(
|
|
|
431
432
|
const reviewRevision = approval.review_revision;
|
|
432
433
|
if (reviewRevision && approval.kind !== "review")
|
|
433
434
|
throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
|
|
435
|
+
if (approval.reviewer_verdict_sha256 && approval.kind !== "review")
|
|
436
|
+
throw new KernelInvariantError(["reviewer_verdict_sha256 is only valid on review approvals"]);
|
|
434
437
|
if (approval.kind === "review") {
|
|
435
438
|
if (!reviewRevision)
|
|
436
439
|
throw new KernelInvariantError(["v4 review approval requires review_revision"]);
|
|
437
440
|
if (reviewRevision.base_head !== record.git_base_head)
|
|
438
441
|
throw new KernelInvariantError(["review_revision.base_head must equal the Enrollment git_base_head"]);
|
|
439
442
|
}
|
|
440
|
-
|
|
443
|
+
// Re-emit the optional review-only fields in the strict parser's emission
|
|
444
|
+
// order so a written attestation stays byte-identical to its parse roundtrip.
|
|
445
|
+
const {
|
|
446
|
+
review_revision: storedReviewRevision,
|
|
447
|
+
advisory_findings: storedAdvisoryFindings,
|
|
448
|
+
reviewer_verdict_sha256: storedReviewerDigest,
|
|
449
|
+
...approvalWithoutExtras
|
|
450
|
+
} = approval;
|
|
441
451
|
record.attestations.push({
|
|
442
|
-
...
|
|
452
|
+
...approvalWithoutExtras,
|
|
443
453
|
acceptance_results: approval.kind === "qa"
|
|
444
454
|
? record.intent_snapshot.acceptance.map((item) => ({
|
|
445
455
|
acceptance_id: item.id,
|
|
@@ -448,12 +458,15 @@ export function reduceTask(
|
|
|
448
458
|
}))
|
|
449
459
|
: [],
|
|
450
460
|
...(storedReviewRevision ? { review_revision: storedReviewRevision } : {}),
|
|
461
|
+
...(storedAdvisoryFindings ? { advisory_findings: storedAdvisoryFindings } : {}),
|
|
462
|
+
...(storedReviewerDigest ? { reviewer_verdict_sha256: storedReviewerDigest } : {}),
|
|
451
463
|
});
|
|
452
464
|
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
453
465
|
break;
|
|
454
466
|
}
|
|
455
467
|
case "revise_intent":
|
|
456
468
|
case "approve_breaking_intent_revision": {
|
|
469
|
+
assertValidSpecBinding(action.next_intent);
|
|
457
470
|
if (record.lifecycle !== "active")
|
|
458
471
|
throw new KernelInvariantError([
|
|
459
472
|
`cannot revise intent while lifecycle is ${record.lifecycle}`,
|
|
@@ -57,6 +57,12 @@ export function readBoundActiveSpec(
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Validate a candidate before it can replace enrolled authority. */
|
|
61
|
+
export function assertValidSpecBinding(intent: TaskIntentV1): void {
|
|
62
|
+
const binding = inspectSpecBinding(intent);
|
|
63
|
+
if (!binding.ok) throw new KernelInvariantError([binding.message]);
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
export interface BoundSpec {
|
|
61
67
|
active: string;
|
|
62
68
|
archive: string;
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
FILE_STORE_CLAIM_RELATIVE,
|
|
54
54
|
FILE_STORE_TRANSACTIONS_RELATIVE,
|
|
55
55
|
FILE_STORE_WORKSPACE_RELATIVE,
|
|
56
|
+
inspectRetiredStateResidue,
|
|
56
57
|
stateDatabasePath,
|
|
57
58
|
} from "./storage_paths";
|
|
58
59
|
import { canonicalRecordHash } from "./reducer";
|
|
@@ -262,7 +263,7 @@ function isRetiredFileProvablySuperseded(
|
|
|
262
263
|
/**
|
|
263
264
|
* Refuse mutation while the retired `.imm/state/*.json` file store (or the
|
|
264
265
|
* pre-cutover `.imm/tasks` layout) still holds authority. The check is bounded
|
|
265
|
-
* to
|
|
266
|
+
* to retired paths so it can run on every locked mutation.
|
|
266
267
|
*/
|
|
267
268
|
function assertNoRetiredFileStore(
|
|
268
269
|
root: string,
|
|
@@ -270,11 +271,19 @@ function assertNoRetiredFileStore(
|
|
|
270
271
|
taskId?: string | null,
|
|
271
272
|
): void {
|
|
272
273
|
const canonical = canonicalRoot(root);
|
|
274
|
+
let residue: ReturnType<typeof inspectRetiredStateResidue>;
|
|
275
|
+
try {
|
|
276
|
+
residue = inspectRetiredStateResidue(canonical);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new KernelStoreSecurityError(
|
|
279
|
+
`retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
273
282
|
// Real authority in the retired store: must be imported, never ignored.
|
|
274
283
|
const retired: Array<[string, string]> = [
|
|
275
284
|
[".imm/tasks", "pre-cutover task store"],
|
|
276
285
|
[".imm/workspace.json", "pre-cutover workspace owner"],
|
|
277
|
-
[".imm/state/tasks", "task records"],
|
|
286
|
+
...(residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"] as [string, string]] : []),
|
|
278
287
|
];
|
|
279
288
|
for (const [path, label] of retired) {
|
|
280
289
|
if (existsSync(resolve(canonical, path)))
|
|
@@ -293,6 +302,7 @@ function assertNoRetiredFileStore(
|
|
|
293
302
|
];
|
|
294
303
|
for (const [path, label] of derived) {
|
|
295
304
|
const full = resolve(canonical, path);
|
|
305
|
+
if (path === FILE_STORE_WORKSPACE_RELATIVE && residue.workspace_owner === null) continue;
|
|
296
306
|
if (!existsSync(full)) continue;
|
|
297
307
|
if (db === undefined || typeof taskId !== "string" || taskId.length === 0)
|
|
298
308
|
throw new KernelStoreSecurityError(
|
|
@@ -325,10 +335,16 @@ function retiredFileStoreConflict(
|
|
|
325
335
|
taskId: string | null,
|
|
326
336
|
): string | null {
|
|
327
337
|
const canonical = canonicalRoot(root);
|
|
338
|
+
let residue: ReturnType<typeof inspectRetiredStateResidue>;
|
|
339
|
+
try {
|
|
340
|
+
residue = inspectRetiredStateResidue(canonical);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return `retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`;
|
|
343
|
+
}
|
|
328
344
|
const authority: Array<[string, string]> = [
|
|
329
345
|
[".imm/tasks", "pre-cutover task store"],
|
|
330
346
|
[".imm/workspace.json", "pre-cutover workspace owner"],
|
|
331
|
-
[".imm/state/tasks", "task records"],
|
|
347
|
+
...(residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"] as [string, string]] : []),
|
|
332
348
|
];
|
|
333
349
|
for (const [path, label] of authority)
|
|
334
350
|
if (existsSync(resolve(canonical, path)))
|
|
@@ -340,7 +356,7 @@ function retiredFileStoreConflict(
|
|
|
340
356
|
[FILE_STORE_WORKSPACE_RELATIVE, "workspace owner"],
|
|
341
357
|
];
|
|
342
358
|
for (const [path, label] of derived)
|
|
343
|
-
if (existsSync(resolve(canonical, path)))
|
|
359
|
+
if ((path !== FILE_STORE_WORKSPACE_RELATIVE || residue.workspace_owner !== null) && existsSync(resolve(canonical, path)))
|
|
344
360
|
return `retired file-store authority is present (${label}: ${path}); import it with the supported migration before mutating this worktree`;
|
|
345
361
|
}
|
|
346
362
|
const transactions = resolve(canonical, FILE_STORE_TRANSACTIONS_RELATIVE);
|