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
|
@@ -10,7 +10,9 @@ const read = (path: string) => readFileSync(join(import.meta.dirname, "..", path
|
|
|
10
10
|
const core = read("assets/orchestrator.md");
|
|
11
11
|
const delegation = readDelegationDetail();
|
|
12
12
|
const memory = read("assets/orchestrator-memory.md");
|
|
13
|
-
|
|
13
|
+
// The harness as rendered by extensions/gentle-ai.ts, including the ODD steps
|
|
14
|
+
// it takes from the mirrored provider bundle's routing/pi.md.
|
|
15
|
+
const wrapper = __testing.buildGentlePrompt("gentleman");
|
|
14
16
|
|
|
15
17
|
function containsAll(text: string, clauses: readonly string[]): void {
|
|
16
18
|
for (const clause of clauses) assert.ok(text.includes(clause), `missing contract: ${clause}`);
|
|
@@ -19,10 +21,17 @@ function containsAll(text: string, clauses: readonly string[]): void {
|
|
|
19
21
|
test("organic entry stays read-only without authorization and loads detail before work", () => {
|
|
20
22
|
containsAll(core, [
|
|
21
23
|
"large tasks get ODD tracking and workers only by the Writer rule, else inline",
|
|
22
|
-
"ODD (Default Workflow, harness section above) is mandatory on every request",
|
|
23
24
|
"orchestrator-delegation.md",
|
|
24
25
|
"orchestrator-memory.md",
|
|
25
26
|
]);
|
|
27
|
+
// S11 T3: deduplicated. The core's "ODD (Default Workflow, harness section
|
|
28
|
+
// above) is mandatory on every request" pointer restated the harness; the
|
|
29
|
+
// mandatory workflow and its detail pointers are stated once in the harness.
|
|
30
|
+
containsAll(wrapper, [
|
|
31
|
+
"Default workflow: Organic Driven Development (MANDATORY)",
|
|
32
|
+
"Every request enters it",
|
|
33
|
+
"Detail for steps 3–7: `orchestrator-delegation.md` and `orchestrator-memory.md`.",
|
|
34
|
+
]);
|
|
26
35
|
containsAll(delegation, [
|
|
27
36
|
"Investigation, explanation, review, comparison, and proposal-only requests remain read-only",
|
|
28
37
|
"without a task or storage permission prompt",
|
|
@@ -108,15 +117,45 @@ test("assumption challenge and task checks do not activate or duplicate native r
|
|
|
108
117
|
]);
|
|
109
118
|
});
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
// S14: commit per reviewable slice. The exact canon sentence for ODD step 6,
|
|
121
|
+
// identical in gentle-ai routing.go and this harness.
|
|
122
|
+
const S14_COMMIT_SENTENCE =
|
|
123
|
+
"Commit work units in reviewable slices of about 400 authored changed lines on the feature branch (branch first when on the default branch), with tests and docs alongside the behavior and a Conventional Commit message; a unit whose Risk line names a high-risk item gets its own commit so native review can take it alone, and the last slice closes the feature. Record the commit that holds each task in the feature document as evidence.";
|
|
124
|
+
const PER_TASK_COMMIT_SENTENCE =
|
|
125
|
+
"Every tracked task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence.";
|
|
126
|
+
|
|
127
|
+
test("S14: the rendered parent prompt commits per reviewable slice, not per task", () => {
|
|
128
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
129
|
+
const prompt = __testing.buildGentlePrompt(persona);
|
|
130
|
+
assert.equal(prompt.split(S14_COMMIT_SENTENCE).length - 1, 1, `[${persona}] the parent prompt must carry the S14 sentence once`);
|
|
131
|
+
assert.ok(!prompt.includes(PER_TASK_COMMIT_SENTENCE), `[${persona}] the parent prompt keeps the per-task commit sentence`);
|
|
132
|
+
assert.ok(
|
|
133
|
+
prompt.includes(`${S14_COMMIT_SENTENCE} Work-unit commits on the feature branch are part of authorized large ODD implementation;`),
|
|
134
|
+
`[${persona}] step 6 must keep its authorization sentence right after the S14 sentence`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// S14: commit per reviewable slice. Step 6 commits slices of about 400 authored
|
|
140
|
+
// changed lines (a high-risk unit alone) instead of closing every task with a commit.
|
|
141
|
+
test("ODD commits work units in reviewable slices and reviews the commit or PR slice", () => {
|
|
112
142
|
containsAll(wrapper, [
|
|
113
|
-
"
|
|
114
|
-
"with tests and docs alongside the behavior
|
|
115
|
-
"
|
|
143
|
+
"Commit work units in reviewable slices of about 400 authored changed lines on the feature branch (branch first when on the default branch)",
|
|
144
|
+
"with tests and docs alongside the behavior and a Conventional Commit message",
|
|
145
|
+
"a unit whose Risk line names a high-risk item gets its own commit so native review can take it alone",
|
|
146
|
+
"Record the commit that holds each task in the feature document as evidence",
|
|
116
147
|
"Work-unit commits on the feature branch are part of authorized large ODD implementation; push, pull request creation, and merge remain the user's decisions",
|
|
117
148
|
"The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch",
|
|
118
|
-
"close each tracked task with a work-unit commit",
|
|
119
149
|
]);
|
|
150
|
+
// S11 T3: deduplicated. The harness principle that re-listed the ODD steps
|
|
151
|
+
// ("close each tracked task with a work-unit commit") is gone; step 6 above is
|
|
152
|
+
// the single statement. Core Safety allows exactly those commits and no others.
|
|
153
|
+
assert.ok(!wrapper.includes("close each tracked task with a work-unit commit"), "the ODD steps are re-listed outside step 6");
|
|
154
|
+
assert.ok(
|
|
155
|
+
core.includes("- Never commit unless the user explicitly asks, except the work-unit commits authorized large ODD implementation makes on its feature branch (ODD step 6)."),
|
|
156
|
+
"core Safety must allow only the ODD step 6 work-unit commits without an explicit ask",
|
|
157
|
+
);
|
|
158
|
+
assert.ok(!core.includes("- Never commit unless the user explicitly asks."), "core Safety contradicts ODD step 6 work-unit commits");
|
|
120
159
|
containsAll(delegation, [
|
|
121
160
|
'after each work-unit commit, assess it with that same call and `{"baseRef":"<last reviewed boundary>","committedOnly":true}`',
|
|
122
161
|
"Passive/low: silent structural checks, no reviewer or consent ceremony, and the boundary advances",
|
|
@@ -124,7 +163,8 @@ test("ODD closes each task with a work-unit commit and reviews the commit or PR
|
|
|
124
163
|
"Medium: defer to the PR slice",
|
|
125
164
|
"bounded by the delivery budget of about 400 authored changed lines",
|
|
126
165
|
"The first boundary is the branch point, and every reviewed boundary becomes the next base",
|
|
127
|
-
|
|
166
|
+
// S14: commit per reviewable slice; one commit may hold several tasks, so outcomes are recorded per commit.
|
|
167
|
+
"Record per commit the assessed tier and outcome: granted, declined, passive, deferred to slice, or unavailable",
|
|
128
168
|
"Delivery follows work units",
|
|
129
169
|
"forecast authored changed lines (additions plus deletions, generated files excluded) from the task list",
|
|
130
170
|
"keep a running count from work-unit commits",
|
|
@@ -137,18 +177,28 @@ test("ODD closes each task with a work-unit commit and reviews the commit or PR
|
|
|
137
177
|
"never hardcode their paths",
|
|
138
178
|
"The delivery budget below reads the accumulated branch, not this per-task heuristic",
|
|
139
179
|
]);
|
|
180
|
+
assert.ok(!wrapper.includes("Every tracked task closes with at least one work-unit commit"), "step 6 keeps the per-task commit rule");
|
|
140
181
|
const docs = read("docs/readme-reference.md");
|
|
141
182
|
containsAll(docs, [
|
|
142
|
-
"
|
|
143
|
-
"
|
|
183
|
+
"Commit work units in reviewable slices of about 400 authored changed lines on the feature branch (branch first when on the default branch)",
|
|
184
|
+
"a unit whose Risk line names a high-risk item gets its own commit so native review can take it alone",
|
|
185
|
+
"the feature document records the commit that holds each task as evidence",
|
|
144
186
|
"The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch",
|
|
145
187
|
"**Delivery:**",
|
|
146
|
-
"
|
|
188
|
+
"Commit at a slice boundary; a high-risk unit alone",
|
|
147
189
|
"RDD enabled at work-unit commit boundary",
|
|
148
190
|
"Running authored lines over 400",
|
|
149
191
|
"Apply selected delivery strategy",
|
|
150
192
|
"Single PR; no chain artifacts",
|
|
151
193
|
]);
|
|
194
|
+
assert.ok(!docs.includes("Every task closes with at least one work-unit commit"), "docs keep the per-task commit rule");
|
|
195
|
+
const skill = read("skills/work-unit-commits/SKILL.md");
|
|
196
|
+
containsAll(skill, [
|
|
197
|
+
"Committing ODD work in reviewable slices of about 400 authored changed lines.",
|
|
198
|
+
"ODD commits work units in reviewable slices of about 400 authored changed lines, not one commit per task:",
|
|
199
|
+
"A unit whose Risk line names a high-risk item gets its own commit, so native review takes it alone.",
|
|
200
|
+
]);
|
|
201
|
+
assert.ok(!skill.includes("Every ODD task closes with at least one work-unit commit"), "the skill keeps the per-task commit rule");
|
|
152
202
|
});
|
|
153
203
|
|
|
154
204
|
test("delivery instructions and docs preserve localized ordered outcomes and override chaining", () => {
|
|
@@ -267,13 +317,17 @@ test("ODD defaults to applicable test-first without chat or TUI activation", ()
|
|
|
267
317
|
const verify = read("assets/agents/gentle-ai-verify.md");
|
|
268
318
|
const support = read("assets/support/strict-tdd.md") + read("assets/support/strict-tdd-verify.md");
|
|
269
319
|
const skill = read("skills/gentle-ai/SKILL.md");
|
|
270
|
-
|
|
320
|
+
// S11 T3: deduplicated. The core sits after the harness in the same system
|
|
321
|
+
// prompt; its pointer sentence restated the harness test-first policy, so the
|
|
322
|
+
// rendered parent prompt (harness + core) now carries that policy exactly
|
|
323
|
+
// once, from the harness, instead of the core carrying its own copy.
|
|
324
|
+
const rendered = __testing.buildGentlePrompt("gentleman");
|
|
325
|
+
for (const text of [rendered, delegation, worker, verify, support, skill, wrapper]) {
|
|
271
326
|
containsAll(text, ["applicable", "RED", "GREEN"]);
|
|
272
327
|
assert.doesNotMatch(text, /(?:configured TDD mode|Strict TDD Mode is enabled|explicit user choice|test presence does not enable it|tests existing does not activate it)/i);
|
|
273
328
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
assert.ok(core.includes("the harness's applicable test-first policy"), "core must point at the harness test-first policy");
|
|
329
|
+
assert.equal(rendered.split("use test-first by default").length - 1, 1, "the parent prompt states the test-first policy once");
|
|
330
|
+
assert.ok(!core.includes("GREEN"), "core restates the harness test-first policy");
|
|
277
331
|
for (const [actor, text] of [["delegation", delegation], ["extension", wrapper], ["skill", skill]] as const) {
|
|
278
332
|
assert.match(text, /behavior changes with applicable runnable deterministic tests and a clear expected outcome/i, `${actor} must require applicability, not test presence`);
|
|
279
333
|
assert.match(text, /passive documentation/i, `${actor} must handle passive docs`);
|
|
@@ -287,9 +341,12 @@ test("ODD defaults to applicable test-first without chat or TUI activation", ()
|
|
|
287
341
|
assert.doesNotMatch(core + delegation + worker + support + skill + wrapper, /If tests exist, use strict TDD/i);
|
|
288
342
|
});
|
|
289
343
|
|
|
290
|
-
|
|
344
|
+
// S9 canon change (gentle-ai 87a3b2eb): "These triggers are mandatory, not
|
|
345
|
+
// advisory." read as a default to delegate (L8). A trigger is mandatory only
|
|
346
|
+
// once its condition truly holds; none holds by default, inline when in doubt.
|
|
347
|
+
test("delegation triggers are behavioral in the lazy canonical port and the always-on ODD step", () => {
|
|
291
348
|
for (const clause of [
|
|
292
|
-
"
|
|
349
|
+
"Each trigger holds only when its condition truly holds, and only then is it mandatory, not advisory; no trigger holds by default, and when in doubt work inline.",
|
|
293
350
|
"stop and delegate through the runtime's subagent mechanism before continuing",
|
|
294
351
|
"executing past a fired trigger inline is a routing defect even if the work succeeds",
|
|
295
352
|
"**Mapping trigger",
|
|
@@ -302,12 +359,15 @@ test("mandatory delegation triggers are behavioral in the lazy canonical port an
|
|
|
302
359
|
"so skipped delegation is observable instead of silent",
|
|
303
360
|
"These triggers only choose between direct inline and delegated direct inside ODD",
|
|
304
361
|
]) {
|
|
305
|
-
assert.ok(delegation.includes(clause), `lazy canonical port is missing
|
|
362
|
+
assert.ok(delegation.includes(clause), `lazy canonical port is missing delegation clause: ${clause}`);
|
|
306
363
|
}
|
|
364
|
+
assert.ok(!delegation.includes("These triggers are mandatory, not advisory"), "the lazy port must not read the triggers as mandatory by default");
|
|
307
365
|
assert.ok(
|
|
308
|
-
wrapper.includes("honoring its
|
|
309
|
-
"the always-on ODD step 6 must honor its
|
|
366
|
+
wrapper.includes("honoring its delegation triggers when their conditions truly hold"),
|
|
367
|
+
"the always-on ODD step 6 must honor its delegation triggers only when their conditions truly hold",
|
|
310
368
|
);
|
|
369
|
+
assert.ok(wrapper.includes("No trigger holds by default and, when in doubt, work inline"), "the always-on ODD step 6 must default to inline");
|
|
370
|
+
assert.ok(!wrapper.includes("These triggers are mandatory, not advisory"), "the always-on ODD step 6 must not read the triggers as mandatory by default");
|
|
311
371
|
assert.ok(
|
|
312
372
|
wrapper.includes("executing past a fired trigger inline is a routing defect"),
|
|
313
373
|
"the always-on ODD protocol must state that skipping a fired trigger is a routing defect",
|
|
@@ -363,7 +423,6 @@ test("evidence-budget numbers agree across routing surfaces and retired triggers
|
|
|
363
423
|
"~10k tokens",
|
|
364
424
|
"at most ~2k tokens",
|
|
365
425
|
"`path:line`",
|
|
366
|
-
"~150k tokens",
|
|
367
426
|
"Context backstop",
|
|
368
427
|
]);
|
|
369
428
|
assert.doesNotMatch(text, /4-file rule|20 tool calls|5 exploratory (?:file )?reads/, `${path} keeps a retired trigger`);
|
|
@@ -373,6 +432,20 @@ test("evidence-budget numbers agree across routing surfaces and retired triggers
|
|
|
373
432
|
}
|
|
374
433
|
});
|
|
375
434
|
|
|
435
|
+
// Harness roadmap R1: the backstop is a measured runtime signal (min(absolute cap, fraction x window)), so the
|
|
436
|
+
// prompt modules stop stating a fixed token count and only a unit needing several steps is worth delegating.
|
|
437
|
+
test("context backstop prompt text defers to the runtime signal instead of a fixed token count", () => {
|
|
438
|
+
const reference = read("assets/orchestrator-delegation-reference.md");
|
|
439
|
+
for (const [path, text] of Object.entries({ "assets/orchestrator.md": core, "assets/orchestrator-delegation.md": read("assets/orchestrator-delegation.md"), "assets/orchestrator-delegation-reference.md": reference })) {
|
|
440
|
+
assert.doesNotMatch(text, /~?150k tokens/, `${path} still states a fixed backstop`);
|
|
441
|
+
assert.match(text, /\*\*Context backstop(?::\*\*|\*\*:?)[^\n]*when the runtime signals/, `${path} must defer to the runtime signal`);
|
|
442
|
+
assert.ok(text.includes("several steps"), `${path} must say only a unit needing several steps is delegated`);
|
|
443
|
+
}
|
|
444
|
+
const docs = read("docs/readme-reference.md");
|
|
445
|
+
containsAll(docs, ["GENTLE_PI_CONTEXT_BACKSTOP_TOKENS", "GENTLE_PI_CONTEXT_BACKSTOP_FRACTION", "min(", "`getContextUsage()`", "re-armed"]);
|
|
446
|
+
assert.doesNotMatch(docs, /~150k tokens|150,000-token context backstop/, "docs still describe the fixed backstop");
|
|
447
|
+
});
|
|
448
|
+
|
|
376
449
|
test("ODD protocol is always-on in the rendered system prompt and runs by default", () => {
|
|
377
450
|
const orderedClauses = [
|
|
378
451
|
"Default workflow: Organic Driven Development (MANDATORY)",
|
|
@@ -406,15 +479,27 @@ test("ODD protocol is always-on in the rendered system prompt and runs by defaul
|
|
|
406
479
|
}
|
|
407
480
|
}
|
|
408
481
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
);
|
|
482
|
+
// S11 T3: deduplicated. The harness principle and the core pointer sentence
|
|
483
|
+
// both restated the mandatory workflow; the steps' own header (asserted in
|
|
484
|
+
// order above as "predefined workflow of this orchestrator") is the one statement.
|
|
485
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
486
|
+
const prompt = __testing.buildGentlePrompt(persona);
|
|
487
|
+
assert.equal(prompt.split("is the predefined workflow").length - 1, 1, `[${persona}] the ODD workflow is declared more than once`);
|
|
488
|
+
assert.ok(!prompt.includes("ODD (Default Workflow, harness section above)"), `[${persona}] the core restates the ODD workflow`);
|
|
489
|
+
}
|
|
418
490
|
containsAll(core, ["orchestrator-delegation.md", "orchestrator-memory.md"]);
|
|
419
491
|
assert.doesNotMatch(core + delegation + memory, /SDD|sdd-|OpenSpec|openspec/i);
|
|
420
492
|
});
|
|
493
|
+
|
|
494
|
+
// S14: commit per reviewable slice. Mirrors gentle-ai 06ad2776: a high-risk unit that follows pending medium commits
|
|
495
|
+
// closes the open slice first, so native review takes the high-risk commit alone; outcomes are recorded per commit,
|
|
496
|
+
// since one commit may hold several tasks.
|
|
497
|
+
test("S14: RDD tracking closes the open slice before a high-risk commit and records outcomes per commit", () => {
|
|
498
|
+
const tracking = readFileSync(join(import.meta.dirname, "..", "assets", "orchestrator-tracking.md"), "utf8");
|
|
499
|
+
assert.ok(
|
|
500
|
+
tracking.includes("When a high-risk unit follows pending medium commits still under the slice budget, close the open slice first, assessing and reviewing it as the slice, so the high-risk commit is then reviewed alone against the new boundary."),
|
|
501
|
+
"tracking must close the open slice before a high-risk commit",
|
|
502
|
+
);
|
|
503
|
+
assert.ok(tracking.includes("Record per commit the assessed tier and outcome"), "tracking must record outcomes per commit");
|
|
504
|
+
assert.ok(!tracking.includes("Record per task the assessed tier and outcome"), "tracking must not record outcomes per task");
|
|
505
|
+
});
|
|
@@ -31,12 +31,18 @@ const REPO_ROOT = join(import.meta.dirname, "..");
|
|
|
31
31
|
const REAL_ASSETS_DIR = join(REPO_ROOT, "assets");
|
|
32
32
|
const FIXTURE_PATH = join(import.meta.dirname, "fixtures", "orchestrator.pre-diet.md");
|
|
33
33
|
// gentle-shell#1731 T10 (user decision): 8,192 -> 8,400 B for the narrowed high-risk items 1 and 3.
|
|
34
|
-
|
|
34
|
+
// Verification by named risk (escalate-only rule 3) and the writer-rule fix (triggers hold only when
|
|
35
|
+
// their condition holds; a unit is independent behavior over a size floor), user decision 2026-10-14: 8,400 -> 9,100 B.
|
|
36
|
+
// Weak-model economy (small path reads one batch with no todo; item 1 excludes rebuildable dev/test data), measured in
|
|
37
|
+
// gentle-shell-bench 1321c60 (DeepSeek -24-30% time on x2/x5, -70% on a dev-database rename; Sol unchanged), user decision
|
|
38
|
+
// 2026-10-22: 9,100 -> 9,250 B.
|
|
39
|
+
const BUDGET_BYTES = 9250;
|
|
35
40
|
const MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS = 93;
|
|
36
41
|
|
|
37
42
|
const LAZY_ASSET_NAMES = [
|
|
38
43
|
"orchestrator.md",
|
|
39
44
|
"orchestrator-delegation.md",
|
|
45
|
+
"orchestrator-delegation-reference.md",
|
|
40
46
|
"orchestrator-memory.md",
|
|
41
47
|
"orchestrator-skills.md",
|
|
42
48
|
] as const;
|
|
@@ -123,7 +129,7 @@ function measureOrchestratorPromptBytes(assetsDir: string): number {
|
|
|
123
129
|
// measuring a shorter render again.
|
|
124
130
|
const RDD_WORST_CASE_LINE = "Receipt-driven development: unknown (native status unavailable)";
|
|
125
131
|
|
|
126
|
-
test("getOrchestratorPrompt return value stays within the canonical
|
|
132
|
+
test("getOrchestratorPrompt return value stays within the canonical 9,250 B budget at a short assets root", () => {
|
|
127
133
|
const rendered = __testing.renderOrchestratorPrompt(representativeProductionAssetsDir);
|
|
128
134
|
assert.ok(
|
|
129
135
|
rendered.includes(RDD_WORST_CASE_LINE),
|
|
@@ -303,6 +309,10 @@ const SUPERSEDED_LIFECYCLE_REVIEW_LINES = new Set([
|
|
|
303
309
|
154,
|
|
304
310
|
160,
|
|
305
311
|
166,
|
|
312
|
+
// 280: S11 T3 made the Safety commit line consistent with ODD step 6: it now
|
|
313
|
+
// excepts the work-unit commits of authorized large ODD implementation
|
|
314
|
+
// (asserted verbatim in tests/odd-routing-contract.test.ts).
|
|
315
|
+
280,
|
|
306
316
|
// 282: gentle-shell#1731 T4 relaxed the single-writer Safety line to disjoint
|
|
307
317
|
// Allowed edit surfaces (runtime-enforced) or isolated worktrees.
|
|
308
318
|
282,
|
|
@@ -363,10 +373,12 @@ test("core-alone: load-bearing direct-delegation tokens remain without lazy unio
|
|
|
363
373
|
|
|
364
374
|
test("lazy coordination detail retains subject guidance without inflating the core", () => {
|
|
365
375
|
const core = readRealAsset("orchestrator.md");
|
|
366
|
-
|
|
376
|
+
// S11 T2: the card binds on delegation; the session subject rules are reference material.
|
|
377
|
+
const card = readRealAsset("orchestrator-delegation.md");
|
|
378
|
+
const detail = readRealAsset("orchestrator-delegation-reference.md");
|
|
367
379
|
assert.match(core, /Bind this to the parent Pi session only/);
|
|
368
380
|
assert.match(core, /orchestrator-delegation\.md/);
|
|
369
|
-
assert.match(
|
|
381
|
+
assert.match(card, /on delegation or routing triggers/);
|
|
370
382
|
assert.match(detail, /Once a meaningful task subject is clear, before delegation or cross-session coordination, call `orchestrator_session_id`/);
|
|
371
383
|
assert.match(detail, /short, non-sensitive `subject`/);
|
|
372
384
|
assert.match(detail, /Batch with setup if possible; no extra model call/);
|
|
@@ -384,14 +396,14 @@ test("core-alone: dynamic Gentle AI ownership replaces package lifecycle instruc
|
|
|
384
396
|
});
|
|
385
397
|
|
|
386
398
|
test("lazy delegation detail has no native RDD controller markers", () => {
|
|
387
|
-
const delegation = readRealAsset("orchestrator-delegation.md")
|
|
399
|
+
const delegation = `${readRealAsset("orchestrator-delegation.md")}\n${readRealAsset("orchestrator-delegation-reference.md")}`;
|
|
388
400
|
for (const marker of ["next_transition", "review.capture-result", "reconcile-terminal-mirrors"]) {
|
|
389
401
|
assert.ok(!delegation.includes(marker), `stale RDD marker retained: ${marker}`);
|
|
390
402
|
}
|
|
391
403
|
});
|
|
392
404
|
|
|
393
405
|
test("live orchestrator assets remove the stale strong-gate retry contract", () => {
|
|
394
|
-
const content = `${readRealAsset("orchestrator.md")}\n${readRealAsset("orchestrator-delegation.md")}`;
|
|
406
|
+
const content = `${readRealAsset("orchestrator.md")}\n${readRealAsset("orchestrator-delegation.md")}\n${readRealAsset("orchestrator-delegation-reference.md")}`;
|
|
395
407
|
assert.doesNotMatch(content, /strong gate/i);
|
|
396
408
|
assert.doesNotMatch(content, /extension blocks.*gh pr create/i);
|
|
397
409
|
assert.doesNotMatch(content, /before the user retries the PR command/i);
|
|
@@ -464,14 +476,16 @@ test("every compressed lazy-file pointer in the core still names the material it
|
|
|
464
476
|
// byte budget -- each must keep naming what is over there, not collapse
|
|
465
477
|
// to an opaque "Detail: `file.md`.".
|
|
466
478
|
const namedPointers: ReadonlyArray<{ file: string; mustName: readonly string[] }> = [
|
|
479
|
+
// S11 T2: this reference material moved out of the delegation card.
|
|
467
480
|
{
|
|
468
|
-
file: "orchestrator-delegation.md",
|
|
481
|
+
file: "orchestrator-delegation-reference.md",
|
|
469
482
|
mustName: ["Per-action table", "Work Routing Ladder", "Canonical Workflows"],
|
|
470
483
|
},
|
|
471
484
|
// gentle-shell#1494 per-mechanism modules.
|
|
472
485
|
{ file: "orchestrator-prompts.md", mustName: ["blocking-prompt relays", "provider defects"] },
|
|
473
486
|
{ file: "orchestrator-tracking.md", mustName: ["Track", "feature document"] },
|
|
474
|
-
|
|
487
|
+
// Verification by named risk: rule 3 names the escalated high-risk item.
|
|
488
|
+
{ file: "orchestrator-verification.md", mustName: ["Verification rule", "high-risk item"] },
|
|
475
489
|
{ file: "orchestrator-writer.md", mustName: ["Writer rule", "large task"] },
|
|
476
490
|
{
|
|
477
491
|
file: "orchestrator-memory.md",
|
|
@@ -57,6 +57,35 @@ test("published headers retain the frozen legacy schema-1 contract after metadat
|
|
|
57
57
|
assert.equal(listPresence(profile).entries.length, 1);
|
|
58
58
|
assert.equal(discoverOrchestrators(profile, [peer])[0].freshness, "unknown");
|
|
59
59
|
});
|
|
60
|
+
test("a live peer that predates discovery publication keeps its legacy header label (#2305)", (t) => {
|
|
61
|
+
const profile = fixture(t);
|
|
62
|
+
// A long-lived peer started before discovery publication shipped heartbeats
|
|
63
|
+
// the schema-1 header only and never writes a discovery sidecar.
|
|
64
|
+
const publisher = PresencePublisher.start({ profile, sessionId: "peer", label: "gentle-shell", activity: [] });
|
|
65
|
+
t.after(() => publisher.dispose());
|
|
66
|
+
const [candidate] = discoverOrchestrators(profile, [peer]);
|
|
67
|
+
assert.equal(candidate.freshness, "unknown");
|
|
68
|
+
assert.equal(candidate.legacyLabel, "gentle-shell");
|
|
69
|
+
assert.equal(candidate.workspace, undefined);
|
|
70
|
+
assert.equal(candidate.publication, undefined);
|
|
71
|
+
// An old heartbeat is not a live legacy peer.
|
|
72
|
+
assert.equal(discoverOrchestrators(profile, [peer], Date.now() + 20_000)[0].legacyLabel, undefined);
|
|
73
|
+
});
|
|
74
|
+
test("a present but invalid or foreign discovery sidecar never yields a legacy label", (t) => {
|
|
75
|
+
const profile = fixture(t);
|
|
76
|
+
const publisher = PresencePublisher.start({ profile, sessionId: "peer", label: "gentle-shell", activity: [] });
|
|
77
|
+
t.after(() => publisher.dispose());
|
|
78
|
+
publisher.updateDiscovery(peer, { workspace: "/repo", tasks: [] });
|
|
79
|
+
// Metadata bound to another activation fails closed, without a label.
|
|
80
|
+
const foreign = discoverOrchestrators(profile, [{ ...peer, endpoint: "/socket/replacement" }])[0];
|
|
81
|
+
assert.equal(foreign.freshness, "unknown");
|
|
82
|
+
assert.equal(foreign.legacyLabel, undefined);
|
|
83
|
+
const sidecar = join(profile, "gentle-agents", "presence", `${publisher.target.sessionHash}.${publisher.target.incarnation}.discovery.json`);
|
|
84
|
+
writeFileSync(sidecar, "{broken");
|
|
85
|
+
const broken = discoverOrchestrators(profile, [peer])[0];
|
|
86
|
+
assert.equal(broken.freshness, "unknown");
|
|
87
|
+
assert.equal(broken.legacyLabel, undefined);
|
|
88
|
+
});
|
|
60
89
|
test("joins activation-bound metadata without exporting prompts or thread output", (t) => {
|
|
61
90
|
const profile = fixture(t);
|
|
62
91
|
const publisher = PresencePublisher.start({ profile, sessionId: peer.sessionId, label: "Review auth", activity: [] });
|
|
@@ -2,8 +2,10 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { fork } from "node:child_process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
+
import fsSync from "node:fs";
|
|
6
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
5
7
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
9
|
import test from "node:test";
|
|
8
10
|
import { historyDir, saveTask } from "../lib/agents-history.ts";
|
|
9
11
|
import { TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
@@ -20,7 +22,7 @@ function fixture(t: test.TestContext) {
|
|
|
20
22
|
if (fs.existsSync(root) && !fs.lstatSync(root).isSymbolicLink()) {
|
|
21
23
|
for (const name of fs.readdirSync(root)) {
|
|
22
24
|
const path = join(root, name);
|
|
23
|
-
if (["tasks", "presence"].includes(name) && fs.lstatSync(path).isDirectory()) {
|
|
25
|
+
if (["tasks", "presence", "catalog"].includes(name) && fs.lstatSync(path).isDirectory()) {
|
|
24
26
|
for (const file of fs.readdirSync(path)) fs.unlinkSync(join(path, file));
|
|
25
27
|
fs.rmdirSync(path);
|
|
26
28
|
} else fs.unlinkSync(path);
|
|
@@ -56,6 +58,102 @@ test("heartbeat refreshes a bounded canonical label and stops an invalid source"
|
|
|
56
58
|
assert.deepEqual(listPresence(profile).entries, []);
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
test("warm-cache flush performs no synchronous ancestor validation", (t) => {
|
|
62
|
+
t.mock.timers.enable({ apis: ["Date", "setInterval", "setTimeout"], now: 100_000 });
|
|
63
|
+
const profile = fixture(t);
|
|
64
|
+
const pub = publisher(t, profile);
|
|
65
|
+
const absolute = resolve(profile);
|
|
66
|
+
const ancestors = new Set<string>();
|
|
67
|
+
for (let path = absolute;; path = dirname(path)) {
|
|
68
|
+
ancestors.add(path);
|
|
69
|
+
if (dirname(path) === path) break;
|
|
70
|
+
}
|
|
71
|
+
ancestors.add(join(absolute, "gentle-agents"));
|
|
72
|
+
ancestors.add(join(absolute, "gentle-agents", "presence"));
|
|
73
|
+
const probes: string[] = [];
|
|
74
|
+
const originalLstat = fsSync.lstatSync;
|
|
75
|
+
const spy = t.mock.method(fsSync, "lstatSync", (path: Parameters<typeof fsSync.lstatSync>[0], ...args: unknown[]) => {
|
|
76
|
+
if (typeof path === "string" && ancestors.has(path)) probes.push(path);
|
|
77
|
+
return (originalLstat as (...args: unknown[]) => fs.Stats)(path, ...args);
|
|
78
|
+
});
|
|
79
|
+
syncBuiltinESMExports();
|
|
80
|
+
t.after(() => { spy.mock.restore(); syncBuiltinESMExports(); });
|
|
81
|
+
pub.update(rows("warm cache"));
|
|
82
|
+
t.mock.timers.tick(400);
|
|
83
|
+
assert.deepEqual(probes, [], "writes must consume the validated cache, not re-probe ancestors");
|
|
84
|
+
assert.equal(listPresence(profile).entries[0].generation, 2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a vanished storage tree skips publication without crashing and revalidates asynchronously", async (t) => {
|
|
88
|
+
const profile = fixture(t);
|
|
89
|
+
let heartbeat: (() => void) | undefined;
|
|
90
|
+
const interval = globalThis.setInterval;
|
|
91
|
+
t.mock.method(globalThis, "setInterval", (callback: () => void, ms: number) => {
|
|
92
|
+
if (ms === 5000) heartbeat = callback;
|
|
93
|
+
return interval(callback, ms);
|
|
94
|
+
});
|
|
95
|
+
const pub = publisher(t, profile);
|
|
96
|
+
const root = join(profile, "gentle-agents", "presence");
|
|
97
|
+
for (const name of fs.readdirSync(root)) fs.unlinkSync(join(root, name));
|
|
98
|
+
fs.rmdirSync(root);
|
|
99
|
+
heartbeat!();
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
101
|
+
assert.equal(pub.error, undefined, "a vanished tree skips writes instead of failing the publisher");
|
|
102
|
+
assert.deepEqual(listPresence(profile).entries, []);
|
|
103
|
+
fs.mkdirSync(root, { mode: 0o700 });
|
|
104
|
+
heartbeat!(); // cache is still cold: skip this tick while the heartbeat revalidates
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
106
|
+
heartbeat!(); // warm cache again: publication resumes
|
|
107
|
+
assert.equal(listPresence(profile).entries.length, 1);
|
|
108
|
+
assert.equal(pub.error, undefined);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("heartbeat path validations never overlap", async (t) => {
|
|
112
|
+
const profile = fixture(t);
|
|
113
|
+
let heartbeat: (() => void) | undefined;
|
|
114
|
+
const interval = globalThis.setInterval;
|
|
115
|
+
t.mock.method(globalThis, "setInterval", (callback: () => void, ms: number) => {
|
|
116
|
+
if (ms === 5000) heartbeat = callback;
|
|
117
|
+
return interval(callback, ms);
|
|
118
|
+
});
|
|
119
|
+
const pub = publisher(t, profile);
|
|
120
|
+
let chains = 0;
|
|
121
|
+
let release!: () => void;
|
|
122
|
+
const gate = new Promise<void>((done) => { release = done; });
|
|
123
|
+
const original = fs.promises.lstat;
|
|
124
|
+
const spy = t.mock.method(fs.promises, "lstat", async (path: Parameters<typeof fs.promises.lstat>[0], ...args: unknown[]) => {
|
|
125
|
+
if (String(path) === resolve(profile)) { chains++; await gate; }
|
|
126
|
+
return (original as (...args: unknown[]) => Promise<fs.Stats>).apply(fs.promises, [path, ...args]);
|
|
127
|
+
});
|
|
128
|
+
t.after(() => spy.mock.restore());
|
|
129
|
+
heartbeat!();
|
|
130
|
+
heartbeat!(); // while the first validation waits, this tick must not start another
|
|
131
|
+
release();
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
133
|
+
assert.equal(chains, 1, "overlapping heartbeat validations must be skipped");
|
|
134
|
+
heartbeat!();
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
136
|
+
assert.equal(chains, 2, "the next heartbeat validates again");
|
|
137
|
+
assert.equal(pub.error, undefined);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("first publication lands immediately after start() with no heartbeat wait", (t) => {
|
|
141
|
+
const profile = fixture(t);
|
|
142
|
+
const pub = publisher(t, profile);
|
|
143
|
+
// The first updateDiscovery lands in the same tick: no heartbeat may be
|
|
144
|
+
// required for the first header, discovery, or catalog publication.
|
|
145
|
+
pub.updateDiscovery({ sessionId: "same session", endpoint: "/fixture/first.sock", createdAt: 1 },
|
|
146
|
+
{ workspace: "/repo", tasks: [{ id: "child", label: "Check auth", status: "waiting", cwd: "/repo-child" }] });
|
|
147
|
+
const header = listPresence(profile).entries[0];
|
|
148
|
+
assert.equal(header.label, "Workspace");
|
|
149
|
+
assert.equal(presence.readDiscovery(profile, header)?.workspace, "/repo");
|
|
150
|
+
assert.equal(presence.readDiscovery(profile, header)?.tasks[0].id, "child");
|
|
151
|
+
const catalog = JSON.parse(fs.readFileSync(
|
|
152
|
+
join(profile, "gentle-agents", "catalog", `${header.sessionHash}.${header.incarnation}.json`), "utf8"));
|
|
153
|
+
assert.equal(catalog.schema, 1);
|
|
154
|
+
assert.deepEqual(catalog.catalog.tasks.map((row: { id: string }) => row.id), ["child"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
59
157
|
function rows(text = "full retained output") {
|
|
60
158
|
return [{ task: { id: "t", agent: "worker", label: "summary", status: "running", model: "model",
|
|
61
159
|
createdAt: 1000, startedAt: 1100, endedAt: null, lastActivityAt: 1200,
|
|
@@ -66,11 +66,13 @@ test("rendered parent prompt keeps the RDD boundary while omitting lifecycle mir
|
|
|
66
66
|
// the three renderable forms -- so this IS the worst-case render the core
|
|
67
67
|
// budget below must cover, not a smaller placeholder production later
|
|
68
68
|
// exceeds. gentle-shell#1731 T10 (user decision): budget 8,192 -> 8,400 B.
|
|
69
|
+
// Verification by named risk and the writer-rule fix: 8,400 -> 9,100 B; weak-model economy: 9,100 -> 9,250 B
|
|
70
|
+
// (the canonical budget in orchestrator-budget.test.ts).
|
|
69
71
|
assert.ok(
|
|
70
72
|
rendered.includes("Receipt-driven development: unknown (native status unavailable)"),
|
|
71
73
|
"the default render must include the worst-case RDD status line",
|
|
72
74
|
);
|
|
73
|
-
assert.ok(Buffer.byteLength(rendered, "utf8") <=
|
|
75
|
+
assert.ok(Buffer.byteLength(rendered, "utf8") <= 9250, "the rendered parent prompt must stay within the 9,250 B core budget");
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
test("static prompts retain ODD and delegated-work guidance without SDD", () => {
|
|
@@ -8,6 +8,20 @@ import { OrchestratorScopeCache, validRecordedScope } from "../lib/orchestrator-
|
|
|
8
8
|
import { PresencePublisher, listPresence, readDiscovery } from "../lib/orchestrator-presence.ts";
|
|
9
9
|
import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
|
|
10
10
|
|
|
11
|
+
test("cachedScope serves deferred publications from the snapshot without probing", () => {
|
|
12
|
+
let probes = 0;
|
|
13
|
+
const cache = new OrchestratorScopeCache(() => { probes++; return { root: "/repo", commonDir: "/clone" }; });
|
|
14
|
+
assert.equal(cache.cachedScope(), undefined, "nothing is cached before a projection");
|
|
15
|
+
const snapshot = cache.project("/repo", [], []);
|
|
16
|
+
assert.equal(probes, 1, "the event-time projection resolves eagerly");
|
|
17
|
+
assert.equal(cache.cachedScope(), snapshot);
|
|
18
|
+
assert.equal(cache.cachedScope(), snapshot, "repeated deferred reads never probe");
|
|
19
|
+
assert.equal(probes, 1);
|
|
20
|
+
cache.clear();
|
|
21
|
+
assert.equal(cache.cachedScope(), undefined, "a cleared cache publishes no stale scope");
|
|
22
|
+
assert.equal(probes, 1);
|
|
23
|
+
});
|
|
24
|
+
|
|
11
25
|
test("recorded host/owned child/registered scope correlates clones, isolates ambient Git and caches lifecycle", (t) => {
|
|
12
26
|
const base = realpathSync(mkdtempSync(join(tmpdir(), "orchestrator-scope-")));
|
|
13
27
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -59,6 +73,72 @@ test("recorded host/owned child/registered scope correlates clones, isolates amb
|
|
|
59
73
|
assert.equal(new OrchestratorScopeCache(() => ({ root: "/repo\nother", commonDir: "/clone" })).project(repo, [], []).host.root, null);
|
|
60
74
|
});
|
|
61
75
|
|
|
76
|
+
test("recency reorders preserve repository facts but retain caller order", () => {
|
|
77
|
+
const host = join(tmpdir(), "scope-host"), a = join(tmpdir(), "scope-a"), b = join(tmpdir(), "scope-b");
|
|
78
|
+
let calls = 0, now = 1;
|
|
79
|
+
const cache = new OrchestratorScopeCache(path => {
|
|
80
|
+
calls++;
|
|
81
|
+
if (path === b) throw new Error("unavailable");
|
|
82
|
+
return { root: path, commonDir: host };
|
|
83
|
+
}, () => now++);
|
|
84
|
+
const tasks = [{ id: "a", cwd: a }, { id: "b", cwd: b }];
|
|
85
|
+
const first = cache.project(host, tasks, [a, b]);
|
|
86
|
+
assert.equal(calls, 3);
|
|
87
|
+
const reordered = cache.project(host, [...tasks].reverse(), [b, a]);
|
|
88
|
+
assert.equal(calls, 3, "pure reorders must not probe Git, including cached unknowns");
|
|
89
|
+
assert.deepEqual(reordered.tasks, [...first.tasks].reverse());
|
|
90
|
+
assert.deepEqual(reordered.registered, [...first.registered].reverse());
|
|
91
|
+
assert.deepEqual(reordered.host, first.host);
|
|
92
|
+
assert.equal(reordered.tasks[0].repository.root, null);
|
|
93
|
+
assert.equal(validRecordedScope(reordered), true);
|
|
94
|
+
assert.strictEqual(cache.project(host, [...tasks].reverse(), [b, a]), reordered);
|
|
95
|
+
|
|
96
|
+
const added = cache.project(host, [...tasks, { id: "c", cwd: host }], [a, b]);
|
|
97
|
+
assert.equal(calls, 6, "membership changes refresh even existing facts");
|
|
98
|
+
assert.ok(added.host.resolvedAt > first.host.resolvedAt);
|
|
99
|
+
cache.project(host, tasks, [a, b]);
|
|
100
|
+
assert.equal(calls, 9, "removal refreshes facts");
|
|
101
|
+
cache.project(host, [{ id: "a", cwd: host }, tasks[1]], [a, b]);
|
|
102
|
+
assert.equal(calls, 12, "cwd changes refresh facts");
|
|
103
|
+
cache.project(host, [{ id: "a", cwd: host }, tasks[1]], [a]);
|
|
104
|
+
assert.equal(calls, 15, "registered membership changes refresh facts");
|
|
105
|
+
cache.clear();
|
|
106
|
+
cache.project(host, tasks, [a, b]);
|
|
107
|
+
assert.equal(calls, 18, "session replacement clears facts");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("first-eight visibility evicts hidden facts and retains visible unknowns", () => {
|
|
111
|
+
const host = join(tmpdir(), "scope-visible-host");
|
|
112
|
+
const tasks = Array.from({ length: 9 }, (_, i) => ({ id: `t${i}`, cwd: join(tmpdir(), `scope-visible-task-${i}`) }));
|
|
113
|
+
const calls: string[] = [];
|
|
114
|
+
const cache = new OrchestratorScopeCache(path => {
|
|
115
|
+
calls.push(path);
|
|
116
|
+
if (path === tasks[0].cwd) throw new Error("unavailable");
|
|
117
|
+
return { root: path, commonDir: host };
|
|
118
|
+
});
|
|
119
|
+
const first = cache.project(host, tasks, []);
|
|
120
|
+
assert.deepEqual(calls, [host, ...tasks.slice(0, 8).map(t => t.cwd)]);
|
|
121
|
+
assert.equal(first.tasks[0].repository.root, null);
|
|
122
|
+
const unknown = first.tasks[0].repository;
|
|
123
|
+
const ninthVisible = [tasks[8], ...tasks.slice(0, 8)];
|
|
124
|
+
const expectedCalls = [...calls];
|
|
125
|
+
for (const [order, newlyVisible] of [[ninthVisible, tasks[8]], [tasks, tasks[7]], [ninthVisible, tasks[8]]] as const) {
|
|
126
|
+
const snapshot = cache.project(host, order, []);
|
|
127
|
+
expectedCalls.push(newlyVisible.cwd);
|
|
128
|
+
assert.deepEqual(calls, expectedCalls, "each visibility transition resolves only its evicted/newly visible path");
|
|
129
|
+
assert.deepEqual(snapshot.tasks.map(t => t.id), order.slice(0, 8).map(t => t.id));
|
|
130
|
+
assert.equal(snapshot.tasks.length, 8);
|
|
131
|
+
assert.equal(snapshot.omittedTasks, 1);
|
|
132
|
+
assert.equal(snapshot.complete, false);
|
|
133
|
+
assert.equal(validRecordedScope(snapshot), true);
|
|
134
|
+
assert.strictEqual(snapshot.tasks.find(t => t.id === "t0")!.repository, unknown, "visible unknown facts survive every reorder");
|
|
135
|
+
assert.strictEqual(cache.project(host, order, []), snapshot);
|
|
136
|
+
assert.deepEqual(calls, expectedCalls, "stable snapshots never resolve again");
|
|
137
|
+
}
|
|
138
|
+
assert.equal(calls.filter(path => path === tasks[8].cwd).length, 2, "ninth path is resolved again after eviction");
|
|
139
|
+
assert.equal(calls.filter(path => path === tasks[0].cwd).length, 1, "visible unknown is not retried");
|
|
140
|
+
});
|
|
141
|
+
|
|
62
142
|
test("literal recorded roots never alias Unicode separators to another real repository", (t) => {
|
|
63
143
|
const base = realpathSync(mkdtempSync(join(tmpdir(), "scope-literal-")));
|
|
64
144
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|