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
@@ -0,0 +1,158 @@
1
+ import * as nodeFs from "node:fs";
2
+ import { AnchorStateManager } from "../../harness-anchored-edit/anchor-state.js";
3
+ import { EditExecutor } from "../../harness-anchored-edit/edit-executor.js";
4
+ import { splitAnchor } from "../../harness-anchored-edit/line-protocol.js";
5
+ import type { AnchoredEdit } from "../../harness-anchored-edit/types.js";
6
+ import { tryCorrectIndentationMismatchFromContent } from "./edit-autopatch.js";
7
+ import { retargetReplacementIndentation } from "./indent-retarget.js";
8
+
9
+ function leadingIndent(line: string): string {
10
+ return line.match(/^[\t ]*/)?.[0] ?? "";
11
+ }
12
+
13
+ function isIndentationOnlyChange(before: string, after: string): boolean {
14
+ const beforeLines = before.replace(/\r\n/g, "\n").split("\n");
15
+ const afterLines = after.replace(/\r\n/g, "\n").split("\n");
16
+ if (beforeLines.length !== afterLines.length) return false;
17
+ return beforeLines.every(
18
+ (line, index) => line.trim() === afterLines[index].trim(),
19
+ );
20
+ }
21
+
22
+ type AnchoredEditInput = {
23
+ edits?: AnchoredEdit[];
24
+ };
25
+
26
+ export function isAnchoredEditToolInput(
27
+ editInput: unknown,
28
+ ): editInput is AnchoredEditInput {
29
+ if (!editInput || typeof editInput !== "object") return false;
30
+ const edits = (editInput as AnchoredEditInput).edits;
31
+ if (!Array.isArray(edits) || edits.length === 0) return false;
32
+ return typeof edits[0]?.anchor === "string";
33
+ }
34
+
35
+ /**
36
+ * Indentation-only correction for harness anchored edit.text before apply.
37
+ */
38
+ export function applyAnchoredEditAutopatch(
39
+ filePath: string,
40
+ editInput: AnchoredEditInput,
41
+ taskId: string,
42
+ ): { block: true; reason: string } | undefined {
43
+ const edits = editInput.edits;
44
+ if (!edits?.length) return undefined;
45
+
46
+ let crlfContent: string;
47
+ try {
48
+ crlfContent = nodeFs.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
49
+ } catch {
50
+ return undefined;
51
+ }
52
+
53
+ const lines = crlfContent.split("\n");
54
+ const lineAnchors = AnchorStateManager.reconcile(filePath, lines, taskId);
55
+ const executor = new EditExecutor();
56
+ const { resolvedEdits, failedEdits } = executor.resolveEdits(
57
+ edits,
58
+ lines,
59
+ lineAnchors,
60
+ );
61
+ if (failedEdits.length > 0) return undefined;
62
+
63
+ const corrected: Array<{
64
+ label: string;
65
+ original: string;
66
+ corrected: string;
67
+ indentationOnly: boolean;
68
+ apply: (value: string) => void;
69
+ }> = [];
70
+
71
+ for (const { lineIdx, endIdx, edit } of resolvedEdits) {
72
+ const editType = edit.edit_type ?? "replace";
73
+ const text = edit.text ?? "";
74
+ if (!text.trim()) continue;
75
+
76
+ let referenceBlock: string;
77
+ if (editType === "replace") {
78
+ referenceBlock = lines.slice(lineIdx, endIdx + 1).join("\n");
79
+ } else {
80
+ referenceBlock = lines[lineIdx] ?? "";
81
+ }
82
+
83
+ const correctedText = tryCorrectIndentationMismatchFromContent(
84
+ text,
85
+ crlfContent,
86
+ );
87
+ if (correctedText === undefined) {
88
+ const refIndent = leadingIndent(referenceBlock.split("\n")[0] ?? "");
89
+ const textIndent = leadingIndent(text.split("\n")[0] ?? "");
90
+ if (
91
+ refIndent !== textIndent &&
92
+ isIndentationOnlyChange(
93
+ textIndent + text.trimStart(),
94
+ refIndent + text.trimStart(),
95
+ )
96
+ ) {
97
+ const retargeted = retargetReplacementIndentation(
98
+ text,
99
+ textIndent + text.trimStart(),
100
+ refIndent + text.trimStart(),
101
+ );
102
+ if (retargeted !== undefined) {
103
+ corrected.push({
104
+ label: `edits anchor ${splitAnchor(edit.anchor).anchor}`,
105
+ original: text,
106
+ corrected: retargeted,
107
+ indentationOnly: true,
108
+ apply: (value) => {
109
+ edit.text = value;
110
+ },
111
+ });
112
+ }
113
+ }
114
+ continue;
115
+ }
116
+
117
+ if (correctedText !== text) {
118
+ const retargeted = retargetReplacementIndentation(
119
+ text,
120
+ text,
121
+ correctedText,
122
+ );
123
+ corrected.push({
124
+ label: `edits anchor ${splitAnchor(edit.anchor).anchor}`,
125
+ original: text,
126
+ corrected: retargeted ?? correctedText,
127
+ indentationOnly: isIndentationOnlyChange(text, correctedText),
128
+ apply: (value) => {
129
+ edit.text = value;
130
+ },
131
+ });
132
+ }
133
+ }
134
+
135
+ if (corrected.length === 0) return undefined;
136
+
137
+ const unsafe = corrected.filter((entry) => !entry.indentationOnly);
138
+ if (unsafe.length > 0) {
139
+ const details = unsafe
140
+ .map(({ label, original }) => {
141
+ const preview = original.trimStart().slice(0, 60).replace(/\n/g, "↵");
142
+ return `${label} ("${preview}…") cannot be auto-patched (not indentation-only).`;
143
+ })
144
+ .join("\n");
145
+ return {
146
+ block: true,
147
+ reason:
148
+ `🔄 RETRYABLE — Indentation mismatch on anchored edit text\n\n` +
149
+ `${details}\n\n` +
150
+ `Next action: re-read the relevant section, then retry with text matching file indentation.`,
151
+ };
152
+ }
153
+
154
+ for (const entry of corrected) {
155
+ entry.apply(entry.corrected);
156
+ }
157
+ return undefined;
158
+ }
@@ -7,6 +7,11 @@ import * as nodeFs from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
10
+ import { anchoredEditTaskId } from "../harness-anchored-edit/task-id.js";
11
+ import {
12
+ applyAnchoredEditAutopatch,
13
+ isAnchoredEditToolInput,
14
+ } from "./clients/anchored-edit-autopatch.js";
10
15
  import {
11
16
  tryCorrectIndentationMismatch,
12
17
  tryCorrectIndentationMismatchFromContent,
@@ -496,13 +501,25 @@ export default function harnessLensExtension(pi: ExtensionAPI): void {
496
501
  event as Parameters<typeof isToolCallEventType>[1],
497
502
  )
498
503
  ) {
499
- const editInput = (event as { input?: unknown }).input as {
500
- oldText?: string;
501
- newText?: string;
502
- edits?: Array<{ oldText?: string; newText?: string }>;
503
- };
504
- const block = applyEditAutopatch(filePath, editInput);
505
- if (block) return block;
504
+ const editInput = (event as { input?: unknown }).input;
505
+ if (isAnchoredEditToolInput(editInput)) {
506
+ const block = applyAnchoredEditAutopatch(
507
+ filePath,
508
+ editInput,
509
+ anchoredEditTaskId({
510
+ sessionId: (ctx as { sessionId?: string }).sessionId,
511
+ }),
512
+ );
513
+ if (block) return block;
514
+ } else {
515
+ const legacyInput = editInput as {
516
+ oldText?: string;
517
+ newText?: string;
518
+ edits?: Array<{ oldText?: string; newText?: string }>;
519
+ };
520
+ const block = applyEditAutopatch(filePath, legacyInput);
521
+ if (block) return block;
522
+ }
506
523
  }
507
524
  });
508
525
 
@@ -14,7 +14,7 @@ import {
14
14
  type SpawnAuthForward,
15
15
  } from "../../vendor/pi-subagents/src/subagents.js";
16
16
  import { subagentGovernanceExtensionPath } from "../extensions/subagent-governance.js";
17
- import { getAgentKind } from "./agents-policy.mjs";
17
+ import { getAgentKind, resolveExtensionBundlePaths } from "./agents-policy.mjs";
18
18
  import {
19
19
  delegationEnvFromBundle,
20
20
  mintSubagentDelegation,
@@ -35,10 +35,6 @@ import {
35
35
  recordSpawnStart,
36
36
  } from "./harness-spawn-budget.js";
37
37
  import { parseSpawnContextFromTask } from "./harness-spawn-parse.js";
38
- import {
39
- getRememberedSessionWebArtifactDir,
40
- resolveWebArtifactScope,
41
- } from "./harness-web/artifacts.js";
42
38
  import {
43
39
  isUsableApiKey,
44
40
  resolveConcreteSubagentModel,
@@ -47,6 +43,10 @@ import {
47
43
  inferPhaseForPrecheck,
48
44
  precheckHarnessSubagentSpawn,
49
45
  } from "./harness-subagent-precheck.js";
46
+ import {
47
+ getRememberedSessionWebArtifactDir,
48
+ resolveWebArtifactScope,
49
+ } from "./harness-web/artifacts.js";
50
50
 
51
51
  const spawnBudget = createSpawnBudgetState();
52
52
  let lastSessionId = "harness";
@@ -126,6 +126,8 @@ export function createHarnessSubagentsExtension(
126
126
  packageRoot,
127
127
  subprocessGovernanceExtensionPath: governanceExtPath,
128
128
  harnessSubprocessExtensionPath: governanceExtPath,
129
+ resolveExtensionBundlePaths: (bundleName) =>
130
+ resolveExtensionBundlePaths(packageRoot, bundleName),
129
131
  resolveSubprocessEnv: (task, agent) => {
130
132
  const projectRoot = process.cwd();
131
133
  const base: Record<string, string> = {
@@ -19,7 +19,7 @@ Thin orchestrator for the **steer loop** (ADR 0044). Run only after `/harness-re
19
19
  2. Update `artifacts/steer-state.yaml` (`attempt`, `max_attempts`, `active: true`).
20
20
  3. Set policy phase to **execute** before spawning executor (required for mutating tools).
21
21
  4. One `ask_user` steer gate unless `run-context.steer_approved` is already true.
22
- 5. Spawn **`harness/running/executor`** with `HarnessSpawnContext.mode: repair` and `repair_brief_path: artifacts/repair-brief.yaml`.
22
+ 5. Spawn **`harness/running/executor`** with `HarnessSpawnContext.mode: repair` and `repair_brief_path: artifacts/repair-brief.yaml`. Repair uses the same hash-anchored `read`/`edit`, batching, and pre-handoff verification rules as `/harness-run` (ADR 0051).
23
23
  6. Optional: `node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate --save` after repair to refresh baseline (ADR 0044).
24
24
  7. `next_command`: **`/harness-review`** (always re-verify; tiered adversary on attempts 2+ per practice-map).
25
25
 
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smoke test for native anchored edit apply (no Pi oldText shim).
4
+ */
5
+ import { createRequire } from "node:module";
6
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
12
+ const require = createRequire(join(root, "package.json"));
13
+
14
+ const { applyAnchoredEditsToFile } = await import(
15
+ join(root, ".pi/lib/harness-anchored-edit/apply-anchored-edits.ts")
16
+ );
17
+ const { hashLinesStateful } = await import(
18
+ join(root, ".pi/lib/harness-anchored-edit/anchor-state.ts")
19
+ );
20
+
21
+ const dir = mkdtempSync(join(tmpdir(), "anchored-smoke-"));
22
+ const file = join(dir, "t.ts");
23
+ writeFileSync(file, "line one\nline two\n");
24
+ const plain = readFileSync(file, "utf8");
25
+ const hashed = hashLinesStateful(file, plain, "smoke");
26
+ const line2 = hashed.split("\n").find((l) => l.includes("line two"));
27
+ if (!line2) {
28
+ console.error("smoke FAIL: no anchor line");
29
+ process.exit(1);
30
+ }
31
+ const result = await applyAnchoredEditsToFile(
32
+ file,
33
+ [{ anchor: line2, text: "line TWO", edit_type: "replace" }],
34
+ "smoke",
35
+ );
36
+ if (!result.ok) {
37
+ console.error("smoke FAIL:", result.error);
38
+ process.exit(1);
39
+ }
40
+ const out = readFileSync(file, "utf8");
41
+ if (!out.includes("line TWO")) {
42
+ console.error("smoke FAIL: file not updated:", out);
43
+ process.exit(1);
44
+ }
45
+ console.log("harness-anchored-edit-smoke OK");
@@ -207,6 +207,59 @@ async function checkHarnessLens(pkgJson) {
207
207
  ok("no harness-lens UPSTREAM_PIN.md");
208
208
  }
209
209
 
210
+ async function checkHarnessAnchoredEdit(pkgJson) {
211
+ if (!pkgJson.files?.includes(".pi/lib/harness-anchored-edit")) {
212
+ fail(
213
+ 'package.json "files" must include .pi/lib/harness-anchored-edit',
214
+ );
215
+ }
216
+ ok('package.json files includes .pi/lib/harness-anchored-edit');
217
+
218
+ const resolvePath = join(
219
+ ROOT,
220
+ ".pi",
221
+ "lib",
222
+ "harness-anchored-edit",
223
+ "resolve-to-pi-edit.ts",
224
+ );
225
+ if (await fileExists(resolvePath)) {
226
+ fail("resolve-to-pi-edit.ts must not exist (native anchored apply)");
227
+ }
228
+
229
+ const applyPath = join(
230
+ ROOT,
231
+ ".pi",
232
+ "lib",
233
+ "harness-anchored-edit",
234
+ "apply-anchored-edits.ts",
235
+ );
236
+ if (!(await fileExists(applyPath))) {
237
+ fail("missing .pi/lib/harness-anchored-edit/apply-anchored-edits.ts");
238
+ }
239
+
240
+ const extPath = join(ROOT, ".pi", "extensions", "harness-anchored-edit.ts");
241
+ if (!(await fileExists(extPath))) {
242
+ fail("missing .pi/extensions/harness-anchored-edit.ts");
243
+ }
244
+ const extSrc = await readFile(extPath, "utf8");
245
+ if (extSrc.includes("HARNESS_ANCHORED_EDIT")) {
246
+ fail("harness-anchored-edit must not gate on HARNESS_ANCHORED_EDIT");
247
+ }
248
+ if (extSrc.includes("createEditTool")) {
249
+ fail("harness-anchored-edit must not delegate to createEditTool");
250
+ }
251
+ if (extSrc.includes("resolveAnchoredInputToPiEdit")) {
252
+ fail("harness-anchored-edit must not use resolveAnchoredInputToPiEdit");
253
+ }
254
+ if (extSrc.includes('pi.on("tool_call"')) {
255
+ fail("harness-anchored-edit must not mutate edit input on tool_call");
256
+ }
257
+ if (!extSrc.includes("applyAnchoredEditsToFile")) {
258
+ fail("harness-anchored-edit must call applyAnchoredEditsToFile");
259
+ }
260
+ ok("harness-anchored-edit first-class contract");
261
+ }
262
+
210
263
  async function checkSentruxGate() {
211
264
  await checkSentruxRules();
212
265
 
@@ -278,6 +331,7 @@ async function main() {
278
331
  await readFile(join(ROOT, "package.json"), "utf-8"),
279
332
  );
280
333
  await checkHarnessLens(pkgJson);
334
+ await checkHarnessAnchoredEdit(pkgJson);
281
335
 
282
336
  if (!pkgJson.files?.includes("vendor/pi-subagents")) {
283
337
  fail(
@@ -408,6 +462,19 @@ async function main() {
408
462
  }
409
463
  ok("agents.policy.yaml present");
410
464
 
465
+ const policyYaml = await readFile(AGENTS_POLICY, "utf8");
466
+ if (!/^\s+extension_bundle:\s+executor/m.test(policyYaml)) {
467
+ fail("agents.policy.yaml kinds.executor must set extension_bundle: executor");
468
+ }
469
+ if (
470
+ /harness\/running\/executor:[\s\S]*?extensions:\s+true/m.test(policyYaml)
471
+ ) {
472
+ fail(
473
+ "harness/running/executor must not set extensions: true (use kind extension_bundle)",
474
+ );
475
+ }
476
+ ok("executor extension_bundle policy");
477
+
411
478
  if (!(await fileExists(AGENTS_MANIFEST))) {
412
479
  fail(
413
480
  "missing .pi/harness/agents.manifest.json — run node \"$UP_PKG/.pi/scripts/harness-agents-manifest.mjs\" --write",
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ const TEST_DIR = 'test';
6
+
7
+ const NODE_ONLY_TESTS = [
8
+ 'harness-verify.test.mjs',
9
+ 'harness-ask-user.test.mjs',
10
+ 'harness-subagents-loader.test.mjs',
11
+ 'harness-subagent-precheck.test.mjs',
12
+ 'sentrux-rules-sync.test.mjs',
13
+ 'harness-budget-guard.test.mjs',
14
+ ];
15
+
16
+ const DEFAULT_SUITE_EXCLUDES = new Set([
17
+ 'graphify-kb-updater.test.mjs',
18
+ 'harness-artifact-gate.test.mjs',
19
+ 'harness-spawn-critical-path.test.mjs',
20
+ 'harness-yaml.test.mjs',
21
+ 'plan-debate-lanes.test.mjs',
22
+ ]);
23
+
24
+ function listTests() {
25
+ const entries = readdirSync(TEST_DIR, { withFileTypes: true });
26
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
27
+ const nodeOnlySet = new Set(NODE_ONLY_TESTS);
28
+
29
+ const nodeTests = NODE_ONLY_TESTS.filter((name) => files.includes(name)).map((name) => `${TEST_DIR}/${name}`);
30
+ const tsxTests = files
31
+ .filter(
32
+ (name) =>
33
+ (name.endsWith('.test.mjs') || name.endsWith('.test.ts')) &&
34
+ !name.endsWith('.integration.test.ts') &&
35
+ !nodeOnlySet.has(name) &&
36
+ !DEFAULT_SUITE_EXCLUDES.has(name),
37
+ )
38
+ .map((name) => `${TEST_DIR}/${name}`)
39
+ .sort();
40
+
41
+ return { nodeTests, tsxTests };
42
+ }
43
+
44
+ function run(cmd, args) {
45
+ const result = spawnSync(cmd, args, { stdio: 'inherit' });
46
+ if (result.error) {
47
+ throw result.error;
48
+ }
49
+ if (result.status !== 0) {
50
+ process.exit(result.status ?? 1);
51
+ }
52
+ }
53
+
54
+ const { nodeTests, tsxTests } = listTests();
55
+
56
+ if (nodeTests.length > 0) {
57
+ run('node', ['--test', ...nodeTests]);
58
+ }
59
+
60
+ run('node', ['.pi/harness/evals/smoke/smoke-harness-plan.mjs', '--fixture']);
61
+
62
+ if (tsxTests.length > 0) {
63
+ run('npx', ['-y', 'tsx', '--test', ...tsxTests]);
64
+ }
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ All notable changes to this project are documented in this file.
9
9
  - **Harness lens:** Integrate selected pi-lens capabilities through a harness-owned extension, store lens state under `.pi/harness/.lens`, and route lens findings through harness PostHog telemetry instead of standalone lens health/telemetry surfaces.
10
10
  - **Graphify KB updater:** Productize conservative daily discovery/promotion with explicit repo/release taxonomy, allowlist source-class gates, operator review queue reporting, scheduler smoke validation, and safe Graphify refresh controls.
11
11
 
12
+ ## [v0.20.0] — 2026-05-26
13
+
14
+ ### ✨ Features
15
+
16
+ - **Harness:** Add hash-anchored executor edit flow.
17
+
12
18
  ## [v0.19.1] — 2026-05-26
13
19
 
14
20
  ### 🔧 Chores
@@ -1,3 +1,10 @@
1
+ ## Dirac hash-anchored edit (vendored subset)
2
+
3
+ - **Project:** https://github.com/dirac-run/dirac
4
+ - **License:** Apache-2.0
5
+ - **Location:** [`.pi/lib/harness-anchored-edit/`](.pi/lib/harness-anchored-edit/) (anchor state, line protocol, edit resolve/apply)
6
+ - **Integration:** [`.pi/extensions/harness-anchored-edit.ts`](.pi/extensions/harness-anchored-edit.ts) — first-class harness `read`/`edit` (always on when harness extensions load). harness-lens autopatches anchored `edit.text` at `tool_call`; native apply via `applyAnchoredEditsToFile`.
7
+
1
8
  ## pi-vcc (vendored)
2
9
 
3
10
  - **Project:** https://github.com/sting8k/pi-vcc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-pi",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Ultimate AI coding harness for pi.dev — extensible skills, Obsidian wiki knowledge layer, compressed context, deterministic output",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -59,6 +59,7 @@
59
59
  ".pi/harness/web-heuristic-angles.yaml",
60
60
  ".pi/harness/web-heuristic-angles.json",
61
61
  ".pi/lib/harness-lens",
62
+ ".pi/lib/harness-anchored-edit",
62
63
  ".pi/harness/README.md",
63
64
  ".pi/npm/package.json",
64
65
  ".pi/npm/.gitignore",
@@ -81,13 +82,12 @@
81
82
  "check:ts": "tsc -p tsconfig.check.json",
82
83
  "vendor:sync-vcc": "bash .pi/scripts/vendor-sync-pi-vcc.sh",
83
84
  "vendor:sync-subagents": "bash .pi/scripts/vendor-sync-pi-subagents.sh",
84
- "release": "bash .pi/scripts/release.sh",
85
85
  "lint": "biome check",
86
86
  "lint:fix": "biome check --fix",
87
87
  "format": "biome format --write",
88
88
  "format:check": "biome format",
89
89
  "prepare": "lefthook install",
90
- "test": "node --test test/harness-verify.test.mjs test/harness-ask-user.test.mjs test/harness-subagents-loader.test.mjs test/harness-subagent-precheck.test.mjs test/sentrux-rules-sync.test.mjs test/harness-budget-guard.test.mjs && node .pi/harness/evals/smoke/smoke-harness-plan.mjs --fixture && npx -y tsx --test test/harness-web-cache.test.mjs test/harness-web-artifacts.test.mjs test/harness-subagent-auth.test.mjs test/posthog-client.test.mjs test/harness-agt-policy-load.test.mjs test/harness-agt-policy-matrix.test.mjs test/harness-agt-policy-parity.test.mjs test/harness-agt-packaging.test.mjs test/harness-tool-call-hook-chain.test.mjs test/harness-vcc-settings.test.ts test/harness-run-context-postrun.test.mjs test/harness-tool-payload.test.mjs test/harness-live-widget-status.test.ts test/harness-project-toggle-tui.test.ts test/harness-plan-phase-policy.test.mjs test/harness-context-mode-policy.test.mjs test/harness-subprocess-bootstrap.test.mjs test/harness-subagent-policy.test.mjs test/harness-subagent-precheck-topology.test.mjs test/plan-approval-readiness.test.mjs test/harness-spawn-budget.test.mjs test/harness-spawn-parse.test.mjs test/harness-schema-validate.test.mjs test/harness-turn-routing.test.mjs test/harness-budget-enforce.test.mjs test/harness-submit-policy.test.mjs test/harness-project-agents-policy.test.mjs test/plan-approval-format.test.mjs test/plan-approval-dialog.test.mjs test/plan-approval-sync.test.mjs test/plan-create-plan.test.mjs test/plan-review-format.test.mjs test/debate-plan-phase.test.mjs test/plan-debate-eligibility.test.mjs test/plan-messenger-gate.test.mjs test/plan-debate-lane-apply.test.mjs test/review-integrity-revise-handoff.test.mjs test/harness-plan-revise-reset.test.mjs",
90
+ "test": "node .pi/scripts/run-tests.mjs",
91
91
  "test:vcc": "npx -y tsx --test vendor/pi-vcc/tests/*.test.ts",
92
92
  "harness:sentrux-bootstrap": "node .pi/scripts/harness-sentrux-bootstrap.mjs",
93
93
  "harness:sentrux-sync": "node .pi/scripts/sentrux-rules-sync.mjs --force",
@@ -111,6 +111,7 @@
111
111
  "ajv": "^8.17.1",
112
112
  "ajv-formats": "^3.0.1",
113
113
  "croner": "^9.0.0",
114
+ "diff": "^8.0.4",
114
115
  "jimp": "^1.6.1",
115
116
  "minimatch": "^10.2.5",
116
117
  "nanoid": "^5.1.5",
@@ -19,6 +19,11 @@ export interface AgentConfig {
19
19
  thinking?: string;
20
20
  maxTurns?: number;
21
21
  extensionsOff?: boolean;
22
+ /** Curated subprocess extensions (agents.policy.yaml `extension_bundle`). */
23
+ extensionBundle?: string;
24
+ extensionsFull?: boolean;
25
+ /** When true, subprocess uses --no-builtin-tools so extension read/edit replace builtins. */
26
+ noBuiltinTools?: boolean;
22
27
  skillsOff?: boolean;
23
28
  systemPrompt: string;
24
29
  source: AgentSource;
@@ -46,6 +46,10 @@ export interface HarnessSubagentsOptions {
46
46
  subprocessGovernanceExtensionPath?: string;
47
47
  /** @deprecated Use subprocessGovernanceExtensionPath */
48
48
  harnessSubprocessExtensionPath?: string;
49
+ /** Resolve curated `-e` extension paths for agents.policy `extension_bundle`. */
50
+ resolveExtensionBundlePaths?: (
51
+ bundleName: string,
52
+ ) => string[];
49
53
  /** Extra env vars per subprocess (e.g. HARNESS_RUN_ID, HARNESS_RUN_DIR). */
50
54
  resolveSubprocessEnv?: (
51
55
  task: string,
@@ -453,13 +457,28 @@ function buildAgentArgs(
453
457
  agent.extensionsOff &&
454
458
  (subagentsOptions?.subprocessGovernanceExtensionPath ??
455
459
  subagentsOptions?.harnessSubprocessExtensionPath);
456
- if (agent.extensionsOff) {
460
+ const bundlePaths =
461
+ agent.extensionBundle && subagentsOptions?.resolveExtensionBundlePaths
462
+ ? subagentsOptions.resolveExtensionBundlePaths(agent.extensionBundle)
463
+ : [];
464
+
465
+ if (agent.extensionBundle && bundlePaths.length > 0) {
466
+ args.push("--no-extensions");
467
+ for (const extPath of bundlePaths) {
468
+ args.push("-e", extPath);
469
+ }
470
+ if (agent.skillsOff) args.push("--no-skills");
471
+ } else if (agent.extensionsOff) {
457
472
  args.push("--no-extensions");
458
473
  if (governanceExt) args.push("-e", governanceExt);
459
474
  if (agent.skillsOff) args.push("--no-skills");
460
475
  }
461
- if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
462
- else if (agent.extensionsOff) args.push("--no-tools");
476
+ if (agent.tools?.length) {
477
+ args.push("--tools", agent.tools.join(","));
478
+ if (agent.noBuiltinTools) args.push("--no-builtin-tools");
479
+ } else if (agent.extensionsOff || agent.extensionBundle) {
480
+ args.push("--no-tools");
481
+ }
463
482
  return args;
464
483
  }
465
484