immune-brain 5.1.0 → 5.1.2
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/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +37 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +230 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8712 -7410
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
- package/plugins/immune-brain/dist/imm-planner.md +41 -5
- package/plugins/immune-brain/dist/imm-run.md +123 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +25 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +22 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +31 -4
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- 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 +15 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +120 -59
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +142 -31
- package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
- 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/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- 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/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +25 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +447 -37
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +33 -16
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +44 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +199 -6
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
|
@@ -14,7 +14,14 @@ export type BatchRunState =
|
|
|
14
14
|
| "completed"
|
|
15
15
|
| "budget_stopped"
|
|
16
16
|
| "failed"
|
|
17
|
-
| "rejected"
|
|
17
|
+
| "rejected"
|
|
18
|
+
/**
|
|
19
|
+
* Retired by an explicit literal-user disposition after the batch plan moved
|
|
20
|
+
* past it. Terminal, and not a failure of the batch's own: the record, its
|
|
21
|
+
* children, its commits and its branch stay as evidence, and the retired
|
|
22
|
+
* authorization is never reused for the revised plan.
|
|
23
|
+
*/
|
|
24
|
+
| "superseded";
|
|
18
25
|
|
|
19
26
|
export type BatchChildRunState =
|
|
20
27
|
| "pending"
|
|
@@ -88,6 +95,8 @@ export interface BatchLaneBinding {
|
|
|
88
95
|
base_head: string;
|
|
89
96
|
/** Commit created in the Lane after the Lane Kernel settled the child. */
|
|
90
97
|
lane_commit: string | null;
|
|
98
|
+
/** Kernel run observed at enrollment; absent historical bindings cannot resume a parked child. */
|
|
99
|
+
run_id?: string | null;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
export interface BatchLaneChildRun {
|
|
@@ -100,6 +109,28 @@ export interface BatchLaneChildRun {
|
|
|
100
109
|
commit: string | null;
|
|
101
110
|
lane: BatchLaneBinding | null;
|
|
102
111
|
qa_failures: number;
|
|
112
|
+
/**
|
|
113
|
+
* The Child Issue was closed by the coordinator after this child's commit was
|
|
114
|
+
* integrated. Absent until then; a Lane settlement never closes it.
|
|
115
|
+
*/
|
|
116
|
+
tracker_closed?: boolean;
|
|
117
|
+
/** Coordinator corrections already spent on this child (at most two). */
|
|
118
|
+
corrections?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Set when the child exhausted its rework budget and waits for one
|
|
121
|
+
* design-level coordinator correction instead of parking; cleared when the
|
|
122
|
+
* correction is recorded.
|
|
123
|
+
*/
|
|
124
|
+
correction_due?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface LaneIntervention {
|
|
128
|
+
at: string;
|
|
129
|
+
task_id: string;
|
|
130
|
+
/** `instruction` is an ordinary correction; `correction` is a design-level correction after an exhausted budget. */
|
|
131
|
+
kind: "instruction" | "correction";
|
|
132
|
+
text: string;
|
|
133
|
+
text_digest: string;
|
|
103
134
|
}
|
|
104
135
|
|
|
105
136
|
/**
|
|
@@ -124,6 +155,30 @@ export interface BatchLaneRunStateRecord {
|
|
|
124
155
|
/** Batch-branch commits produced by integration, in integration order. */
|
|
125
156
|
commits: string[];
|
|
126
157
|
adopted_heads?: Array<{ from: string; to: string }>;
|
|
158
|
+
/**
|
|
159
|
+
* Lane writes that leaked into the coordinator checkout and were restored on
|
|
160
|
+
* re-entry, each with the backup that undoes it. Absent until one happens.
|
|
161
|
+
*/
|
|
162
|
+
restores?: import("./batch_leak_restore").LaneLeakRestore[];
|
|
163
|
+
/**
|
|
164
|
+
* Every text instruction the coordinator sent a Lane session, recorded before
|
|
165
|
+
* it was sent, so coordinator intervention is told apart from the Executor's
|
|
166
|
+
* own behavior afterwards.
|
|
167
|
+
*/
|
|
168
|
+
interventions?: LaneIntervention[];
|
|
169
|
+
/** Commands run on the batch branch once every child is integrated; fixed when the batch starts. */
|
|
170
|
+
final_verification?: string[];
|
|
171
|
+
/**
|
|
172
|
+
* ADR 0018: the literal user's grant, in this batch's confirmation, to let the
|
|
173
|
+
* coordinator approve in-envelope Lane Intent revisions. Absent: not granted.
|
|
174
|
+
*/
|
|
175
|
+
revision_delegation?: import("./batch_delegation").RevisionDelegationGrant;
|
|
176
|
+
/** Every revision applied under that grant. */
|
|
177
|
+
delegated_revisions?: import("./batch_delegation").DelegatedRevisionRecord[];
|
|
178
|
+
/** Each child's TaskIntent identity behind `plan_digest`; resealed only for a delegated revision. */
|
|
179
|
+
intent_identities?: Record<string, { intent_path: string; intent_revision: number; intent_content_hash: string }>;
|
|
180
|
+
/** Each `plan_digest` reseal at integration, with the digest before and after. */
|
|
181
|
+
reseals?: Array<{ at: string; task_id: string; from_digest: string; to_digest: string; intent_revision: number; intent_content_hash: string }>;
|
|
127
182
|
created_at: string;
|
|
128
183
|
updated_at: string;
|
|
129
184
|
}
|
|
@@ -145,7 +200,7 @@ const CHILD_RUN_STATES: ReadonlySet<string> = new Set([
|
|
|
145
200
|
"skipped_blocked",
|
|
146
201
|
]);
|
|
147
202
|
|
|
148
|
-
const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
|
|
203
|
+
export const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
|
|
149
204
|
"prepared",
|
|
150
205
|
"running",
|
|
151
206
|
"needs_human",
|
|
@@ -153,6 +208,7 @@ const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
|
|
|
153
208
|
"budget_stopped",
|
|
154
209
|
"failed",
|
|
155
210
|
"rejected",
|
|
211
|
+
"superseded",
|
|
156
212
|
]);
|
|
157
213
|
|
|
158
214
|
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
@@ -286,7 +342,8 @@ function validateRecordShape(value: unknown, batchId: string): asserts value is
|
|
|
286
342
|
if (
|
|
287
343
|
(record.batch_state === "budget_stopped" ||
|
|
288
344
|
record.batch_state === "failed" ||
|
|
289
|
-
record.batch_state === "rejected"
|
|
345
|
+
record.batch_state === "rejected" ||
|
|
346
|
+
record.batch_state === "superseded") &&
|
|
290
347
|
record.children.some((child) => child.state === "enrolled" || child.state === "settled")
|
|
291
348
|
)
|
|
292
349
|
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
@@ -398,6 +455,25 @@ export function replaceBatchRunState(root: string, expected: Buffer, next: Batch
|
|
|
398
455
|
});
|
|
399
456
|
}
|
|
400
457
|
|
|
458
|
+
/** Replace a record of either version only after a locked expected-byte check. */
|
|
459
|
+
export function replaceAnyBatchRunState(
|
|
460
|
+
root: string,
|
|
461
|
+
expected: Buffer,
|
|
462
|
+
next: AnyBatchRunStateRecord,
|
|
463
|
+
validate: () => void,
|
|
464
|
+
): AnyBatchRunStateRecord {
|
|
465
|
+
const path = statePath(next.batch_id);
|
|
466
|
+
if (isLaneBatchRecord(next)) validateLaneRecordShape(next, next.batch_id);
|
|
467
|
+
else validateRecordShape(next, next.batch_id);
|
|
468
|
+
return withKernelStoreLock(root, () => {
|
|
469
|
+
if (!readSecureProjectBytes(root, path).equals(expected)) throw new Error("batch state CAS mismatch");
|
|
470
|
+
validate();
|
|
471
|
+
const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
|
|
472
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
473
|
+
return stored;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
401
477
|
export function writeBatchRunState(
|
|
402
478
|
root: string,
|
|
403
479
|
record: BatchRunStateRecord,
|
|
@@ -493,6 +569,66 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
|
|
|
493
569
|
))
|
|
494
570
|
)
|
|
495
571
|
throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
|
|
572
|
+
if (
|
|
573
|
+
record.restores !== undefined &&
|
|
574
|
+
(!Array.isArray(record.restores) ||
|
|
575
|
+
record.restores.some(
|
|
576
|
+
(r: unknown) =>
|
|
577
|
+
typeof r !== "object" || r === null ||
|
|
578
|
+
typeof (r as { backup?: unknown }).backup !== "string" ||
|
|
579
|
+
!Array.isArray((r as { paths?: unknown }).paths),
|
|
580
|
+
))
|
|
581
|
+
)
|
|
582
|
+
throw new Error(`batch run state ${batchId} has an invalid restores list`);
|
|
583
|
+
if (
|
|
584
|
+
record.final_verification !== undefined &&
|
|
585
|
+
(!Array.isArray(record.final_verification) ||
|
|
586
|
+
record.final_verification.length === 0 ||
|
|
587
|
+
record.final_verification.some((c: unknown) => typeof c !== "string" || !c))
|
|
588
|
+
)
|
|
589
|
+
throw new Error(`batch run state ${batchId} has an invalid final_verification list`);
|
|
590
|
+
if (
|
|
591
|
+
record.interventions !== undefined &&
|
|
592
|
+
(!Array.isArray(record.interventions) ||
|
|
593
|
+
record.interventions.some(
|
|
594
|
+
(i: unknown) =>
|
|
595
|
+
typeof i !== "object" || i === null ||
|
|
596
|
+
typeof (i as LaneIntervention).task_id !== "string" ||
|
|
597
|
+
((i as LaneIntervention).kind !== "instruction" && (i as LaneIntervention).kind !== "correction") ||
|
|
598
|
+
typeof (i as LaneIntervention).text !== "string" ||
|
|
599
|
+
typeof (i as LaneIntervention).text_digest !== "string",
|
|
600
|
+
))
|
|
601
|
+
)
|
|
602
|
+
throw new Error(`batch run state ${batchId} has an invalid interventions list`);
|
|
603
|
+
// ADR 0018 fields carry authority effects, so a malformed one fails closed.
|
|
604
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
605
|
+
const isHash = (v: unknown) => typeof v === "string" && /^sha256:[0-9a-f]{64}$/.test(v);
|
|
606
|
+
const isRevision = (v: unknown) => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
607
|
+
if (record.revision_delegation !== undefined &&
|
|
608
|
+
(!isObject(record.revision_delegation) || Object.keys(record.revision_delegation).join() !== "confirmation_time" ||
|
|
609
|
+
!isCanonicalTimestamp(record.revision_delegation.confirmation_time)))
|
|
610
|
+
throw new Error(`batch run state ${batchId} has an invalid revision_delegation`);
|
|
611
|
+
if (record.delegated_revisions !== undefined &&
|
|
612
|
+
(!Array.isArray(record.delegated_revisions) || record.delegated_revisions.some((r: unknown) =>
|
|
613
|
+
!isObject(r) || typeof r.task_id !== "string" || !isRevision(r.from_revision) || !isRevision(r.to_revision) ||
|
|
614
|
+
(r.to_revision as number) <= (r.from_revision as number) || !isHash(r.intent_content_hash) ||
|
|
615
|
+
typeof r.confirmation_ref !== "string" || !r.confirmation_ref.startsWith(`delegated-batch:${batchId}@`) ||
|
|
616
|
+
!isCanonicalTimestamp(r.at))))
|
|
617
|
+
throw new Error(`batch run state ${batchId} has an invalid delegated_revisions list`);
|
|
618
|
+
if (record.delegated_revisions?.length && !record.revision_delegation)
|
|
619
|
+
throw new Error(`batch run state ${batchId} records delegated revisions without a delegation grant`);
|
|
620
|
+
if (record.intent_identities !== undefined &&
|
|
621
|
+
(!isObject(record.intent_identities) || Object.values(record.intent_identities).some((i: unknown) =>
|
|
622
|
+
!isObject(i) || typeof i.intent_path !== "string" || !i.intent_path || !isRevision(i.intent_revision) || typeof i.intent_content_hash !== "string" || !i.intent_content_hash)))
|
|
623
|
+
throw new Error(`batch run state ${batchId} has an invalid intent_identities map`);
|
|
624
|
+
if (record.reseals !== undefined &&
|
|
625
|
+
(!Array.isArray(record.reseals) || record.reseals.some((r: unknown) =>
|
|
626
|
+
!isObject(r) || typeof r.task_id !== "string" || typeof r.from_digest !== "string" || typeof r.to_digest !== "string" ||
|
|
627
|
+
!isRevision(r.intent_revision) || !isHash(r.intent_content_hash) ||
|
|
628
|
+
!(record.delegated_revisions as Array<{ task_id: string; intent_content_hash: string }> | undefined)?.some((d) => d.task_id === r.task_id && d.intent_content_hash === r.intent_content_hash))))
|
|
629
|
+
throw new Error(`batch run state ${batchId} has an invalid reseals list`);
|
|
630
|
+
if (record.reseals?.length && record.reseals[record.reseals.length - 1].to_digest !== record.plan_digest)
|
|
631
|
+
throw new Error(`batch run state ${batchId} plan_digest does not match its last reseal`);
|
|
496
632
|
const seenTaskIds = new Set<string>();
|
|
497
633
|
for (const child of record.children) {
|
|
498
634
|
if (
|
|
@@ -515,6 +651,14 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
|
|
|
515
651
|
throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
|
|
516
652
|
if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
|
|
517
653
|
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
|
|
654
|
+
if (child.corrections !== undefined && (!Number.isInteger(child.corrections) || child.corrections < 0 || child.corrections > 2))
|
|
655
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid corrections count`);
|
|
656
|
+
if (child.correction_due !== undefined && (typeof child.correction_due !== "string" || !child.correction_due || child.state !== "enrolled"))
|
|
657
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid correction_due`);
|
|
658
|
+
if (child.tracker_closed !== undefined && child.tracker_closed !== true)
|
|
659
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid tracker_closed`);
|
|
660
|
+
if (child.tracker_closed === true && child.state !== "integrated" && child.state !== "released")
|
|
661
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} closed its tracker Issue before integration`);
|
|
518
662
|
const lane = child.lane as BatchLaneBinding | null | undefined;
|
|
519
663
|
if (lane !== null) {
|
|
520
664
|
if (
|
|
@@ -525,7 +669,8 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
|
|
|
525
669
|
!lane.branch ||
|
|
526
670
|
typeof lane.base_head !== "string" ||
|
|
527
671
|
!lane.base_head ||
|
|
528
|
-
(lane.lane_commit !== null && typeof lane.lane_commit !== "string")
|
|
672
|
+
(lane.lane_commit !== null && typeof lane.lane_commit !== "string") ||
|
|
673
|
+
(lane.run_id !== undefined && lane.run_id !== null && (typeof lane.run_id !== "string" || !lane.run_id))
|
|
529
674
|
)
|
|
530
675
|
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
|
|
531
676
|
}
|
|
@@ -558,7 +703,8 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
|
|
|
558
703
|
child.state === "settled" ||
|
|
559
704
|
child.state === "lane_committed";
|
|
560
705
|
if (
|
|
561
|
-
(record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected"
|
|
706
|
+
(record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" ||
|
|
707
|
+
record.batch_state === "superseded") &&
|
|
562
708
|
record.children.some(inFlight)
|
|
563
709
|
)
|
|
564
710
|
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
@@ -580,6 +726,8 @@ export function prepareBatchLaneRunState(input: {
|
|
|
580
726
|
budget: { max_children: number; qa_failure_limit: number };
|
|
581
727
|
max_parallel: number;
|
|
582
728
|
now: string;
|
|
729
|
+
final_verification?: string[];
|
|
730
|
+
revision_delegation?: import("./batch_delegation").RevisionDelegationGrant;
|
|
583
731
|
}): BatchLaneRunStateRecord {
|
|
584
732
|
validateBatchId(input.batch_id);
|
|
585
733
|
return {
|
|
@@ -604,6 +752,13 @@ export function prepareBatchLaneRunState(input: {
|
|
|
604
752
|
qa_failures: 0,
|
|
605
753
|
})),
|
|
606
754
|
commits: [],
|
|
755
|
+
...(input.final_verification?.length ? { final_verification: [...input.final_verification] } : {}),
|
|
756
|
+
...(input.revision_delegation ? { revision_delegation: { ...input.revision_delegation } } : {}),
|
|
757
|
+
intent_identities: Object.fromEntries(
|
|
758
|
+
input.children
|
|
759
|
+
.filter((child) => child.intent_path !== null && child.intent_revision !== null && child.intent_content_hash !== null)
|
|
760
|
+
.map((child) => [child.task_id, { intent_path: child.intent_path!, intent_revision: child.intent_revision!, intent_content_hash: child.intent_content_hash! }]),
|
|
761
|
+
),
|
|
607
762
|
created_at: input.now,
|
|
608
763
|
updated_at: input.now,
|
|
609
764
|
};
|
|
@@ -693,6 +848,23 @@ export interface BatchLaneRunReport {
|
|
|
693
848
|
handoffs: BatchLaneHandoff[];
|
|
694
849
|
/** Offers refused at admission; a refused offer wrote nothing. */
|
|
695
850
|
lane_refusals?: Array<{ task_id: string; path: string; reason: string }>;
|
|
851
|
+
/**
|
|
852
|
+
* Child Issue closures this tick attempted after integration. A failure is a
|
|
853
|
+
* tracker observation beside the batch result, retried by the next tick.
|
|
854
|
+
*/
|
|
855
|
+
tracker_observations?: Array<{ task_id: string; status: string; message: string }>;
|
|
856
|
+
/** The completion run of the recorded final verification commands; `passed: false` marks the batch not passed. */
|
|
857
|
+
final_verification?: import("./batch_final_verification").FinalVerificationReport;
|
|
858
|
+
/** Coordinator instructions sent to Lane sessions (see BatchLaneRunStateRecord.interventions). */
|
|
859
|
+
interventions?: LaneIntervention[];
|
|
860
|
+
/** This tick's decision on the coordinator's `lane_revision` (ADR 0018). */
|
|
861
|
+
lane_revision?: { task_id: string; accepted: boolean; reason: string | null; fallback?: string };
|
|
862
|
+
delegated_revisions?: import("./batch_delegation").DelegatedRevisionRecord[];
|
|
863
|
+
reseals?: BatchLaneRunStateRecord["reseals"];
|
|
864
|
+
/** This tick's decision on the Parent's `lane_instruction`: send the text only when accepted. */
|
|
865
|
+
lane_instruction?: { task_id: string; kind: LaneIntervention["kind"]; accepted: boolean; reason: string | null };
|
|
866
|
+
/** Restored Lane leaks with their backup location (see BatchLaneRunStateRecord.restores). */
|
|
867
|
+
restores?: import("./batch_leak_restore").LaneLeakRestore[];
|
|
696
868
|
next_action: string;
|
|
697
869
|
created_at: string;
|
|
698
870
|
}
|
|
@@ -707,6 +879,21 @@ export type BatchLaneHandoff =
|
|
|
707
879
|
executor_hosts: readonly string[];
|
|
708
880
|
}
|
|
709
881
|
| { role: "lane-steward"; action: "release"; task_id: string; lane_branch: string }
|
|
882
|
+
| {
|
|
883
|
+
/**
|
|
884
|
+
* The child exhausted its rework budget. The Parent reads the findings of
|
|
885
|
+
* every round from the Lane's Kernel state, decides whether they share one
|
|
886
|
+
* root cause, starts a new Lane session and sends one design-level
|
|
887
|
+
* correction as `lane_instruction` kind `correction`.
|
|
888
|
+
*/
|
|
889
|
+
role: "coordinator";
|
|
890
|
+
action: "correct";
|
|
891
|
+
task_id: string;
|
|
892
|
+
reason: string;
|
|
893
|
+
corrections_used: number;
|
|
894
|
+
corrections_left: number;
|
|
895
|
+
lane_path: string;
|
|
896
|
+
}
|
|
710
897
|
| {
|
|
711
898
|
role: "executor";
|
|
712
899
|
task_id: string;
|
|
@@ -756,5 +943,11 @@ function canonicalReportBytes(report: PersistableReport): string {
|
|
|
756
943
|
|
|
757
944
|
/** All terminal batch states; needs_human is a park, not terminal. */
|
|
758
945
|
export function isTerminalBatchState(state: BatchRunState): boolean {
|
|
759
|
-
return
|
|
946
|
+
return (
|
|
947
|
+
state === "completed" ||
|
|
948
|
+
state === "budget_stopped" ||
|
|
949
|
+
state === "failed" ||
|
|
950
|
+
state === "rejected" ||
|
|
951
|
+
state === "superseded"
|
|
952
|
+
);
|
|
760
953
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Read-only identification of a Lane worktree from its checked-out branch.
|
|
2
|
+
// A Lane sits on `imm-lane/<initiative-slug>/<task-id>` (ADR 0013), so the
|
|
3
|
+
// settling Host can tell a Lane settlement from a coordinator or single-task
|
|
4
|
+
// settlement without reading any batch state.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
|
|
9
|
+
const LANE_BRANCH = /^imm-lane\/[^/]+\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
|
|
10
|
+
|
|
11
|
+
/** The task id the Lane at `root` was cut for, or null when `root` is not a Lane. */
|
|
12
|
+
export function laneTaskOfWorkspace(root: string): string | null {
|
|
13
|
+
const branch = spawnSync("git", ["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
+
});
|
|
17
|
+
if (branch.status !== 0) return null;
|
|
18
|
+
return LANE_BRANCH.exec((branch.stdout ?? "").trim())?.[1] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True when `root` is the Lane of `taskId`: its tracker close waits for integration. */
|
|
22
|
+
export function isLaneWorkspaceForTask(root: string, taskId: string): boolean {
|
|
23
|
+
return laneTaskOfWorkspace(root) === taskId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function realpathOfNearest(path: string): string {
|
|
27
|
+
let current = path;
|
|
28
|
+
const suffix: string[] = [];
|
|
29
|
+
for (;;) {
|
|
30
|
+
try {
|
|
31
|
+
return join(realpathSync(current), ...suffix.reverse());
|
|
32
|
+
} catch {
|
|
33
|
+
const parent = dirname(current);
|
|
34
|
+
if (parent === current) return path;
|
|
35
|
+
suffix.push(basename(current));
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refusal text for an `edit`/`write` a Lane session aims outside its Lane, or
|
|
43
|
+
* null when `cwd` is not a Lane or the target is inside it. The target is
|
|
44
|
+
* resolved against `cwd` and through symlinks of its nearest existing
|
|
45
|
+
* ancestor. Only file-editing tools are checked: a shell command is not
|
|
46
|
+
* intercepted, and the contract says so.
|
|
47
|
+
*/
|
|
48
|
+
export function laneWriteRefusal(cwd: string, target: unknown): string | null {
|
|
49
|
+
if (typeof target !== "string" || !target) return null;
|
|
50
|
+
if (laneTaskOfWorkspace(cwd) === null) return null;
|
|
51
|
+
const top = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
52
|
+
if (top.status !== 0) return null;
|
|
53
|
+
const laneRoot = realpathOfNearest((top.stdout ?? "").trim());
|
|
54
|
+
const resolved = realpathOfNearest(isAbsolute(target) ? target : resolve(cwd, target));
|
|
55
|
+
const inside = relative(laneRoot, resolved);
|
|
56
|
+
if (inside === "" || (inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside))) return null;
|
|
57
|
+
return `Lane write refused: ${resolved} is outside this Lane. In a batch Lane every edit and write stays under ${laneRoot}; use the same repository-relative path inside the Lane.`;
|
|
58
|
+
}
|
|
@@ -32,4 +32,4 @@ worktree, or unmanaged implementation.
|
|
|
32
32
|
Every sourced branch closes as resolved, explicitly excluded, explicitly
|
|
33
33
|
deferred, or dependent-blocked with its blocking node named, including for a
|
|
34
34
|
clear zero-question request; a confirmed outcome is never conserved as a
|
|
35
|
-
smaller deliverable.
|
|
35
|
+
smaller deliverable, and a confirmed user scenario is a `BR-SCN-*` item with Actor, Given, When, Then, written in English, whose Then is a result the actor observes directly; framing without a user-observable change gets no invented scenario.
|
|
@@ -16,7 +16,9 @@ grants execution authority — only the native Enrollment gate can. Every Spec d
|
|
|
16
16
|
user-owned one with no source is removed or returned for clarification. Before
|
|
17
17
|
handoff it maps every upstream item and required phase exactly once and reports
|
|
18
18
|
each Slice result plus the remaining gap; a plan that conserves a smaller
|
|
19
|
-
deliverable than the confirmed outcome fails that check instead of passing
|
|
19
|
+
deliverable than the confirmed outcome fails that check instead of passing, and a
|
|
20
|
+
Spec carries user scenarios mapped one way to acceptance, or records `no
|
|
21
|
+
user-visible change` with a reason.
|
|
20
22
|
|
|
21
23
|
Section routes - load a section's instructions only when its branch applies.
|
|
22
24
|
Read each linked heading body up to the next heading; nested sections and
|