ultimate-pi 0.19.1 → 0.20.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.
Files changed (32) hide show
  1. package/.pi/agents/harness/running/executor.md +42 -1
  2. package/.pi/extensions/harness-anchored-edit.ts +141 -0
  3. package/.pi/harness/agents.manifest.json +4 -4
  4. package/.pi/harness/agents.policy.yaml +3 -4
  5. package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
  6. package/.pi/harness/docs/adrs/README.md +2 -0
  7. package/.pi/harness/docs/practice-map.md +11 -0
  8. package/.pi/lib/agents-policy.d.mts +4 -0
  9. package/.pi/lib/agents-policy.mjs +43 -1
  10. package/.pi/lib/agents-policy.ts +1 -0
  11. package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
  12. package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
  13. package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
  14. package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
  15. package/.pi/lib/harness-anchored-edit/index.ts +9 -0
  16. package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
  17. package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
  18. package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
  19. package/.pi/lib/harness-anchored-edit/types.ts +19 -0
  20. package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
  21. package/.pi/lib/harness-lens/index.ts +24 -7
  22. package/.pi/lib/harness-subagents-bridge.ts +7 -5
  23. package/.pi/prompts/harness-steer.md +1 -1
  24. package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
  25. package/.pi/scripts/harness-verify.mjs +67 -0
  26. package/.pi/scripts/run-tests.mjs +64 -0
  27. package/CHANGELOG.md +6 -0
  28. package/THIRD_PARTY_NOTICES.md +7 -0
  29. package/package.json +4 -3
  30. package/vendor/pi-subagents/src/agents.ts +5 -0
  31. package/vendor/pi-subagents/src/subagents.ts +22 -3
  32. package/.pi/scripts/release.sh +0 -338
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  description: Harness executor that implements only within approved PlanPacket scope.
3
- extensions: true
4
3
  thinking: medium
5
4
  max_turns: 20
6
5
  ---
@@ -41,3 +40,45 @@ Call **`submit_executor_handoff`** with a document matching `harness-executor-ha
41
40
  - `files_changed`, `validation_summary`, `rollback_refs`, `handoff_ready`
42
41
 
43
42
  Do not write `artifacts/executor-rollback.json` — rollback is emitted as YAML by the submit pipeline.
43
+
44
+ ## Hash-anchored read/edit (default)
45
+
46
+ `read` returns each line as `AnchorWord§line text`. `edit` uses anchors from the latest read — not raw `oldText`/`newText`.
47
+
48
+ - For **single-line replace**, set `anchor` and omit `end_anchor` (defaults to the same line) or set `end_anchor` to the same anchored line.
49
+ - For **multi-line replace**, set `anchor` and `end_anchor` to the first and last lines of the range (with matching line text after `§`).
50
+ - **Batch** all edits for one file in one `edit` call. Edit independent files in the same turn when lakes allow.
51
+ - Do not re-read a file unless anchors failed or the file changed outside your session.
52
+
53
+ harness-lens may fix indentation on anchored `edit.text` before apply.
54
+
55
+ ## Context discipline (read order)
56
+
57
+ 1. Lake `context_bundle_path` and plan scope.
58
+ 2. `sg -p '…'` via bash for structural search (never `grep`/`find` for code).
59
+ 3. `ccc search` when you need semantic matches.
60
+ 4. Targeted **read** only for lines you will edit — no full-file reads for discovery.
61
+
62
+ ## Batching discipline
63
+
64
+ - One read pass per file before editing it.
65
+ - Group work by lake; batch all edits per file in one `edit` call.
66
+ - Independent files may be edited in the same turn when safe.
67
+
68
+ ## Structural refactor (no AST tools)
69
+
70
+ 1. **Locate** with `sg -p 'pattern'` (bash).
71
+ 2. **Read** anchored regions you will change.
72
+ 3. **Edit** minimally with batched anchored `edit`.
73
+
74
+ Never use `replace_symbol`, `rename_symbol`, or similar — use `sg` + anchored edit only ([ADR 0045](.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md)).
75
+
76
+ ## Post-edit verification (before handoff)
77
+
78
+ Do **not** call `submit_executor_handoff` until:
79
+
80
+ 1. Plan **`acceptance_checks`** for touched scope have been run (record commands and outcomes in `validation_summary`).
81
+ 2. **Lens/LSP blockers** on changed files are resolved when extensions are enabled (fix errors, do not ignore).
82
+ 3. **`files_changed`** stays within approved `PlanPacket` scope.
83
+
84
+ You still do not self-certify final quality — `/harness-review` owns adversary and Sentrux gate.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Hash-anchored read/edit — first-class harness read and edit tools (always on).
3
+ * @see .pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md
4
+ */
5
+ import { resolve } from "node:path";
6
+ import type { TextContent } from "@earendil-works/pi-ai";
7
+ import {
8
+ createReadTool,
9
+ type ExtensionAPI,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import { Type } from "typebox";
12
+ import {
13
+ anchoredEditTaskId,
14
+ applyAnchoredEditsToFile,
15
+ hashLinesStateful,
16
+ } from "../lib/harness-anchored-edit/index.js";
17
+ import type { AnchoredEdit } from "../lib/harness-anchored-edit/types.js";
18
+
19
+ const anchoredEditEntrySchema = Type.Object({
20
+ anchor: Type.String({
21
+ description:
22
+ "Start anchor from read output, format Word§exact line text (e.g. Apple§const x = 1).",
23
+ }),
24
+ end_anchor: Type.Optional(
25
+ Type.String({
26
+ description: "End anchor for replace ranges (same format as anchor).",
27
+ }),
28
+ ),
29
+ edit_type: Type.Optional(
30
+ Type.Union([
31
+ Type.Literal("replace"),
32
+ Type.Literal("insert_after"),
33
+ Type.Literal("insert_before"),
34
+ ]),
35
+ ),
36
+ text: Type.String({ description: "New text for insert/replace." }),
37
+ });
38
+
39
+ const anchoredEditSchema = Type.Object({
40
+ path: Type.String({ description: "Path to the file to edit." }),
41
+ edits: Type.Array(anchoredEditEntrySchema, {
42
+ description:
43
+ "Batch all edits for this file in one call. Use anchors from the latest read.",
44
+ }),
45
+ });
46
+
47
+ const readSchema = Type.Object({
48
+ path: Type.String({ description: "Path to the file to read." }),
49
+ offset: Type.Optional(Type.Number({ description: "1-based start line." })),
50
+ limit: Type.Optional(Type.Number({ description: "Max lines to read." })),
51
+ });
52
+
53
+ function stripAnchoredFromReadOutput(text: string): string {
54
+ return text
55
+ .split("\n")
56
+ .map((line) => {
57
+ const idx = line.indexOf("§");
58
+ if (idx === -1) return line;
59
+ const prefix = line.slice(0, idx);
60
+ if (/^[A-Z][a-zA-Z]*$/.test(prefix)) {
61
+ return line.slice(idx + 1);
62
+ }
63
+ return line;
64
+ })
65
+ .join("\n");
66
+ }
67
+
68
+ export default function harnessAnchoredEdit(pi: ExtensionAPI): void {
69
+ const readToolByCwd = new Map<string, ReturnType<typeof createReadTool>>();
70
+
71
+ function getReadTool(cwd: string) {
72
+ let tool = readToolByCwd.get(cwd);
73
+ if (!tool) {
74
+ tool = createReadTool(cwd);
75
+ readToolByCwd.set(cwd, tool);
76
+ }
77
+ return tool;
78
+ }
79
+
80
+ pi.registerTool({
81
+ name: "read",
82
+ label: "read",
83
+ description:
84
+ "Read a file; each line is prefixed with a stable anchor (Word§line). Use those anchors in edit.",
85
+ parameters: readSchema,
86
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
87
+ const base = getReadTool(ctx.cwd);
88
+ const result = await base.execute(
89
+ toolCallId,
90
+ params,
91
+ signal,
92
+ onUpdate,
93
+ ctx,
94
+ );
95
+ const taskId = anchoredEditTaskId(ctx);
96
+ const absolutePath = resolve(ctx.cwd, params.path);
97
+ for (const block of result.content) {
98
+ if (block.type !== "text") continue;
99
+ const plain = stripAnchoredFromReadOutput(block.text);
100
+ block.text = hashLinesStateful(absolutePath, plain, taskId);
101
+ }
102
+ return result;
103
+ },
104
+ });
105
+
106
+ pi.registerTool({
107
+ name: "edit",
108
+ label: "edit",
109
+ description:
110
+ "Edit using line anchors from read. Batch multiple edits per file. For replace, set end_anchor (defaults to anchor for single-line replace).",
111
+ parameters: anchoredEditSchema,
112
+ promptGuidelines: [
113
+ "Use anchors from the latest read output (Word§line).",
114
+ "Batch all edits for one file in a single edit call.",
115
+ "For renames across files: sg -p to locate, then minimal anchored edits — do not use replace_symbol tools.",
116
+ ],
117
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
118
+ const absolutePath = resolve(ctx.cwd, params.path);
119
+ const taskId = anchoredEditTaskId(ctx);
120
+ const edits = params.edits as AnchoredEdit[];
121
+
122
+ const result = await applyAnchoredEditsToFile(
123
+ absolutePath,
124
+ edits,
125
+ taskId,
126
+ );
127
+
128
+ if (!result.ok) {
129
+ return {
130
+ content: [{ type: "text", text: result.error }] as TextContent[],
131
+ details: { error: true },
132
+ };
133
+ }
134
+
135
+ return {
136
+ content: [{ type: "text", text: result.message }] as TextContent[],
137
+ details: result.details,
138
+ };
139
+ },
140
+ });
141
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schema_version": "1.0.0",
3
3
  "package": "ultimate-pi",
4
- "package_version": "0.19.0",
5
- "generated_at": "2026-05-26T06:39:19.250Z",
6
- "policy_sha256": "f0eb41bc6a877a237087e87762ce11908e93755a42d58f19d6573665b198665f",
4
+ "package_version": "0.19.1",
5
+ "generated_at": "2026-05-26T07:37:49.163Z",
6
+ "policy_sha256": "5a1e71f380df7dde8f6d1f33b366da8bba191689476a6440b3bcdbd776dc617a",
7
7
  "agents": {
8
8
  "pi-pi/agent-expert": {
9
9
  "path": ".pi/agents/pi-pi/agent-expert.md",
@@ -87,7 +87,7 @@
87
87
  },
88
88
  "harness/running/executor": {
89
89
  "path": ".pi/agents/harness/running/executor.md",
90
- "sha256": "219c9307567acc95a9c1b1340f899fac860406fb2c2e84f51b4a8c3ba3a0e2ec"
90
+ "sha256": "1b8aa6e05ffacc8550569465d47cc8525902b53431e36b1c0f973ca633eab450"
91
91
  },
92
92
  "harness/reviewing/adversary": {
93
93
  "path": ".pi/agents/harness/reviewing/adversary.md",
@@ -22,10 +22,10 @@ kinds:
22
22
  - write
23
23
  - edit
24
24
  - bash
25
- - grep
26
- - find
27
25
  - ls
28
- extensions: true
26
+ # Subprocess bundle (governance + anchored read/edit + lens) — not full .pi/extensions (parent phase hooks).
27
+ extension_bundle: executor
28
+ extensions: false
29
29
  read_only: false
30
30
  evaluator:
31
31
  tools:
@@ -141,7 +141,6 @@ agents:
141
141
  kind: executor
142
142
  tools_add:
143
143
  - submit_executor_handoff
144
- extensions: true
145
144
  max_turns: 20
146
145
  thinking: medium
147
146
  submit_tool: submit_executor_handoff
@@ -0,0 +1,41 @@
1
+ # ADR 0051: Hash-anchored read/edit (Dirac-inspired)
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-05-26
5
+
6
+ ## Context
7
+
8
+ Harness executor sessions used Pi’s `oldText`/`newText` `edit` tool. Line-number and fuzzy-match edits fail when files drift between read and edit, causing retries and token waste. [Dirac](https://github.com/dirac-run/dirac) demonstrates stable **hash-anchored** line targeting with Myers-diff anchor reconciliation; user evaluation chose adoption for the harness.
9
+
10
+ We already own adjacent concerns: **harness-lens** (LSP, indentation autopatch, format), **shell `sg`** (structural search per ADR 0045), **Sentrux** (architecture gate). We must not duplicate those layers.
11
+
12
+ ## Decision
13
+
14
+ 1. **First-class tools** — [`.pi/extensions/harness-anchored-edit.ts`](../../../extensions/harness-anchored-edit.ts) registers harness `read` and `edit`. No env toggle.
15
+ 2. **Vendored core** in [`.pi/lib/harness-anchored-edit/`](../../../lib/harness-anchored-edit/) (Apache-2.0 subset from Dirac: anchor state, line protocol, resolve/apply).
16
+ 3. **`read`** output: `AnchorWord§line` per line; anchor state scoped per session/task id.
17
+ 4. **`edit`** input: `anchor`, optional `end_anchor` (defaults to `anchor` for single-line replace), `edit_type`, `text`; batch `edits[]` per file.
18
+ 5. **Native apply** — `applyAnchoredEditsToFile` writes disk directly (Pi `edit-diff` for unified diff in tool result). No `resolve-to-pi-edit` / no delegation to `createEditTool`.
19
+ 6. **Lens** — `tool_call` autopatch corrects indentation on anchored `edits[].text` only ([`anchored-edit-autopatch.ts`](../../../lib/harness-lens/clients/anchored-edit-autopatch.ts)). Legacy `oldText` autopatch remains for non-harness agents that still use Pi edit.
20
+ 7. **Executor subprocess bundle** — `kinds.executor.extension_bundle: executor` loads `--no-extensions` plus curated `-e` modules only: `subagent-governance.ts`, `harness-anchored-edit.ts`, `harness-lens.ts` ([`resolveExtensionBundlePaths`](../../../lib/agents-policy.mjs)). Avoids loading the full `.pi/extensions` tree (parent orchestration, run-context phase hooks) while still providing hash-anchored read/edit and lens format-on-result.
21
+ 8. **Builtin suppression** — Executor subprocesses use `--no-builtin-tools` + tool allowlist ([`agents.policy.yaml`](../agents.policy.yaml) via `noBuiltinTools` when `extension_bundle` or full extensions apply).
22
+ 9. **Executor policy** (prompt + practice-map): batching discipline, post-edit verification before `submit_executor_handoff`, structural refactor via `sg -p` → anchored edit — **no** `replace_symbol` Pi tools.
23
+ 10. **Remove** `grep`/`find` from executor tool policy; use `bash` + `sg` for code search.
24
+
25
+ ## Consequences
26
+
27
+ ### Positive
28
+
29
+ - More stable edits across multi-step executor and steer repair loops.
30
+ - Single anchored edit surface; no Pi text-match shim on the hot path.
31
+
32
+ ### Negative
33
+
34
+ - Models must learn anchor `§` protocol.
35
+ - Vendored Dirac code must be kept in sync for security fixes (small surface).
36
+
37
+ ## References
38
+
39
+ - [practice-map.md](../practice-map.md) — Executor edit discipline
40
+ - [ADR 0045](0045-harness-lens-minimal-contract.md) — lens vs sg vs Sentrux
41
+ - [ADR 0044](0044-harness-steer-loop.md) — repair mode uses same edit rules
@@ -36,6 +36,8 @@ Team-shared ADRs for the ultimate-pi harness live under `.pi/harness/docs/adrs/`
36
36
  | [0047](0047-agt-layered-security.md) | AGT layered security (rings, prompt defense, CI) | Accepted |
37
37
  | [0048](0048-tool-call-hook-order.md) | tool_call hook interaction matrix | Accepted |
38
38
  | [0049](0049-agents-policy-manifest.md) | agents.policy.yaml SSOT + native discovery | Accepted |
39
+ | [0050](0050-agentic-web-retrieval-stack.md) | Agentic Web Retrieval Stack (WRS) | Accepted |
40
+ | [0051](0051-hash-anchored-executor-edits.md) | Hash-anchored read/edit (Dirac-inspired) | Accepted |
39
41
 
40
42
  ## Practice map
41
43
 
@@ -75,6 +75,17 @@ See also: [ADRs](adrs/README.md), [ADR 0040](adrs/0040-practice-grounded-orchest
75
75
  | Handoff | Generator–evaluator | `submit_executor_handoff` | Executor |
76
76
  | Next | Always verify | **`/harness-review`** (not replan on blocked) | Parent routing |
77
77
 
78
+
79
+ ### Executor edit discipline (ADR 0051)
80
+
81
+ | Practice | Agent rule |
82
+ |----------|------------|
83
+ | Hash-anchored targeting | `read` → `Anchor§line`; `edit` uses anchors (default harness tools) |
84
+ | Batching | All edits per file in one `edit`; independent files same turn when safe |
85
+ | Pre-handoff verify | Run `acceptance_checks`; clear lens blockers; then `submit_executor_handoff` |
86
+ | Structural refactor | `sg -p` locate → read slice → anchored edit — no `replace_symbol` tools |
87
+ | Code search | `sg` / `ccc` only — not `grep`/`find` on executor |
88
+
78
89
  ## `/harness-review` — Monitoring and Controlling
79
90
 
80
91
  | Phase | Practice | Agent translation | Actor |
@@ -6,6 +6,10 @@ export interface AgentPolicySpec {
6
6
  kind: string;
7
7
  effectiveTools: string[];
8
8
  extensionsOff: boolean;
9
+ /** Subprocess-only: load curated -e extensions instead of full .pi/extensions. */
10
+ extensionBundle?: string;
11
+ extensionsFull: boolean;
12
+ noBuiltinTools: boolean;
9
13
  readOnly: boolean;
10
14
  maxTurns?: number;
11
15
  thinking?: string;
@@ -31,10 +31,26 @@ const MUTATING_TOOLS = new Set(["write", "edit"]);
31
31
 
32
32
  const cache = new Map();
33
33
 
34
+ const EXTENSION_BUNDLE_MODULES = {
35
+ executor: [
36
+ "subagent-governance.ts",
37
+ "harness-anchored-edit.ts",
38
+ "harness-lens.ts",
39
+ ],
40
+ };
41
+
34
42
  export function packageAgentsPolicyPath(packageRoot) {
35
43
  return join(packageRoot, ".pi", "harness", "agents.policy.yaml");
36
44
  }
37
45
 
46
+ /** Absolute paths for subprocess `-e` loads (curated; avoids parent-only extensions). */
47
+ export function resolveExtensionBundlePaths(packageRoot, bundleName) {
48
+ const modules = EXTENSION_BUNDLE_MODULES[bundleName];
49
+ if (!modules) return [];
50
+ const extDir = join(packageRoot, ".pi", "extensions");
51
+ return modules.map((name) => join(extDir, name));
52
+ }
53
+
38
54
  export function projectAgentsPolicyPath(projectRoot) {
39
55
  return join(projectRoot, ".pi", "agents.policy.yaml");
40
56
  }
@@ -52,12 +68,19 @@ function readYamlFile(path) {
52
68
  }
53
69
  }
54
70
 
71
+ function normalizeExtensionBundle(raw) {
72
+ if (typeof raw.extension_bundle !== "string") return undefined;
73
+ const bundle = raw.extension_bundle.trim();
74
+ return bundle.length > 0 ? bundle : undefined;
75
+ }
76
+
55
77
  function normalizeKindEntry(raw) {
56
78
  if (!raw || typeof raw !== "object") return null;
57
79
  const tools = Array.isArray(raw.tools) ? raw.tools.map(String) : [];
58
80
  return {
59
81
  tools,
60
82
  extensions: raw.extensions === false ? false : Boolean(raw.extensions),
83
+ extensionBundle: normalizeExtensionBundle(raw),
61
84
  readOnly: raw.read_only === true,
62
85
  maxTurns:
63
86
  typeof raw.max_turns === "number" && raw.max_turns > 0
@@ -93,6 +116,7 @@ function normalizeAgentEntry(raw) {
93
116
  : raw.extensions === true
94
117
  ? true
95
118
  : undefined,
119
+ extensionBundle: normalizeExtensionBundle(raw),
96
120
  maxTurns:
97
121
  typeof raw.max_turns === "number" && raw.max_turns > 0
98
122
  ? raw.max_turns
@@ -162,10 +186,25 @@ export function resolveEffectiveTools(agentId, merged) {
162
186
  for (const t of entry.toolsDeny ?? []) base.delete(t);
163
187
  for (const t of BUILTIN_DENY_TOOLS) base.delete(t);
164
188
 
189
+ const extensionBundle =
190
+ entry.extensionBundle ?? kind.extensionBundle ?? undefined;
191
+ const extensionsFull =
192
+ !extensionBundle &&
193
+ (entry.extensions === true
194
+ ? true
195
+ : entry.extensions === false
196
+ ? false
197
+ : Boolean(kind.extensions));
198
+ const extensionsOff = !extensionsFull;
199
+
165
200
  return {
166
201
  kind: kindName,
167
202
  effectiveTools: [...base],
168
- extensionsOff: entry.extensions === true ? false : entry.extensions === false ? true : !kind.extensions,
203
+ extensionsOff,
204
+ extensionBundle,
205
+ extensionsFull,
206
+ /** Suppress Pi builtins when harness read/edit register (full extensions or subprocess bundle). */
207
+ noBuiltinTools: extensionsFull || Boolean(extensionBundle),
169
208
  readOnly: kind.readOnly,
170
209
  maxTurns: entry.maxTurns ?? kind.maxTurns,
171
210
  thinking: entry.thinking ?? kind.thinking,
@@ -304,6 +343,9 @@ export function applyAgentPolicyToConfig(agent, packageRoot, projectRoot) {
304
343
  ...agent,
305
344
  tools: spec.effectiveTools.length > 0 ? spec.effectiveTools : undefined,
306
345
  extensionsOff: spec.extensionsOff,
346
+ extensionBundle: spec.extensionBundle,
347
+ extensionsFull: spec.extensionsFull,
348
+ noBuiltinTools: spec.noBuiltinTools,
307
349
  maxTurns: spec.maxTurns ?? agent.maxTurns,
308
350
  thinking: spec.thinking ?? agent.thinking,
309
351
  model: spec.model ?? agent.model,
@@ -16,4 +16,5 @@ export {
16
16
  projectAgentsPolicyPath,
17
17
  projectPoliciesDir,
18
18
  resolveEffectiveTools,
19
+ resolveExtensionBundlePaths,
19
20
  } from "./agents-policy.mjs";