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.
- package/.pi/agents/harness/running/executor.md +42 -1
- package/.pi/extensions/harness-anchored-edit.ts +141 -0
- package/.pi/harness/agents.manifest.json +4 -4
- package/.pi/harness/agents.policy.yaml +3 -4
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/docs/practice-map.md +11 -0
- package/.pi/lib/agents-policy.d.mts +4 -0
- package/.pi/lib/agents-policy.mjs +43 -1
- package/.pi/lib/agents-policy.ts +1 -0
- package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
- package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
- package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
- package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
- package/.pi/lib/harness-anchored-edit/index.ts +9 -0
- package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
- package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
- package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
- package/.pi/lib/harness-anchored-edit/types.ts +19 -0
- package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/index.ts +24 -7
- package/.pi/lib/harness-subagents-bridge.ts +7 -5
- package/.pi/prompts/harness-steer.md +1 -1
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-verify.mjs +67 -0
- package/.pi/scripts/run-tests.mjs +64 -0
- package/CHANGELOG.md +6 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +4 -3
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- 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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
462
|
-
|
|
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
|
|