gentle-pi 4.2.0 → 4.4.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 +340 -138
- 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/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- 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 +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- 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 +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- 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-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- 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 +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- 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/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- 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 +645 -21
- 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-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- 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 +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- 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-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- 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 +70 -10
- 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 +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test, { type TestContext } from "node:test";
|
|
7
|
+
import type { ContextUsage, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
9
|
+
import {
|
|
10
|
+
CONTEXT_BACKSTOP_FRACTION_ENV,
|
|
11
|
+
CONTEXT_BACKSTOP_TOKENS_ENV,
|
|
12
|
+
contextBackstopReached,
|
|
13
|
+
contextBackstopThreshold,
|
|
14
|
+
createThrottledContextMeasure,
|
|
15
|
+
type MeasuredContext,
|
|
16
|
+
} from "../lib/context-backstop.ts";
|
|
17
|
+
|
|
18
|
+
// Harness roadmap R1: the parent-context backstop was a fixed "~150k tokens" in prompt text, so the model had to
|
|
19
|
+
// estimate its own context. The runtime now measures it against min(absolute cap, fraction x context window) and
|
|
20
|
+
// tells the primary session once per crossing.
|
|
21
|
+
|
|
22
|
+
test("the threshold is the smaller of the absolute cap and 15% of the context window", () => {
|
|
23
|
+
assert.equal(contextBackstopThreshold(1_000_000, {}), 150_000, "a large window keeps the absolute cap");
|
|
24
|
+
assert.equal(contextBackstopThreshold(400_000, {}), 60_000);
|
|
25
|
+
// Bench gate (T12): a 272k window reaches it near 41k, where delegating the wide task paid off.
|
|
26
|
+
assert.equal(contextBackstopThreshold(272_000, {}), 40_800);
|
|
27
|
+
assert.equal(contextBackstopThreshold(200_000, {}), 30_000, "a small window reaches it at 15% of its size");
|
|
28
|
+
assert.equal(contextBackstopThreshold(128_000, {}), 19_200);
|
|
29
|
+
for (const unknown of [undefined, null, 0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
30
|
+
assert.equal(contextBackstopThreshold(unknown, {}), 150_000, `window ${unknown} falls back to the cap`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("the cap and the fraction are overridable by environment; invalid values keep the defaults", () => {
|
|
35
|
+
assert.equal(contextBackstopThreshold(1_000_000, { [CONTEXT_BACKSTOP_TOKENS_ENV]: "100000" }), 100_000);
|
|
36
|
+
assert.equal(contextBackstopThreshold(4_000_000, { [CONTEXT_BACKSTOP_TOKENS_ENV]: "300000" }), 300_000, "a raised cap applies where the fraction allows it");
|
|
37
|
+
assert.equal(contextBackstopThreshold(1_000_000, { [CONTEXT_BACKSTOP_FRACTION_ENV]: "0.1" }), 100_000);
|
|
38
|
+
assert.equal(contextBackstopThreshold(200_000, { [CONTEXT_BACKSTOP_TOKENS_ENV]: "80000", [CONTEXT_BACKSTOP_FRACTION_ENV]: "1" }), 80_000);
|
|
39
|
+
for (const value of ["", "abc", "0", "-5", "1.5e999"]) {
|
|
40
|
+
assert.equal(contextBackstopThreshold(1_000_000, { [CONTEXT_BACKSTOP_TOKENS_ENV]: value }), 150_000, `tokens ${JSON.stringify(value)}`);
|
|
41
|
+
}
|
|
42
|
+
for (const value of ["", "abc", "0", "-0.5", "1.5"]) {
|
|
43
|
+
assert.equal(contextBackstopThreshold(200_000, { [CONTEXT_BACKSTOP_FRACTION_ENV]: value }), 30_000, `fraction ${JSON.stringify(value)}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// R1 review advisory: a tiny window or fraction floored the threshold to 0, so an
|
|
48
|
+
// empty context counted as reaching the backstop (and admitted every writer).
|
|
49
|
+
test("the threshold is never below one token, so an empty context never reaches it", () => {
|
|
50
|
+
assert.equal(contextBackstopThreshold(200_000, { [CONTEXT_BACKSTOP_FRACTION_ENV]: "0.000001" }), 1);
|
|
51
|
+
assert.equal(contextBackstopThreshold(1, {}), 1);
|
|
52
|
+
assert.equal(contextBackstopReached({ tokens: 0, contextWindow: 1 }, {}), false);
|
|
53
|
+
assert.equal(contextBackstopReached({ tokens: 1, contextWindow: 1 }, {}), true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("unknown tokens never count as reaching the backstop", () => {
|
|
57
|
+
assert.equal(contextBackstopReached({ tokens: 150_000, contextWindow: 1_000_000 }, {}), true);
|
|
58
|
+
assert.equal(contextBackstopReached({ tokens: 149_999, contextWindow: 1_000_000 }, {}), false);
|
|
59
|
+
assert.equal(contextBackstopReached({ tokens: 19_200, contextWindow: 128_000 }, {}), true);
|
|
60
|
+
assert.equal(contextBackstopReached({ tokens: 19_199, contextWindow: 128_000 }, {}), false);
|
|
61
|
+
assert.equal(contextBackstopReached({ tokens: null, contextWindow: 128_000 }, {}), false, "null right after compaction");
|
|
62
|
+
assert.equal(contextBackstopReached(undefined, {}), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
type Handler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
66
|
+
type Patch = { content?: Array<{ type: string; text?: string }>; structuredContent?: unknown; isError?: boolean } | undefined;
|
|
67
|
+
|
|
68
|
+
function session(t: TestContext, child: boolean, options: { now?: () => number; minIntervalMs?: number } = {}) {
|
|
69
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), "context-backstop-")));
|
|
70
|
+
const configHome = mkdtempSync(join(tmpdir(), "context-backstop-config-"));
|
|
71
|
+
const previous = process.env.GENTLE_PI_CONFIG_HOME;
|
|
72
|
+
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
73
|
+
t.after(() => {
|
|
74
|
+
if (previous === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
75
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previous;
|
|
76
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
77
|
+
rmSync(configHome, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
const handlers = new Map<string, Handler>();
|
|
80
|
+
const pi = { on: (name: string, handler: Handler) => { handlers.set(name, handler); }, events: { on() {}, emit() {} }, registerCommand() {}, registerTool() {} } as unknown as ExtensionAPI;
|
|
81
|
+
// Semantic tests without options keep per-call measuring (minIntervalMs 0); callers that pass
|
|
82
|
+
// options (a fake clock) keep the production window resolution.
|
|
83
|
+
const measureOptions = Object.keys(options).length === 0 ? { minIntervalMs: 0 } : options;
|
|
84
|
+
createGentleAiExtension({
|
|
85
|
+
nativeReviewCli: null,
|
|
86
|
+
processEnv: { GENTLE_PI_AGENTS_CHILD: child ? "1" : "0", GENTLE_AI_TELEMETRY: "0" },
|
|
87
|
+
contextBackstopMeasure: measureOptions,
|
|
88
|
+
})(pi);
|
|
89
|
+
let usage: ContextUsage | undefined = { tokens: 10_000, contextWindow: 1_000_000, percent: 1 };
|
|
90
|
+
let measures = 0;
|
|
91
|
+
const sessionManager = { getSessionId: () => "session-backstop", getCwd: () => cwd, getEntries: () => [], getBranch: () => [] };
|
|
92
|
+
const ctx = { cwd, hasUI: false, ui: { notify() {} }, sessionManager, getContextUsage: () => { measures++; return usage; } } as unknown as ExtensionContext;
|
|
93
|
+
const measureCount = () => measures;
|
|
94
|
+
const setTokens = (tokens: number | null, contextWindow = 1_000_000) => {
|
|
95
|
+
usage = { tokens, contextWindow, percent: tokens === null ? null : (tokens / contextWindow) * 100 };
|
|
96
|
+
};
|
|
97
|
+
const read = (overrides: Record<string, unknown> = {}) => handlers.get("tool_result")!({
|
|
98
|
+
toolName: "read", toolCallId: "call-read", isError: false, input: { path: "a.ts" },
|
|
99
|
+
content: [{ type: "text", text: "file body" }], structuredContent: { lines: 1 }, ...overrides,
|
|
100
|
+
}, ctx) as Promise<Patch>;
|
|
101
|
+
return { cwd, ctx, handlers, setTokens, read, measureCount, options };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test("the primary session gets one notice when its measured context first reaches the backstop", async (t) => {
|
|
105
|
+
const s = session(t, false);
|
|
106
|
+
assert.equal(await s.read(), undefined, "below the backstop nothing is appended");
|
|
107
|
+
s.setTokens(160_000);
|
|
108
|
+
const crossed = await s.read();
|
|
109
|
+
assert.equal(crossed?.content?.[0]?.text, "file body", "the tool output is kept first");
|
|
110
|
+
const notice = crossed?.content?.[1]?.text ?? "";
|
|
111
|
+
assert.match(notice, /Context backstop/);
|
|
112
|
+
assert.match(notice, /160,000 tokens/, "measured tokens");
|
|
113
|
+
assert.match(notice, /16% of the 1,000,000-token context window/, "percent of the window");
|
|
114
|
+
assert.match(notice, /150,000-token backstop/, "the threshold");
|
|
115
|
+
assert.match(notice, /delegate the next bounded unit that needs several steps \(its reads, edits, and checks\) with a short handoff/);
|
|
116
|
+
assert.match(notice, /single small edits and state checks stay inline/);
|
|
117
|
+
assert.deepEqual(crossed?.structuredContent, { lines: 1 }, "structured content is kept");
|
|
118
|
+
assert.equal(crossed && "isError" in crossed, false, "the error flag is never altered");
|
|
119
|
+
assert.equal(await s.read(), undefined, "once per crossing");
|
|
120
|
+
s.setTokens(null);
|
|
121
|
+
assert.equal(await s.read(), undefined, "unknown tokens neither notify nor re-arm");
|
|
122
|
+
s.setTokens(170_000);
|
|
123
|
+
assert.equal(await s.read(), undefined, "still past the same crossing");
|
|
124
|
+
s.setTokens(40_000);
|
|
125
|
+
assert.equal(await s.read(), undefined, "dropping below the backstop re-arms it");
|
|
126
|
+
s.setTokens(19_200, 128_000);
|
|
127
|
+
const again = await s.read({ isError: true, structuredContent: undefined });
|
|
128
|
+
assert.match(again?.content?.[1]?.text ?? "", /19,200 tokens \(15% of the 128,000-token context window\), at or past the 19,200-token backstop/, "a small window reaches it at 15% of its size");
|
|
129
|
+
assert.equal(again && "structuredContent" in again, false, "absent structured content stays absent");
|
|
130
|
+
assert.equal(again && "isError" in again, false, "an error result keeps its flag");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("child sessions never get the notice", async (t) => {
|
|
134
|
+
const s = session(t, true);
|
|
135
|
+
s.setTokens(390_000);
|
|
136
|
+
assert.equal(await s.read(), undefined);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a parent commit result carries both the slice line and the backstop notice", async (t) => {
|
|
140
|
+
const s = session(t, false);
|
|
141
|
+
const git = (...args: string[]) => execFileSync("git", ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: s.cwd, encoding: "utf8" });
|
|
142
|
+
git("init", "-q", "-b", "main");
|
|
143
|
+
writeFileSync(join(s.cwd, "README.md"), "base\n");
|
|
144
|
+
git("add", "README.md");
|
|
145
|
+
git("commit", "-q", "-m", "base");
|
|
146
|
+
git("switch", "-q", "-c", "feat/x");
|
|
147
|
+
writeFileSync(join(s.cwd, "a.ts"), "one\n");
|
|
148
|
+
git("add", "a.ts");
|
|
149
|
+
git("commit", "-q", "-m", "feat: a");
|
|
150
|
+
s.setTokens(200_000);
|
|
151
|
+
const result = await s.handlers.get("tool_result")!({
|
|
152
|
+
toolName: "bash", toolCallId: "call-1", isError: false, input: { command: "git commit -m 'feat: a'" },
|
|
153
|
+
content: [{ type: "text", text: "[feat/x 1234567] feat: a" }], structuredContent: { exitCode: 0 },
|
|
154
|
+
}, s.ctx) as Patch;
|
|
155
|
+
assert.equal(result?.content?.[0]?.text, "[feat/x 1234567] feat: a");
|
|
156
|
+
assert.match(result?.content?.[1]?.text ?? "", /Commit slice: 1 authored changed lines/);
|
|
157
|
+
assert.match(result?.content?.[2]?.text ?? "", /Context backstop/);
|
|
158
|
+
assert.deepEqual(result?.structuredContent, { exitCode: 0 });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a parent write result carries the notice after its own bookkeeping", async (t) => {
|
|
162
|
+
const s = session(t, false);
|
|
163
|
+
s.setTokens(150_000);
|
|
164
|
+
const result = await s.handlers.get("tool_result")!({
|
|
165
|
+
toolName: "write", toolCallId: "call-w", isError: false, input: { path: "src/a.ts" },
|
|
166
|
+
content: [{ type: "text", text: "Wrote src/a.ts" }],
|
|
167
|
+
}, s.ctx) as Patch;
|
|
168
|
+
assert.equal(result?.content?.[0]?.text, "Wrote src/a.ts");
|
|
169
|
+
assert.match(result?.content?.[1]?.text ?? "", /Context backstop/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The measurement behind the backstop (ctx.getContextUsage()) rebuilds the whole session
|
|
173
|
+
// projection, so it is sampled at most once per window per session instead of once per
|
|
174
|
+
// tool result. The window never decides the notice: it only decides when the context is
|
|
175
|
+
// looked at again.
|
|
176
|
+
test("the throttled measure samples once per window and the first sample of a session is immediate", () => {
|
|
177
|
+
let clock = 1_000;
|
|
178
|
+
const measure = createThrottledContextMeasure({ now: () => clock });
|
|
179
|
+
let calls = 0;
|
|
180
|
+
const sample = (value: MeasuredContext | undefined) => measure("session-a", () => { calls++; return value; });
|
|
181
|
+
assert.deepEqual(sample({ tokens: 10_000 }), { tokens: 10_000 }, "the first sample of a session measures immediately");
|
|
182
|
+
assert.equal(calls, 1);
|
|
183
|
+
clock = 2_000;
|
|
184
|
+
sample({ tokens: 10_000 });
|
|
185
|
+
assert.equal(calls, 1, "a sample inside the window skips the measurement");
|
|
186
|
+
clock = 6_001;
|
|
187
|
+
sample({ tokens: 10_000 });
|
|
188
|
+
assert.equal(calls, 2, "the window expired: measure again");
|
|
189
|
+
clock = 6_002;
|
|
190
|
+
sample({ tokens: 10_000 });
|
|
191
|
+
assert.equal(calls, 2, "the window restarts at every measurement");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("the throttled measure is per session and a throwing measure never poisons the window", () => {
|
|
195
|
+
let clock = 0;
|
|
196
|
+
const measure = createThrottledContextMeasure({ now: () => clock });
|
|
197
|
+
assert.ok(measure("session-a", () => ({ tokens: 1 })), "session a measures");
|
|
198
|
+
assert.ok(measure("session-b", () => ({ tokens: 2 })), "session b inside the same window still measures");
|
|
199
|
+
assert.equal(measure("session-a", () => ({ tokens: 3 })), undefined, "session a inside its own window skips");
|
|
200
|
+
clock = 5_001;
|
|
201
|
+
let boom = false;
|
|
202
|
+
assert.equal(measure("session-a", () => { boom = true; throw new Error("boom"); }), undefined, "a throwing measure returns undefined");
|
|
203
|
+
assert.equal(boom, true, "the measure itself actually ran");
|
|
204
|
+
assert.equal(measure("session-a", () => ({ tokens: 4 }))?.tokens, undefined, "the failed measurement still consumed its window");
|
|
205
|
+
clock = 10_002;
|
|
206
|
+
assert.equal(measure("session-a", () => ({ tokens: 4 }))?.tokens, 4, "the next window measures again");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("the extension samples the backstop context at most once per window per session", async (t) => {
|
|
210
|
+
let clock = 0;
|
|
211
|
+
const s = session(t, false, { now: () => clock });
|
|
212
|
+
assert.equal(s.measureCount(), 0);
|
|
213
|
+
await s.read();
|
|
214
|
+
assert.equal(s.measureCount(), 1, "the first tool result measures immediately");
|
|
215
|
+
await s.read();
|
|
216
|
+
await s.read();
|
|
217
|
+
assert.equal(s.measureCount(), 1, "tool results inside the window skip the measurement");
|
|
218
|
+
clock = 5_001;
|
|
219
|
+
s.setTokens(160_000);
|
|
220
|
+
const crossed = await s.read();
|
|
221
|
+
assert.equal(s.measureCount(), 2, "the window expired: measure again");
|
|
222
|
+
assert.match(crossed?.content?.[1]?.text ?? "", /Context backstop/, "a crossing is announced on the next sampled result");
|
|
223
|
+
await s.read();
|
|
224
|
+
assert.equal(s.measureCount(), 2, "and the window restarts after that sample");
|
|
225
|
+
});
|
|
@@ -30,11 +30,19 @@ function lineStarting(text: string, prefix: string): string {
|
|
|
30
30
|
const coreWriter = lineStarting(core, "5. **Writer rule**");
|
|
31
31
|
const delegationWriter = lineStarting(delegation, "5. **Writer trigger (Writer rule):**");
|
|
32
32
|
|
|
33
|
+
// Writer-rule fix (benchmark finding): a low-effort orchestrator read "2+
|
|
34
|
+
// independent units, disjoint files" as tests vs README and split one behavior
|
|
35
|
+
// across writers. The core rule now defines a unit as independent behavior,
|
|
36
|
+
// keeps tests, docs, and code of one behavior together, and sets a size floor.
|
|
33
37
|
test("AC1: the core writer rule fires on named reasons, never on size or file count", () => {
|
|
34
38
|
for (const clause of [
|
|
35
39
|
"never by file count or a large task alone",
|
|
36
40
|
"`orchestrator-writer.md`",
|
|
37
|
-
"2+ independent units
|
|
41
|
+
"2+ independent units of behavior (separate features or fixes that do not depend on each other), each heavier than a subagent start",
|
|
42
|
+
"a unit qualifies only when it needs several minutes of work: several files or roughly 100+ changed lines",
|
|
43
|
+
"a small change to one module or one command is done inline even when several such changes are independent",
|
|
44
|
+
"Tests, docs, and code of the same behavior are one unit, never separate writers",
|
|
45
|
+
"a delivery that depends on an earlier one is not independent",
|
|
38
46
|
"Context backstop",
|
|
39
47
|
]) {
|
|
40
48
|
assert.ok(coreWriter.includes(clause), `core Writer rule is missing: ${clause}`);
|
|
@@ -65,13 +73,19 @@ test("AC1: the lazy writer trigger names its reasons and keeps the logbook on th
|
|
|
65
73
|
assert.ok(skill.includes("a writer reason (parallel units or context)"), "the skill must name the writer reasons");
|
|
66
74
|
});
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
76
|
+
// S9 canon change (gentle-ai 87a3b2eb): the lazy trigger mirrors the canon's
|
|
77
|
+
// behavior unit and size threshold instead of "clearly heavier than starting a
|
|
78
|
+
// subagent; medium tasks included".
|
|
79
|
+
test("AC2: parallelism needs 2+ independent behavior units, disjoint surfaces, each worth several minutes", () => {
|
|
80
|
+
for (const clause of [
|
|
81
|
+
"(a) parallelism — 2+ independent units of behavior with disjoint edit surfaces, each needing several minutes of work (several files or ~100+ changed lines; a subagent spends 1-2 minutes reading before it writes), launched together",
|
|
82
|
+
"A unit is one independent piece of behavior with its code, tests, and docs together: never split one behavior into test, docs, and code writers",
|
|
83
|
+
"a delivery that depends on another is not independent",
|
|
84
|
+
"several small independent changes run inline, one after another",
|
|
85
|
+
]) {
|
|
86
|
+
assert.ok(delegationWriter.includes(clause), `lazy Writer trigger is missing: ${clause}`);
|
|
87
|
+
}
|
|
88
|
+
assert.ok(!delegationWriter.includes("medium tasks included"), "lazy Writer trigger keeps the retired medium-task clause");
|
|
75
89
|
assert.ok(writer.includes("parallel units"), "the writer module must name the parallelism reason");
|
|
76
90
|
});
|
|
77
91
|
|
|
@@ -96,16 +110,20 @@ test("T24/L49: configured per-agent model routing still applies to writers launc
|
|
|
96
110
|
);
|
|
97
111
|
});
|
|
98
112
|
|
|
113
|
+
// Verification by named risk changed the core rule 3 text: only a named
|
|
114
|
+
// high-risk item, passed as assess `escalate`, adds the independent verifier.
|
|
115
|
+
// odd/tasks/verifier-admission.md S6: the rule names the runtime admission of the verifier launch.
|
|
99
116
|
test("AC4: verification stays risk-gated and the writer reasons never include risk", () => {
|
|
100
117
|
assert.ok(
|
|
101
118
|
core.includes(
|
|
102
|
-
"3. **Verification rule** — high
|
|
119
|
+
"3. **Verification rule** — only a named high-risk item (assess `escalate`) → independent `gentle-ai-verify`; else inline; runtime admission, gates/overengineering: `orchestrator-verification.md`.",
|
|
103
120
|
),
|
|
104
121
|
"core Verification rule changed",
|
|
105
122
|
);
|
|
106
123
|
assert.ok(
|
|
107
124
|
delegation.includes(
|
|
108
|
-
|
|
125
|
+
// S9 canon change (gentle-ai 6b9a71ef): only a named high-risk item adds the verifier.
|
|
126
|
+
"3. **Verification rule** (gentle-pi#661/#662, RDD-aware): only a change whose Risk line names a high-risk item (Task Size, assess `escalate`) gets an independent `gentle-ai-verify` run after the change's own checks, as rarely as possible; otherwise whoever made the change runs its focused test and suite inline, small tasks included.",
|
|
109
127
|
),
|
|
110
128
|
"lazy Verification rule changed",
|
|
111
129
|
);
|
|
@@ -177,38 +195,41 @@ test("T12/S6: one independent verify per delegated unit after its self-review, n
|
|
|
177
195
|
"once, after its final self-review",
|
|
178
196
|
"never per worker cycle or retry",
|
|
179
197
|
"its Risk line",
|
|
180
|
-
"
|
|
198
|
+
"high-risk list in Task Size",
|
|
181
199
|
]) {
|
|
182
200
|
assert.ok(item.includes(clause), `independent verify timing is missing: ${clause}`);
|
|
183
201
|
}
|
|
184
202
|
assert.ok(!item.includes("(1) "), "the timing rule restates the high-risk list");
|
|
203
|
+
// Verification by named risk retired the small-model bias.
|
|
204
|
+
assert.ok(!item.includes("small-model bias"), "the per-unit verify must not add a verifier for the writer's model");
|
|
185
205
|
});
|
|
186
206
|
|
|
187
|
-
// T15 (S6, L38): after3 B x4 paid two same-model verifies with 0 defects
|
|
188
|
-
//
|
|
189
|
-
|
|
207
|
+
// T15 (S6, L38): after3 B x4 paid two same-model verifies with 0 defects.
|
|
208
|
+
// Verification by named risk (user decision: the verifier follows the change's
|
|
209
|
+
// risk, never the model that wrote it) dropped the smaller-model exception.
|
|
210
|
+
test("T15/S6: inline code over several deliveries gets one verify at the feature's end, whatever model wrote it", () => {
|
|
190
211
|
const item = reviewItem("2. **Independent verify per unit**");
|
|
191
212
|
for (const clause of [
|
|
192
|
-
"
|
|
213
|
+
"Inline code over several deliveries of one feature",
|
|
193
214
|
"one verify at the feature's end",
|
|
194
|
-
"smaller-model writer code stays per unit",
|
|
195
215
|
"only when that unit is high risk",
|
|
196
|
-
"small-model bias per the tier table",
|
|
197
216
|
]) {
|
|
198
217
|
assert.ok(item.includes(clause), `independent verify exception is missing: ${clause}`);
|
|
199
218
|
}
|
|
219
|
+
assert.ok(!item.includes("smaller-model"), "the writer's model never keeps a per-unit verify");
|
|
200
220
|
});
|
|
201
221
|
|
|
202
222
|
test("T11/S4: the parent hands verify the whole spec, authorizes its probes and resolves every unmet item", () => {
|
|
203
223
|
const item = reviewItem("4. **Verify handoff**");
|
|
204
224
|
for (const clause of [
|
|
205
225
|
"the whole feature document",
|
|
206
|
-
"
|
|
226
|
+
"the assigned unit and its linked `S#` plus affected interactions",
|
|
227
|
+
"final feature verification covers every `S#`",
|
|
207
228
|
"the baseline commit",
|
|
208
229
|
"probes on isolated state",
|
|
209
230
|
"typecheck",
|
|
210
231
|
"`gentle-ai-verify` holds the checklist",
|
|
211
|
-
"every unmet item before closing",
|
|
232
|
+
"every in-scope unmet item before closing the unit",
|
|
212
233
|
"the writer commits its probes as regression tests",
|
|
213
234
|
]) {
|
|
214
235
|
assert.ok(item.includes(clause), `verify handoff item is missing: ${clause}`);
|
|
@@ -216,12 +237,17 @@ test("T11/S4: the parent hands verify the whole spec, authorizes its probes and
|
|
|
216
237
|
});
|
|
217
238
|
|
|
218
239
|
// verify-always-rdd-high S1 deliberately rewrote this section: verification follows the
|
|
219
|
-
// risk tier in every RDD state.
|
|
240
|
+
// risk tier in every RDD state. Verification by named risk deliberately rewrote
|
|
241
|
+
// the tier table again (escalate-only verifier, no small-model bias), and
|
|
242
|
+
// verifier admission (odd/tasks/verifier-admission.md S6) names the runtime
|
|
243
|
+
// check of the verifier launch; the PR #2290 review (odd/tasks/pr-2290-review.md S4)
|
|
244
|
+
// names every line that admits it. The pin still guards against accidental drift.
|
|
220
245
|
test("AC4/S4: the normative verification rule text is unchanged by the review protocol", () => {
|
|
221
246
|
const normative = sectionFrom(verification, "## Verification rule (normative)");
|
|
247
|
+
// Re-pinned on purpose: the runtime admission now also accepts a `Risk: item N` sentence (verifier-admission.ts).
|
|
222
248
|
assert.equal(
|
|
223
249
|
createHash("sha256").update(normative).digest("hex"),
|
|
224
|
-
"
|
|
250
|
+
"f052518b2905e4560d629397876057079aa71b45352272646261a611fb4a1e17",
|
|
225
251
|
"the normative Verification rule section changed",
|
|
226
252
|
);
|
|
227
253
|
assert.ok(verification.includes("or a delegated writer returns"), "the module must still load when a delegated writer returns");
|
|
@@ -298,7 +324,8 @@ test("T10: escalate labels name the core high-risk items", () => {
|
|
|
298
324
|
test("T9/AC2: the parallelism reason launches every unit together in background and waits for all", () => {
|
|
299
325
|
const parallel = lineStarting(writer, "- **Parallelism**");
|
|
300
326
|
for (const clause of [
|
|
301
|
-
|
|
327
|
+
// Writer-rule fix: a unit is one independent piece of behavior.
|
|
328
|
+
"every independent unit in the same turn",
|
|
302
329
|
'`subagent_run` with `mode: "background"`',
|
|
303
330
|
"wait for all completions",
|
|
304
331
|
"serial launches void the reason",
|
|
@@ -317,11 +344,52 @@ test("AC6: parallel writers need disjoint Allowed edit surfaces (runtime-enforce
|
|
|
317
344
|
const harness = read("extensions/gentle-ai.ts");
|
|
318
345
|
const docs = read("docs/readme-reference.md");
|
|
319
346
|
assert.ok(core.includes(`- P${rule}.`), "core Safety must carry the relaxed writer rule");
|
|
320
|
-
|
|
321
|
-
|
|
347
|
+
// S11 T3: deduplicated. The harness principle repeated core Safety in the same
|
|
348
|
+
// parent prompt; the rendered prompt now carries the rule once, from Safety.
|
|
349
|
+
assert.ok(!harness.includes(rule), "harness principles restate the core Safety writer rule");
|
|
350
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
351
|
+
assert.equal(__testing.buildGentlePrompt(persona).split(rule).length - 1, 1, `[${persona}] the parent prompt must carry the writer rule once`);
|
|
352
|
+
}
|
|
353
|
+
for (const [path, text] of Object.entries({ core, delegation, skill, docs })) {
|
|
322
354
|
assert.ok(text.includes(rule), `${path} is missing the relaxed writer rule`);
|
|
355
|
+
}
|
|
356
|
+
for (const [path, text] of Object.entries({ core, delegation, harness, skill, docs })) {
|
|
323
357
|
for (const retired of ["single-threaded", "Never run parallel writers in one worktree", "do not run parallel writers unless isolated worktrees", "preserves a single writer thread", "one writer per task"]) {
|
|
324
358
|
assert.ok(!text.includes(retired), `${path} keeps the single-writer wording: ${retired}`);
|
|
325
359
|
}
|
|
326
360
|
}
|
|
327
361
|
});
|
|
362
|
+
|
|
363
|
+
// Writer-rule fix (benchmark finding): "Mandatory Delegation Triggers ...
|
|
364
|
+
// and is then mandatory" read as a default to delegate. The core heading now
|
|
365
|
+
// says a trigger holds only when its condition truly holds, inline when in doubt.
|
|
366
|
+
test("the core delegation triggers hold only when their condition truly holds, none by default", () => {
|
|
367
|
+
const intro = lineStarting(core, "Delegation Triggers — ");
|
|
368
|
+
for (const clause of [
|
|
369
|
+
"each mechanism turns on only when its own trigger condition truly holds, and only then is it mandatory",
|
|
370
|
+
"no trigger holds by default",
|
|
371
|
+
"when in doubt work inline",
|
|
372
|
+
]) {
|
|
373
|
+
assert.ok(intro.includes(clause), `core delegation triggers are missing: ${clause}`);
|
|
374
|
+
}
|
|
375
|
+
assert.ok(!core.includes("Mandatory Delegation Triggers"), "the core must not read the triggers as mandatory by default");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("the writer module sets the size threshold and keeps one behavior in one unit", () => {
|
|
379
|
+
const size = lineStarting(writer, "- **Size threshold**");
|
|
380
|
+
for (const clause of [
|
|
381
|
+
"a subagent costs one to two minutes of reading and setup before it writes anything",
|
|
382
|
+
"delegate a unit only when it needs several minutes of work (several files or roughly 100+ changed lines)",
|
|
383
|
+
"Several small independent changes are faster inline, one after another",
|
|
384
|
+
]) {
|
|
385
|
+
assert.ok(size.includes(clause), `writer size threshold is missing: ${clause}`);
|
|
386
|
+
}
|
|
387
|
+
const parallel = lineStarting(writer, "- **Parallelism**");
|
|
388
|
+
for (const clause of [
|
|
389
|
+
"a unit is one independent piece of behavior with its own code, tests, and docs",
|
|
390
|
+
"never split one behavior into a test writer, a docs writer, and inline code",
|
|
391
|
+
"never delegate a unit that depends on another unit still in progress",
|
|
392
|
+
]) {
|
|
393
|
+
assert.ok(parallel.includes(clause), `writer parallelism unit rule is missing: ${clause}`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { deliveredTestViolation, evaluateDeliveredTestTool, isTestFilePath } from "../lib/delivered-test-lint.ts";
|
|
4
|
+
|
|
5
|
+
// Harness roadmap T5 (R1): blind review found delivered tests that ran
|
|
6
|
+
// `git show <baseline commit>` and failed in another clone, and orchestrators
|
|
7
|
+
// that pointed TMPDIR into the repository. The prompt rule did not hold, so the
|
|
8
|
+
// runtime blocks writing such a test instead of asking the model not to.
|
|
9
|
+
|
|
10
|
+
test("isTestFilePath recognizes common test layouts and nothing else", () => {
|
|
11
|
+
for (const path of ["tests/ledger.test.ts", "src/__tests__/a.ts", "test/cli.spec.js", "pkg/store_test.go", "tests/test_cli.py", "/abs/repo/test/x.mjs"]) {
|
|
12
|
+
assert.equal(isTestFilePath(path), true, path);
|
|
13
|
+
}
|
|
14
|
+
for (const path of ["src/ledger.ts", "README.md", "src/testing-utils.ts", "docs/tests.md"]) {
|
|
15
|
+
assert.equal(isTestFilePath(path), false, path);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a test that reads a literal commit from git history is a violation", () => {
|
|
20
|
+
for (const content of [
|
|
21
|
+
'const old = execSync("git show 3f9c2e1:src/ledger.ts").toString();',
|
|
22
|
+
"execFileSync('git', ['show', 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0:src/cli.ts']);",
|
|
23
|
+
"git checkout 9e8d7c6 -- src/cli.ts",
|
|
24
|
+
"git diff 4c5d6e7f HEAD -- src",
|
|
25
|
+
"git archive 77aa12b | tar -x -C \"$scratch\"",
|
|
26
|
+
]) {
|
|
27
|
+
assert.match(deliveredTestViolation(content) ?? "", /git history/, content);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a test that points TMPDIR inside the working directory is a violation", () => {
|
|
32
|
+
for (const content of [
|
|
33
|
+
"TMPDIR=$PWD/.test-tmp node --test",
|
|
34
|
+
'const env = { ...process.env, TMPDIR: join(process.cwd(), ".tmp") };',
|
|
35
|
+
"process.env.TMPDIR = path.join(__dirname, 'tmp');",
|
|
36
|
+
"export TMPDIR=\"$(pwd)/tmp\"",
|
|
37
|
+
'os.environ["TMPDIR"] = os.getcwd()',
|
|
38
|
+
]) {
|
|
39
|
+
assert.match(deliveredTestViolation(content) ?? "", /TMPDIR/, content);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// PR #2290 review: a relative TMPDIR resolves against the working directory just
|
|
44
|
+
// like `$PWD/...`, so test scratch entered the diff unflagged.
|
|
45
|
+
test("a test that sets TMPDIR to a relative path is a violation", () => {
|
|
46
|
+
for (const content of [
|
|
47
|
+
"TMPDIR=./.tmp node --test",
|
|
48
|
+
"TMPDIR=.tmp node --test",
|
|
49
|
+
"export TMPDIR=tmp/x",
|
|
50
|
+
'const env = { ...process.env, TMPDIR: "./.tmp" };',
|
|
51
|
+
"process.env.TMPDIR = '.tmp';",
|
|
52
|
+
'process.env.TMPDIR = "tmp/x";',
|
|
53
|
+
'os.environ["TMPDIR"] = "../scratch"',
|
|
54
|
+
]) {
|
|
55
|
+
assert.match(deliveredTestViolation(content) ?? "", /TMPDIR/, content);
|
|
56
|
+
}
|
|
57
|
+
for (const content of [
|
|
58
|
+
'TMPDIR="$TMPDIR/ledger" node --test',
|
|
59
|
+
"TMPDIR=${TMPDIR:-/tmp}/ledger node --test",
|
|
60
|
+
"export TMPDIR=/var/tmp/ledger",
|
|
61
|
+
'const env = { TMPDIR: "/tmp/ledger" };',
|
|
62
|
+
"const env = { TMPDIR: os.tmpdir() };",
|
|
63
|
+
"const env = { TMPDIR: path.join(tmpdir(), 'ledger') };",
|
|
64
|
+
"TMPDIR=~/scratch node --test",
|
|
65
|
+
'process.env.TMPDIR = "C:\\\\Temp";',
|
|
66
|
+
"const env = { TMPDIR: `${tmpdir()}/ledger` };",
|
|
67
|
+
'const task = runner.run(request({ env: { TMPDIR: "reltmp" } }));',
|
|
68
|
+
]) {
|
|
69
|
+
assert.equal(deliveredTestViolation(content), undefined, content);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("ordinary tests, git use in throwaway repos, and outside scratch stay allowed", () => {
|
|
74
|
+
for (const content of [
|
|
75
|
+
'const dir = mkdtempSync(join(tmpdir(), "ledger-"));',
|
|
76
|
+
'execFileSync("git", ["init", "-q"], { cwd: dir }); execFileSync("git", ["stash"], { cwd: dir });',
|
|
77
|
+
'git show HEAD:src/a.ts',
|
|
78
|
+
"const env = { TMPDIR: scratch, PATH: [dirname(process.execPath)].join(':') };",
|
|
79
|
+
"git log -1 --format='%h' 53d62fbf3",
|
|
80
|
+
'assert.equal(hash("deadbeef"), "cafebabe");',
|
|
81
|
+
"assert.equal(env.TMPDIR === process.cwd(), false);",
|
|
82
|
+
]) {
|
|
83
|
+
assert.equal(deliveredTestViolation(content), undefined, content);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("evaluateDeliveredTestTool blocks write and edit of a test file, never a source file", () => {
|
|
88
|
+
const bad = 'execSync("git show 3f9c2e1:src/ledger.ts")';
|
|
89
|
+
const write = evaluateDeliveredTestTool("write", { path: "tests/ledger.test.ts", content: bad });
|
|
90
|
+
assert.equal(write?.block, true);
|
|
91
|
+
assert.match(write?.reason ?? "", /tests\/ledger\.test\.ts/);
|
|
92
|
+
assert.match(write?.reason ?? "", /mktemp/);
|
|
93
|
+
assert.equal(evaluateDeliveredTestTool("edit", { path: "tests/ledger.test.ts", edits: [{ oldText: "a", newText: bad }] })?.block, true);
|
|
94
|
+
assert.equal(evaluateDeliveredTestTool("edit", { path: "tests/ledger.test.ts", oldText: "a", newText: bad })?.block, true);
|
|
95
|
+
assert.equal(evaluateDeliveredTestTool("write", { path: "scripts/diff-check.sh", content: bad }), undefined);
|
|
96
|
+
assert.equal(evaluateDeliveredTestTool("read", { path: "tests/ledger.test.ts" }), undefined);
|
|
97
|
+
assert.equal(evaluateDeliveredTestTool("write", { path: "tests/ledger.test.ts", content: "assert.ok(true);" }), undefined);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("absolute paths are judged relative to the session cwd, so a project under a test/ directory is not a test", () => {
|
|
101
|
+
const bad = 'execSync("git show 3f9c2e1:src/ledger.ts")';
|
|
102
|
+
assert.equal(evaluateDeliveredTestTool("write", { path: "/home/u/test/app/src/release.ts", content: bad }, "/home/u/test/app"), undefined);
|
|
103
|
+
assert.equal(evaluateDeliveredTestTool("write", { path: "/home/u/test/app/tests/a.test.ts", content: bad }, "/home/u/test/app")?.block, true);
|
|
104
|
+
assert.equal(evaluateDeliveredTestTool("write", { path: "/elsewhere/tests/a.ts", content: bad }, "/home/u/test/app")?.block, true, "outside the cwd the path is judged whole");
|
|
105
|
+
});
|