gentle-pi 2.3.0 → 2.5.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 +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -114,8 +114,21 @@ function measureOrchestratorPromptBytes(assetsDir: string): number {
|
|
|
114
114
|
// 2.2 — Byte budget (Spec: Always-On Injection Byte Budget)
|
|
115
115
|
// ---------------------------------------------------------------------------
|
|
116
116
|
|
|
117
|
+
// gentle-pi#661: `renderOrchestratorPrompt`/`getOrchestratorPrompt` default
|
|
118
|
+
// `rddStatusLine` to the "unknown (native status unavailable)" line -- the
|
|
119
|
+
// longest of the three renderable RDD status lines -- precisely so that a
|
|
120
|
+
// no-argument call renders the worst case this budget measures, not a
|
|
121
|
+
// smaller placeholder that production would later exceed. Assert that line
|
|
122
|
+
// is actually present so a future default change cannot silently start
|
|
123
|
+
// measuring a shorter render again.
|
|
124
|
+
const RDD_WORST_CASE_LINE = "Receipt-driven development: unknown (native status unavailable)";
|
|
125
|
+
|
|
117
126
|
test("getOrchestratorPrompt return value stays within the canonical 8,192 B budget at a short assets root", () => {
|
|
118
127
|
const rendered = __testing.renderOrchestratorPrompt(representativeProductionAssetsDir);
|
|
128
|
+
assert.ok(
|
|
129
|
+
rendered.includes(RDD_WORST_CASE_LINE),
|
|
130
|
+
"the default render must include the worst-case RDD status line to measure the real production budget",
|
|
131
|
+
);
|
|
119
132
|
const bytes = Buffer.byteLength(rendered, "utf8");
|
|
120
133
|
assert.ok(
|
|
121
134
|
bytes <= BUDGET_BYTES,
|
|
@@ -125,6 +138,10 @@ test("getOrchestratorPrompt return value stays within the canonical 8,192 B budg
|
|
|
125
138
|
|
|
126
139
|
test(`getOrchestratorPrompt keeps a controlled long (>= ${MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS} char) assets root within the canonical budget`, () => {
|
|
127
140
|
const rendered = __testing.renderOrchestratorPrompt(controlledLongAssetsDir);
|
|
141
|
+
assert.ok(
|
|
142
|
+
rendered.includes(RDD_WORST_CASE_LINE),
|
|
143
|
+
"the default render must include the worst-case RDD status line to measure the real production budget",
|
|
144
|
+
);
|
|
128
145
|
const bytes = measureOrchestratorPromptBytes(controlledLongAssetsDir);
|
|
129
146
|
assert.equal(bytes, Buffer.byteLength(rendered, "utf8"), "child-process and direct render byte counts must match");
|
|
130
147
|
assert.ok(
|
|
@@ -347,8 +364,8 @@ test("core-alone: load-bearing direct-delegation tokens remain without lazy unio
|
|
|
347
364
|
|
|
348
365
|
test("core-alone: dynamic Gentle AI ownership replaces package lifecycle instructions", () => {
|
|
349
366
|
const core = readRealAsset("orchestrator.md");
|
|
350
|
-
assert.match(core, /
|
|
351
|
-
assert.match(core, /
|
|
367
|
+
assert.match(core, /injects the mirrored provider-bundle review execution contract into this session's system prompt at start/);
|
|
368
|
+
assert.match(core, /Absent that mirrored contract, this package invents no lifecycle instructions/);
|
|
352
369
|
assert.doesNotMatch(core, /start -> finalize -> validate/i);
|
|
353
370
|
assert.doesNotMatch(core, /receipt validation/i);
|
|
354
371
|
});
|
|
@@ -420,3 +437,55 @@ test("getOrchestratorPrompt memoizes the return across calls", () => {
|
|
|
420
437
|
const second = __testing.getOrchestratorPrompt();
|
|
421
438
|
assert.equal(second, first, "second call must return the memoized string");
|
|
422
439
|
});
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// gentle-pi#661 follow-up: byte-budget compression must not turn a pointer
|
|
443
|
+
// into an opaque "Detail: `file.md`" -- each pointer line must still name
|
|
444
|
+
// the lazy-loaded material it points to, in the shortest form that still
|
|
445
|
+
// says what is over there.
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
test("every compressed lazy-file pointer in the core still names the material it points to", () => {
|
|
449
|
+
const core = readFileSync(join(REAL_ASSETS_DIR, "orchestrator.md"), "utf8");
|
|
450
|
+
// One-sentence "<named material>: `file.md`." pointers, compressed for the
|
|
451
|
+
// byte budget -- each must keep naming what is over there, not collapse
|
|
452
|
+
// to an opaque "Detail: `file.md`.".
|
|
453
|
+
const namedPointers: ReadonlyArray<{ file: string; mustName: readonly string[] }> = [
|
|
454
|
+
{
|
|
455
|
+
file: "orchestrator-delegation.md",
|
|
456
|
+
mustName: ["Per-action table", "Work Routing Ladder", "Canonical Workflows", "blocking-prompt relays"],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
file: "orchestrator-memory.md",
|
|
460
|
+
mustName: ["Phase table", "artifact keys"],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
file: "orchestrator-skills.md",
|
|
464
|
+
mustName: ["Discovery order", "intent hints"],
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
for (const { file, mustName } of namedPointers) {
|
|
468
|
+
// The pointer sentence: from the previous sentence boundary up to and
|
|
469
|
+
// including the backtick-quoted filename. `orchestrator-delegation.md`
|
|
470
|
+
// is referenced more than once in the core (a language-boundary pointer
|
|
471
|
+
// earlier, this compressed per-action pointer later) -- take the LAST
|
|
472
|
+
// occurrence, which is the one under test here.
|
|
473
|
+
const fileToken = `\`${file}\``;
|
|
474
|
+
const fileIndex = core.lastIndexOf(fileToken);
|
|
475
|
+
assert.ok(fileIndex >= 0, `core is missing a pointer to ${file}`);
|
|
476
|
+
const sentenceStart = core.lastIndexOf(".", fileIndex);
|
|
477
|
+
const pointerSentence = core.slice(sentenceStart + 1, fileIndex + fileToken.length);
|
|
478
|
+
for (const name of mustName) {
|
|
479
|
+
assert.ok(
|
|
480
|
+
pointerSentence.includes(name),
|
|
481
|
+
`the pointer to ${file} must name "${name}", got: ${JSON.stringify(pointerSentence.trim())}`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// The SDD workflow pointer was never compressed to a bare filename; its
|
|
486
|
+
// surrounding paragraphs already name the material at length.
|
|
487
|
+
assert.match(
|
|
488
|
+
core,
|
|
489
|
+
/SDD phases, native dispatcher rules, status contract, preflight\/init guards, artifact-store policy, execution mode, Strict TDD forwarding, phase result contract, and review workload guard/,
|
|
490
|
+
);
|
|
491
|
+
});
|
|
@@ -6,7 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
const ROOT = join(import.meta.dirname, "..");
|
|
7
7
|
const ASSETS = join(ROOT, "assets");
|
|
8
8
|
const BOUNDARY =
|
|
9
|
-
"
|
|
9
|
+
"This package injects the mirrored provider-bundle review execution contract into this session's system prompt at start; Gentle AI writes nothing into the Pi system prompt, and this package owns everything else here. Absent that mirrored contract, this package invents no lifecycle instructions.";
|
|
10
10
|
|
|
11
11
|
function read(relativePath: string): string {
|
|
12
12
|
return readFileSync(join(ROOT, relativePath), "utf8");
|
|
@@ -60,6 +60,15 @@ test("rendered parent prompt keeps the RDD boundary while omitting lifecycle mir
|
|
|
60
60
|
for (const marker of ["Authority-First Terminal Procedure", "reconcile-terminal-mirrors", "next_transition"]) {
|
|
61
61
|
assert.ok(!rendered.includes(marker), `rendered parent prompt leaked: ${marker}`);
|
|
62
62
|
}
|
|
63
|
+
// gentle-pi#661: getOrchestratorPrompt()'s no-argument default renders the
|
|
64
|
+
// "unknown (native status unavailable)" RDD status line -- the longest of
|
|
65
|
+
// the three renderable forms -- so this IS the worst-case render the 8 KiB
|
|
66
|
+
// budget below must cover, not a smaller placeholder production later
|
|
67
|
+
// exceeds.
|
|
68
|
+
assert.ok(
|
|
69
|
+
rendered.includes("Receipt-driven development: unknown (native status unavailable)"),
|
|
70
|
+
"the default render must include the worst-case RDD status line",
|
|
71
|
+
);
|
|
63
72
|
assert.ok(Buffer.byteLength(rendered, "utf8") <= 8192, "the rendered parent prompt must stay below the reduced 8 KiB budget");
|
|
64
73
|
});
|
|
65
74
|
|
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
rmSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
12
|
+
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import test from "node:test";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { applyModelConfig } from "../extensions/gentle-ai.ts";
|
|
17
|
+
import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
|
|
17
18
|
import { installSddAssets } from "../lib/sdd-preflight.ts";
|
|
18
19
|
|
|
19
20
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -79,6 +80,8 @@ interface PackageJson {
|
|
|
79
80
|
files?: string[];
|
|
80
81
|
scripts?: Record<string, string>;
|
|
81
82
|
dependencies?: Record<string, string>;
|
|
83
|
+
peerDependencies?: Record<string, string>;
|
|
84
|
+
devDependencies?: Record<string, string>;
|
|
82
85
|
bundledDependencies?: string[];
|
|
83
86
|
bundleDependencies?: string[];
|
|
84
87
|
repository?: {
|
|
@@ -98,6 +101,15 @@ function readPackageJson(): PackageJson {
|
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
test("package declares the tested Pi minimum required for agent_settled", () => {
|
|
105
|
+
const manifest = readPackageJson();
|
|
106
|
+
assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=0.85.1");
|
|
107
|
+
assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], "0.85.1");
|
|
108
|
+
const readme = readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8");
|
|
109
|
+
assert.match(readme, /Pi 0\.85\.1 or newer/);
|
|
110
|
+
assert.match(readme, /agent_settled/);
|
|
111
|
+
});
|
|
112
|
+
|
|
101
113
|
test("package manifest has no obsolete native activation build surface", () => {
|
|
102
114
|
const packageJson = readPackageJson();
|
|
103
115
|
const manifest = JSON.stringify(packageJson);
|
|
@@ -261,20 +273,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
261
273
|
);
|
|
262
274
|
});
|
|
263
275
|
|
|
264
|
-
test("package verification binds the published Gentle AI v2.
|
|
276
|
+
test("package verification binds the published Gentle AI v2.7.0 runtime pin", () => {
|
|
265
277
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
266
278
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
267
279
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
268
280
|
|
|
269
|
-
assert.match(installer, /INSTALLER_VERSION = "2\.
|
|
281
|
+
assert.match(installer, /INSTALLER_VERSION = "2\.7\.0"/);
|
|
270
282
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
271
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
283
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:SE4KCLo3y1qWaTq\+7HJKZ1DkAAAl\+w\+82YY0\+qQkFwM="/);
|
|
272
284
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
273
285
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
274
286
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
275
287
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
276
288
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
277
|
-
assert.match(verifier, /v2\.
|
|
289
|
+
assert.match(verifier, /v2\.7\.0/);
|
|
278
290
|
});
|
|
279
291
|
|
|
280
292
|
|
|
@@ -895,6 +907,22 @@ test("jd-fix-agent packaged allowlist includes write tools", () => {
|
|
|
895
907
|
}
|
|
896
908
|
});
|
|
897
909
|
|
|
910
|
+
test("sdd-explore packages its CodeGraph-enabled exploration allowlist", () => {
|
|
911
|
+
const agentPath = join(PACKAGE_ROOT, "assets", "agents", "sdd-explore.md");
|
|
912
|
+
const { name, tools } = readAgentDefinition(agentPath);
|
|
913
|
+
|
|
914
|
+
assert.equal(name, "sdd-explore");
|
|
915
|
+
assert.deepEqual(tools, [
|
|
916
|
+
"read",
|
|
917
|
+
"grep",
|
|
918
|
+
"find",
|
|
919
|
+
"codegraph",
|
|
920
|
+
"edit",
|
|
921
|
+
"write",
|
|
922
|
+
"mem_save",
|
|
923
|
+
]);
|
|
924
|
+
});
|
|
925
|
+
|
|
898
926
|
test("gentle-ai-worker packages the exact scoped writer contract", () => {
|
|
899
927
|
const agentsDir = join(PACKAGE_ROOT, "assets", "agents");
|
|
900
928
|
const agentPath = join(agentsDir, "gentle-ai-worker.md");
|
|
@@ -1055,6 +1083,103 @@ test("installSddAssets installs gentle-ai-worker with a loader-compatible scoped
|
|
|
1055
1083
|
);
|
|
1056
1084
|
});
|
|
1057
1085
|
|
|
1086
|
+
test("agent home resolver centralizes Gentle and Pi agent-dir precedence", () => {
|
|
1087
|
+
const explicitGentleHome = mkdtempSync(join(tmpdir(), "gentle-pi-resolver-explicit-"));
|
|
1088
|
+
const piAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-resolver-pi-dir-"));
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
assert.equal(
|
|
1092
|
+
resolveGentlePiAgentHome({
|
|
1093
|
+
GENTLE_PI_AGENT_HOME: explicitGentleHome,
|
|
1094
|
+
PI_CODING_AGENT_DIR: piAgentDir,
|
|
1095
|
+
}),
|
|
1096
|
+
explicitGentleHome,
|
|
1097
|
+
);
|
|
1098
|
+
assert.equal(resolveGentlePiAgentHome({ PI_CODING_AGENT_DIR: piAgentDir }), piAgentDir);
|
|
1099
|
+
assert.equal(resolveGentlePiAgentHome({}), join(homedir(), ".pi", "agent"));
|
|
1100
|
+
assert.equal(
|
|
1101
|
+
resolveGentlePiAgentHome({ GENTLE_PI_AGENT_HOME: "", PI_CODING_AGENT_DIR: piAgentDir }),
|
|
1102
|
+
piAgentDir,
|
|
1103
|
+
"an empty explicit override falls through like Pi Subagents does",
|
|
1104
|
+
);
|
|
1105
|
+
assert.equal(
|
|
1106
|
+
resolveGentlePiAgentHome({ PI_CODING_AGENT_DIR: "" }),
|
|
1107
|
+
join(homedir(), ".pi", "agent"),
|
|
1108
|
+
"an empty PI_CODING_AGENT_DIR falls through like Pi Subagents does",
|
|
1109
|
+
);
|
|
1110
|
+
} finally {
|
|
1111
|
+
rmSync(explicitGentleHome, { recursive: true, force: true });
|
|
1112
|
+
rmSync(piAgentDir, { recursive: true, force: true });
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test("asset installation uses PI_CODING_AGENT_DIR as the Pi agent home when no explicit Gentle override is set", () => {
|
|
1117
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
1118
|
+
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
1119
|
+
const temporaryPiAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-agent-dir-"));
|
|
1120
|
+
const explicitGentleHome = mkdtempSync(join(tmpdir(), "gentle-pi-explicit-home-"));
|
|
1121
|
+
|
|
1122
|
+
try {
|
|
1123
|
+
delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1124
|
+
process.env.PI_CODING_AGENT_DIR = temporaryPiAgentDir;
|
|
1125
|
+
|
|
1126
|
+
installSddAssets(PACKAGE_ROOT, true);
|
|
1127
|
+
|
|
1128
|
+
const installedPath = join(temporaryPiAgentDir, "agents", "gentle-ai-explore.md");
|
|
1129
|
+
assert.ok(existsSync(installedPath), "managed agents must install where Pi Subagents reads global definitions");
|
|
1130
|
+
assert.deepEqual(readAgentDefinition(installedPath).tools, MANAGED_EXEMPLAR_TOOLS);
|
|
1131
|
+
assert.ok(
|
|
1132
|
+
!existsSync(join(explicitGentleHome, "agents", "gentle-ai-explore.md")),
|
|
1133
|
+
"the explicit override fixture must still be untouched before it is selected",
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
process.env.GENTLE_PI_AGENT_HOME = explicitGentleHome;
|
|
1137
|
+
installSddAssets(PACKAGE_ROOT, true);
|
|
1138
|
+
assert.ok(
|
|
1139
|
+
existsSync(join(explicitGentleHome, "agents", "gentle-ai-explore.md")),
|
|
1140
|
+
"GENTLE_PI_AGENT_HOME remains the explicit test/operator override",
|
|
1141
|
+
);
|
|
1142
|
+
} finally {
|
|
1143
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1144
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
1145
|
+
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
1146
|
+
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
|
1147
|
+
rmSync(temporaryPiAgentDir, { recursive: true, force: true });
|
|
1148
|
+
rmSync(explicitGentleHome, { recursive: true, force: true });
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("global model routing uses PI_CODING_AGENT_DIR for package-installed agents", () => {
|
|
1153
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
1154
|
+
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
1155
|
+
const temporaryPiAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-model-agent-dir-"));
|
|
1156
|
+
const temporaryProject = mkdtempSync(join(tmpdir(), "gentle-pi-model-project-"));
|
|
1157
|
+
|
|
1158
|
+
try {
|
|
1159
|
+
delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1160
|
+
process.env.PI_CODING_AGENT_DIR = temporaryPiAgentDir;
|
|
1161
|
+
installSddAssets(PACKAGE_ROOT, true);
|
|
1162
|
+
|
|
1163
|
+
const result = applyModelConfig(temporaryProject, {
|
|
1164
|
+
"gentle-ai-explore": { model: "provider/model", thinking: "high" },
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
assert.equal(result.updated, 2);
|
|
1168
|
+
const config = JSON.parse(readFileSync(join(temporaryPiAgentDir, "subagents.json"), "utf8"));
|
|
1169
|
+
assert.deepEqual(config.model_profiles["gentle-ai-explore"], {
|
|
1170
|
+
model: "provider/model",
|
|
1171
|
+
effort: "high",
|
|
1172
|
+
});
|
|
1173
|
+
} finally {
|
|
1174
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1175
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
1176
|
+
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
1177
|
+
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
|
1178
|
+
rmSync(temporaryPiAgentDir, { recursive: true, force: true });
|
|
1179
|
+
rmSync(temporaryProject, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1058
1183
|
test("normal and forced installation copy generic agents with complete role contracts", () => {
|
|
1059
1184
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
1060
1185
|
const expectedTools = {
|
|
@@ -1146,8 +1271,8 @@ test("orchestrator routes generic roles without static RDD lens routing", () =>
|
|
|
1146
1271
|
}
|
|
1147
1272
|
|
|
1148
1273
|
const core = readFileSync(join(PACKAGE_ROOT, "assets", "orchestrator.md"), "utf8");
|
|
1149
|
-
assert.match(core, /
|
|
1150
|
-
assert.match(core, /this package
|
|
1274
|
+
assert.match(core, /injects the mirrored provider-bundle review execution contract/);
|
|
1275
|
+
assert.match(core, /this package invents no lifecycle instructions/);
|
|
1151
1276
|
});
|
|
1152
1277
|
|
|
1153
1278
|
test("pi-pretty wrapper uses real package path resolution for pnpm symlink installs", () => {
|
|
@@ -1163,9 +1288,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
|
|
|
1163
1288
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1164
1289
|
});
|
|
1165
1290
|
|
|
1166
|
-
test("v2.
|
|
1291
|
+
test("v2.5.0 release package and runtime stop before publication", () => {
|
|
1167
1292
|
const packageJson = readPackageJson();
|
|
1168
|
-
assert.equal(packageJson.version, "2.
|
|
1293
|
+
assert.equal(packageJson.version, "2.5.0", "the release manifest must remain explicitly pinned to v2.5.0");
|
|
1169
1294
|
assert.equal(
|
|
1170
1295
|
packageJson.scripts?.test,
|
|
1171
1296
|
"node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
@@ -35,6 +35,16 @@ function fixtureEntries(): Map<string, Buffer> {
|
|
|
35
35
|
return new Map(FIXTURE_PATHS.map((path) => [path, readFileSync(join(FIXTURE_ROOT, path))]));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Fixture provenance: the real gentle-ai provider contract bundle 1.2.0
|
|
39
|
+
// release archive (contract semver 1.2.0), extracted. Adds `orchestration`
|
|
40
|
+
// to the manifest: gentle-pi#560 / gentle-ai#4056, #4057.
|
|
41
|
+
const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
|
|
42
|
+
const FIXTURE_1_2_0_PATHS = [...FIXTURE_PATHS, "orchestration/pi.md"] as const;
|
|
43
|
+
|
|
44
|
+
function fixture120Entries(): Map<string, Buffer> {
|
|
45
|
+
return new Map(FIXTURE_1_2_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_2_0_ROOT, path))]));
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
function sha256Hex(bytes: Buffer): string {
|
|
39
49
|
return createHash("sha256").update(bytes).digest("hex");
|
|
40
50
|
}
|
|
@@ -177,11 +187,25 @@ test("generated baselines project roles, capabilities, and runtime registration
|
|
|
177
187
|
assert.equal(capabilities.transport_capability, PROVIDER_TRANSPORT_CAPABILITY);
|
|
178
188
|
assert.deepEqual(capabilities.mandatory_capabilities, [PROVIDER_TRANSPORT_CAPABILITY]);
|
|
179
189
|
assert.equal(capabilities.pi_registered, true);
|
|
190
|
+
assert.deepEqual(capabilities.orchestration, []);
|
|
180
191
|
// Byte determinism: regenerating from the same bundle is identical.
|
|
181
192
|
const again = generateProviderContractBaselines(verifyProviderContractBundleTree(FIXTURE_ROOT));
|
|
182
193
|
assert.deepEqual([...again.entries()], [...baselines.entries()]);
|
|
183
194
|
});
|
|
184
195
|
|
|
196
|
+
test("generated 1.2.0 baselines record the mirrored orchestration entry", () => {
|
|
197
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT);
|
|
198
|
+
const baselines = generateProviderContractBaselines(bundle);
|
|
199
|
+
const capabilities = JSON.parse(baselines.get("provider-capabilities.baseline.json") as string) as { orchestration: { runtime: string; path: string; sha256: string }[] };
|
|
200
|
+
assert.deepEqual(capabilities.orchestration, [
|
|
201
|
+
{
|
|
202
|
+
runtime: PI_RUNTIME_IDENTITY,
|
|
203
|
+
path: "orchestration/pi.md",
|
|
204
|
+
sha256: bundle.entrySha256.get("orchestration/pi.md"),
|
|
205
|
+
},
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
185
209
|
// --- inventory and hash rejections ---------------------------------------------
|
|
186
210
|
|
|
187
211
|
test("rejects a ninth file in the inventory", () => {
|
|
@@ -315,6 +339,58 @@ test("rejects a schema whose $id does not match the manifest result schema id",
|
|
|
315
339
|
);
|
|
316
340
|
});
|
|
317
341
|
|
|
342
|
+
// --- orchestration (contract 1.2.0) -------------------------------------------------
|
|
343
|
+
|
|
344
|
+
test("verifies the 1.2.0 fixture tree and exposes the mirrored pi orchestration text", () => {
|
|
345
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT);
|
|
346
|
+
assert.equal(bundle.contractSemver, "1.2.0");
|
|
347
|
+
assert.deepEqual([bundle.major, bundle.minor, bundle.patch], [1, 2, 0]);
|
|
348
|
+
assert.equal(bundle.entries.size, 9);
|
|
349
|
+
assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
|
|
350
|
+
const piText = bundle.orchestration.get(PI_RUNTIME_IDENTITY);
|
|
351
|
+
assert.equal(piText, readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration/pi.md"), "utf8"));
|
|
352
|
+
assert.equal(bundle.entrySha256.get("orchestration/pi.md"), sha256Hex(readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration/pi.md"))));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("rejects a 1.2.0 manifest without the required orchestration array", () => {
|
|
356
|
+
assertRejects(
|
|
357
|
+
withManifest(fixture120Entries(), (manifest) => {
|
|
358
|
+
delete manifest.orchestration;
|
|
359
|
+
}),
|
|
360
|
+
"manifest.orchestration is required from contract 1.2.0",
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("rejects a 1.2.0 orchestration entry whose runtime is not registered in manifest.runtimes", () => {
|
|
365
|
+
assertRejects(
|
|
366
|
+
withManifest(fixture120Entries(), (manifest) => {
|
|
367
|
+
const orchestration = manifest.orchestration as { runtime: string; file: { path: string } }[];
|
|
368
|
+
orchestration[0]!.runtime = "aider";
|
|
369
|
+
orchestration[0]!.file.path = "orchestration/aider.md";
|
|
370
|
+
}),
|
|
371
|
+
'manifest.orchestration[0].runtime "aider" is not registered in manifest.runtimes',
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("rejects a 1.2.0 orchestration entry whose file bytes do not match its manifest SHA-256", () => {
|
|
376
|
+
const entries = fixture120Entries();
|
|
377
|
+
const tampered = Buffer.from(entries.get("orchestration/pi.md") as Buffer);
|
|
378
|
+
tampered[tampered.length - 2] = 0x20;
|
|
379
|
+
entries.set("orchestration/pi.md", tampered);
|
|
380
|
+
assertRejects(entries, "does not match its manifest SHA-256");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("rejects an orchestration array on a 1.1.0 manifest", () => {
|
|
384
|
+
assertRejects(
|
|
385
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
386
|
+
manifest.orchestration = [
|
|
387
|
+
{ runtime: PI_RUNTIME_IDENTITY, file: { path: "orchestration/pi.md", sha256: "0".repeat(64) } },
|
|
388
|
+
];
|
|
389
|
+
}),
|
|
390
|
+
"manifest.orchestration is not part of contract 1.1.0",
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
318
394
|
// --- archive safety ------------------------------------------------------------------
|
|
319
395
|
|
|
320
396
|
test("rejects a tar entry that escapes the bundle root", () => {
|
|
@@ -27,6 +27,10 @@ const FIXTURE_PATHS = [
|
|
|
27
27
|
"vectors/targeted-validator.json",
|
|
28
28
|
] as const;
|
|
29
29
|
|
|
30
|
+
// Fixture provenance: the real gentle-ai provider contract bundle 1.2.0
|
|
31
|
+
// release archive, extracted. gentle-pi#560 / gentle-ai#4056, #4057.
|
|
32
|
+
const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
|
|
33
|
+
|
|
30
34
|
interface LockRecord {
|
|
31
35
|
schema: string;
|
|
32
36
|
acquisition: string;
|
|
@@ -105,6 +109,21 @@ test("mirrors a verified local bundle tree and the offline drift check passes",
|
|
|
105
109
|
});
|
|
106
110
|
});
|
|
107
111
|
|
|
112
|
+
test("mirrors a verified local 1.2.0 bundle tree, including its orchestration entry, and the offline drift check passes", () => {
|
|
113
|
+
withTemporaryRoot((packageRoot) => {
|
|
114
|
+
mirrorProviderContractBundle(FIXTURE_1_2_0_ROOT, packageRoot);
|
|
115
|
+
const lockPath = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE);
|
|
116
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8")) as LockRecord;
|
|
117
|
+
assert.equal(lock.contract_semver, "1.2.0");
|
|
118
|
+
assert.equal(Object.keys(lock.entries).length, 9);
|
|
119
|
+
assert.ok("orchestration/pi.md" in lock.entries, "the mirrored lock must record orchestration/pi.md");
|
|
120
|
+
const mirrored = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.2.0", "bundle", "orchestration", "pi.md");
|
|
121
|
+
assert.ok(existsSync(mirrored), "orchestration/pi.md must be mirrored");
|
|
122
|
+
assert.deepEqual(readFileSync(mirrored), readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration", "pi.md")));
|
|
123
|
+
assert.deepEqual(checkProviderContractMirror(packageRoot), []);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
108
127
|
test("mirrors a verified local archive and records the archive digest in the lock", () => {
|
|
109
128
|
withTemporaryRoot((packageRoot) => {
|
|
110
129
|
const archivePath = join(packageRoot, "gentle-ai-review-provider-contract-1.1.0.tar.gz");
|