gentle-pi 4.2.0 → 4.3.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 +336 -137
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/gentle-shell.mjs +170 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +33 -3
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +5 -4
- package/docs/install-wizard.md +8 -1
- package/docs/permission-modes.md +1 -1
- package/docs/readme-reference.md +107 -19
- package/docs/yolo-mode.md +21 -8
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +99 -12
- package/extensions/gentle-ai.ts +284 -52
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +27 -12
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +39 -3
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +145 -11
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-todo.ts +23 -4
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +38 -2
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +143 -9
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/autonomous-guard.test.ts +122 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +1 -1
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +282 -3
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +169 -6
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +79 -0
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +42 -9
- package/tests/gentle-todo.test.ts +179 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +24 -20
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +18 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -5,7 +5,7 @@ import { test } from 'node:test';
|
|
|
5
5
|
const PI = '@earendil-works/pi-coding-agent';
|
|
6
6
|
// A virtual symlink filesystem: listed global links point into a pnpm store.
|
|
7
7
|
// The store's adjacent peer is a sibling, not Shell/node_modules/Pi.
|
|
8
|
-
function launch(args: string[], scenario = 'owned') {
|
|
8
|
+
function launch(args: string[], scenario = 'owned', piVersion = '1.2.0') {
|
|
9
9
|
const adjacent = scenario.startsWith('adjacent');
|
|
10
10
|
const npm = scenario.startsWith('npm-');
|
|
11
11
|
const nodeModules = '/global/v11/selected/node_modules';
|
|
@@ -31,7 +31,7 @@ function launch(args: string[], scenario = 'owned') {
|
|
|
31
31
|
source = source.replace('const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));', ${JSON.stringify(`const packageRoot = '${shellRoot}';`)});
|
|
32
32
|
source = source.replace('const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });', 'const bundled = undefined;');
|
|
33
33
|
source = source.replace('runtime = resolvePiRuntime({', ${JSON.stringify(`runtime = ({ kind: '${adjacent ? 'bundled' : 'env'}', command: '${adjacent ? 'node' : piRoot + '/dist/bundle/cli.js'}', args: ${JSON.stringify(adjacent ? [piRoot + '/dist/bundle/cli.js'] : [])} }); /*`)}).replace('nodeExecPath: process.execPath,\\n\\t\\t});', 'nodeExecPath: process.execPath, */');
|
|
34
|
-
source = source.replace('const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {', 'const versionProbe = { stdout: "
|
|
34
|
+
source = source.replace('const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {', 'const versionProbe = { stdout: "${piVersion}" }; /*').replace('env: bundledEnv ?? process.env,\\n\\t});', 'env: bundledEnv ?? process.env, */');
|
|
35
35
|
source = source.replace('async function packageManagerAdapters() {', 'async function packageManagerAdapters() { return globalThis.fakeAdapters; } async function unusedAdapters() {');
|
|
36
36
|
source = source.replace('let homeHadContentBeforeBootstrap = false;', 'if (passthrough.length === 1) { console.error("error: pi cannot self-update this installation. This installation is not managed by a global pnpm install."); process.exitCode = 1; return; } console.log("FORWARD " + JSON.stringify(passthrough)); return; let homeHadContentBeforeBootstrap = false;');
|
|
37
37
|
return {format:'module', source, shortCircuit:true};
|
|
@@ -79,6 +79,25 @@ for (const scenario of ['ambiguous', 'malformed', 'npm-link', 'npm-file', 'share
|
|
|
79
79
|
assert.doesNotMatch(result.stdout + result.stderr, /MUTATION/);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
// `update` is how an old Pi gets replaced, so the minimum-version gate must not block it.
|
|
83
|
+
test('public update replaces a Pi older than the required minimum', () => {
|
|
84
|
+
const result = launch(['update'], 'owned', '1.1.0');
|
|
85
|
+
assert.equal(result.status, 0, result.stderr);
|
|
86
|
+
assert.match(result.stdout, /MUTATION \["add","-g","@earendil-works\/pi-coding-agent@latest"\]/);
|
|
87
|
+
assert.doesNotMatch(result.stdout + result.stderr, /older than the required minimum/);
|
|
88
|
+
assert.equal(result.stderr, '');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('an old Pi still fails the minimum-version gate outside a self update', () => {
|
|
92
|
+
for (const args of [['list'], ['update', '--extensions-only']]) {
|
|
93
|
+
const result = launch(args, 'owned', '1.1.0');
|
|
94
|
+
assert.equal(result.status, 1, result.stdout);
|
|
95
|
+
assert.equal(result.stdout, '');
|
|
96
|
+
assert.match(result.stderr, /pi version 1\.1\.0 is older than the required minimum 1\.2\.0\./);
|
|
97
|
+
assert.doesNotMatch(result.stdout, /MUTATION/);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
82
101
|
test('public extensions-only update preserves the forwarded command', () => {
|
|
83
102
|
const result = launch(['update', '--extensions-only']);
|
|
84
103
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test, { after, before } from "node:test";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
|
|
9
|
+
// gentle-ai R3 (odd/tasks/pi-routing-from-bundle.md): the harness ODD step
|
|
10
|
+
// lines 1-7, the phase-reporting line, and the resume line come from the
|
|
11
|
+
// mirrored provider contract bundle's routing/pi.md instead of a hand-copied
|
|
12
|
+
// literal, and the rendered primary-session prompt stays byte-identical.
|
|
13
|
+
//
|
|
14
|
+
// The golden was captured from the base commit (main 4372ff2d, after R1) before the steps moved into the
|
|
15
|
+
// bundle, with the inputs below and the package assets root normalized. When a
|
|
16
|
+
// prompt change is intended, regenerate it with
|
|
17
|
+
// UPDATE_PRIMARY_SESSION_PROMPT_GOLDEN=1 and review the golden diff.
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
20
|
+
const GOLDEN_PATH = join(REPO_ROOT, "tests", "fixtures", "primary-session-prompt", "gentleman.golden.md");
|
|
21
|
+
const ACTIVE_TOOLS = ["read", "bash", "edit", "write", "subagent_run"] as const;
|
|
22
|
+
const MIRROR_ROOT = join(REPO_ROOT, "contracts", "review-provider-contract-mirror");
|
|
23
|
+
|
|
24
|
+
let scratch: string;
|
|
25
|
+
const previousEnvironment = new Map<string, string | undefined>();
|
|
26
|
+
before(() => {
|
|
27
|
+
scratch = mkdtempSync(join(tmpdir(), "gentle-pi-primary-prompt-"));
|
|
28
|
+
for (const key of ["GENTLE_PI_CONFIG_HOME", "GENTLE_PI_BACKGROUND_SUBAGENTS"]) previousEnvironment.set(key, process.env[key]);
|
|
29
|
+
process.env.GENTLE_PI_CONFIG_HOME = join(scratch, "config");
|
|
30
|
+
delete process.env.GENTLE_PI_BACKGROUND_SUBAGENTS;
|
|
31
|
+
});
|
|
32
|
+
after(() => {
|
|
33
|
+
for (const [key, value] of previousEnvironment) {
|
|
34
|
+
if (value === undefined) delete process.env[key];
|
|
35
|
+
else process.env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function renderPrimaryPrompt(): string {
|
|
41
|
+
const prompt = __testing.buildGentlePrompt("gentleman", join(scratch, "project"), ACTIVE_TOOLS);
|
|
42
|
+
return prompt.replaceAll(join(REPO_ROOT, "assets"), "{{GENTLE_PI_ASSETS_ROOT}}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mirroredRoutingText(): string {
|
|
46
|
+
const lock = JSON.parse(readFileSync(join(MIRROR_ROOT, "provider-contract.lock.json"), "utf8")) as { contract_semver: string };
|
|
47
|
+
return readFileSync(join(MIRROR_ROOT, `v${lock.contract_semver}`, "bundle", "routing", "pi.md"), "utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Writes a one-file mirror whose lock records `entrySha256` for routing/pi.md.
|
|
51
|
+
function tempRoutingMirror(text: string, entrySha256 = createHash("sha256").update(text, "utf8").digest("hex")): string {
|
|
52
|
+
const root = mkdtempSync(join(scratch, "mirror-"));
|
|
53
|
+
const routingDir = join(root, "v9.9.9", "bundle", "routing");
|
|
54
|
+
mkdirSync(routingDir, { recursive: true });
|
|
55
|
+
writeFileSync(join(routingDir, "pi.md"), text, "utf8");
|
|
56
|
+
writeFileSync(join(root, "provider-contract.lock.json"), JSON.stringify({ contract_semver: "9.9.9", entries: { "routing/pi.md": entrySha256 } }), "utf8");
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("the rendered primary-session prompt is byte-identical to the golden captured from the base", () => {
|
|
61
|
+
const rendered = renderPrimaryPrompt();
|
|
62
|
+
if (process.env.UPDATE_PRIMARY_SESSION_PROMPT_GOLDEN === "1") writeFileSync(GOLDEN_PATH, rendered, "utf8");
|
|
63
|
+
assert.equal(rendered, readFileSync(GOLDEN_PATH, "utf8"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the ODD steps, phase reporting, and resume lines come from the mirrored routing/pi.md without its blank lines", () => {
|
|
67
|
+
const routing = mirroredRoutingText();
|
|
68
|
+
const section = routing.slice(routing.indexOf("### ODD protocol"), routing.indexOf("\n### Task Size"));
|
|
69
|
+
const expected = section.split("\n").slice(1).filter((line) => line !== "");
|
|
70
|
+
assert.equal(expected.length, 9);
|
|
71
|
+
assert.equal(__testing.readMirroredOddProtocolSteps(), expected.join("\n"));
|
|
72
|
+
assert.ok(renderPrimaryPrompt().includes(`Run these steps in this order on every request:\n${expected.join("\n")}\n\nHarness principles:`));
|
|
73
|
+
assert.ok(routing.includes(`${expected[6]}\n\n${expected[7]}\n\n${expected[8]}`), "the bundle separates step 7, phase reporting, and resume with blank lines");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("extensions/gentle-ai.ts no longer carries the ODD step lines as string literals", () => {
|
|
77
|
+
const source = readFileSync(join(REPO_ROOT, "extensions", "gentle-ai.ts"), "utf8");
|
|
78
|
+
for (const literal of ["1. **Authorize.**", "5. **Track before the first write.**", "7. **Close.**", "Phase reporting: the Gentle Shell prompt label", "Resume an interrupted feature with"]) {
|
|
79
|
+
assert.ok(!source.includes(literal), `extensions/gentle-ai.ts still hard-codes ${JSON.stringify(literal)}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a changed mirrored routing block changes the rendered steps", () => {
|
|
84
|
+
const routing = mirroredRoutingText().replace("1. **Authorize.** ", "1. **Authorize.** Changed upstream. ");
|
|
85
|
+
const mirror = tempRoutingMirror(routing);
|
|
86
|
+
assert.match(__testing.readMirroredOddProtocolSteps(mirror), /^1\. \*\*Authorize\.\*\* Changed upstream\. /);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a tampered or missing mirrored routing/pi.md fails closed with an explicit error", () => {
|
|
90
|
+
const unavailable = /Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing\/pi\.md is missing, unreadable, or fails digest verification/;
|
|
91
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(mirroredRoutingText(), "0".repeat(64))), unavailable);
|
|
92
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(join(scratch, "absent-mirror")), unavailable);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a routing block whose ODD protocol section changed shape fails closed", () => {
|
|
96
|
+
const unexpectedShape = /routing\/pi\.md ODD protocol section has an unexpected shape/;
|
|
97
|
+
const withoutResume = mirroredRoutingText().replace(/\nResume an interrupted feature[^\n]*\n/, "\n");
|
|
98
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(withoutResume)), unexpectedShape);
|
|
99
|
+
const withoutSection = mirroredRoutingText().replace("### ODD protocol (MANDATORY, in this order, on every request)", "### ODD protocol");
|
|
100
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(withoutSection)), unexpectedShape);
|
|
101
|
+
});
|
|
@@ -45,6 +45,26 @@ function fixture120Entries(): Map<string, Buffer> {
|
|
|
45
45
|
return new Map(FIXTURE_1_2_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_2_0_ROOT, path))]));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Fixture provenance: the gentle-ai provider contract bundle 1.4.0 archive
|
|
49
|
+
// built by goreleaser (archives `review-provider-contract`) from gentle-ai
|
|
50
|
+
// 9e6f24f6, extracted. Adds `routing` to the manifest: Pi's ODD routing steps.
|
|
51
|
+
const FIXTURE_1_4_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.4.0");
|
|
52
|
+
const FIXTURE_1_4_0_PATHS = [...FIXTURE_1_2_0_PATHS, "routing/pi.md"] as const;
|
|
53
|
+
|
|
54
|
+
function fixture140Entries(): Map<string, Buffer> {
|
|
55
|
+
return new Map(FIXTURE_1_4_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_4_0_ROOT, path))]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A valid 1.3.0-shaped bundle: the 1.4.0 fixture without the routing field and file.
|
|
59
|
+
function contractOneThreeEntries(): Map<string, Buffer> {
|
|
60
|
+
const entries = withManifest(fixture140Entries(), (manifest) => {
|
|
61
|
+
manifest.contract_semver = "1.3.0";
|
|
62
|
+
delete manifest.routing;
|
|
63
|
+
});
|
|
64
|
+
entries.delete("routing/pi.md");
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
function sha256Hex(bytes: Buffer): string {
|
|
49
69
|
return createHash("sha256").update(bytes).digest("hex");
|
|
50
70
|
}
|
|
@@ -391,6 +411,104 @@ test("rejects an orchestration array on a 1.1.0 manifest", () => {
|
|
|
391
411
|
);
|
|
392
412
|
});
|
|
393
413
|
|
|
414
|
+
// --- routing (contract 1.4.0) ----------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
test("verifies the 1.4.0 fixture tree and exposes the mirrored pi routing text", () => {
|
|
417
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_1_4_0_ROOT);
|
|
418
|
+
assert.equal(bundle.contractSemver, "1.4.0");
|
|
419
|
+
assert.equal(bundle.entries.size, 10);
|
|
420
|
+
assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
|
|
421
|
+
assert.deepEqual([...bundle.routing.keys()], [PI_RUNTIME_IDENTITY]);
|
|
422
|
+
assert.equal(bundle.routing.get(PI_RUNTIME_IDENTITY), readFileSync(join(FIXTURE_1_4_0_ROOT, "routing/pi.md"), "utf8"));
|
|
423
|
+
assert.equal(bundle.entrySha256.get("routing/pi.md"), sha256Hex(readFileSync(join(FIXTURE_1_4_0_ROOT, "routing/pi.md"))));
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("verifies the 1.4.0 release archive in memory", () => {
|
|
427
|
+
withTemporaryDirectory((directory) => {
|
|
428
|
+
const archivePath = join(directory, "gentle-ai-review-provider-contract-1.4.0.tar.gz");
|
|
429
|
+
writeFileSync(archivePath, buildTarGz([...fixture140Entries()].map(([name, bytes]) => ({ name, bytes }))));
|
|
430
|
+
const { bundle } = verifyProviderContractBundleArchive(archivePath);
|
|
431
|
+
assert.equal(bundle.contractSemver, "1.4.0");
|
|
432
|
+
assert.deepEqual([...bundle.routing.keys()], [PI_RUNTIME_IDENTITY]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a 1.3.0 bundle without routing still verifies with an empty routing inventory", () => {
|
|
437
|
+
const bundle = verifyProviderContractBundleEntries(contractOneThreeEntries());
|
|
438
|
+
assert.equal(bundle.contractSemver, "1.3.0");
|
|
439
|
+
assert.equal(bundle.entries.size, 9);
|
|
440
|
+
assert.equal(bundle.routing.size, 0);
|
|
441
|
+
assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("rejects a 1.4.0 manifest without the required routing array", () => {
|
|
445
|
+
const entries = withManifest(fixture140Entries(), (manifest) => {
|
|
446
|
+
delete manifest.routing;
|
|
447
|
+
});
|
|
448
|
+
assertRejects(entries, "manifest.routing is required from contract 1.4.0");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("rejects a routing array on a 1.3.0 manifest", () => {
|
|
452
|
+
const entries = withManifest(fixture140Entries(), (manifest) => {
|
|
453
|
+
manifest.contract_semver = "1.3.0";
|
|
454
|
+
});
|
|
455
|
+
assertRejects(entries, "manifest.routing is not part of contract 1.3.0");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("rejects a 1.4.0 routing entry whose runtime is not registered in manifest.runtimes", () => {
|
|
459
|
+
assertRejects(
|
|
460
|
+
withManifest(fixture140Entries(), (manifest) => {
|
|
461
|
+
const routing = manifest.routing as { runtime: string; file: { path: string } }[];
|
|
462
|
+
routing[0]!.runtime = "aider";
|
|
463
|
+
routing[0]!.file.path = "routing/aider.md";
|
|
464
|
+
}),
|
|
465
|
+
'manifest.routing[0].runtime "aider" is not registered in manifest.runtimes',
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("rejects a 1.4.0 routing entry that points outside routing/<runtime>.md", () => {
|
|
470
|
+
assertRejects(
|
|
471
|
+
withManifest(fixture140Entries(), (manifest) => {
|
|
472
|
+
(manifest.routing as { file: { path: string } }[])[0]!.file.path = "orchestration/pi.md";
|
|
473
|
+
}),
|
|
474
|
+
'manifest.routing[0].file.path must be "routing/pi.md"',
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("rejects a 1.4.0 routing entry whose file bytes do not match its manifest SHA-256", () => {
|
|
479
|
+
const entries = fixture140Entries();
|
|
480
|
+
const tampered = Buffer.from(entries.get("routing/pi.md") as Buffer);
|
|
481
|
+
tampered[tampered.length - 2] = 0x20;
|
|
482
|
+
entries.set("routing/pi.md", tampered);
|
|
483
|
+
assertRejects(entries, "does not match its manifest SHA-256");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("rejects a 1.4.0 bundle missing its routing file", () => {
|
|
487
|
+
const entries = fixture140Entries();
|
|
488
|
+
entries.delete("routing/pi.md");
|
|
489
|
+
assertRejects(entries, "bundle inventory has 9 files; exactly 10 are expected");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("the raw-entry bound leaves room for one routing file per runtime before the exact count decides", () => {
|
|
493
|
+
const entries = fixture140Entries();
|
|
494
|
+
for (let index = 0; index < 70; index += 1) entries.set(`extra/${index}.md`, Buffer.from("x\n"));
|
|
495
|
+
assertRejects(entries, "bundle inventory has 80 files; exactly 10 are expected");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("generated 1.4.0 baselines record the mirrored routing entry", () => {
|
|
499
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_1_4_0_ROOT);
|
|
500
|
+
const capabilities = JSON.parse(
|
|
501
|
+
generateProviderContractBaselines(bundle).get("provider-capabilities.baseline.json") as string,
|
|
502
|
+
) as { routing: { runtime: string; path: string; sha256: string }[] };
|
|
503
|
+
assert.deepEqual(capabilities.routing, [
|
|
504
|
+
{ runtime: PI_RUNTIME_IDENTITY, path: "routing/pi.md", sha256: bundle.entrySha256.get("routing/pi.md") },
|
|
505
|
+
]);
|
|
506
|
+
const older = JSON.parse(
|
|
507
|
+
generateProviderContractBaselines(verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT)).get("provider-capabilities.baseline.json") as string,
|
|
508
|
+
) as { routing: unknown[] };
|
|
509
|
+
assert.deepEqual(older.routing, []);
|
|
510
|
+
});
|
|
511
|
+
|
|
394
512
|
// --- archive safety ------------------------------------------------------------------
|
|
395
513
|
|
|
396
514
|
test("rejects a tar entry that escapes the bundle root", () => {
|
|
@@ -31,6 +31,10 @@ const FIXTURE_PATHS = [
|
|
|
31
31
|
// release archive, extracted. gentle-pi#560 / gentle-ai#4056, #4057.
|
|
32
32
|
const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
|
|
33
33
|
|
|
34
|
+
// Fixture provenance: the gentle-ai provider contract bundle 1.4.0 archive
|
|
35
|
+
// built by goreleaser, extracted. Adds Pi's ODD routing block as routing/pi.md.
|
|
36
|
+
const FIXTURE_1_4_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.4.0");
|
|
37
|
+
|
|
34
38
|
interface LockRecord {
|
|
35
39
|
schema: string;
|
|
36
40
|
acquisition: string;
|
|
@@ -124,6 +128,26 @@ test("mirrors a verified local 1.2.0 bundle tree, including its orchestration en
|
|
|
124
128
|
});
|
|
125
129
|
});
|
|
126
130
|
|
|
131
|
+
test("mirrors a verified local 1.4.0 bundle tree, including its routing entry, and the offline drift check passes", () => {
|
|
132
|
+
withTemporaryRoot((packageRoot) => {
|
|
133
|
+
mirrorProviderContractBundle(FIXTURE_1_4_0_ROOT, packageRoot);
|
|
134
|
+
const lock = JSON.parse(
|
|
135
|
+
readFileSync(join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE), "utf8"),
|
|
136
|
+
) as LockRecord;
|
|
137
|
+
assert.equal(lock.contract_semver, "1.4.0");
|
|
138
|
+
assert.equal(Object.keys(lock.entries).length, 10);
|
|
139
|
+
assert.ok("routing/pi.md" in lock.entries, "the mirrored lock must record routing/pi.md");
|
|
140
|
+
const mirrored = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.4.0", "bundle", "routing", "pi.md");
|
|
141
|
+
assert.deepEqual(readFileSync(mirrored), readFileSync(join(FIXTURE_1_4_0_ROOT, "routing", "pi.md")));
|
|
142
|
+
assert.deepEqual(checkProviderContractMirror(packageRoot), []);
|
|
143
|
+
|
|
144
|
+
writeFileSync(mirrored, `${readFileSync(mirrored, "utf8")}tampered\n`);
|
|
145
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
146
|
+
assert.equal(problems.length, 1);
|
|
147
|
+
assert.match(problems[0] ?? "", /mirrored bundle failed verification: .*"routing\/pi\.md" does not match its manifest SHA-256/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
127
151
|
test("mirrors a verified local archive and records the archive digest in the lock", () => {
|
|
128
152
|
withTemporaryRoot((packageRoot) => {
|
|
129
153
|
const archivePath = join(packageRoot, "gentle-ai-review-provider-contract-1.1.0.tar.gz");
|
|
@@ -1232,6 +1232,29 @@ test("quiet tool rendering preserves complete expanded text and delegates saniti
|
|
|
1232
1232
|
assert.doesNotMatch(image, /\x1b/);
|
|
1233
1233
|
});
|
|
1234
1234
|
|
|
1235
|
+
test("quiet tool rendering re-renders expanded read-with-image results without crashing when lastComponent is passed (#1620)", () => {
|
|
1236
|
+
const tools = registeredQuietTools();
|
|
1237
|
+
const readTool = tools.get("read");
|
|
1238
|
+
const result = {
|
|
1239
|
+
content: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }],
|
|
1240
|
+
details: { path: "image.png" },
|
|
1241
|
+
};
|
|
1242
|
+
const context = { args: { path: "image.png" }, cwd: "/repo", showImages: false, isError: false };
|
|
1243
|
+
|
|
1244
|
+
const initialComponent = readTool.renderResult(result, { expanded: true, isPartial: false }, passthroughTheme, context);
|
|
1245
|
+
const initialOutput = renderToString(initialComponent);
|
|
1246
|
+
assert.ok(initialOutput.includes(imageFallback("image/png")));
|
|
1247
|
+
|
|
1248
|
+
const reRenderComponent = readTool.renderResult(
|
|
1249
|
+
result,
|
|
1250
|
+
{ expanded: true, isPartial: false },
|
|
1251
|
+
passthroughTheme,
|
|
1252
|
+
{ ...context, lastComponent: initialComponent },
|
|
1253
|
+
);
|
|
1254
|
+
const reRenderOutput = renderToString(reRenderComponent);
|
|
1255
|
+
assert.ok(reRenderOutput.includes(imageFallback("image/png")));
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1235
1258
|
test("quiet tool rendering preserves directional visual preview rows at narrow widths", () => {
|
|
1236
1259
|
const tools = registeredQuietTools();
|
|
1237
1260
|
const bash = tools.get("bash");
|
|
@@ -48,13 +48,18 @@ const ROUTING_SENTENCE =
|
|
|
48
48
|
'After the writer returns, call `gentle_review` with `{"operation":"assess"}` over the writer\'s diff and follow the returned plan, whatever the `Receipt-driven development:` line reads (`on`, `off`, or `unknown`), instead of judging non-triviality from the task description: the operation resolves the native risk tier and states exactly who verifies next.';
|
|
49
49
|
const RDD_ON_SENTENCE =
|
|
50
50
|
"When the line reads `on`, the native review is an additional outside view of the change; it never replaces or skips the tier's verification, and the tier's verifier runs before review.";
|
|
51
|
-
|
|
51
|
+
// Verification by named risk changed the tier table and replaced the
|
|
52
|
+
// small-model bias sentence: the independent verifier runs only through an
|
|
53
|
+
// `escalate` naming a Task Size high-risk item; a native high, a failed
|
|
54
|
+
// assessment, and the writer's model or effort inform only.
|
|
55
|
+
const TIER_TABLE_HEADER = "| Tier | Verification (any RDD state) |";
|
|
52
56
|
const PASSIVE_TIER_ROW = "| passive | structural readback by the parent; no separate verifier, no tests |";
|
|
53
|
-
const MEDIUM_TIER_ROW = "| medium | writer self-verification stands;
|
|
54
|
-
const HIGH_TIER_ROW = "| high | writer self-verification plus
|
|
55
|
-
const UNASSESSABLE_TIER_ROW = "| unknown
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const MEDIUM_TIER_ROW = "| medium | writer self-verification stands; no separate verifier, whatever model wrote the change |";
|
|
58
|
+
const HIGH_TIER_ROW = "| high | only through `escalate` naming a Task Size high-risk item: writer self-verification plus one separate `gentle-ai-verify` run |";
|
|
59
|
+
const UNASSESSABLE_TIER_ROW = "| native high, unknown, or assess failed | informs only: writer self-verification stands; escalate when a named high-risk item applies |";
|
|
60
|
+
// odd/tasks/verifier-admission.md S6: the sentence names the runtime admission of the verifier launch.
|
|
61
|
+
const NAMED_RISK_SENTENCE =
|
|
62
|
+
"The independent verifier runs only when the change touches a named Task Size high-risk item, passed as `escalate` (the runtime admits its launch on a `Risk: item N` line, a feature-document task line, or past the context backstop); never for size, a native tier alone, a failed assessment, or the model or effort that wrote the change. An unknown `Receipt-driven development:` line never lowers a tier below `off`.";
|
|
58
63
|
const SPOT_CHECK_SENTENCE =
|
|
59
64
|
"The parent spot check (re-running one reported command before delivery) stays required in every tier.";
|
|
60
65
|
test("trigger 5 (Verification rule) routes every RDD line through assess and the risk tier", () => {
|
|
@@ -74,8 +79,9 @@ test("trigger 5 states the native risk tier table exactly once, with all four ro
|
|
|
74
79
|
}
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
test("trigger 5 states the
|
|
78
|
-
assert.ok(delegation.includes(
|
|
82
|
+
test("trigger 5 states the named-risk verifier rule and the unknown-never-lowers-a-tier rule", () => {
|
|
83
|
+
assert.ok(delegation.includes(NAMED_RISK_SENTENCE), "trigger 5 is missing the named-risk verifier sentence");
|
|
84
|
+
assert.ok(!delegation.includes("small-model bias"), "the retired small-model bias must not survive in any orchestrator module");
|
|
79
85
|
});
|
|
80
86
|
|
|
81
87
|
test("trigger 5 keeps the parent spot check requirement in every tier", () => {
|
|
@@ -99,10 +105,11 @@ test("trigger 5 never restates the retired #661 off/unknown non-trivial judgment
|
|
|
99
105
|
assert.doesNotMatch(delegation, /purely passive documentation with no behavior to verify/);
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
// S9 canon change (gentle-ai 87a3b2eb): the trigger section heading dropped "Mandatory".
|
|
102
109
|
test("the Simple Delegation paragraph references trigger 3 instead of restating the on/off/unknown routing", () => {
|
|
103
110
|
assert.match(
|
|
104
111
|
delegation,
|
|
105
|
-
/per the RDD-aware Verification rule \(trigger 3 under
|
|
112
|
+
/per the RDD-aware Verification rule \(trigger 3 under Delegation Triggers, gentle-pi#661\)/,
|
|
106
113
|
);
|
|
107
114
|
assert.match(delegation, /the normative on\/off\/unknown routing lives in `orchestrator-verification.md`, not here/);
|
|
108
115
|
});
|
|
@@ -223,7 +230,9 @@ test("worker asset keeps the existing Return contract fields", () => {
|
|
|
223
230
|
// delegation asset, and docs/delegated-verification.md -- and they must say
|
|
224
231
|
// the same thing about the behavior ASSESS implements: closure only from the
|
|
225
232
|
// native `candidate.consumed` fact for this candidate, the runtime writer
|
|
226
|
-
// profile with a caller fallback,
|
|
233
|
+
// profile with a caller fallback, a writer profile that never adds a verifier
|
|
234
|
+
// (verification by named risk retired the small-model bias), the escalate-only
|
|
235
|
+
// independent verifier, the
|
|
227
236
|
// declined/unavailable/unknown fallback, and functional checks that a native
|
|
228
237
|
// review never replaces. Assertions use short stable phrases, normalized for
|
|
229
238
|
// case, quoting, and line wrapping, not whole paragraphs.
|
|
@@ -267,7 +276,8 @@ const SHARED_POLICY_PHRASES: ReadonlyArray<readonly [string, string]> = [
|
|
|
267
276
|
["caller closure claim is not authority", "a caller-supplied closed is not authority"],
|
|
268
277
|
["runtime writer profile", "runtime-recorded model and effort of the pending mutations"],
|
|
269
278
|
["caller profile is only a fallback", "only a fallback when no runtime evidence exists"],
|
|
270
|
-
["
|
|
279
|
+
["writer profile never adds a verifier", "the writer profile is reported but never adds a verifier"],
|
|
280
|
+
["independent verifier only through escalate", "only when escalate names a task size high-risk item"],
|
|
271
281
|
["native review never replaces verification (verify-always-rdd-high S1)", "native review is an additional outside view"],
|
|
272
282
|
["functional checks still run", "a native code review is not a substitute for applicable functional checks"],
|
|
273
283
|
];
|
|
@@ -280,6 +290,14 @@ test("the three verification policy surfaces carry the same ASSESS closure, prof
|
|
|
280
290
|
}
|
|
281
291
|
});
|
|
282
292
|
|
|
293
|
+
test("no verification policy surface still carries the retired small-model bias or unassessable-as-high rule", () => {
|
|
294
|
+
for (const [surface, text] of POLICY_SURFACES) {
|
|
295
|
+
for (const stale of ["small-model bias", "treated as high", "verified exactly like high", "fail closed to high"]) {
|
|
296
|
+
assert.ok(!text.includes(stale), `${surface} still carries the retired wording: "${stale}"`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
283
301
|
test("no verification policy surface still tells callers to pass closed explicitly (gentle-pi#1175)", () => {
|
|
284
302
|
for (const [surface, text] of POLICY_SURFACES) {
|
|
285
303
|
for (const stale of ["closed is never derived", "closed is never auto-derived", "closed is never inferred", "pass it explicitly", "pass it only right after"]) {
|
|
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
4
4
|
import {
|
|
5
|
+
NATIVE_REVIEW_ERROR_CODE,
|
|
5
6
|
NATIVE_REVIEW_MODE_OPERATION,
|
|
7
|
+
NATIVE_REVIEW_OPERATION,
|
|
8
|
+
NativeReviewCliError,
|
|
6
9
|
NATIVE_REVIEW_MODE_SOURCE,
|
|
7
10
|
type NativeReviewCli,
|
|
8
11
|
type NativeReviewModeRequest,
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
|
|
32
35
|
const {
|
|
33
36
|
renderRddStatusLine,
|
|
37
|
+
pinRddStatusLine,
|
|
34
38
|
resolveRddModeStatus,
|
|
35
39
|
resolveRddStatusLine,
|
|
36
40
|
getOrchestratorPrompt,
|
|
@@ -358,3 +362,78 @@ test("resolveRddModeStatus stays unknown when no status was ever read", async ()
|
|
|
358
362
|
const cli = fakeReviewMode(() => { throw new Error("never available"); });
|
|
359
363
|
assert.equal(await resolveRddModeStatus(cli, "/repo-never-known"), undefined);
|
|
360
364
|
});
|
|
365
|
+
|
|
366
|
+
// gentle-shell#2250: the one-shot unavailable notice must name the cause so a
|
|
367
|
+
// user on a slow or foreign-owned filesystem can tell a timeout from a native
|
|
368
|
+
// refusal instead of reading a generic "absent, timed out, or failed".
|
|
369
|
+
function captureNotices(): { ctx: { hasUI: true; ui: { notify(message: string, level: string): void } }; messages: string[] } {
|
|
370
|
+
const messages: string[] = [];
|
|
371
|
+
return { ctx: { hasUI: true, ui: { notify(message: string) { messages.push(message); } } }, messages };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test("the unavailable notice names the native failure and its stderr", async () => {
|
|
375
|
+
clearRddStatusMemoForTesting();
|
|
376
|
+
const { ctx, messages } = captureNotices();
|
|
377
|
+
const cli = fakeReviewMode(() => {
|
|
378
|
+
throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.MODE, true, false, "native process failed", {
|
|
379
|
+
operation: NATIVE_REVIEW_OPERATION.MODE,
|
|
380
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
|
|
381
|
+
exit_code: 1,
|
|
382
|
+
timed_out: false,
|
|
383
|
+
output_limit_exceeded: false,
|
|
384
|
+
stderr: "fatal: detected dubious ownership in repository",
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
assert.equal(await resolveRddModeStatus(cli, "/repo-native-failure", undefined, Date.now, ctx as never), undefined);
|
|
388
|
+
assert.equal(messages.length, 1);
|
|
389
|
+
assert.match(messages[0], /status is unavailable: native `gentle-ai review mode status` failed \(non-zero, exit 1\): fatal: detected dubious ownership in repository\./);
|
|
390
|
+
assert.match(messages[0], /keeps the last known status/);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("the unavailable notice reports a timeout distinctly", async () => {
|
|
394
|
+
clearRddStatusMemoForTesting();
|
|
395
|
+
const { ctx, messages } = captureNotices();
|
|
396
|
+
const keepalive = setTimeout(() => {}, 2000);
|
|
397
|
+
try {
|
|
398
|
+
await resolveRddModeStatus(fakeReviewMode(() => new Promise<never>(() => {})), "/repo-timeout", AbortSignal.timeout(50), Date.now, ctx as never);
|
|
399
|
+
} finally {
|
|
400
|
+
clearTimeout(keepalive);
|
|
401
|
+
}
|
|
402
|
+
assert.equal(messages.length, 1);
|
|
403
|
+
assert.match(messages[0], new RegExp(`status is unavailable: native \`gentle-ai review mode status\` did not answer within ${RDD_STATUS_TIMEOUT_MS} ms\.`));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("the unavailable notice reports an absent native review CLI", async () => {
|
|
407
|
+
clearRddStatusMemoForTesting();
|
|
408
|
+
const { ctx, messages } = captureNotices();
|
|
409
|
+
await resolveRddModeStatus(null, "/repo-absent", undefined, Date.now, ctx as never);
|
|
410
|
+
assert.equal(messages.length, 1);
|
|
411
|
+
assert.match(messages[0], /status is unavailable: the native gentle-ai review CLI is not available\./);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("the unavailable notice still fires only once per session", async () => {
|
|
415
|
+
clearRddStatusMemoForTesting();
|
|
416
|
+
const { ctx, messages } = captureNotices();
|
|
417
|
+
const cli = fakeReviewMode(() => { throw new Error("boom"); });
|
|
418
|
+
await resolveRddModeStatus(cli, "/repo-once-a", undefined, Date.now, ctx as never);
|
|
419
|
+
await resolveRddModeStatus(cli, "/repo-once-b", undefined, Date.now, ctx as never);
|
|
420
|
+
assert.equal(messages.length, 1);
|
|
421
|
+
assert.match(messages[0], /status is unavailable: boom\./);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("pinRddStatusLine keeps the session's first line until the off state changes (#2195)", () => {
|
|
425
|
+
const unknown = "Receipt-driven development: unknown (native status unavailable)";
|
|
426
|
+
const onGlobal = "Receipt-driven development: on (decided by global)";
|
|
427
|
+
const onClone = "Receipt-driven development: on (decided by clone)";
|
|
428
|
+
const off = "Receipt-driven development: off (decided by clone)";
|
|
429
|
+
const offGlobal = "Receipt-driven development: off (decided by global)";
|
|
430
|
+
assert.equal(pinRddStatusLine(undefined, onGlobal), onGlobal, "the first render of a session is the resolved line");
|
|
431
|
+
assert.equal(pinRddStatusLine(unknown, onGlobal), unknown, "unknown to on keeps the cached prompt prefix");
|
|
432
|
+
assert.equal(pinRddStatusLine(onGlobal, unknown), onGlobal, "on to unknown keeps the cached prompt prefix");
|
|
433
|
+
assert.equal(pinRddStatusLine(onGlobal, onClone), onGlobal, "a deciding-source change alone keeps the prefix");
|
|
434
|
+
assert.equal(pinRddStatusLine(off, offGlobal), off, "a source change while off keeps the prefix");
|
|
435
|
+
assert.equal(pinRddStatusLine(onGlobal, off), off, "turning RDD off re-renders, since it drops the review contract");
|
|
436
|
+
assert.equal(pinRddStatusLine(unknown, off), off, "unknown to off re-renders");
|
|
437
|
+
assert.equal(pinRddStatusLine(off, onGlobal), onGlobal, "turning RDD back on re-renders");
|
|
438
|
+
assert.equal(pinRddStatusLine(off, unknown), unknown, "off to unknown re-renders, since unknown keeps the review contract");
|
|
439
|
+
});
|
|
@@ -128,13 +128,18 @@ function toolContext(context: ExtensionContext): ExtensionToolContext {
|
|
|
128
128
|
async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, initializeGit = true): Promise<T> {
|
|
129
129
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
130
130
|
const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
131
|
+
// Each call made three temp dirs and never removed them (87 left behind per suite run).
|
|
132
|
+
const created: string[] = [];
|
|
131
133
|
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-agent-home-"));
|
|
134
|
+
created.push(process.env.GENTLE_PI_AGENT_HOME);
|
|
132
135
|
// Isolates both the model-config sweep and the dev-binary registration
|
|
133
136
|
// lookup from this machine's real ~/.pi/gentle-ai, so `session_start`'s
|
|
134
137
|
// unrelated notifications never leak into these assertions.
|
|
135
138
|
process.env.GENTLE_PI_CONFIG_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-config-home-"));
|
|
139
|
+
created.push(process.env.GENTLE_PI_CONFIG_HOME);
|
|
136
140
|
try {
|
|
137
141
|
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-cwd-"));
|
|
142
|
+
created.push(cwd);
|
|
138
143
|
if (initializeGit) childProcess.execFileSync("git", ["init", "--quiet", cwd]);
|
|
139
144
|
await mkdir(join(cwd, "src"));
|
|
140
145
|
await writeFile(join(cwd, "src/example.ts"), "export const value = 1;");
|
|
@@ -144,6 +149,7 @@ async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, ini
|
|
|
144
149
|
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
145
150
|
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
146
151
|
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
152
|
+
for (const dir of created) await rm(dir, { recursive: true, force: true });
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
@@ -1326,6 +1326,55 @@ test("candidate view recursively protects nested content and worktree metadata,
|
|
|
1326
1326
|
}
|
|
1327
1327
|
});
|
|
1328
1328
|
|
|
1329
|
+
// macOS File Provider (iCloud-synced Desktop/Documents) asynchronously resets
|
|
1330
|
+
// directory modes to exactly 0700 while leaving file modes intact.
|
|
1331
|
+
function simulateFileProviderDirectoryReset(root: string, directories: readonly string[]): void {
|
|
1332
|
+
for (const directory of directories) chmodSync(join(root, directory), 0o700);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
test("candidate view repairs owner-only directories reset by macOS File Provider and stays verifiable", { skip: process.platform === "win32" }, (t) => {
|
|
1336
|
+
const contributorRoot = repository(t);
|
|
1337
|
+
mkdirSync(join(contributorRoot, "nested", "deeper"), { recursive: true });
|
|
1338
|
+
writeFileSync(join(contributorRoot, "nested", "deeper", "candidate.txt"), "candidate\n");
|
|
1339
|
+
const view = createCandidateView({ contributorRoot });
|
|
1340
|
+
try {
|
|
1341
|
+
simulateFileProviderDirectoryReset(view.root, ["nested/deeper", "nested", "."]);
|
|
1342
|
+
view.verify();
|
|
1343
|
+
for (const directory of [".", "nested", "nested/deeper"]) assert.equal(lstatSync(join(view.root, directory)).mode & 0o777, 0o555, directory);
|
|
1344
|
+
assert.equal(lstatSync(join(view.root, "nested", "deeper", "candidate.txt")).mode & 0o777, 0o444);
|
|
1345
|
+
} finally {
|
|
1346
|
+
view.cleanup();
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test("candidate view does not repair owner-only directories carrying special mode bits", { skip: process.platform === "win32" }, (t) => {
|
|
1351
|
+
const contributorRoot = repository(t);
|
|
1352
|
+
mkdirSync(join(contributorRoot, "nested"), { recursive: true });
|
|
1353
|
+
writeFileSync(join(contributorRoot, "nested", "candidate.txt"), "candidate\n");
|
|
1354
|
+
const view = createCandidateView({ contributorRoot });
|
|
1355
|
+
try {
|
|
1356
|
+
chmodSync(join(view.root, "nested"), 0o1700);
|
|
1357
|
+
assert.throws(() => view.verify(), /candidate view directory is unsafe or writable/);
|
|
1358
|
+
} finally {
|
|
1359
|
+
view.cleanup();
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
test("candidate view refuses an ignored entry injected into an owner-only directory it would repair", { skip: process.platform === "win32" }, (t) => {
|
|
1364
|
+
const contributorRoot = repository(t);
|
|
1365
|
+
mkdirSync(join(contributorRoot, "nested"), { recursive: true });
|
|
1366
|
+
writeFileSync(join(contributorRoot, ".gitignore"), "*.log\n");
|
|
1367
|
+
writeFileSync(join(contributorRoot, "nested", "candidate.txt"), "candidate\n");
|
|
1368
|
+
const view = createCandidateView({ contributorRoot });
|
|
1369
|
+
try {
|
|
1370
|
+
simulateFileProviderDirectoryReset(view.root, ["nested"]);
|
|
1371
|
+
writeFileSync(join(view.root, "nested", "injected.log"), "injected\n");
|
|
1372
|
+
assert.throws(() => view.verify(), /candidate view directory contains unexpected entries/);
|
|
1373
|
+
} finally {
|
|
1374
|
+
view.cleanup();
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1329
1378
|
test("candidate view registry rejects unsafe, moved, writable, stale, and unselected lens contexts before dispatch", (t) => {
|
|
1330
1379
|
const contributorRoot = repository(t);
|
|
1331
1380
|
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
@@ -109,9 +109,12 @@ test("before_agent_start injects the mirrored review execution contract for the
|
|
|
109
109
|
assert.match(appended, /no meaningful RED, explain why and run proportionate ordinary functional or structural verification/);
|
|
110
110
|
assert.match(appended, /Never invent lifecycle evidence or skip checks/);
|
|
111
111
|
assert.doesNotMatch(appended, /If tests exist, use strict TDD/);
|
|
112
|
-
|
|
112
|
+
// S11 T3: deduplicated. The core pointer sentence restated the harness; the
|
|
113
|
+
// mandatory ODD entry is injected once, from the harness steps' header.
|
|
114
|
+
assert.match(appended, /Organic Driven Development \(ODD\) is the predefined workflow of this orchestrator\. Every request enters it/);
|
|
115
|
+
assert.doesNotMatch(appended, /ODD \(Default Workflow, harness section above\)/);
|
|
113
116
|
assert.doesNotMatch(appended, /Prefer SDD\/OpenSpec artifacts/);
|
|
114
|
-
assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.
|
|
117
|
+
assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.4\.0\)/);
|
|
115
118
|
assert.ok(appended.includes(expected), "the mirrored orchestration/pi.md text must appear verbatim");
|
|
116
119
|
assert.match(appended, /call `gentle_review` with {"operation":"inspect"}/);
|
|
117
120
|
assert.match(appended, /call `gentle_review` with operation `status`, the exact retained `lineageId`, and `workspaceRoot`/);
|