immune-brain 5.1.1 → 5.2.0
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 +13 -6
- package/README.zh-CN.md +12 -5
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +14 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +93 -0
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +81 -13
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +20 -0
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8573 -7449
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +6 -6
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -37
- package/plugins/immune-brain/dist/imm-run.md +137 -12
- package/plugins/immune-brain/dist/imm-test-prune.md +233 -0
- package/plugins/immune-brain/dist/registry.yaml +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +15 -0
- package/plugins/immune-brain/hooks/hooks.json +31 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/alias_file.ts +24 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +9 -0
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +143 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +10 -1
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +32 -6
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +210 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +85 -7
- package/plugins/immune-brain/runtime/claude/review_host.ts +6 -6
- 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/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +15 -0
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +396 -23
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +25 -4
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +3 -2
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +42 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +155 -0
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +3 -2
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -6
- package/plugins/immune-brain/skills/imm-run/SKILL.md +2 -1
- package/plugins/immune-brain/skills/imm-test-prune/SKILL.md +28 -0
- package/plugins/immune-brain/skills/registry.yaml +9 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Pi and Claude Code are the supported hosts. Undeclared adapters remain unsupport
|
|
|
28
28
|
- [Recommended Workflows: Composing with mattpocock/skills](#recommended-workflows-composing-with-mattpocockskills)
|
|
29
29
|
- [Real-World Case Studies](#real-world-case-studies)
|
|
30
30
|
- [Core Philosophy: Turn "Judgment" into "Table Lookup"](#core-philosophy-turn-judgment-into-table-lookup-harnessing-multi-model-tiers-with-determinism)
|
|
31
|
-
- [The
|
|
31
|
+
- [The 8 Skills](#the-8-skills)
|
|
32
32
|
- [Lifecycle](#lifecycle)
|
|
33
33
|
- [Unattended Batch Runs](#unattended-batch-runs)
|
|
34
34
|
- [Configuration](#configuration)
|
|
@@ -54,7 +54,7 @@ Pi discovers Skills and extensions from `package.json` (or your global Pi config
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
No extra server config is needed. Installing the package via Pi makes all
|
|
57
|
+
No extra server config is needed. Installing the package via Pi makes all 8 Skills available automatically.
|
|
58
58
|
|
|
59
59
|
### In Claude Code
|
|
60
60
|
|
|
@@ -123,6 +123,7 @@ Immune-Brain provides two clean modes: **Host-native** for daily coding, and **M
|
|
|
123
123
|
| PR has review comments or failing CI | `/imm-pr-fix` on that PR | → Standalone repair: minimal scoped fix in place, no managed task created |
|
|
124
124
|
| Project docs out of date | `/imm-doc-prune` | → Read-only audit; deletes only user-approved stale docs from manifest |
|
|
125
125
|
| Agent instructions bloated | `/imm-doc-slim` | → Minimizes tracked `AGENTS.md` / `CLAUDE.md` to essential non-discoverable rules |
|
|
126
|
+
| Test suite bloated with redundant cases | `/imm-test-prune` | → Audits tests read-only, then merges or deletes only approved entries backed by retirement or coverage evidence |
|
|
126
127
|
| Which model's edits keep coming back for review | `/imm-retro` | → Ranks models by review load and reports project usage from session logs |
|
|
127
128
|
|
|
128
129
|
> **Core Principle: Skill-Explicit Entry**
|
|
@@ -412,7 +413,7 @@ Without explicit cost levers, multi-agent workflows compound expenses exponentia
|
|
|
412
413
|
|
|
413
414
|
---
|
|
414
415
|
|
|
415
|
-
## The
|
|
416
|
+
## The 8 Skills
|
|
416
417
|
|
|
417
418
|
| Skill | Type | When to use | What it does |
|
|
418
419
|
|---|---|---|---|
|
|
@@ -422,11 +423,12 @@ Without explicit cost levers, multi-agent workflows compound expenses exponentia
|
|
|
422
423
|
| `imm-pr-fix` | Standalone | CI failed / review comments on a PR | Repairs one PR in place, no managed authority |
|
|
423
424
|
| `imm-doc-prune` | Standalone | Stale current docs | Deletes only the hash-approved manifest entries |
|
|
424
425
|
| `imm-doc-slim` | Standalone | Bloated agent instructions | Minimizes tracked AGENTS/CLAUDE/GEMINI.md to necessary context |
|
|
426
|
+
| `imm-test-prune` | Standalone | Bloated or redundant tests | Merges, parameterizes, or deletes approved test entries without weakening assertions |
|
|
425
427
|
| `imm-retro` | Standalone | Compare models by review load | Ranks authors of reviewed code and reports project usage |
|
|
426
428
|
|
|
427
429
|
Internal roles (Executor, QA, Review, Compounder) are dispatched by `imm-run` — you never invoke them directly.
|
|
428
430
|
|
|
429
|
-
All
|
|
431
|
+
All 8 skills are invoked explicitly. For new features, start with `imm-brainstorm` (if requirements are uncertain) or `imm-planner` (if requirements are clear), then proceed to `imm-run` once enrolled.
|
|
430
432
|
|
|
431
433
|
### Managed Path entries (brainstorm → planner → loop)
|
|
432
434
|
|
|
@@ -474,6 +476,11 @@ The three repair/maintenance skills are host-native: they never create a managed
|
|
|
474
476
|
- **Trigger:** explicit request to minimize tracked `AGENTS.md` / `CLAUDE.md` / `GEMINI.md`.
|
|
475
477
|
- **What it does:** keeps only the necessary non-discoverable rules in agent instruction files, under the same read-only-audit + hash-bound-manifest-approval model as `imm-doc-prune`.
|
|
476
478
|
|
|
479
|
+
#### `imm-test-prune` — test suite pruning
|
|
480
|
+
|
|
481
|
+
- **Trigger:** explicit request to audit or prune redundant, bloated, or orphaned tests.
|
|
482
|
+
- **What it does:** inventories tests with the repository's own test and coverage commands, then merges, parameterizes, or deletes only entries you approved in an exact hash-bound manifest. Deletion needs retirement proof or coverage-backed subsumption; without coverage tooling those entries stay unverified. It never weakens an assertion or touches production source.
|
|
483
|
+
|
|
477
484
|
#### `imm-retro` — review load and project usage
|
|
478
485
|
|
|
479
486
|
- **Trigger:** explicit request for a cross-model review retro or project usage look-back.
|
|
@@ -652,7 +659,7 @@ See [`docs/reference/immune-brain-config.md`](docs/reference/immune-brain-config
|
|
|
652
659
|
package.json # Pi package manifest (skills + extensions)
|
|
653
660
|
plugins/immune-brain/
|
|
654
661
|
├── .pi-extension/ # Pi TUI + Kernel authority extension
|
|
655
|
-
├── skills/ #
|
|
662
|
+
├── skills/ # 8 public Skills (trigger shims)
|
|
656
663
|
├── dist/ # Built skill contracts & references
|
|
657
664
|
├── runtime/ # Bun + TypeScript runtime & Kernel
|
|
658
665
|
└── bin/ # CLI wrappers (→ runtime/v4_runtime.ts)
|
|
@@ -670,7 +677,7 @@ docs/specs/ # Living specs (updated in place)
|
|
|
670
677
|
|
|
671
678
|
## FAQ
|
|
672
679
|
|
|
673
|
-
**Do I need to learn all
|
|
680
|
+
**Do I need to learn all 8 skills?** No. Most of the time you only need `/imm-planner` (to plan and enroll a task) and `/imm-run` (to build and verify it). Use `imm-brainstorm` when requirements need clarifying first, and the maintenance skills (`imm-pr-fix`, etc.) only when specific repair needs arise. Ordinary chat and simple edits don't need any skills at all.
|
|
674
681
|
|
|
675
682
|
**What if I interrupt or close the session mid-task?** State is safely stored on disk (`.imm/` + TaskIntent). In Pi or Claude Code, simply re-enter `/imm-run` to resume — the Kernel projection is authoritative.
|
|
676
683
|
|
package/README.zh-CN.md
CHANGED
|
@@ -28,7 +28,7 @@ Pi 与 Claude Code 是支持的宿主。未声明的适配器仍不受支持。C
|
|
|
28
28
|
- [推荐工作流:与 mattpocock/skills 组合使用](#推荐工作流与-mattpocockskills-组合使用)
|
|
29
29
|
- [实战案例](#实战案例)
|
|
30
30
|
- [核心设计哲学:把“判断”变成“查表”](#核心设计哲学把判断变成查表用确定性工程驾驭多模型)
|
|
31
|
-
- [
|
|
31
|
+
- [8 个 Skills](#8-个-skills)
|
|
32
32
|
- [生命周期](#生命周期)
|
|
33
33
|
- [无人值守批次运行](#无人值守批次运行)
|
|
34
34
|
- [配置](#配置)
|
|
@@ -54,7 +54,7 @@ Pi 通过 `package.json`(或全局 Pi 配置)自动发现 Skills 与扩展
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
无需额外 server 配置,通过 Pi 安装本 package 后
|
|
57
|
+
无需额外 server 配置,通过 Pi 安装本 package 后 8 个 Skill 即自动可用。
|
|
58
58
|
|
|
59
59
|
### 在 Claude Code 中使用
|
|
60
60
|
|
|
@@ -123,6 +123,7 @@ Immune-Brain 提供两种清晰的工作模式:日常轻量编码走 **Host-na
|
|
|
123
123
|
| PR 被评论 / CI 挂了 | 对该 PR 使用 `/imm-pr-fix` | → 独立修复:在当前 PR 内针对性修复,不创建新 managed 任务 |
|
|
124
124
|
| 文档过时需要清理 | `/imm-doc-prune` | → 只读审计过时文档,仅删除经哈希审批的条目 |
|
|
125
125
|
| Agent 指令文件膨胀 | `/imm-doc-slim` | → 将 tracked `AGENTS.md` / `CLAUDE.md` 压到最小必要上下文 |
|
|
126
|
+
| 测试用例膨胀、重复 | `/imm-test-prune` | → 只读审计测试,仅合并或删除经审批、有退休或覆盖率证据的条目 |
|
|
126
127
|
| 想知道哪个模型的改动总被审查 | `/imm-retro` | → 按模型排名审查负载,并从 session logs 汇报项目使用量 |
|
|
127
128
|
|
|
128
129
|
> **核心原则:Skill 显式调用**
|
|
@@ -415,7 +416,7 @@ Immune-Brain 的核心假设是:**不要让弱模型做架构决策,把它
|
|
|
415
416
|
|
|
416
417
|
---
|
|
417
418
|
|
|
418
|
-
##
|
|
419
|
+
## 8 个 Skills
|
|
419
420
|
|
|
420
421
|
| Skill | 类型 | 何时使用 | 职责 |
|
|
421
422
|
|---|---|---|---|
|
|
@@ -425,11 +426,12 @@ Immune-Brain 的核心假设是:**不要让弱模型做架构决策,把它
|
|
|
425
426
|
| `imm-pr-fix` | 独立 | PR 需修复 | 原地修复单个 PR,不触及 managed authority |
|
|
426
427
|
| `imm-doc-prune` | 独立 | 清理过时文档 | 仅删除哈希绑定的 manifest 条目 |
|
|
427
428
|
| `imm-doc-slim` | 独立 | Agent instruction 膨胀 | 将 tracked AGENTS/CLAUDE/GEMINI.md 压到最小必要上下文 |
|
|
429
|
+
| `imm-test-prune` | 独立 | 测试膨胀或重复 | 合并、参数化或删除经审批的测试条目,不弱化断言 |
|
|
428
430
|
| `imm-retro` | 独立 | 比较模型的审查负载 | 排名被审查代码的作者并汇报项目使用量 |
|
|
429
431
|
|
|
430
432
|
Executor、QA、Review、Compounder 等为 `imm-run` 内部调度的角色,无需手动调用。
|
|
431
433
|
|
|
432
|
-
所有
|
|
434
|
+
所有 8 个 Skill 均显式调用。新需求开发时:若需求含糊先调 `imm-brainstorm`,目标清晰直接调 `imm-planner`,完成确认后调 `imm-run` 推进闭环。
|
|
433
435
|
|
|
434
436
|
### Managed Path 入口(brainstorm → planner → loop)
|
|
435
437
|
|
|
@@ -477,6 +479,11 @@ Executor、QA、Review、Compounder 等为 `imm-run` 内部调度的角色,无
|
|
|
477
479
|
- **触发方式:** 显式要求精简版本控制下的 `AGENTS.md` / `CLAUDE.md` / `GEMINI.md`。
|
|
478
480
|
- **职责:** 遵循与 `imm-doc-prune` 相同的「只读审计 + 哈希清单审批」模式,仅保留无法直接推导的必要规则。
|
|
479
481
|
|
|
482
|
+
#### `imm-test-prune` — 测试套件瘦身
|
|
483
|
+
|
|
484
|
+
- **触发方式:** 显式要求审计或清理冗余、膨胀或孤立的测试。
|
|
485
|
+
- **职责:** 使用仓库自身的测试与覆盖率命令盘点测试,按用户明确审批的哈希绑定 manifest 合并、参数化或删除条目。删除需要行为退休证明或覆盖率支撑的包含关系;项目没有覆盖率工具时这类条目保持 unverified。从不弱化断言,也不修改生产代码。
|
|
486
|
+
|
|
480
487
|
#### `imm-retro` — 审查负载与项目使用回顾
|
|
481
488
|
|
|
482
489
|
- **触发方式:** 显式要求跨模型审查复盘或项目使用量回顾。
|
|
@@ -660,7 +667,7 @@ Immune-Brain **没有独立配置文件**,偏好设置写在仓库根目录下
|
|
|
660
667
|
package.json # Pi package manifest(skills + extensions)
|
|
661
668
|
plugins/immune-brain/
|
|
662
669
|
├── .pi-extension/ # Pi TUI + Kernel 扩展
|
|
663
|
-
├── skills/ #
|
|
670
|
+
├── skills/ # 8 个公开 Skills(触发 shim)
|
|
664
671
|
├── dist/ # 构建后的 skill 契约与参考文档
|
|
665
672
|
├── runtime/ # Bun + TypeScript 运行时与 Kernel
|
|
666
673
|
└── bin/ # CLI wrappers(→ runtime/v4_runtime.ts)
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
enrollmentGitBaseNotice,
|
|
27
27
|
initializeEnrollmentGitBase,
|
|
28
28
|
} from "../runtime/assurance/enrollment_git_base";
|
|
29
|
-
import { readTaskIntentForRecord } from "./imm-canary-work";
|
|
29
|
+
import { piRunEntryRefusal, readTaskIntentForRecord } from "./imm-canary-work";
|
|
30
30
|
import {
|
|
31
31
|
presentTaskRail,
|
|
32
32
|
presentTaskRailResult,
|
|
@@ -609,7 +609,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
609
609
|
description: "Run one exact Kernel new-task enrollment synchronously in the foreground with host cancellation before authority commit.",
|
|
610
610
|
promptSnippet: "Kernel enrollment: invoke once in foreground and consume the direct terminal result.",
|
|
611
611
|
promptGuidelines: [
|
|
612
|
-
"Call from the Parent
|
|
612
|
+
"Call from the Parent only as the first step of an explicit imm-run entry, never from imm-planner or imm-brainstorm; execute once in the foreground and consume the terminal Tool result.",
|
|
613
613
|
"Do not run this Tool in background, poll for completion, or issue a cancel subcommand; host cancellation is the only pre-commit cancellation path.",
|
|
614
614
|
],
|
|
615
615
|
parameters: Type.Object(
|
|
@@ -647,6 +647,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
647
647
|
message: "imm_canary_enrollment is TUI-only",
|
|
648
648
|
next_action: "invoke the TUI launcher",
|
|
649
649
|
});
|
|
650
|
+
// The phase entry gate (ADR 0019), before any preparation or dialog.
|
|
651
|
+
const entryRefusal = piRunEntryRefusal("enroll", ctx);
|
|
652
|
+
if (entryRefusal)
|
|
653
|
+
throwToolFailure({
|
|
654
|
+
tool: "imm_canary_enrollment",
|
|
655
|
+
task_id: taskId,
|
|
656
|
+
operation: action,
|
|
657
|
+
state: "blocked",
|
|
658
|
+
code: entryRefusal.reason_code,
|
|
659
|
+
message: entryRefusal.detail,
|
|
660
|
+
next_action: entryRefusal.recovery_action,
|
|
661
|
+
});
|
|
650
662
|
const authorityRegistry = await registry();
|
|
651
663
|
const result = await coordinator.run(action, taskId, signal, (foregroundSignal, beginCommit) =>
|
|
652
664
|
executeForegroundEnrollment(
|
|
@@ -21,6 +21,7 @@ import type { ConfirmationReferenceSource } from "../runtime/assurance/host_port
|
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
22
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
23
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
24
25
|
import { join, resolve } from "node:path";
|
|
25
26
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
27
|
import { Type } from "typebox";
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
writeNativeReviewEvidence,
|
|
36
37
|
} from "./pi-canary-review-bundle";
|
|
37
38
|
import type { InvocationToken } from "./pi-canary-invocations";
|
|
39
|
+
import { readAliasFile } from "../runtime/assurance/alias_file";
|
|
38
40
|
import { runDeterministicQa } from "../runtime/assurance/qa";
|
|
39
41
|
import {
|
|
40
42
|
captureStagedIntent,
|
|
@@ -130,6 +132,17 @@ import {
|
|
|
130
132
|
type GithubTrackerResult,
|
|
131
133
|
} from "../runtime/github_issue_tracker";
|
|
132
134
|
import { reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
135
|
+
import { laneWriteRefusal } from "../runtime/unattended/lane_workspace";
|
|
136
|
+
import { activeTaskNotice } from "../runtime/assurance/active_task_notice";
|
|
137
|
+
import {
|
|
138
|
+
admitPhaseOperation,
|
|
139
|
+
applyPrompt,
|
|
140
|
+
NO_PHASE_ENTRY,
|
|
141
|
+
recognizePhaseEntry,
|
|
142
|
+
type AliasReader,
|
|
143
|
+
type PhaseAdmission,
|
|
144
|
+
type PhaseEntryState,
|
|
145
|
+
} from "../runtime/assurance/phase_entry";
|
|
133
146
|
|
|
134
147
|
const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
|
|
135
148
|
const LOOP_TARGETS = [
|
|
@@ -285,6 +298,67 @@ export async function advancePiTask(root: string, taskId: string): Promise<any>
|
|
|
285
298
|
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
|
|
286
299
|
}
|
|
287
300
|
|
|
301
|
+
const GLOBAL_PI_PHASE_ENTRY_KEY = Symbol.for("immune_brain.pi_phase_entry");
|
|
302
|
+
|
|
303
|
+
type PhaseEntryContext = { cwd?: string; sessionManager?: { getSessionId?: () => unknown } } | undefined;
|
|
304
|
+
|
|
305
|
+
// Pi loads each extension file as its own module graph, so the entry state
|
|
306
|
+
// lives under a global key that every copy of this module shares. It is
|
|
307
|
+
// process memory: a resumed session starts without an entry and refuses.
|
|
308
|
+
function piPhaseEntries(): Map<string, PhaseEntryState> {
|
|
309
|
+
const holder = globalThis as unknown as Record<symbol, Map<string, PhaseEntryState> | undefined>;
|
|
310
|
+
return holder[GLOBAL_PI_PHASE_ENTRY_KEY] ??= new Map();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function piSessionId(ctx: PhaseEntryContext): string {
|
|
314
|
+
try {
|
|
315
|
+
const id = ctx?.sessionManager?.getSessionId?.();
|
|
316
|
+
return typeof id === "string" ? id : "";
|
|
317
|
+
} catch {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Prompt templates in the order Pi resolves them: the user's, then the project's. */
|
|
323
|
+
function piAliasReader(cwd: string): AliasReader {
|
|
324
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
325
|
+
return (name) => {
|
|
326
|
+
for (const dir of [join(agentDir, "prompts"), join(cwd, ".pi", "prompts")]) {
|
|
327
|
+
const body = readAliasFile(join(dir, `${name}.md`));
|
|
328
|
+
if (body !== null) return body;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* One Host input: record whether this literal prompt entered a phase. Input an
|
|
336
|
+
* extension injected is never the user's and leaves the entry state untouched.
|
|
337
|
+
*/
|
|
338
|
+
export function recordPiPhaseInput(event: { text?: unknown; source?: unknown }, ctx: PhaseEntryContext): void {
|
|
339
|
+
if (event.source === "extension") return;
|
|
340
|
+
const sessionId = piSessionId(ctx);
|
|
341
|
+
if (!sessionId) return;
|
|
342
|
+
const entries = piPhaseEntries();
|
|
343
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
344
|
+
const entry = recognizePhaseEntry(text, piAliasReader(ctx?.cwd ?? process.cwd()));
|
|
345
|
+
entries.set(sessionId, applyPrompt(entries.get(sessionId) ?? NO_PHASE_ENTRY, entry));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The phase entry gate for a Pi tool, checked before any preparation or
|
|
350
|
+
* authority dialog. A session without an identity refuses.
|
|
351
|
+
*/
|
|
352
|
+
export function piRunEntryRefusal(
|
|
353
|
+
operation: "enroll" | "start_unattended_batch",
|
|
354
|
+
ctx: PhaseEntryContext,
|
|
355
|
+
): Extract<PhaseAdmission, { ok: false }> | null {
|
|
356
|
+
const sessionId = piSessionId(ctx);
|
|
357
|
+
const state = sessionId ? piPhaseEntries().get(sessionId) ?? NO_PHASE_ENTRY : null;
|
|
358
|
+
const admission = admitPhaseOperation({ kind: "tool", name: operation }, state, { activeClaim: false });
|
|
359
|
+
return admission.ok ? null : admission;
|
|
360
|
+
}
|
|
361
|
+
|
|
288
362
|
export default function (
|
|
289
363
|
pi: ExtensionAPI,
|
|
290
364
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
@@ -340,6 +414,7 @@ export default function (
|
|
|
340
414
|
});
|
|
341
415
|
pi.on("input", async (event, ctx) => {
|
|
342
416
|
if (event.source === "extension") return { action: "continue" } as const;
|
|
417
|
+
recordPiPhaseInput(event, ctx);
|
|
343
418
|
railContext = ctx;
|
|
344
419
|
clearTerminalTaskRailOnInput(ctx);
|
|
345
420
|
await refreshTaskRail(ctx);
|
|
@@ -371,8 +446,16 @@ export default function (
|
|
|
371
446
|
if (ctx.mode !== "tui") return;
|
|
372
447
|
await refreshTaskRail(ctx);
|
|
373
448
|
});
|
|
449
|
+
pi.on("before_agent_start", async (event: { prompt?: string; systemPrompt?: string }, ctx?: ExtensionContext) => {
|
|
450
|
+
// Advisory only: an active Managed task is announced, never resumed.
|
|
451
|
+
const notice = await activeTaskNotice(ctx?.cwd ?? process.cwd());
|
|
452
|
+
if (!notice || typeof event.systemPrompt !== "string") return undefined;
|
|
453
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${notice}` };
|
|
454
|
+
});
|
|
374
455
|
pi.on("tool_call", (event: { toolName?: string; input?: unknown; toolCallId?: string }, ctx?: ExtensionContext) => {
|
|
375
456
|
if (ctx) railContext = ctx;
|
|
457
|
+
const laneRefusal = laneEditRefusal(event, ctx?.cwd ?? process.cwd());
|
|
458
|
+
if (laneRefusal) return { block: true, reason: laneRefusal };
|
|
376
459
|
if (event.toolName === "Agent") {
|
|
377
460
|
// ADR 0017: correlate a reserved reviewer dispatch to its reservation.
|
|
378
461
|
progression.piReviewHost.observeReviewDispatch(event.input as { prompt?: unknown; subagent_type?: unknown } | undefined, event.toolCallId);
|
|
@@ -1162,6 +1245,16 @@ export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskI
|
|
|
1162
1245
|
return readTaskIntent(root, taskId, currentPath);
|
|
1163
1246
|
}
|
|
1164
1247
|
|
|
1248
|
+
/**
|
|
1249
|
+
* In a batch Lane, an `edit` or `write` whose resolved target is outside the
|
|
1250
|
+
* Lane is refused before it runs; `bash` is not intercepted.
|
|
1251
|
+
*/
|
|
1252
|
+
export function laneEditRefusal(event: { toolName?: string; input?: unknown }, cwd: string): string | null {
|
|
1253
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return null;
|
|
1254
|
+
const input = (event.input ?? {}) as { path?: unknown; file_path?: unknown };
|
|
1255
|
+
return laneWriteRefusal(cwd, input.path ?? input.file_path);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1165
1258
|
async function markGithubTaskTerminal(
|
|
1166
1259
|
root: string,
|
|
1167
1260
|
input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
|
|
@@ -15,13 +15,16 @@ import {
|
|
|
15
15
|
startBatch,
|
|
16
16
|
type BatchRunnerKernelPort,
|
|
17
17
|
type BatchRunReport,
|
|
18
|
+
type BatchTrackerPort,
|
|
18
19
|
} from "../runtime/unattended/batch_runner";
|
|
19
20
|
import { createBatchKernelPort } from "../runtime/unattended/batch_kernel_port";
|
|
20
21
|
import type { BatchRunnerGitPort } from "../runtime/unattended/batch_git";
|
|
21
22
|
import type { InitiativeObservationReader } from "../runtime/unattended/types";
|
|
22
|
-
import { advancePiTask } from "./imm-canary-work";
|
|
23
|
+
import { advancePiTask, piRunEntryRefusal } from "./imm-canary-work";
|
|
23
24
|
import { batchReason } from "../runtime/unattended/batch_reasons";
|
|
24
|
-
import {
|
|
25
|
+
import { parseFinalVerification } from "../runtime/unattended/batch_final_verification";
|
|
26
|
+
import { parseLaneRevision, REVISION_DELEGATION_TEXT, type LaneRevisionRequest } from "../runtime/unattended/batch_delegation";
|
|
27
|
+
import { createBatchTrackerPort, laneRuntimeContractRefusal, parseLaneInstruction, type LaneInstructionRequest, parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
|
|
25
28
|
import {
|
|
26
29
|
authorizeBatch,
|
|
27
30
|
projectBatchPreflight,
|
|
@@ -70,8 +73,9 @@ export async function readSettledTaskRecord(
|
|
|
70
73
|
* is the testable unit, and the direct-execution suites cover the decision
|
|
71
74
|
* outcomes (decline -> rejected, cancel/Escape -> cancelled) with zero writes.
|
|
72
75
|
*/
|
|
73
|
-
export function mapDialogSelection(selected: string | undefined): "accept" | "decline" | "cancel" {
|
|
76
|
+
export function mapDialogSelection(selected: string | undefined): "accept" | "accept_delegate" | "decline" | "cancel" {
|
|
74
77
|
if (selected === "confirm") return "accept";
|
|
78
|
+
if (selected === "confirm_delegate") return "accept_delegate";
|
|
75
79
|
if (selected === "decline") return "decline";
|
|
76
80
|
return "cancel";
|
|
77
81
|
}
|
|
@@ -107,8 +111,16 @@ export interface PiBatchExecutionOptions {
|
|
|
107
111
|
max_parallel?: number;
|
|
108
112
|
/** Lane mode: Lanes someone else created, offered to the batch. */
|
|
109
113
|
lane_offers?: LaneOffer[];
|
|
114
|
+
/** Lane mode: the project's full verification commands, run once every child is integrated. */
|
|
115
|
+
final_verification?: string[];
|
|
116
|
+
/** Lane mode: one instruction for an idle Lane session, recorded before it is sent. */
|
|
117
|
+
lane_instruction?: LaneInstructionRequest;
|
|
118
|
+
/** Lane mode, ADR 0018: one breaking Intent revision the coordinator approves under the batch's grant. */
|
|
119
|
+
lane_revision?: LaneRevisionRequest;
|
|
110
120
|
batchKernel?: Partial<BatchRunnerKernelPort>;
|
|
111
121
|
batchGit?: BatchRunnerGitPort;
|
|
122
|
+
/** Lane mode: closes an integrated child's Issue; defaults to the GitHub projection. */
|
|
123
|
+
batchTracker?: BatchTrackerPort;
|
|
112
124
|
readInitiative?: InitiativeObservationReader;
|
|
113
125
|
confirmBatch?: (details: {
|
|
114
126
|
title: string;
|
|
@@ -116,7 +128,9 @@ export interface PiBatchExecutionOptions {
|
|
|
116
128
|
details: string;
|
|
117
129
|
planDigest: string;
|
|
118
130
|
signal?: AbortSignal;
|
|
119
|
-
|
|
131
|
+
/** ADR 0018: offer the opt-in delegation choice (lane mode only). */
|
|
132
|
+
offerRevisionDelegation?: boolean;
|
|
133
|
+
}) => Promise<"accept" | "accept_delegate" | "decline" | "cancel">;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
export type PiBatchExecutionResult =
|
|
@@ -148,7 +162,7 @@ export async function executePiUnattendedBatch(
|
|
|
148
162
|
if (!reuseOnly && !interactive) return { state: "rejected", ...nonInteractiveRefusal() };
|
|
149
163
|
|
|
150
164
|
// A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
|
|
151
|
-
const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers);
|
|
165
|
+
const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers, options.lane_instruction ?? options.lane_revision);
|
|
152
166
|
if (resolvedParallel !== undefined) options = { ...options, max_parallel: resolvedParallel };
|
|
153
167
|
|
|
154
168
|
// 1. Host-independent batch preflight: claim ownership, branch availability,
|
|
@@ -171,6 +185,12 @@ export async function executePiUnattendedBatch(
|
|
|
171
185
|
const budget = preflight.projection.budget;
|
|
172
186
|
const planDigest = preflight.projection.plan_digest;
|
|
173
187
|
const recoveryChildren = preflight.projection.recovery_children;
|
|
188
|
+
// A new lane batch whose Lane runtime cannot read the coordinator's Kernel
|
|
189
|
+
// contracts is refused before the gate opens; a match adds nothing.
|
|
190
|
+
if (options.max_parallel !== undefined && !isResuming) {
|
|
191
|
+
const contractRefusal = laneRuntimeContractRefusal(root);
|
|
192
|
+
if (contractRefusal) return batchReason("batch_run_rejected", contractRefusal);
|
|
193
|
+
}
|
|
174
194
|
|
|
175
195
|
// 4. Literal-user gate plus the shared reuse decision, the post-gate
|
|
176
196
|
// claim/drift cascade, and the Batch Authorization binding. The Host supplies
|
|
@@ -198,15 +218,16 @@ export async function executePiUnattendedBatch(
|
|
|
198
218
|
}
|
|
199
219
|
const confirmDetails = {
|
|
200
220
|
title: `Authorize Unattended Batch: ${facts.initiative_slug}`,
|
|
201
|
-
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}`,
|
|
221
|
+
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.max_parallel !== undefined && options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
|
|
202
222
|
details: `Ordered children (${facts.children.length}):\n${facts.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [risk: ${child.risk}] [status: ${child.status === "already_settled" ? "completed" : "pending execution"}]`).join("\n")}${facts.excluded.length > 0 ? `\n\nExcluded children:\n${facts.excluded.map((child) => ` - ${child.task_id} (${child.slice_id}): ${child.reason}`).join("\n")}` : ""}${options.max_parallel !== undefined ? `\n\nParallel groups (${facts.parallel_groups.length}):\n${facts.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}${facts.scope_conflicts.length > 0 ? `\n\nSerialized by overlapping scope:\n${facts.scope_conflicts.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : ""}` : ""}${facts.reuse_blockers.length > 0 ? `\n\nRe-confirmation required: ${facts.reuse_blockers.join(", ")}.\nRecovery: confirm to issue a fresh authorization bound to the current plan and HEAD.` : ""}`,
|
|
203
223
|
planDigest: facts.plan_digest,
|
|
204
224
|
signal,
|
|
225
|
+
...(options.max_parallel !== undefined ? { offerRevisionDelegation: true } : {}),
|
|
205
226
|
};
|
|
206
227
|
|
|
207
228
|
// The gate settles only on the literal user's answer or the caller's
|
|
208
229
|
// cancellation signal.
|
|
209
|
-
let decision: "accept" | "decline" | "cancel";
|
|
230
|
+
let decision: "accept" | "accept_delegate" | "decline" | "cancel";
|
|
210
231
|
try {
|
|
211
232
|
decision = await options.confirmBatch(confirmDetails);
|
|
212
233
|
} catch (err) {
|
|
@@ -223,10 +244,14 @@ export async function executePiUnattendedBatch(
|
|
|
223
244
|
if (decision === "decline") {
|
|
224
245
|
return { kind: "host_rejection", value: batchReason("confirmation_declined") };
|
|
225
246
|
}
|
|
226
|
-
if (decision !== "accept") {
|
|
247
|
+
if (decision !== "accept" && decision !== "accept_delegate") {
|
|
227
248
|
return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
|
|
228
249
|
}
|
|
229
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
kind: "confirmed",
|
|
252
|
+
request_id: randomUUID(),
|
|
253
|
+
...(decision === "accept_delegate" && options.max_parallel !== undefined ? { delegate_revisions: true } : {}),
|
|
254
|
+
};
|
|
230
255
|
},
|
|
231
256
|
confirmationRef: ({ batch_id, request_id }) =>
|
|
232
257
|
piConfirmationRef({
|
|
@@ -292,7 +317,16 @@ export async function executePiUnattendedBatch(
|
|
|
292
317
|
now,
|
|
293
318
|
kernel: kernelPort,
|
|
294
319
|
git: options.batchGit,
|
|
295
|
-
...(options.max_parallel !== undefined
|
|
320
|
+
...(options.max_parallel !== undefined
|
|
321
|
+
? {
|
|
322
|
+
max_parallel: options.max_parallel,
|
|
323
|
+
tracker: options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
|
|
324
|
+
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
325
|
+
...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
|
|
326
|
+
...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
|
|
327
|
+
...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
296
330
|
...(options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}),
|
|
297
331
|
});
|
|
298
332
|
|
|
@@ -393,7 +427,7 @@ export default function (
|
|
|
393
427
|
description: "Start an unattended serial batch run for an Initiative after native confirmation.",
|
|
394
428
|
promptSnippet: "Start unattended batch run: invoke once in foreground after plan confirmation.",
|
|
395
429
|
promptGuidelines: [
|
|
396
|
-
"Call only after plan confirmation; execute once in the foreground and consume the direct terminal result.",
|
|
430
|
+
"Call only inside an explicit imm-run entry, after plan confirmation; execute once in the foreground and consume the direct terminal result.",
|
|
397
431
|
"Do not run this Tool in background, poll for completion, or issue a cancel subcommand.",
|
|
398
432
|
],
|
|
399
433
|
parameters: Type.Object({
|
|
@@ -403,10 +437,21 @@ export default function (
|
|
|
403
437
|
lane_offers: Type.Optional(Type.Array(
|
|
404
438
|
Type.Object({ task_id: Type.String(), path: Type.String() }, { additionalProperties: false }),
|
|
405
439
|
)),
|
|
440
|
+
final_verification: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 8 })),
|
|
441
|
+
lane_instruction: Type.Optional(Type.Object({
|
|
442
|
+
task_id: Type.String(),
|
|
443
|
+
kind: Type.Optional(Type.Union([Type.Literal("instruction"), Type.Literal("correction")])),
|
|
444
|
+
text: Type.String(),
|
|
445
|
+
session_state: Type.Union([Type.Literal("idle"), Type.Literal("working"), Type.Literal("blocked"), Type.Literal("done")]),
|
|
446
|
+
}, { additionalProperties: false })),
|
|
447
|
+
lane_revision: Type.Optional(Type.Object({
|
|
448
|
+
task_id: Type.String(),
|
|
449
|
+
next_intent: Type.Object({}, { additionalProperties: true }),
|
|
450
|
+
}, { additionalProperties: false })),
|
|
406
451
|
}, { additionalProperties: false }),
|
|
407
452
|
execute: async (
|
|
408
453
|
_toolCallId: string,
|
|
409
|
-
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }
|
|
454
|
+
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; lane_instruction?: unknown; lane_revision?: unknown },
|
|
410
455
|
signal: AbortSignal | undefined,
|
|
411
456
|
_onUpdate: unknown,
|
|
412
457
|
ctx: ExtensionContext,
|
|
@@ -424,15 +469,34 @@ export default function (
|
|
|
424
469
|
next_action: refusal.recovery_action,
|
|
425
470
|
});
|
|
426
471
|
}
|
|
472
|
+
// The phase entry gate (ADR 0019), before any preflight or dialog. The
|
|
473
|
+
// batch's own reuseOnly continuation never passes through this Tool.
|
|
474
|
+
const entryRefusal = piRunEntryRefusal("start_unattended_batch", ctx);
|
|
475
|
+
if (entryRefusal)
|
|
476
|
+
throwToolFailure({
|
|
477
|
+
tool: "imm_canary_enrollment",
|
|
478
|
+
task_id: initiativeSlug,
|
|
479
|
+
operation: "start_unattended_batch",
|
|
480
|
+
state: "blocked",
|
|
481
|
+
code: entryRefusal.reason_code,
|
|
482
|
+
message: entryRefusal.detail,
|
|
483
|
+
next_action: entryRefusal.recovery_action,
|
|
484
|
+
});
|
|
427
485
|
|
|
428
486
|
const maxParallel = parseMaxParallel(params.max_parallel);
|
|
429
487
|
const laneOffers = parseLaneOffers(params.lane_offers);
|
|
488
|
+
const finalVerification = parseFinalVerification(params.final_verification);
|
|
489
|
+
const laneInstruction = parseLaneInstruction(params.lane_instruction);
|
|
490
|
+
const laneRevision = parseLaneRevision(params.lane_revision);
|
|
430
491
|
|
|
431
492
|
const result = await executePiUnattendedBatch({
|
|
432
493
|
root: ctx.cwd,
|
|
433
494
|
initiativeSlug,
|
|
434
495
|
...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
|
|
435
496
|
...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
|
|
497
|
+
...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
|
|
498
|
+
...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
|
|
499
|
+
...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
|
|
436
500
|
interactive: ctx.mode === "tui",
|
|
437
501
|
signal,
|
|
438
502
|
readInitiative: dependencies.readInitiative,
|
|
@@ -461,6 +525,9 @@ export default function (
|
|
|
461
525
|
signal: details.signal,
|
|
462
526
|
actions: [
|
|
463
527
|
{ value: "confirm", label: "Authorize batch run", description: "Start the unattended serial batch run" },
|
|
528
|
+
...(details.offerRevisionDelegation
|
|
529
|
+
? [{ value: "confirm_delegate", label: "Authorize and delegate in-Lane revisions", description: REVISION_DELEGATION_TEXT }]
|
|
530
|
+
: []),
|
|
464
531
|
{ value: "decline", label: "Decline batch", description: "Reject this batch authorization; repository and authority stay unchanged" },
|
|
465
532
|
{ value: "cancel", label: "Cancel", description: "Leave repository and authority unchanged" },
|
|
466
533
|
],
|
|
@@ -573,7 +640,8 @@ export default function (
|
|
|
573
640
|
],
|
|
574
641
|
},
|
|
575
642
|
);
|
|
576
|
-
|
|
643
|
+
const mapped = mapDialogSelection(selected);
|
|
644
|
+
return mapped === "accept_delegate" ? "accept" : mapped;
|
|
577
645
|
},
|
|
578
646
|
});
|
|
579
647
|
|
|
@@ -32,6 +32,11 @@ material task-local maintenance risks. Pure naming, length, complexity
|
|
|
32
32
|
thresholds, formatting, and design preference are not findings and must not
|
|
33
33
|
cause style-only rework.
|
|
34
34
|
|
|
35
|
+
Test-suite growth is advisory, never blocking: note a delivered test that
|
|
36
|
+
duplicates an existing assertion, asserts only private implementation, or
|
|
37
|
+
leaves a test protecting behavior the change retired. Weakened or missing
|
|
38
|
+
tests for an acceptance remain blocking under the rules above.
|
|
39
|
+
|
|
35
40
|
When a finding repeats an acceptance id or anchor that already produced rework in
|
|
36
41
|
this task, do not file it as a fresh single-trigger defect. Name the invariant
|
|
37
42
|
class it belongs to and state in `evidence.trigger` why the previously accepted
|
|
@@ -40,6 +45,13 @@ about the fix's scope, not a new bug. A trigger the accepted contract explicitly
|
|
|
40
45
|
excludes is reported as an advisory note against that boundary, never as blocking
|
|
41
46
|
rework.
|
|
42
47
|
|
|
48
|
+
A defect that lies inside the TaskIntent goal and `scope_hint` is a blocking
|
|
49
|
+
finding even when no acceptance assertion names its exact case word for word:
|
|
50
|
+
cite the acceptance whose principle it violates as `acceptance_id` and
|
|
51
|
+
`violated.ref`. Ask for the fix and a test that pins the case, never for the case
|
|
52
|
+
to be written into the acceptance text; a missing literal clause is not itself a
|
|
53
|
+
finding and does not call for an Intent revision.
|
|
54
|
+
|
|
43
55
|
## Invariant and Evidence Coverage
|
|
44
56
|
|
|
45
57
|
Judge whether the change actually closes the invariant its acceptance names,
|
|
@@ -59,6 +71,14 @@ seam, or a seam silently replaced by a different one, is a finding carrying
|
|
|
59
71
|
named, its absence is not a finding: candidates written before this rule carries
|
|
60
72
|
no defect for lacking one.
|
|
61
73
|
|
|
74
|
+
When the Spec referenced by an acceptance maps an `automated` user scenario to
|
|
75
|
+
it, check that the delivered tests at that acceptance's agreed seam assert the
|
|
76
|
+
scenario's `Then`: a scenario whose observable result is asserted nowhere is a
|
|
77
|
+
finding carrying `violated.kind` of `acceptance` with `ref` the acceptance id.
|
|
78
|
+
A `manual` scenario, and an acceptance with no mapped scenario, produce no such
|
|
79
|
+
finding. This adds no finding kind and no verdict branch: it is the same
|
|
80
|
+
`acceptance` finding the seam rule above already defines.
|
|
81
|
+
|
|
62
82
|
Before pass, enumerate the immutable change set and read each complete per-file
|
|
63
83
|
diff, including deletions. Page bounded reads until every hunk is visible; a
|
|
64
84
|
truncated aggregate output or summary is incomplete coverage. Trace changed
|
|
@@ -108,7 +108,7 @@ An existing grant for one of these never inherits another: a stage-only
|
|
|
108
108
|
instruction stays stage-only, a commit is not push or publication authority,
|
|
109
109
|
and a valid batch capability commits only its own scope-bounded child.
|
|
110
110
|
|
|
111
|
-
Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation.
|
|
111
|
+
Routine Managed enrollment is the first step of an explicit `imm-run` entry and uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Planner and Brainstorm stop with candidate artifacts and never invoke Enrollment, whether or not the request also asks for execution; explicit Plan-only requests stop the same way. `imm-run` opens the native gate directly without chat pre-confirmation. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. Once the user has entered `imm-run`, the routine task proceeds from that single confirmation through enrollment, execution, and QA with no further confirmation. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them. Managed native-authority failures fail closed with one stable reason and exactly one same-Host recovery action; never offer a Pi, Direct Path, cross-Host/worktree, unmanaged, or automatic-retry fallback.
|
|
112
112
|
|
|
113
113
|
For an unborn symbolic Git HEAD, the same Enrollment confirmation discloses an empty root commit. After approval, the Host creates that commit without staging project content or changing the index/worktree, then revalidates ownership and rehearses Enrollment against the new base. Git identity uses existing environment/config values with an explicit automation fallback, never config writes. Decline or pre-initialization cancellation leaves HEAD unborn; if later Enrollment fails or is cancelled, report the remaining commit. Invalid Git refs are errors, not initialization candidates.
|
|
114
114
|
|