immune-brain 3.6.4 → 3.6.5
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 +45 -18
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +147 -48
- package/plugins/immune-brain/dist/imm-loop.md +8 -2
- package/plugins/immune-brain/dist/imm-planner.md +9 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +18 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +31 -9
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
- package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +94 -3
- package/plugins/immune-brain/runtime/kernel/application.ts +1 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +17 -8
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +72 -13
- package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
- package/plugins/immune-brain/runtime/kernel/reducer.ts +34 -8
- package/plugins/immune-brain/runtime/kernel/types.ts +1 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
|
@@ -163,18 +163,23 @@ After approval, author, stage, and validate every TaskIntent in the decompositio
|
|
|
163
163
|
with `valid: true` and `enrollment_ready: true`. Resolve `../bin/imm-tracker` from this packaged contract; do not assume a bare command is on `PATH`. Submit the entire approved set once through
|
|
164
164
|
`imm-tracker publish-initiative --stdin --json`. Its input contains the confirmed
|
|
165
165
|
Initiative slug and goal, Parent projection, and every Child's `slice_id`,
|
|
166
|
-
canonical TaskIntent path,
|
|
166
|
+
canonical TaskIntent path, bounded public `acceptance` summaries, and public
|
|
167
|
+
projection. The Parent projection requires
|
|
167
168
|
`problem`, `result`, and `design`, and may include `decisions`,
|
|
168
169
|
`testing_strategy`, and `out_of_scope`. `design` records Initiative-level
|
|
169
170
|
invariants, Slice boundaries and ordering, shared interfaces or state flow, and
|
|
170
171
|
material compatibility decisions. Every Parent Slice must correspond to one
|
|
171
172
|
published Child; future checklist-only Slices are not allowed in the batch.
|
|
172
173
|
|
|
173
|
-
Each Child
|
|
174
|
+
Each Child must provide public `acceptance` entries with `id` and a 1-500
|
|
175
|
+
character `summary`. Their IDs must match every canonical TaskIntent acceptance
|
|
176
|
+
ID exactly once. Canonical assertion prose is authority evidence and must never
|
|
177
|
+
be copied into public GitHub projection. Each Child projection may contain
|
|
178
|
+
`result`, `current_behavior`,
|
|
174
179
|
`desired_behavior`, `key_interfaces`, `verification`, `blocked_by` Task IDs,
|
|
175
180
|
`out_of_scope`, and `agent_handoff`. The tracker rereads every canonical
|
|
176
|
-
TaskIntent for identity, risk, and acceptance; projection fields
|
|
177
|
-
TaskIntent scope or authority. It validates the complete dependency graph before
|
|
181
|
+
TaskIntent for identity, risk, and acceptance IDs; projection fields and public
|
|
182
|
+
summaries never widen TaskIntent scope or authority. It validates the complete dependency graph before
|
|
178
183
|
remote writes, creates the Parent once, creates all Children, attaches every
|
|
179
184
|
Child as a native Sub-issue, creates native `blocked_by` relations, and rereads
|
|
180
185
|
the complete topology. The Child Agent Brief includes a direct Parent Issue link.
|
|
@@ -436,6 +436,7 @@ export class AssuranceCoordinator {
|
|
|
436
436
|
sessionGenerationValue(): number { return this.sessionGeneration; }
|
|
437
437
|
|
|
438
438
|
async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
|
|
439
|
+
if (this.isInvocationOpen(taskId)) return { state: "blocked", reason: "an authority invocation is already open" };
|
|
439
440
|
const active = this.active(taskId);
|
|
440
441
|
if (active?.state === "review_ready") {
|
|
441
442
|
const reservation = this.reviewReservations.get(taskId);
|
|
@@ -819,6 +820,17 @@ export class AssuranceCoordinator {
|
|
|
819
820
|
return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
|
|
820
821
|
}
|
|
821
822
|
|
|
823
|
+
isReviewVerdictValid(taskId: string, verdictInput: unknown): boolean {
|
|
824
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
825
|
+
if (!reservation) return false;
|
|
826
|
+
try {
|
|
827
|
+
parseAssuranceVerdict(verdictInput, reservation.snapshot);
|
|
828
|
+
return true;
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
822
834
|
abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult {
|
|
823
835
|
const reservation = this.reviewReservations.get(taskId);
|
|
824
836
|
if (!reservation) return { state: "blocked", reason };
|
|
@@ -833,6 +845,12 @@ export class AssuranceCoordinator {
|
|
|
833
845
|
return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch };
|
|
834
846
|
}
|
|
835
847
|
|
|
848
|
+
releaseStoppedReview(taskId: string): void {
|
|
849
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
850
|
+
if (reservation) this.releaseReviewReservation(taskId, reservation);
|
|
851
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
852
|
+
}
|
|
853
|
+
|
|
836
854
|
private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
|
|
837
855
|
if (this.reviewReservations.get(taskId) !== reservation) return;
|
|
838
856
|
this.reviewReservations.delete(taskId);
|
|
@@ -119,13 +119,16 @@ export async function submitClaudeReview(
|
|
|
119
119
|
if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
|
|
120
120
|
return { state: "blocked", reason: observed.reason };
|
|
121
121
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
123
|
+
if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
124
|
+
const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
|
|
125
|
+
if (!receiptValid) {
|
|
126
|
+
return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
const parentJson = extractVerdictJson(verdictInput)!;
|
|
129
|
+
const receiptJson = extractVerdictJson(observed.receipt.result)!;
|
|
130
|
+
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
131
|
+
return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
|
|
129
132
|
}
|
|
130
133
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
131
134
|
}
|
|
@@ -525,6 +528,19 @@ export class ClaudeRuntime {
|
|
|
525
528
|
return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
|
|
526
529
|
}
|
|
527
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Ordinary Kernel operation, not a privileged one: canary_application builds
|
|
533
|
+
* the action without a capability and the Pi Host lists resolve_finding in
|
|
534
|
+
* its ordinary KERNEL_OPERATIONS. The reducer owns every precondition, so
|
|
535
|
+
* this port reads no findings and tests no kind.
|
|
536
|
+
*/
|
|
537
|
+
async resolveFinding(taskId: string, findingId: string) {
|
|
538
|
+
return this.executeOrdinary({ cwd: this.cwd }, {
|
|
539
|
+
taskId,
|
|
540
|
+
operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
528
544
|
async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
|
|
529
545
|
if (operation === "repair_authority_state") {
|
|
530
546
|
const authority = reconcileKernelAuthority(this.cwd, taskId);
|
|
@@ -534,7 +550,7 @@ export class ClaudeRuntime {
|
|
|
534
550
|
return repairKernelAuthority(this.cwd, taskId, authority.revision);
|
|
535
551
|
}
|
|
536
552
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
|
|
537
|
-
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" = operation;
|
|
553
|
+
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" = operation;
|
|
538
554
|
let decisionOp: { finding_id: string; resolution: string } | undefined;
|
|
539
555
|
const projection = await this.status(taskId);
|
|
540
556
|
if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
|
|
@@ -551,6 +567,8 @@ export class ClaudeRuntime {
|
|
|
551
567
|
if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
|
|
552
568
|
op = "resolve_user_decision";
|
|
553
569
|
decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
|
|
570
|
+
} else if (readiness.state === "authorize_rework") {
|
|
571
|
+
op = "authorize_rework";
|
|
554
572
|
} else {
|
|
555
573
|
throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
|
|
556
574
|
}
|
|
@@ -668,7 +686,11 @@ export class ClaudeRuntime {
|
|
|
668
686
|
diffProvider: diffSnapshotOf,
|
|
669
687
|
now,
|
|
670
688
|
});
|
|
671
|
-
if (
|
|
689
|
+
if (
|
|
690
|
+
op === "stop" ||
|
|
691
|
+
op === "authorize_rework" ||
|
|
692
|
+
op === "approve_breaking_intent_revision"
|
|
693
|
+
) stagePlanningArtifactTransition(this.cwd, result.record);
|
|
672
694
|
return result;
|
|
673
695
|
} catch (error) {
|
|
674
696
|
if (sidecar && priorBytes && priorIndexState) {
|
|
@@ -773,7 +795,7 @@ export class ClaudeRuntime {
|
|
|
773
795
|
}));
|
|
774
796
|
}
|
|
775
797
|
|
|
776
|
-
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } }) {
|
|
798
|
+
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string } }) {
|
|
777
799
|
const { app } = await this.authority();
|
|
778
800
|
const operation = input.operation.op === "revise_intent"
|
|
779
801
|
? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
|
|
@@ -26,6 +26,7 @@ export const TOOLS = [
|
|
|
26
26
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
27
27
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
28
28
|
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
29
|
+
{ name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
|
|
29
30
|
] as const;
|
|
30
31
|
|
|
31
32
|
export function listMcpTools() {
|
|
@@ -39,8 +40,13 @@ export function listMcpTools() {
|
|
|
39
40
|
...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
|
|
40
41
|
...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
|
|
41
42
|
...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
|
|
43
|
+
...(tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}),
|
|
42
44
|
},
|
|
43
|
-
required: tool.name === "submit_review"
|
|
45
|
+
required: tool.name === "submit_review"
|
|
46
|
+
? ["task_id", "verdict"]
|
|
47
|
+
: tool.name === "resolve_finding"
|
|
48
|
+
? ["task_id", "finding_id"]
|
|
49
|
+
: ["task_id"],
|
|
44
50
|
},
|
|
45
51
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
|
|
46
52
|
}));
|
|
@@ -119,6 +125,13 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
119
125
|
if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
|
|
120
126
|
return runtime.submitReview(taskId, args.verdict);
|
|
121
127
|
}
|
|
128
|
+
if (name === "resolve_finding") {
|
|
129
|
+
// Structural only. Which findings may be resolved, and when, stays
|
|
130
|
+
// the reducer's decision; duplicating it here would create a second
|
|
131
|
+
// authority that could drift from the Kernel.
|
|
132
|
+
if (typeof args.finding_id !== "string" || !args.finding_id) throw new Error("finding_id is required");
|
|
133
|
+
return runtime.resolveFinding(taskId, args.finding_id);
|
|
134
|
+
}
|
|
122
135
|
if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
|
|
123
136
|
return runtime.authorize(taskId, name, toolMeta, args);
|
|
124
137
|
}
|
|
@@ -248,19 +248,6 @@ function runInspect(root: string): KernelExecution {
|
|
|
248
248
|
),
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
-
let declared: TaskRisk;
|
|
252
|
-
let intent: TaskIntentV1;
|
|
253
|
-
try {
|
|
254
|
-
const raw = JSON.parse(
|
|
255
|
-
readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
|
|
256
|
-
) as { risk?: unknown };
|
|
257
|
-
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
258
|
-
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
259
|
-
declared = raw.risk;
|
|
260
|
-
intent = parseTaskIntentV1(raw);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
return sourceFailure("inspect", error);
|
|
263
|
-
}
|
|
264
251
|
let recordRead: ReturnType<typeof readTaskRecordRaw>;
|
|
265
252
|
try {
|
|
266
253
|
recordRead = readTaskRecordRaw(root, claim.task_id);
|
|
@@ -285,6 +272,21 @@ function runInspect(root: string): KernelExecution {
|
|
|
285
272
|
};
|
|
286
273
|
}
|
|
287
274
|
const record = recordRead.record;
|
|
275
|
+
let declared: TaskRisk;
|
|
276
|
+
let intent: TaskIntentV1;
|
|
277
|
+
try {
|
|
278
|
+
const raw = JSON.parse(
|
|
279
|
+
readSecureProjectFile(root, record.intent_ref.path),
|
|
280
|
+
) as { risk?: unknown };
|
|
281
|
+
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
282
|
+
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
283
|
+
declared = raw.risk;
|
|
284
|
+
intent = parseTaskIntentV1(raw);
|
|
285
|
+
if (canonicalIntentHash(intent) !== record.intent_ref.content_hash)
|
|
286
|
+
throw new Error("TaskIntent sidecar does not match TaskRecord content hash");
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return sourceFailure("inspect", error);
|
|
289
|
+
}
|
|
288
290
|
const workspaceState = readWorkspaceStateRaw(root);
|
|
289
291
|
let identity;
|
|
290
292
|
try {
|
|
@@ -57,6 +57,7 @@ export interface InitiativePublicationInput {
|
|
|
57
57
|
tasks: Array<{
|
|
58
58
|
slice_id: string;
|
|
59
59
|
intent: string;
|
|
60
|
+
acceptance: Array<{ id: string; summary: string }>;
|
|
60
61
|
projection?: TaskProjection;
|
|
61
62
|
}>;
|
|
62
63
|
}
|
|
@@ -85,6 +86,18 @@ export interface GithubInitiativePublicationResult {
|
|
|
85
86
|
message: string;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export interface GithubInitiativeObservation {
|
|
90
|
+
contract: "immune_brain/github_initiative_observation/v1";
|
|
91
|
+
initiative_id: string;
|
|
92
|
+
issue_number: number;
|
|
93
|
+
tasks: Array<{
|
|
94
|
+
task_id: string;
|
|
95
|
+
slice_id: string;
|
|
96
|
+
issue_number: number;
|
|
97
|
+
blocked_by: string[];
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
export interface TaskProjection {
|
|
89
102
|
result?: string;
|
|
90
103
|
current_behavior?: string;
|
|
@@ -552,6 +565,63 @@ async function readBlockedByIds(
|
|
|
552
565
|
}
|
|
553
566
|
}
|
|
554
567
|
|
|
568
|
+
export async function observeGithubInitiative(
|
|
569
|
+
root: string,
|
|
570
|
+
initiativeId: string,
|
|
571
|
+
gh: GhTransport = createGhTransport(),
|
|
572
|
+
): Promise<GithubInitiativeObservation> {
|
|
573
|
+
const id = identifier(initiativeId, "initiative_id");
|
|
574
|
+
const source = await snapshot(resolve(root), gh, "create-initiative");
|
|
575
|
+
if ("contract" in source) throw new Error(source.message);
|
|
576
|
+
const parent = initiativeLookup(source.issues, source.repository.id, id);
|
|
577
|
+
if (parent.kind === "missing") throw new Error(`Initiative ${id} is not published`);
|
|
578
|
+
if (parent.kind === "ambiguous") throw new Error(parent.message);
|
|
579
|
+
const subIssueNumbers = await readSubIssueNumbers(root, gh, "create-initiative", source.repository, parent.issue.number);
|
|
580
|
+
if (!Array.isArray(subIssueNumbers)) throw new Error(subIssueNumbers.message);
|
|
581
|
+
if (new Set(subIssueNumbers).size !== subIssueNumbers.length)
|
|
582
|
+
throw new Error(`Initiative ${id} has duplicate native Sub-issue relations`);
|
|
583
|
+
const tasks = subIssueNumbers.map((issueNumber) => {
|
|
584
|
+
const matches = source.issues.filter((issue) => issue.number === issueNumber);
|
|
585
|
+
if (matches.length !== 1) throw new Error(`Initiative ${id} references an unreadable Sub-issue #${issueNumber}`);
|
|
586
|
+
const issue = matches[0];
|
|
587
|
+
const taskId = ownershipMarkerValue(issue.body, "task-id");
|
|
588
|
+
const sliceId = ownershipMarkerValue(issue.body, "slice-id");
|
|
589
|
+
if (!taskId || !sliceId || ownershipMarkerValue(issue.body, "initiative-id") !== id)
|
|
590
|
+
throw new Error(`Sub-issue #${issueNumber} has invalid Initiative ownership markers`);
|
|
591
|
+
const owned = ownedTaskLookup(source.issues, source.repository.id, taskId, id, sliceId);
|
|
592
|
+
if (owned.kind !== "found" || owned.issue.number !== issueNumber)
|
|
593
|
+
throw new Error(owned.kind === "ambiguous" ? owned.message : `Sub-issue #${issueNumber} has invalid Task ownership`);
|
|
594
|
+
return { task_id: taskId, slice_id: sliceId, issue_number: issueNumber, issue_id: issue.id };
|
|
595
|
+
});
|
|
596
|
+
if (new Set(tasks.map((task) => task.task_id)).size !== tasks.length)
|
|
597
|
+
throw new Error(`Initiative ${id} has duplicate Task identities`);
|
|
598
|
+
if (new Set(tasks.map((task) => task.slice_id)).size !== tasks.length)
|
|
599
|
+
throw new Error(`Initiative ${id} has duplicate Slice identities`);
|
|
600
|
+
const taskByIssueId = new Map(tasks.map((task) => [task.issue_id, task.task_id]));
|
|
601
|
+
const observed: GithubInitiativeObservation["tasks"] = [];
|
|
602
|
+
for (const task of tasks.sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0)) {
|
|
603
|
+
const blockerIds = await readBlockedByIds(root, gh, "create-initiative", source.repository, task.issue_number);
|
|
604
|
+
if (!Array.isArray(blockerIds)) throw new Error(blockerIds.message);
|
|
605
|
+
const blockedBy = blockerIds.map((blockerId) => {
|
|
606
|
+
const blocker = taskByIssueId.get(blockerId);
|
|
607
|
+
if (!blocker) throw new Error(`Task ${task.task_id} depends on an Issue outside Initiative ${id}`);
|
|
608
|
+
return blocker;
|
|
609
|
+
}).sort();
|
|
610
|
+
observed.push({
|
|
611
|
+
task_id: task.task_id,
|
|
612
|
+
slice_id: task.slice_id,
|
|
613
|
+
issue_number: task.issue_number,
|
|
614
|
+
blocked_by: blockedBy,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
contract: "immune_brain/github_initiative_observation/v1",
|
|
619
|
+
initiative_id: id,
|
|
620
|
+
issue_number: parent.issue.number,
|
|
621
|
+
tasks: observed,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
555
625
|
async function confirmBlockedBy(
|
|
556
626
|
root: string,
|
|
557
627
|
gh: GhTransport,
|
|
@@ -1070,7 +1140,7 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
|
|
|
1070
1140
|
const publications = input.tasks.map((task, index) => {
|
|
1071
1141
|
if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`tasks[${index}] must be an object`);
|
|
1072
1142
|
if (typeof task.intent !== "string") throw new Error(`tasks[${index}].intent must be a string`);
|
|
1073
|
-
return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.projection);
|
|
1143
|
+
return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.acceptance, task.projection);
|
|
1074
1144
|
});
|
|
1075
1145
|
const operations = publications.map((publication) => publication.operation);
|
|
1076
1146
|
const taskIds = new Set<string>();
|
|
@@ -1264,7 +1334,14 @@ function isSuccessfulTrackerStatus(status: TrackerStatus): boolean {
|
|
|
1264
1334
|
return status === "created" || status === "updated" || status === "already_current";
|
|
1265
1335
|
}
|
|
1266
1336
|
|
|
1267
|
-
function taskPublication(
|
|
1337
|
+
function taskPublication(
|
|
1338
|
+
root: string,
|
|
1339
|
+
initiativeId: string,
|
|
1340
|
+
sliceId: string,
|
|
1341
|
+
intentPath: string,
|
|
1342
|
+
acceptance: unknown,
|
|
1343
|
+
projection?: TaskProjection,
|
|
1344
|
+
): PreparedPublicationTask {
|
|
1268
1345
|
const absoluteRoot = resolve(root);
|
|
1269
1346
|
const absolutePath = resolve(absoluteRoot, intentPath);
|
|
1270
1347
|
const rel = relative(absoluteRoot, absolutePath);
|
|
@@ -1275,6 +1352,20 @@ function taskPublication(root: string, initiativeId: string, sliceId: string, in
|
|
|
1275
1352
|
const read = readTaskIntent(absoluteRoot, taskId);
|
|
1276
1353
|
if (read.intent_ref.path !== rel) throw new Error("TaskIntent path must match its canonical sidecar path");
|
|
1277
1354
|
const intent = read.intent;
|
|
1355
|
+
if (!Array.isArray(acceptance)) throw new Error(`Task ${taskId} requires public acceptance summaries`);
|
|
1356
|
+
const expectedIds = new Set(intent.acceptance.map((item) => item.id));
|
|
1357
|
+
const publicById = new Map<string, { id: string; summary: string }>();
|
|
1358
|
+
acceptance.forEach((item, index) => {
|
|
1359
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
1360
|
+
throw new Error(`Task ${taskId} acceptance[${index}] must be an object`);
|
|
1361
|
+
const raw = item as Record<string, unknown>;
|
|
1362
|
+
const id = identifier(raw.id, `Task ${taskId} acceptance[${index}].id`);
|
|
1363
|
+
if (!expectedIds.has(id)) throw new Error(`Task ${taskId} has unknown public acceptance id: ${id}`);
|
|
1364
|
+
if (publicById.has(id)) throw new Error(`Task ${taskId} has duplicate public acceptance id: ${id}`);
|
|
1365
|
+
publicById.set(id, { id, summary: projectionText(raw.summary, `Task ${taskId} acceptance[${index}].summary`, 500) });
|
|
1366
|
+
});
|
|
1367
|
+
const missingIds = [...expectedIds].filter((id) => !publicById.has(id));
|
|
1368
|
+
if (missingIds.length) throw new Error(`Task ${taskId} is missing public acceptance ids: ${missingIds.join(", ")}`);
|
|
1278
1369
|
return {
|
|
1279
1370
|
operation: validateOperation({
|
|
1280
1371
|
op: "upsert-task",
|
|
@@ -1283,7 +1374,7 @@ function taskPublication(root: string, initiativeId: string, sliceId: string, in
|
|
|
1283
1374
|
slice_id: sliceId,
|
|
1284
1375
|
goal: intent.goal,
|
|
1285
1376
|
risk: intent.risk,
|
|
1286
|
-
acceptance: intent.acceptance.map((item) => (
|
|
1377
|
+
acceptance: intent.acceptance.map((item) => publicById.get(item.id)!),
|
|
1287
1378
|
projection,
|
|
1288
1379
|
}) as Extract<TrackerOperation, { op: "upsert-task" }>,
|
|
1289
1380
|
intent_path: read.intent_ref.path,
|
|
@@ -160,6 +160,7 @@ export function applyTaskAction(
|
|
|
160
160
|
action.type === "record_approval" ||
|
|
161
161
|
action.type === "approve_breaking_intent_revision" ||
|
|
162
162
|
action.type === "request_rework" ||
|
|
163
|
+
action.type === "authorize_rework" ||
|
|
163
164
|
action.type === "stop" ||
|
|
164
165
|
action.type === "resolve_user_decision";
|
|
165
166
|
|
|
@@ -25,9 +25,10 @@ export interface AssuranceAuthorizationReadiness {
|
|
|
25
25
|
/**
|
|
26
26
|
* Kernel-decidable authorization readiness:
|
|
27
27
|
* - "resolve_user_decision": exactly one open unresolved-user-decision finding;
|
|
28
|
+
* - "authorize_rework": an open replan boundary can be overridden by the user;
|
|
28
29
|
* - "none": nothing uniquely decidable from Kernel facts.
|
|
29
30
|
*/
|
|
30
|
-
state: "resolve_user_decision" | "none";
|
|
31
|
+
state: "resolve_user_decision" | "authorize_rework" | "none";
|
|
31
32
|
/** Non-null only when Kernel facts prove the authorization is blocked. */
|
|
32
33
|
blocked: string | null;
|
|
33
34
|
}
|
|
@@ -75,6 +76,8 @@ export function deriveAssuranceAuthorization(input: {
|
|
|
75
76
|
state: "none",
|
|
76
77
|
blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
|
|
77
78
|
};
|
|
79
|
+
if (input.next_obligation === "revise_intent")
|
|
80
|
+
return { state: "authorize_rework", blocked: null };
|
|
78
81
|
return { state: "none", blocked: null };
|
|
79
82
|
}
|
|
80
83
|
|