gentle-pi 4.1.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 +352 -128
- 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/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- 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 +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- 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/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- 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 +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- 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/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- 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-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- 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/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- 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/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -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 +35 -7
- 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 +443 -8
- 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 +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- 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/odd-runtime-delegation-gate.test.ts +4 -1
- 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 +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- 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 +131 -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/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -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-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- 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/vim-editor-adapter.test.ts +41 -15
- 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 +24 -1
- 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
|
@@ -111,7 +111,7 @@ test("T31: high-risk item 3 is about unrequested breaks, not the requested chang
|
|
|
111
111
|
);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
// T33 (L74): in
|
|
114
|
+
// T33 (L74): in a benchmark run both cells declared `Risk: none` after adding --year to
|
|
115
115
|
// the shared budget options; one shipped `budget set --year` silently saving a
|
|
116
116
|
// wrong budget, the regression verify used to catch. The Risk line must check
|
|
117
117
|
// shared code before claiming none.
|
|
@@ -122,3 +122,38 @@ test("T33: the Close step checks shared code before Risk: none", () => {
|
|
|
122
122
|
assert.ok(step?.includes(SHARED_CHECK), `${persona} Close step must check shared code before Risk: none`);
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
// C8 (benchmark finding): the PRESERVE test alone missed the `budget set
|
|
127
|
+
// --year` regression in 1 of 2 runs. A differential run of every existing
|
|
128
|
+
// command that shares the changed code, old code against new, caught it in
|
|
129
|
+
// 2 of 2. Inline work and the worker carry the same check.
|
|
130
|
+
// S15: the differential check runs in scratch and is never committed as a test (blind round 4 found tests pinning a git commit).
|
|
131
|
+
const DIFFERENTIAL = "Before finishing, run a differential check: for each existing command that shares the code you changed, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a `mktemp -d` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test.";
|
|
132
|
+
test("C8: inline test-first and the worker PRESERVE step run the differential check", () => {
|
|
133
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
134
|
+
const principle = __testing.buildGentlePrompt(persona).split("\n").find((line) => line.includes("use test-first by default"));
|
|
135
|
+
assert.ok(principle?.includes(`${TOUCHED}. ${DIFFERENTIAL}`), `${persona} principle must run the differential check after PRESERVE`);
|
|
136
|
+
}
|
|
137
|
+
const step = read("assets/agents/gentle-ai-worker.md").split("\n").find((line) => line.startsWith("3. PRESERVE — "));
|
|
138
|
+
assert.ok(step?.includes(DIFFERENTIAL), "worker step 3 must run the differential check");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// C3 (benchmark finding): parallel workers appended a shared `Fees` entry
|
|
142
|
+
// inline, unsorted, instead of reusing the helper that keeps the order. The
|
|
143
|
+
// worker reuses the existing function for shared state, and each writer prompt
|
|
144
|
+
// names the helpers to reuse, because parallel units cannot see each other.
|
|
145
|
+
test("C3: the worker reuses the existing function for shared state and keeps its invariants", () => {
|
|
146
|
+
const worker = read("assets/agents/gentle-ai-worker.md");
|
|
147
|
+
assert.ok(
|
|
148
|
+
worker.includes("- Before you create or change shared state (a record, list entry, file, or setting that other commands also create or read), find the existing function that already does it and reuse it, keeping every invariant it enforces (ordering, uniqueness, validation, normalization). Never re-implement that step inline with a different shape."),
|
|
149
|
+
"worker must reuse the existing shared-state function",
|
|
150
|
+
);
|
|
151
|
+
const invariants = read("assets/orchestrator-writer.md").split("\n").find((line) => line.startsWith("- **Invariants**"));
|
|
152
|
+
for (const clause of [
|
|
153
|
+
"each writer prompt names the existing helpers its unit must reuse for shared state",
|
|
154
|
+
"and the invariants they keep",
|
|
155
|
+
"units built in parallel cannot see each other",
|
|
156
|
+
]) {
|
|
157
|
+
assert.ok(invariants?.includes(clause), `writer module invariants reason is missing: ${clause}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// Pain-point iteration S13 (C9/C10, from the S12 session analysis): subagents
|
|
7
|
+
// spent 78% of their turns on a single tool call, and 31% of their turns were
|
|
8
|
+
// consecutive read-only calls one turn could batch; the parent read repo files
|
|
9
|
+
// and then delegated, and 69% of the files it read were read again by the
|
|
10
|
+
// worker. Codex reads 8 files with one command. These are instruction-delivery
|
|
11
|
+
// contracts, not proof of model adherence.
|
|
12
|
+
|
|
13
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
14
|
+
const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
|
|
15
|
+
|
|
16
|
+
function containsAll(text: string, clauses: readonly string[], label: string): void {
|
|
17
|
+
for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function declaredTools(prompt: string): string[] {
|
|
21
|
+
const frontmatter = prompt.split("\n---\n")[0];
|
|
22
|
+
return [...frontmatter.matchAll(/^\s+- (\S+)$/gm)].map((match) => match[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("S13 C9: every generic subagent prompt batches independent tool calls in one turn", () => {
|
|
26
|
+
for (const agent of ["gentle-ai-worker", "gentle-ai-verify", "gentle-ai-explore"]) {
|
|
27
|
+
const prompt = read(`assets/agents/${agent}.md`);
|
|
28
|
+
const rule = prompt.split("\n").find((line) => line.includes("**Batch tool calls:**"));
|
|
29
|
+
assert.ok(rule, `${agent} must carry the batch tool calls rule`);
|
|
30
|
+
containsAll(rule, [
|
|
31
|
+
"issue independent calls together in the same turn (parallel tool calls)",
|
|
32
|
+
"read each relevant file whole once instead of many small ranges",
|
|
33
|
+
], agent);
|
|
34
|
+
const tools = declaredTools(prompt);
|
|
35
|
+
// A shell-less agent must not be told to read files or run tests with a command it cannot run.
|
|
36
|
+
if (tools.includes("bash")) {
|
|
37
|
+
containsAll(rule, ["read several files with one command", "the focused test and the suite without status-only turns"], agent);
|
|
38
|
+
} else {
|
|
39
|
+
assert.doesNotMatch(rule, /one command|the suite/, `${agent} has no shell, so its rule must not name a command`);
|
|
40
|
+
}
|
|
41
|
+
if (tools.includes("edit")) containsAll(rule, ["all edits to one file in one `edit` call"], agent);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("S13 C10: the delegation card keeps the parent from reading what the worker will read", () => {
|
|
46
|
+
const card = read("assets/orchestrator-delegation.md");
|
|
47
|
+
const rule = card.split("\n").find((line) => line.startsWith("**Preparation trigger:**"));
|
|
48
|
+
assert.ok(rule, "the delegation card must carry the Preparation trigger");
|
|
49
|
+
containsAll(rule, [
|
|
50
|
+
"When delegating, read only what decides the route",
|
|
51
|
+
"never the files the worker will read",
|
|
52
|
+
"pass paths, the spec reference, and what to change, not file contents",
|
|
53
|
+
], "delegation card preparation trigger");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// S13 C9 parent: 95 of 389 parent turns did only coordination (todo 55, orchestrator_session_id 17,
|
|
57
|
+
// gentle_review assess 14, subagent_list_agents 8) and parent multi-tool turns were ~0.
|
|
58
|
+
test("S13 C9: the always-on orchestrator batches independent calls and never spends a turn only on coordination", () => {
|
|
59
|
+
const core = read("assets/orchestrator.md");
|
|
60
|
+
containsAll(core, [
|
|
61
|
+
"Batch independent tool calls in one turn (parallel calls or one multi-file command).",
|
|
62
|
+
"Never spend a turn only on a `todo` or status update.",
|
|
63
|
+
], "orchestrator core");
|
|
64
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
6
|
+
const verify = read("assets/agents/gentle-ai-verify.md");
|
|
7
|
+
const policy = read("assets/orchestrator-verification.md");
|
|
8
|
+
const parent = read("assets/orchestrator.md");
|
|
9
|
+
|
|
10
|
+
// Instruction-delivery checks, not evidence of model adherence or delivery time.
|
|
11
|
+
test("unit verification keeps whole-feature context without blocking on future work", () => {
|
|
12
|
+
for (const text of [verify, policy]) {
|
|
13
|
+
assert.ok(text.includes("out of scope, not unmet"));
|
|
14
|
+
assert.ok(text.includes("affected interactions"));
|
|
15
|
+
}
|
|
16
|
+
assert.ok(!verify.includes("no partial-scope carve-out"));
|
|
17
|
+
assert.ok(verify.includes("At final feature verification, cover every requirement"));
|
|
18
|
+
assert.ok(policy.includes("final feature verification covers every `S#`"));
|
|
19
|
+
assert.ok(verify.includes("met, unmet, not implemented, or out of scope"));
|
|
20
|
+
assert.ok(verify.includes("unrelated future work must not block that unit"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("observed writer evidence is reusable but does not replace independent probes", () => {
|
|
24
|
+
assert.ok(verify.includes("Reuse observed writer checks when their candidate and scope remain valid"));
|
|
25
|
+
assert.ok(verify.includes("independent probes"));
|
|
26
|
+
assert.ok(!verify.includes("never evidence of correctness"));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("auxiliary gates must protect a concrete requirement or threat", () => {
|
|
30
|
+
assert.ok(parent.includes("gates/overengineering: `orchestrator-verification.md`"));
|
|
31
|
+
assert.ok(policy.includes("acceptance criterion, concrete threat, or authoritative contract"));
|
|
32
|
+
assert.ok(policy.includes("must not become a delivery blocker"));
|
|
33
|
+
assert.ok(policy.includes("Do not build a framework for a bounded capability probe"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("overengineering warnings visibly simplify the plan without weakening safeguards", () => {
|
|
37
|
+
assert.ok(policy.includes("remove or collapse optional artifacts and checks"));
|
|
38
|
+
assert.ok(policy.includes("State what was removed, what remains indispensable, and the next application outcome"));
|
|
39
|
+
assert.ok(policy.includes("Keep required functional checks, security controls, and native review authority intact"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("existing verification isolation and bounded correction remain required", () => {
|
|
43
|
+
assert.ok(verify.includes("run it only against isolated state"));
|
|
44
|
+
assert.ok(verify.includes("one correction batch and one recheck limited to the reported blockers"));
|
|
45
|
+
assert.ok(policy.includes("Advisories never start a correction"));
|
|
46
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
|
|
9
|
+
|
|
10
|
+
// odd/tasks/verifier-admission.md S2: a bench gate run launched `gentle-ai-verify` on a small, not high-risk task
|
|
11
|
+
// although the prompt's Verification rule reserves the independent verifier for a named high-risk item. The runtime
|
|
12
|
+
// now admits a primary-session verifier launch only with a `Risk: item N` line, a feature-document task, or a
|
|
13
|
+
// measured context at the backstop.
|
|
14
|
+
|
|
15
|
+
const quiet = { tokens: 20_000, contextWindow: 400_000 };
|
|
16
|
+
|
|
17
|
+
function workspace(withDocument: boolean): string {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "verifier-admission-"));
|
|
19
|
+
if (withDocument) {
|
|
20
|
+
mkdirSync(join(root, "odd", "tasks"), { recursive: true });
|
|
21
|
+
writeFileSync(join(root, "odd", "tasks", "ledger.md"), "# Ledger\n");
|
|
22
|
+
}
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("S3/S4: a verifier launch without a high-risk line is rejected with an inline instruction", (t) => {
|
|
27
|
+
const root = workspace(false);
|
|
28
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
29
|
+
for (const task of ["Verify the annual budget change.", "Verify it.\nRisk: none", "Verify it.\nRisk: none (local rename)"]) {
|
|
30
|
+
const denied = rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task }, { root, contextUsage: quiet });
|
|
31
|
+
assert.ok(denied?.block, `${JSON.stringify(task)} must be blocked`);
|
|
32
|
+
assert.match(denied.reason, /independent verifier runs only for a change that declares a named high-risk item/);
|
|
33
|
+
assert.match(denied.reason, /focused test and suite inline/);
|
|
34
|
+
assert.match(denied.reason, /do not retry the launch/);
|
|
35
|
+
assert.match(denied.reason, /never add a Risk line or create a feature document only to get a verifier admitted/);
|
|
36
|
+
assert.doesNotMatch(denied.reason, /claude|gpt|openai|anthropic|gemini|sonnet|opus/i, "the message stays generic");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("S3: a Risk line naming a Task Size high-risk item admits the verifier", (t) => {
|
|
41
|
+
const root = workspace(false);
|
|
42
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
43
|
+
for (const line of ["Risk: item 3 (shared parser)", "- Risk: item 2, item 3 (auth and parser)", "Risk: item 6", " Risk:item 1"]) {
|
|
44
|
+
assert.equal(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task: `Verify the change.\n${line}\n` }, { root, contextUsage: quiet }), undefined, line);
|
|
45
|
+
}
|
|
46
|
+
const viaContext = { agent: "gentle-ai-verify", task: "Verify the change.", context: "Risk: item 4 (migration)" };
|
|
47
|
+
assert.equal(rejectUnriskedVerifierLaunch(viaContext, { root, contextUsage: quiet }), undefined, "the context carries the line");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("S3: an item outside 1-6, a Risk mention inside prose, or none first is rejected", (t) => {
|
|
51
|
+
const root = workspace(false);
|
|
52
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
53
|
+
for (const line of ["Risk: item 7", "Risk: item 0", "Risk: item 12", "Do not write Risk: item 3 here.", "Risk: none; item 3 does not apply", "Risk: items"]) {
|
|
54
|
+
assert.ok(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task: `Verify it.\n${line}` }, { root, contextUsage: quiet })?.block, line);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("S3: a task of an existing feature document admits the verifier; a missing document does not", (t) => {
|
|
59
|
+
const documented = workspace(true);
|
|
60
|
+
const bare = workspace(false);
|
|
61
|
+
t.after(() => { rmSync(documented, { recursive: true, force: true }); rmSync(bare, { recursive: true, force: true }); });
|
|
62
|
+
const input = { agent: "gentle-ai-verify", task: 'Spec: odd/tasks/ledger.md (read until "## Log"). Verify T2; S3.' };
|
|
63
|
+
assert.equal(rejectUnriskedVerifierLaunch(input, { root: documented, contextUsage: quiet }), undefined);
|
|
64
|
+
assert.ok(rejectUnriskedVerifierLaunch(input, { root: bare, contextUsage: quiet })?.block, "a missing document does not admit");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("S3: the measured context backstop admits the verifier", (t) => {
|
|
68
|
+
const root = workspace(false);
|
|
69
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
70
|
+
const input = { agent: "gentle-ai-verify", task: "Verify the change." };
|
|
71
|
+
assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 150_000, contextWindow: 1_000_000 } }), undefined);
|
|
72
|
+
assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 19_200, contextWindow: 128_000 } }), undefined, "15% of a small window admits");
|
|
73
|
+
assert.ok(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 149_999, contextWindow: 1_000_000 } })?.block);
|
|
74
|
+
assert.ok(rejectUnriskedVerifierLaunch(input, { root, contextUsage: undefined })?.block, "missing context usage does not admit");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("S5: other agents are not gated by the verifier admission", (t) => {
|
|
78
|
+
const root = workspace(false);
|
|
79
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
80
|
+
for (const agent of ["gentle-ai-worker", "worker", "gentle-ai-explore", "jd-fix-agent", "jd-judge-a", "jd-judge-b", "review-risk", "verify"]) {
|
|
81
|
+
assert.equal(rejectUnriskedVerifierLaunch({ agent, task: "Check src/budget.ts" }, { root, contextUsage: quiet }), undefined, agent);
|
|
82
|
+
}
|
|
83
|
+
for (const input of [undefined, null, "gentle-ai-verify", ["gentle-ai-verify"], { task: "Verify it." }]) {
|
|
84
|
+
assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: quiet }), undefined, JSON.stringify(input));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
type ToolCallHandler = (event: { toolName: string; input: unknown }, ctx: ExtensionContext) => Promise<{ block?: boolean; reason?: string } | undefined>;
|
|
89
|
+
|
|
90
|
+
function toolCallThroughHook(toolName: string, input: Record<string, unknown>, cwd: string, tokens: number, processEnv: NodeJS.ProcessEnv = {}) {
|
|
91
|
+
const handlers = new Map<string, ToolCallHandler>();
|
|
92
|
+
const pi = { on(name: string, handler: ToolCallHandler) { handlers.set(name, handler); }, events: { emit() {} }, registerCommand() {}, registerTool() {} } as unknown as ExtensionAPI;
|
|
93
|
+
createGentleAiExtension({ nativeReviewCli: null, processEnv })(pi);
|
|
94
|
+
return handlers.get("tool_call")!({ toolName, input }, {
|
|
95
|
+
cwd, hasUI: false, ui: { confirm: async () => true },
|
|
96
|
+
getContextUsage: () => ({ tokens, contextWindow: 272_000, percent: Math.round((tokens / 272_000) * 100) }),
|
|
97
|
+
} as unknown as ExtensionContext);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test("S3/S5: the orchestrator tool_call hook gates only primary verifier launches", async (t) => {
|
|
101
|
+
const bare = workspace(false);
|
|
102
|
+
const documented = workspace(true);
|
|
103
|
+
t.after(() => { rmSync(bare, { recursive: true, force: true }); rmSync(documented, { recursive: true, force: true }); });
|
|
104
|
+
const blocked = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change." }, bare, 20_000);
|
|
105
|
+
assert.equal(blocked?.block, true);
|
|
106
|
+
assert.match(blocked?.reason ?? "", /focused test and suite inline/);
|
|
107
|
+
const risked = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change.\nRisk: item 3 (shared parser)" }, bare, 20_000);
|
|
108
|
+
assert.equal(risked?.block, undefined, "a named high-risk item admits");
|
|
109
|
+
const documentedLaunch = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", workspace_root: documented, task: "Spec: odd/tasks/ledger.md, T1. Verify it." }, bare, 20_000);
|
|
110
|
+
assert.equal(documentedLaunch?.block, undefined, "the feature document resolves under the selected workspace root");
|
|
111
|
+
const backstop = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change." }, bare, 136_000);
|
|
112
|
+
assert.equal(backstop?.block, undefined, "half of the measured window admits");
|
|
113
|
+
const child = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify it." }, bare, 0, { GENTLE_PI_AGENTS_CHILD: "1" });
|
|
114
|
+
assert.equal(child?.block, undefined, "a child session is not gated");
|
|
115
|
+
const continuation = await toolCallThroughHook("subagent_continue", { agent: "gentle-ai-verify", task_id: "t-1", task: "Recheck it." }, bare, 0);
|
|
116
|
+
assert.notEqual(continuation?.reason, blocked?.reason, "a continuation is not gated by the verifier admission");
|
|
117
|
+
for (const agent of ["gentle-ai-explore", "jd-judge-a", "jd-judge-b"]) {
|
|
118
|
+
const other = await toolCallThroughHook("subagent_run", { agent, mode: "task", task: "Map src/budget.ts" }, bare, 0);
|
|
119
|
+
assert.equal(other?.block, undefined, `${agent} is unaffected`);
|
|
120
|
+
}
|
|
121
|
+
// A native review relay keeps its own candidate-view gate, which already rejects a dispatch without a frozen view.
|
|
122
|
+
const relay = await toolCallThroughHook("subagent_run", { agent: "review-risk", mode: "task", task: "Review the candidate." }, bare, 0);
|
|
123
|
+
assert.match(relay?.reason ?? "", /^review subagent dispatch /, "review relays keep their previous gate");
|
|
124
|
+
const writer = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-worker", mode: "background", task: "Implement it.\nRisk: item 3\n\n## Allowed edit surfaces\nsrc/budget.ts\n" }, bare, 0);
|
|
125
|
+
assert.match(writer?.reason ?? "", /write this unit inline yourself/, "writer admission is unchanged and ignores Risk lines");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("S6: the Verification rule and the reference docs point at the runtime admission", () => {
|
|
129
|
+
const read = (path: string) => readFileSync(join(import.meta.dirname, "..", path), "utf8");
|
|
130
|
+
assert.ok(read("assets/orchestrator.md").includes("independent `gentle-ai-verify`; else inline; runtime admission, gates/overengineering"), "the core rule names the runtime check");
|
|
131
|
+
assert.ok(read("assets/orchestrator-verification.md").includes("(the runtime admits its launch on a `Risk: item N` line, a feature-document task line, or past the context backstop)"), "the normative rule names the admitted line");
|
|
132
|
+
const docs = read("docs/readme-reference.md");
|
|
133
|
+
const writer = docs.indexOf("A second admission check stops small-task delegation");
|
|
134
|
+
const verifier = docs.indexOf("A third admission check keeps the independent verifier for high-risk changes");
|
|
135
|
+
assert.ok(writer >= 0 && verifier > writer, "the verifier admission is documented right after the writer admission");
|
|
136
|
+
assert.match(docs.slice(verifier, docs.indexOf("\n\n", verifier)), /`gentle-ai-verify` launch is admitted only when[^]*`Risk: item N`[^]*context backstop/);
|
|
137
|
+
});
|
|
@@ -23,11 +23,11 @@ function containsAll(text: string, clauses: readonly string[], label: string): v
|
|
|
23
23
|
for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
test("T11.1: verify
|
|
27
|
-
assert.ok(verify.includes("
|
|
26
|
+
test("T11.1: verify reuses valid evidence without replacing independent spec-derived probes", () => {
|
|
27
|
+
assert.ok(verify.includes("they are evidence, not a substitute for the independent probes required by the risk tier"));
|
|
28
28
|
containsAll(checklistItem(1), [
|
|
29
29
|
"verbatim request",
|
|
30
|
-
"every numbered rule or requirement",
|
|
30
|
+
"every in-scope numbered rule or requirement",
|
|
31
31
|
"derive your own probes",
|
|
32
32
|
"positive examples",
|
|
33
33
|
"negative and boundary cases",
|
|
@@ -54,13 +54,13 @@ test("T11.4: verify runs the typecheck or build and flags scope creep", () => {
|
|
|
54
54
|
containsAll(checklistItem(4), ["typecheck or build", "when the project has one", "outside the request's scope"], "build and scope item");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test("T11.5: verify
|
|
57
|
+
test("T11.5: verify distinguishes unit exclusions from unmet spec items", () => {
|
|
58
58
|
containsAll(checklistItem(5), [
|
|
59
59
|
"verdict for EVERY spec item",
|
|
60
|
-
"met, unmet, or
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"the parent resolves every unmet item before closing, with one correction batch and one recheck limited to the reported blockers",
|
|
60
|
+
"met, unmet, not implemented, or out of scope",
|
|
61
|
+
"assess only its requirements and affected interactions",
|
|
62
|
+
"unrelated future work must not block that unit",
|
|
63
|
+
"the parent resolves every in-scope unmet item before closing, with one correction batch and one recheck limited to the reported blockers",
|
|
64
64
|
], "verdict item");
|
|
65
65
|
assert.ok(verify.includes("verdict per `S#`"), "the feature-document verdict per S# must stay");
|
|
66
66
|
});
|
|
@@ -5,12 +5,17 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
|
-
import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { CURSOR_MARKER, Editor, TuiMainScreen, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import { AUDITED_PI_EDITOR_VERSIONS, createVimEditorAdapter, isAuditedPiEditorVersion } from "../lib/vim-editor-adapter.ts";
|
|
10
10
|
import { resolveVimRuntime, VIM_AGENT_INDEX_PATTERN, VIM_CLI_ENTRY_PATTERN } from "../extensions/gentle-shell.ts";
|
|
11
11
|
import { VimOperatorEngine } from "../lib/vim-operator-engine.ts";
|
|
12
12
|
import { VimVisualEngine } from "../lib/vim-visual-engine.ts";
|
|
13
13
|
|
|
14
|
+
function resolveFakeCursors(rows: string[], hardware = false): string[] {
|
|
15
|
+
const host = TuiMainScreen.prototype as unknown as { resolveFakeCursors(rows: string[]): string[] };
|
|
16
|
+
return host.resolveFakeCursors.call({ showHardwareCursor: hardware }, rows);
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
function editor(): Editor {
|
|
15
20
|
return new Editor({ terminal: { rows: 24 }, requestRender() {} } as never, { borderColor: (s: string) => s } as never);
|
|
16
21
|
}
|
|
@@ -20,6 +25,8 @@ function editor(): Editor {
|
|
|
20
25
|
function paintedCells(row: string): Array<{ char: string; inverse: boolean }> {
|
|
21
26
|
const cells: Array<{ char: string; inverse: boolean }> = [];
|
|
22
27
|
let inverse = false;
|
|
28
|
+
// Decode the actual host post-pass, including hardware cursor suppression.
|
|
29
|
+
row = resolveFakeCursors([row])[0]!;
|
|
23
30
|
const tokens = row.match(/\x1b\[[0-9;]*m|\x1b_pi:c\x07|[^\x1b]/g) ?? [];
|
|
24
31
|
assert.equal(tokens.join(""), row, "only known editor escape sequences are present");
|
|
25
32
|
for (const token of tokens) {
|
|
@@ -270,12 +277,12 @@ test("resolveVimRuntime resolves local bundle cli entrypoint when provided", ()
|
|
|
270
277
|
});
|
|
271
278
|
|
|
272
279
|
test("audited Pi editor releases are one frozen exact allowlist that includes the installed TUI", () => {
|
|
273
|
-
assert.deepEqual([...AUDITED_PI_EDITOR_VERSIONS], ["0.99.1", "0.99.2", "1.0.0", "1.1.0"]);
|
|
280
|
+
assert.deepEqual([...AUDITED_PI_EDITOR_VERSIONS], ["0.99.1", "0.99.2", "1.0.0", "1.1.0", "1.2.0"]);
|
|
274
281
|
assert.equal(bundledAgent.VERSION, INSTALLED_PI, "bundled and local metadata must describe the same actual installed release");
|
|
275
282
|
assert.ok(Object.isFrozen(AUDITED_PI_EDITOR_VERSIONS));
|
|
276
283
|
assert.throws(() => (AUDITED_PI_EDITOR_VERSIONS as unknown as string[]).push("0.99.3"), TypeError);
|
|
277
284
|
assert.ok(isAuditedPiEditorVersion(INSTALLED_PI), `installed pi-tui ${INSTALLED_PI} must be audited`);
|
|
278
|
-
for (const unknown of ["0.99.3", "0.99.10", "0.99", "v0.99.2", " 0.99.2", "0.99.2-rc.1", ">=0.99.2", "1.0.1", ">=1.0.0", "v1.0.0", " 1.0.0", "1.0.0-rc.1", undefined, 99.2]) {
|
|
285
|
+
for (const unknown of ["0.99.3", "0.99.10", "0.99", "v0.99.2", " 0.99.2", "0.99.2-rc.1", ">=0.99.2", "1.0.1", ">=1.0.0", "v1.0.0", " 1.0.0", "1.0.0-rc.1", "1.2.1", "1.2.0-rc.1", "v1.2.0", ">=1.2.0", undefined, 99.2]) {
|
|
279
286
|
assert.equal(isAuditedPiEditorVersion(unknown), false, String(unknown));
|
|
280
287
|
}
|
|
281
288
|
});
|
|
@@ -309,6 +316,8 @@ test("bundled host admits each audited release and rejects unknown or mismatched
|
|
|
309
316
|
assert.equal(resolveVimRuntime(fabricatedPiCli(t, version), bundledAgent.CustomEditor), undefined, version);
|
|
310
317
|
}
|
|
311
318
|
assert.equal(resolveVimRuntime(fabricatedPiCli(t, "1.0.0", "1.0.0", "0.99.2"), bundledAgent.CustomEditor), undefined);
|
|
319
|
+
assert.equal(resolveVimRuntime(fabricatedPiCli(t, "1.2.1"), bundledAgent.CustomEditor), undefined);
|
|
320
|
+
assert.equal(resolveVimRuntime(fabricatedPiCli(t, "1.2.0", "1.2.0", "1.1.0"), bundledAgent.CustomEditor), undefined);
|
|
312
321
|
});
|
|
313
322
|
|
|
314
323
|
test("installed agent/TUI pair admits each audited release and skips unknown or mismatched pairs", (t) => {
|
|
@@ -343,6 +352,8 @@ test("adapter admits each verified audited release and rejects unknown or mismat
|
|
|
343
352
|
assert.throws(() => createVimEditorAdapter(editor(), version), /unsupported/i, version);
|
|
344
353
|
}
|
|
345
354
|
assert.throws(() => createVimEditorAdapter(e, "1.0.0", BundledEditor, "0.99.2"), /unsupported/i);
|
|
355
|
+
assert.throws(() => createVimEditorAdapter(e, "1.2.1", BundledEditor, "1.2.1"), /unsupported/i);
|
|
356
|
+
assert.throws(() => createVimEditorAdapter(e, "1.2.0", BundledEditor, "1.1.0"), /unsupported/i);
|
|
346
357
|
assert.equal(e.getText(), "untouched");
|
|
347
358
|
assert.deepEqual(e.getCursor(), cursor);
|
|
348
359
|
assert.deepEqual(e.render(30), frame);
|
|
@@ -638,24 +649,39 @@ test("operator insertion and registered marker guards keep one undo snapshot", (
|
|
|
638
649
|
assert.equal(paste.getExpandedText(), "z".repeat(1001));
|
|
639
650
|
});
|
|
640
651
|
|
|
652
|
+
test("Pi 1.2 selection preserves fake markers and host hardware-cursor suppression", () => {
|
|
653
|
+
const e = editor();
|
|
654
|
+
e.setText("abcd");
|
|
655
|
+
e.focused = true;
|
|
656
|
+
const adapter = createVimEditorAdapter(e, INSTALLED_PI);
|
|
657
|
+
adapter.move({ line: 0, col: 1 });
|
|
658
|
+
const rows = adapter.renderSelection(20, { line: 0, col: 0 }, { line: 0, col: 4 });
|
|
659
|
+
assert.ok(rows[1]!.includes(`${CURSOR_MARKER}\x1b_pi:fc\x07`));
|
|
660
|
+
assert.ok(rows[1]!.includes("\x1b_pi:/fc\x07"));
|
|
661
|
+
for (const hardware of [false, true]) {
|
|
662
|
+
const resolved = resolveFakeCursors(rows, hardware);
|
|
663
|
+
assert.deepEqual(paintedCells(resolved[1]!).slice(0, 4), "abcd".split("").map(char => ({ char, inverse: true })));
|
|
664
|
+
assert.ok(resolved.every(row => visibleWidth(row) === 20));
|
|
665
|
+
}
|
|
666
|
+
const boundary = adapter.renderSelection(20, { line: 0, col: 0 }, { line: 0, col: 1 });
|
|
667
|
+
assert.ok(boundary[1]!.includes(`${CURSOR_MARKER}\x1b_pi:fc\x07`));
|
|
668
|
+
assert.deepEqual(selectedColumns(resolveFakeCursors(boundary, true)[1]!), [0]);
|
|
669
|
+
assert.deepEqual(selectedColumns(resolveFakeCursors(boundary, false)[1]!), [0, 1]);
|
|
670
|
+
const legacy = e.render(20);
|
|
671
|
+
legacy[1] = legacy[1]!.replaceAll("\x1b_pi:fc\x07", "\x1b[7m").replaceAll("\x1b_pi:/fc\x07", "\x1b[0m");
|
|
672
|
+
assert.deepEqual(selectedColumns(adapter.renderSelection(20, { line: 0, col: 0 }, { line: 0, col: 4 }, legacy)[1]!), [0, 1, 2, 3]);
|
|
673
|
+
const unknown = e.render(20).map(row => row.replace("\x1b_pi:fc\x07", "\x1b_pi:unknown\x07"));
|
|
674
|
+
assert.deepEqual(adapter.renderSelection(20, { line: 0, col: 0 }, { line: 0, col: 4 }, unknown), unknown);
|
|
675
|
+
});
|
|
676
|
+
|
|
641
677
|
test("selection survives fake cursor reset and retains surrounding color", () => {
|
|
642
678
|
const e = editor();
|
|
643
679
|
e.setText("abcd");
|
|
644
680
|
createVimEditorAdapter(e, INSTALLED_PI).move({ line: 0, col: 1 });
|
|
645
681
|
const frame = createVimEditorAdapter(e, INSTALLED_PI).renderSelection(20, { line: 0, col: 0 }, { line: 0, col: 4 });
|
|
646
682
|
const row = frame.find((line) => line.includes("a")) ?? frame.join("");
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
for (const token of row.matchAll(/\x1b\[[0-9;]*m|[^\x1b]/g)) {
|
|
650
|
-
const value = token[0];
|
|
651
|
-
if (value.startsWith("\x1b[")) {
|
|
652
|
-
for (const code of value.slice(2, -1).split(";").map(Number)) {
|
|
653
|
-
if (code === 0 || code === 27) reverse = false;
|
|
654
|
-
if (code === 7) reverse = true;
|
|
655
|
-
}
|
|
656
|
-
} else if ("abcd".includes(value)) selected.push(reverse ? value : `!${value}`);
|
|
657
|
-
}
|
|
658
|
-
assert.deepEqual(selected, ["a", "b", "c", "d"], "every visible cell must have effective reverse SGR");
|
|
683
|
+
assert.deepEqual(paintedCells(row).slice(0, 4), "abcd".split("").map(char => ({ char, inverse: true })),
|
|
684
|
+
"every visible cell must have effective reverse SGR after the host post-pass");
|
|
659
685
|
});
|
|
660
686
|
|
|
661
687
|
test("truecolor channels do not change inverse state across selected and unselected cells", () => {
|
|
@@ -37,6 +37,8 @@ function fakeGitResult(command: string, args: readonly string[], cwd: string): s
|
|
|
37
37
|
if (args.includes("--git-common-dir")) return `${cwd}/.git`;
|
|
38
38
|
if (args.includes("--show-object-format")) return "sha1";
|
|
39
39
|
if (args.includes("--is-shallow-repository")) return "false";
|
|
40
|
+
if (args.includes("--is-inside-work-tree")) return "true";
|
|
41
|
+
if (args.includes("info/exclude")) return join(cwd, ".git", "info", "exclude");
|
|
40
42
|
if (args.includes("--git-path")) return join(cwd, ".git", "gentle-ai", "reviews");
|
|
41
43
|
if (args.includes("--max-parents=0")) return "a".repeat(40);
|
|
42
44
|
if (args[0] === "cat-file" && args[1] === "-t") return "tree";
|
|
@@ -169,6 +171,9 @@ async function captureOwnedRoutes(t: TestContext): Promise<{ captures: Capture[]
|
|
|
169
171
|
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
170
172
|
{ kind: "execFile", command: "codegraph", args: ["init", cwd] },
|
|
171
173
|
...fallbackCalls,
|
|
174
|
+
// A successful init keeps .codegraph/ out of git through the local exclude file.
|
|
175
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--is-inside-work-tree"] },
|
|
176
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--git-path", "info/exclude"] },
|
|
172
177
|
]);
|
|
173
178
|
const graphText = String((graphResult as { content?: Array<{ text?: string }> }).content?.[0]?.text ?? "");
|
|
174
179
|
assert.match(graphText, /indexed/, "CodeGraph must return the fixture command result");
|
|
@@ -54,10 +54,13 @@ function dispatchWriter(input: Record<string, unknown>) {
|
|
|
54
54
|
assert.equal(typeof toolCall, "function");
|
|
55
55
|
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-writer-surfaces-"));
|
|
56
56
|
scratchRoots.push(cwd);
|
|
57
|
+
// S16: these tests cover the surface rule; a measured context at the backstop admits the launch so the
|
|
58
|
+
// feature-document admission (tests/writer-feature-document-admission.test.ts) does not decide here.
|
|
57
59
|
return toolCall!({ toolName: "subagent_run", input }, {
|
|
58
60
|
cwd,
|
|
59
61
|
hasUI: false,
|
|
60
62
|
ui: { confirm: async () => true },
|
|
63
|
+
getContextUsage: () => ({ tokens: 150_000, contextWindow: 272_000, percent: 55 }),
|
|
61
64
|
} as unknown as ExtensionContext);
|
|
62
65
|
}
|
|
63
66
|
|