gentle-pi 3.7.0 → 4.0.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
7
|
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
7
9
|
// gentle-pi#661/#662: RDD-aware verification rule for delegated work.
|
|
@@ -130,10 +132,11 @@ test("delegation overlay keeps the required headings", () => {
|
|
|
130
132
|
"### Delegation Rules",
|
|
131
133
|
"#### Background Subagent Policy",
|
|
132
134
|
"#### Allowed edit surfaces (MANDATORY)",
|
|
133
|
-
"###
|
|
135
|
+
"### 2. Simple Delegation",
|
|
134
136
|
]) {
|
|
135
137
|
assert.ok(delegation.includes(heading), `delegation overlay lost required heading: ${heading}`);
|
|
136
138
|
}
|
|
139
|
+
assert.doesNotMatch(delegation, /### 3\. SDD \(optional\)/);
|
|
137
140
|
});
|
|
138
141
|
|
|
139
142
|
test("worker asset declares the Verification section after Test discipline", () => {
|
|
@@ -224,3 +227,75 @@ test("worker asset keeps the existing Return contract fields", () => {
|
|
|
224
227
|
assert.ok(worker.includes(field), `worker asset lost Return contract field: ${field}`);
|
|
225
228
|
}
|
|
226
229
|
});
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// gentle-pi#1175: the verification policy is stated on three surfaces -- the
|
|
233
|
+
// `gentle_review` tool contract (description, guidelines, parameters), the
|
|
234
|
+
// delegation asset, and docs/delegated-verification.md -- and they must say
|
|
235
|
+
// the same thing about the behavior ASSESS implements: closure only from the
|
|
236
|
+
// native `candidate.consumed` fact for this candidate, the runtime writer
|
|
237
|
+
// profile with a caller fallback, the conservative small-model bias, the
|
|
238
|
+
// declined/unavailable/unknown fallback, and functional checks that a native
|
|
239
|
+
// review never replaces. Assertions use short stable phrases, normalized for
|
|
240
|
+
// case, quoting, and line wrapping, not whole paragraphs.
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
interface RegisteredPolicyTool {
|
|
244
|
+
name: string;
|
|
245
|
+
description?: string;
|
|
246
|
+
promptSnippet?: string;
|
|
247
|
+
promptGuidelines?: readonly string[];
|
|
248
|
+
parameters?: unknown;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function registeredReviewToolContract(): string {
|
|
252
|
+
const tools = new Map<string, RegisteredPolicyTool>();
|
|
253
|
+
const pi = {
|
|
254
|
+
on() {},
|
|
255
|
+
registerCommand() {},
|
|
256
|
+
registerTool(definition: RegisteredPolicyTool) {
|
|
257
|
+
tools.set(definition.name, definition);
|
|
258
|
+
},
|
|
259
|
+
} as unknown as ExtensionAPI;
|
|
260
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
261
|
+
const tool = tools.get("gentle_review");
|
|
262
|
+
assert.ok(tool, "gentle_review must be registered");
|
|
263
|
+
return [tool.description ?? "", tool.promptSnippet ?? "", ...(tool.promptGuidelines ?? []), JSON.stringify(tool.parameters)].join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizePolicyText(text: string): string {
|
|
267
|
+
return text.replace(/\\"/g, "").replace(/[`"']/g, "").replace(/\s+/g, " ").toLowerCase();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const POLICY_SURFACES: ReadonlyArray<readonly [string, string]> = [
|
|
271
|
+
["gentle_review tool contract", normalizePolicyText(registeredReviewToolContract())],
|
|
272
|
+
["assets/orchestrator-delegation.md", normalizePolicyText(delegation)],
|
|
273
|
+
["docs/delegated-verification.md", normalizePolicyText(read("docs/delegated-verification.md"))],
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const SHARED_POLICY_PHRASES: ReadonlyArray<readonly [string, string]> = [
|
|
277
|
+
["closure only from native consumed evidence", "only from the native candidate.consumed fact for this exact candidate"],
|
|
278
|
+
["caller closure claim is not authority", "a caller-supplied closed is not authority"],
|
|
279
|
+
["runtime writer profile", "runtime-recorded model and effort of the pending mutations"],
|
|
280
|
+
["caller profile is only a fallback", "only a fallback when no runtime evidence exists"],
|
|
281
|
+
["conservative small-model bias", "keeps the conservative small-model bias"],
|
|
282
|
+
["non-closed outcomes fall back", "declined, unavailable, or unknown"],
|
|
283
|
+
["unknown is never closed", "unknown is never treated as closed"],
|
|
284
|
+
["functional checks still run", "a native code review is not a substitute for applicable functional checks"],
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
test("the three verification policy surfaces carry the same ASSESS closure, profile, fallback, and functional-check statements (gentle-pi#1175)", () => {
|
|
288
|
+
for (const [surface, text] of POLICY_SURFACES) {
|
|
289
|
+
for (const [statement, phrase] of SHARED_POLICY_PHRASES) {
|
|
290
|
+
assert.ok(text.includes(phrase), `${surface} is missing the ${statement} statement: "${phrase}"`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("no verification policy surface still tells callers to pass closed explicitly (gentle-pi#1175)", () => {
|
|
296
|
+
for (const [surface, text] of POLICY_SURFACES) {
|
|
297
|
+
for (const stale of ["closed is never derived", "closed is never auto-derived", "closed is never inferred", "pass it explicitly", "pass it only right after"]) {
|
|
298
|
+
assert.ok(!text.includes(stale), `${surface} still carries the retired explicit-closure wording: "${stale}"`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
@@ -176,10 +176,15 @@ test("resolveRddModeStatus resolves to undefined within the deadline when review
|
|
|
176
176
|
const neverSettling = fakeReviewMode(() => new Promise<never>(() => {}));
|
|
177
177
|
const deadlineMs = 150;
|
|
178
178
|
const start = Date.now();
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
const keepalive = setTimeout(() => {}, deadlineMs + 2000);
|
|
180
|
+
try {
|
|
181
|
+
const status = await resolveRddModeStatus(neverSettling, "/repo-hung", AbortSignal.timeout(deadlineMs));
|
|
182
|
+
const elapsed = Date.now() - start;
|
|
183
|
+
assert.equal(status, undefined);
|
|
184
|
+
assert.ok(elapsed < deadlineMs + 1000, `expected the read to resolve near the ${deadlineMs}ms deadline, took ${elapsed}ms`);
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(keepalive);
|
|
187
|
+
}
|
|
183
188
|
});
|
|
184
189
|
|
|
185
190
|
test("resolveRddModeStatus memoizes a resolved status per cwd for RDD_STATUS_MEMO_TTL_MS", async () => {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import resumeHint, { resetResumeHintState } from "../extensions/resume-hint.ts";
|
|
7
|
+
import { RESUME_HANDOFF_ENV, parseResumeHandoff } from "../lib/gentle-shell-resume-hint.ts";
|
|
8
|
+
|
|
9
|
+
const ID = "01a0e0a0-6d7b-7314-89c1-537d47bbf4f3";
|
|
10
|
+
|
|
11
|
+
// A private dir shaped like the launcher's (<tmpdir>/gentle-shell-resume-XXXXXX).
|
|
12
|
+
const handoffDir = () => mkdtempSync(join(tmpdir(), "gentle-shell-resume-"));
|
|
13
|
+
|
|
14
|
+
type Handler = (event: { reason: string }, ctx: unknown) => void;
|
|
15
|
+
|
|
16
|
+
function loadExtension(env: NodeJS.ProcessEnv): Handler[] {
|
|
17
|
+
const shutdown: Handler[] = [];
|
|
18
|
+
const pi = { on: (name: string, handler: Handler) => { if (name === "session_shutdown") shutdown.push(handler); } };
|
|
19
|
+
resumeHint(pi as never, env);
|
|
20
|
+
return shutdown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The session cwd defaults to the launch directory (pi never changes it).
|
|
24
|
+
function fakeContext(dir: string, mode = "tui", cwd = process.cwd()) {
|
|
25
|
+
const sessionFile = join(dir, "session.jsonl");
|
|
26
|
+
writeFileSync(sessionFile, "{}\n");
|
|
27
|
+
return {
|
|
28
|
+
mode,
|
|
29
|
+
sessionManager: {
|
|
30
|
+
getSessionId: () => ID,
|
|
31
|
+
getSessionDir: () => dir,
|
|
32
|
+
getSessionFile: () => sessionFile,
|
|
33
|
+
getCwd: () => cwd,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("extension is inert without the launcher handoff env", () => {
|
|
39
|
+
assert.equal(loadExtension({}).length, 0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("extension claims the env var so child processes cannot inherit it", () => {
|
|
43
|
+
const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: join(tmpdir(), "gentle-shell-resume-unused", "handoff.json") };
|
|
44
|
+
loadExtension(env);
|
|
45
|
+
resetResumeHintState();
|
|
46
|
+
assert.equal(env[RESUME_HANDOFF_ENV], undefined);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("extension keeps the claimed handoff across /reload", () => {
|
|
50
|
+
const dir = handoffDir();
|
|
51
|
+
try {
|
|
52
|
+
const handoffPath = join(dir, "handoff.json");
|
|
53
|
+
const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: handoffPath };
|
|
54
|
+
loadExtension(env);
|
|
55
|
+
// A reload re-runs the factory with the env var already claimed.
|
|
56
|
+
const [onShutdown] = loadExtension(env);
|
|
57
|
+
assert.ok(onShutdown);
|
|
58
|
+
onShutdown({ reason: "quit" }, fakeContext(dir));
|
|
59
|
+
assert.equal(parseResumeHandoff(readFileSync(handoffPath, "utf8"))?.sessionId, ID);
|
|
60
|
+
} finally {
|
|
61
|
+
resetResumeHintState();
|
|
62
|
+
rmSync(dir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("extension writes the handoff only for an interactive quit", () => {
|
|
67
|
+
const dir = handoffDir();
|
|
68
|
+
try {
|
|
69
|
+
const handoffPath = join(dir, "handoff.json");
|
|
70
|
+
const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
|
|
71
|
+
for (const reason of ["reload", "new", "resume", "fork"]) onShutdown({ reason }, fakeContext(dir));
|
|
72
|
+
for (const mode of ["rpc", "print", "json"]) onShutdown({ reason: "quit" }, fakeContext(dir, mode));
|
|
73
|
+
assert.throws(() => readFileSync(handoffPath, "utf8"));
|
|
74
|
+
onShutdown({ reason: "quit" }, fakeContext(dir));
|
|
75
|
+
assert.deepEqual(parseResumeHandoff(readFileSync(handoffPath, "utf8")), { sessionId: ID, sessionDir: dir });
|
|
76
|
+
} finally {
|
|
77
|
+
resetResumeHintState();
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("extension ignores a handoff path that is not the launcher's private file", () => {
|
|
83
|
+
const dir = handoffDir();
|
|
84
|
+
try {
|
|
85
|
+
const target = join(dir, "not-a-handoff.json");
|
|
86
|
+
const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: target };
|
|
87
|
+
assert.equal(loadExtension(env).length, 0);
|
|
88
|
+
assert.equal(env[RESUME_HANDOFF_ENV], undefined);
|
|
89
|
+
} finally {
|
|
90
|
+
resetResumeHintState();
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("extension never overwrites an existing file or follows a planted symlink", { skip: process.platform === "win32" && "creating symlinks needs privileges on Windows" }, () => {
|
|
96
|
+
const dir = handoffDir();
|
|
97
|
+
try {
|
|
98
|
+
const victim = join(dir, "victim.txt");
|
|
99
|
+
writeFileSync(victim, "keep");
|
|
100
|
+
const handoffPath = join(dir, "handoff.json");
|
|
101
|
+
symlinkSync(victim, handoffPath);
|
|
102
|
+
const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
|
|
103
|
+
onShutdown({ reason: "quit" }, fakeContext(dir));
|
|
104
|
+
assert.equal(readFileSync(victim, "utf8"), "keep");
|
|
105
|
+
} finally {
|
|
106
|
+
resetResumeHintState();
|
|
107
|
+
rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("extension hands off the session file for a session from another project", () => {
|
|
112
|
+
const dir = handoffDir();
|
|
113
|
+
try {
|
|
114
|
+
const handoffPath = join(dir, "handoff.json");
|
|
115
|
+
const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
|
|
116
|
+
onShutdown({ reason: "quit" }, fakeContext(dir, "tui", resolve("/home/u/other-project")));
|
|
117
|
+
assert.deepEqual(parseResumeHandoff(readFileSync(handoffPath, "utf8")), { sessionId: ID, sessionFile: join(dir, "session.jsonl") });
|
|
118
|
+
} finally {
|
|
119
|
+
resetResumeHintState();
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import childProcess from "node:child_process";
|
|
3
3
|
import { syncBuiltinESMExports } from "node:module";
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
5
|
-
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
5
|
+
import { mkdir, mkdtemp, rename, writeFile, rm } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import test from "node:test";
|
|
9
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import test, { after, before } from "node:test";
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
11
11
|
import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
12
12
|
import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
@@ -29,8 +29,41 @@ type AnyHandler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
|
29
29
|
type RegisteredTool = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
30
30
|
type SentMessage = { message: Record<string, unknown>; options: Record<string, unknown> };
|
|
31
31
|
|
|
32
|
+
let fixtureCwd: string;
|
|
33
|
+
let fixtureRoot: string | undefined;
|
|
34
|
+
const fixtureEnvironment: NodeJS.ProcessEnv = {};
|
|
35
|
+
const previousEnvironment = new Map<string, string | undefined>();
|
|
36
|
+
before(async () => {
|
|
37
|
+
const root = await mkdtemp(join(tmpdir(), "gentle-pi-preflight-"));
|
|
38
|
+
fixtureRoot = root;
|
|
39
|
+
fixtureCwd = join(root, "project");
|
|
40
|
+
const home = join(root, "home");
|
|
41
|
+
for (const directory of [fixtureCwd, home, join(fixtureCwd, "src"), join(fixtureCwd, "tests")]) await mkdir(directory);
|
|
42
|
+
await writeFile(join(fixtureCwd, "src/example.ts"), "export const value = 1;");
|
|
43
|
+
childProcess.execFileSync("git", ["init", "--quiet", fixtureCwd]);
|
|
44
|
+
fixtureCwd = realpathSync(fixtureCwd);
|
|
45
|
+
Object.assign(fixtureEnvironment, {
|
|
46
|
+
HOME: home, USERPROFILE: home,
|
|
47
|
+
GENTLE_PI_CONFIG_HOME: join(home, "config"),
|
|
48
|
+
GENTLE_PI_AGENT_HOME: join(home, "agents"),
|
|
49
|
+
PI_CODING_AGENT_DIR: join(home, "pi"),
|
|
50
|
+
XDG_CONFIG_HOME: join(home, "xdg"),
|
|
51
|
+
});
|
|
52
|
+
for (const [key, value] of Object.entries(fixtureEnvironment)) {
|
|
53
|
+
previousEnvironment.set(key, process.env[key]);
|
|
54
|
+
process.env[key] = value;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
after(async () => {
|
|
58
|
+
for (const [key, value] of previousEnvironment) {
|
|
59
|
+
if (value === undefined) delete process.env[key];
|
|
60
|
+
else process.env[key] = value;
|
|
61
|
+
}
|
|
62
|
+
if (fixtureRoot !== undefined) await rm(fixtureRoot, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
32
65
|
type CustomEntry = { type: string; customType: string; data: unknown };
|
|
33
|
-
function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[] = []): {
|
|
66
|
+
function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[] = [], processEnv?: NodeJS.ProcessEnv): {
|
|
34
67
|
handlers: Map<string, AnyHandler>;
|
|
35
68
|
sent: SentMessage[];
|
|
36
69
|
tools: Map<string, RegisteredTool>;
|
|
@@ -53,11 +86,16 @@ function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[]
|
|
|
53
86
|
sent.push({ message, options });
|
|
54
87
|
},
|
|
55
88
|
} as unknown as ExtensionAPI;
|
|
56
|
-
createGentleAiExtension({
|
|
89
|
+
createGentleAiExtension({
|
|
90
|
+
nativeReviewCli,
|
|
91
|
+
processEnv: { ...fixtureEnvironment, GENTLE_PI_AGENTS_CHILD: "0", ...processEnv, GENTLE_AI_TELEMETRY: "0" },
|
|
92
|
+
resolveTelemetryTriggerBinary: () => join(fixtureCwd, "never-executed"),
|
|
93
|
+
telemetryTriggerSpawn: () => assert.fail("Preflight fixtures must not spawn telemetry"),
|
|
94
|
+
})(pi);
|
|
57
95
|
return { handlers, sent, tools };
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
function ctx(sessionId: string, hasUI = true, cwd =
|
|
98
|
+
function ctx(sessionId: string, hasUI = true, cwd = fixtureCwd): ExtensionContext {
|
|
61
99
|
return {
|
|
62
100
|
cwd,
|
|
63
101
|
hasUI,
|
|
@@ -66,7 +104,17 @@ function ctx(sessionId: string, hasUI = true, cwd = process.cwd()): ExtensionCon
|
|
|
66
104
|
} as unknown as ExtensionContext;
|
|
67
105
|
}
|
|
68
106
|
|
|
69
|
-
|
|
107
|
+
function toolContext(context: ExtensionContext): ExtensionToolContext {
|
|
108
|
+
return {
|
|
109
|
+
...context,
|
|
110
|
+
tools: [],
|
|
111
|
+
async executeTool() {
|
|
112
|
+
assert.fail("Review preflight must not execute nested tools");
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, initializeGit = true): Promise<T> {
|
|
70
118
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
71
119
|
const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
72
120
|
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-agent-home-"));
|
|
@@ -76,7 +124,7 @@ async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>): Pr
|
|
|
76
124
|
process.env.GENTLE_PI_CONFIG_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-config-home-"));
|
|
77
125
|
try {
|
|
78
126
|
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-cwd-"));
|
|
79
|
-
childProcess.execFileSync("git", ["init", "--quiet", cwd]);
|
|
127
|
+
if (initializeGit) childProcess.execFileSync("git", ["init", "--quiet", cwd]);
|
|
80
128
|
await mkdir(join(cwd, "src"));
|
|
81
129
|
await writeFile(join(cwd, "src/example.ts"), "export const value = 1;");
|
|
82
130
|
return await callback(cwd);
|
|
@@ -127,7 +175,7 @@ const agentEndEvent = { type: "agent_end", messages: [] };
|
|
|
127
175
|
let mutationCall = 0;
|
|
128
176
|
async function directWrite(handlers: Map<string, AnyHandler>, session: ExtensionContext): Promise<void> {
|
|
129
177
|
await handlers.get("tool_result")!({ type: "tool_result", toolName: "write", toolCallId: `own-${++mutationCall}`,
|
|
130
|
-
input: { path:
|
|
178
|
+
input: { path: "src/example.ts" },
|
|
131
179
|
content: [], isError: false }, session);
|
|
132
180
|
}
|
|
133
181
|
|
|
@@ -138,7 +186,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
|
|
|
138
186
|
test(`agent_end after approved acknowledgement: ${scenario}`, async (t) => {
|
|
139
187
|
const changedTarget = scenario === "changed";
|
|
140
188
|
const unsuccessful = scenario === "failed" || scenario === "unknown";
|
|
141
|
-
const cwd =
|
|
189
|
+
const cwd = fixtureCwd;
|
|
142
190
|
const siblingRoot = realpathSync(tmpdir());
|
|
143
191
|
if (scenario === "sibling-root") {
|
|
144
192
|
// Model a sibling worktree sharing the real common directory without
|
|
@@ -210,7 +258,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
|
|
|
210
258
|
const review = tools.get("gentle_review");
|
|
211
259
|
assert.ok(review);
|
|
212
260
|
await directWrite(handlers, session);
|
|
213
|
-
const result = await review.execute("post-ack", { operation: "acknowledge-approved", lineageId }, undefined, undefined, session);
|
|
261
|
+
const result = await review.execute("post-ack", { operation: "acknowledge-approved", lineageId }, undefined, undefined, toolContext(session));
|
|
214
262
|
if (unsuccessful) {
|
|
215
263
|
assert.equal(result.details.outcome, scenario === "failed" ? "native-operation-failed" : "native-mutation-status-reconciled");
|
|
216
264
|
assert.equal(result.details.mutation_outcome, scenario === "failed" ? "none" : "unknown");
|
|
@@ -242,6 +290,22 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
|
|
|
242
290
|
});
|
|
243
291
|
}
|
|
244
292
|
|
|
293
|
+
test("passive session events outside Git with RDD off never invoke native STATUS or initialize Git", async () => {
|
|
294
|
+
await withSessionStartEnv(async (cwd) => {
|
|
295
|
+
const calls: string[] = [];
|
|
296
|
+
const native = {
|
|
297
|
+
reviewMode: async () => { calls.push("reviewMode"); return onMode("off")!({} as never); },
|
|
298
|
+
targetStatus: async () => { calls.push("targetStatus"); return executeStartStatus("outside-git"); },
|
|
299
|
+
} as unknown as NativeReviewCli;
|
|
300
|
+
const { handlers } = harness(native);
|
|
301
|
+
const session = ctx("outside-git", true, cwd);
|
|
302
|
+
await handlers.get("session_start")!({ type: "session_start" }, session);
|
|
303
|
+
await handlers.get("agent_end")!(agentEndEvent, session);
|
|
304
|
+
assert.deepEqual(calls, [], "passive non-Git events need no native entry");
|
|
305
|
+
assert.equal(existsSync(join(cwd, ".git")), false, "passive events must not initialize Git");
|
|
306
|
+
}, false);
|
|
307
|
+
});
|
|
308
|
+
|
|
245
309
|
test("agent_end performs no STATUS call and sends nothing when RDD is off", async () => {
|
|
246
310
|
const statusRequests: unknown[] = [];
|
|
247
311
|
const native = {
|
|
@@ -383,6 +447,25 @@ test("gentle-ai-worker agent_end never queries native review", async () => {
|
|
|
383
447
|
assert.deepEqual(sent, []);
|
|
384
448
|
});
|
|
385
449
|
|
|
450
|
+
test("agent_end sends nothing and performs no STATUS call when GENTLE_PI_AGENTS_CHILD is 1", async () => {
|
|
451
|
+
const statusRequests: unknown[] = [];
|
|
452
|
+
const native = {
|
|
453
|
+
reviewMode: onMode("on"),
|
|
454
|
+
targetStatus: async (request: unknown) => {
|
|
455
|
+
statusRequests.push(request);
|
|
456
|
+
throw new Error("targetStatus must not be called in a child session");
|
|
457
|
+
},
|
|
458
|
+
} as unknown as NativeReviewCli;
|
|
459
|
+
const { handlers, sent } = harness(native, [], { GENTLE_PI_AGENTS_CHILD: "1" });
|
|
460
|
+
const agentEnd = handlers.get("agent_end");
|
|
461
|
+
assert.equal(typeof agentEnd, "function");
|
|
462
|
+
const session = ctx("agent-end-child");
|
|
463
|
+
await directWrite(handlers, session);
|
|
464
|
+
await agentEnd!(agentEndEvent, session);
|
|
465
|
+
assert.deepEqual(statusRequests, []);
|
|
466
|
+
assert.deepEqual(sent, []);
|
|
467
|
+
});
|
|
468
|
+
|
|
386
469
|
test("agent_end resets the subagent depth when a fresh primary loop starts", async () => {
|
|
387
470
|
const targetIdentity = `sha256:${"1".repeat(64)}`;
|
|
388
471
|
const native = {
|
|
@@ -520,6 +603,87 @@ for (const toolName of ["read", "bash", "subagent_run", "edit", "write"]) {
|
|
|
520
603
|
});
|
|
521
604
|
}
|
|
522
605
|
|
|
606
|
+
for (const mode of ["on", "off", "unknown", "missing", "malformed", "incompatible", "throws"] as const) {
|
|
607
|
+
test(`non-Git successful source write, never startup, prepares only with validated RDD on: ${mode}`, async () => {
|
|
608
|
+
await withSessionStartEnv(async (cwd) => {
|
|
609
|
+
const requests: unknown[] = [];
|
|
610
|
+
const native = {
|
|
611
|
+
reviewMode: mode === "missing" ? undefined : async () => {
|
|
612
|
+
if (mode === "throws") throw new Error("unavailable");
|
|
613
|
+
if (mode === "incompatible") throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, "review/mode", false, false, "incompatible");
|
|
614
|
+
if (mode === "malformed") return { status: { effective: "on", source: "unrecognized" } };
|
|
615
|
+
return { operation: "status", scope: "clone", status: { global: "", cloneLocal: "", effective: mode, source: "global" } };
|
|
616
|
+
},
|
|
617
|
+
targetStatus: async (request: unknown) => {
|
|
618
|
+
requests.push(request);
|
|
619
|
+
// The fake native owns bootstrap, not the extension.
|
|
620
|
+
childProcess.execFileSync("git", ["init", "--quiet", cwd]);
|
|
621
|
+
return executeStartStatus(`sha256:${"8".repeat(64)}`);
|
|
622
|
+
},
|
|
623
|
+
} as unknown as NativeReviewCli;
|
|
624
|
+
const { handlers, sent } = harness(native);
|
|
625
|
+
const session = ctx(`non-git-${mode}`, true, cwd);
|
|
626
|
+
await handlers.get("session_start")!({}, session);
|
|
627
|
+
assert.equal(requests.length, 0, "passive startup cannot bootstrap");
|
|
628
|
+
assert.equal(existsSync(join(cwd, ".git")), false);
|
|
629
|
+
await handlers.get("agent_end")!(agentEndEvent, session);
|
|
630
|
+
assert.equal(requests.length, 0, "conversation cannot bootstrap");
|
|
631
|
+
await directWrite(handlers, session);
|
|
632
|
+
assert.equal(requests.length, mode === "on" ? 1 : 0);
|
|
633
|
+
assert.equal(existsSync(join(cwd, ".git")), mode === "on");
|
|
634
|
+
assert.deepEqual(sent, [], "preparation alone does not remind");
|
|
635
|
+
}, false);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
for (const path of ["odd/tasks/feature.md", "README.md", ".pi/settings.json", "memory.md", "secrets/app.ts", "../foreign.ts"]) {
|
|
640
|
+
test(`bookkeeping or unsafe write cannot prepare: ${path}`, async () => {
|
|
641
|
+
await withSessionStartEnv(async (cwd) => {
|
|
642
|
+
let calls = 0;
|
|
643
|
+
const h = harness({ reviewMode: onMode("on"), targetStatus: async () => { calls++; return stopStatus("unused"); } } as unknown as NativeReviewCli);
|
|
644
|
+
const session = ctx("non-source", false, cwd);
|
|
645
|
+
await h.handlers.get("session_start")!({}, session);
|
|
646
|
+
await h.handlers.get("tool_result")!({ toolName: "write", toolCallId: "bookkeeping", input: { path }, isError: false }, session);
|
|
647
|
+
assert.equal(calls, 0);
|
|
648
|
+
assert.equal(existsSync(join(cwd, ".git")), false);
|
|
649
|
+
}, false);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
for (const initiallyGit of [true, false]) {
|
|
654
|
+
test(`source preparation never reinitializes established metadata after loss: initially Git=${initiallyGit}`, async () => {
|
|
655
|
+
await withSessionStartEnv(async cwd => {
|
|
656
|
+
let calls = 0;
|
|
657
|
+
const h = harness({ reviewMode: onMode("on"), targetStatus: async () => {
|
|
658
|
+
calls++; childProcess.execFileSync("git", ["init", "--quiet", cwd]); return stopStatus("prepared");
|
|
659
|
+
} } as unknown as NativeReviewCli);
|
|
660
|
+
const session = ctx("established-source", false, cwd);
|
|
661
|
+
await h.handlers.get("session_start")!({}, session);
|
|
662
|
+
if (!initiallyGit) { await directWrite(h.handlers, session); assert.equal(calls, 1); }
|
|
663
|
+
await rename(join(cwd, ".git"), join(cwd, "saved-git"));
|
|
664
|
+
calls = 0;
|
|
665
|
+
await directWrite(h.handlers, session);
|
|
666
|
+
assert.equal(calls, 0, "no native bootstrap STATUS after previously established Git disappears");
|
|
667
|
+
assert.equal(existsSync(join(cwd, ".git")), false, "lost metadata remains lost, not reinitialized");
|
|
668
|
+
}, initiallyGit);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
test("headless successful source write prepares after binding without a reminder", async () => {
|
|
673
|
+
await withSessionStartEnv(async cwd => {
|
|
674
|
+
let calls = 0;
|
|
675
|
+
const h = harness({ reviewMode: onMode("on"), targetStatus: async () => {
|
|
676
|
+
calls++; childProcess.execFileSync("git", ["init", "--quiet", cwd]); return stopStatus("prepared");
|
|
677
|
+
} } as unknown as NativeReviewCli);
|
|
678
|
+
const session = ctx("headless-source", false, cwd);
|
|
679
|
+
await h.handlers.get("session_start")!({}, session);
|
|
680
|
+
await directWrite(h.handlers, session);
|
|
681
|
+
assert.equal(calls, 1);
|
|
682
|
+
assert.equal(existsSync(join(cwd, ".git")), true);
|
|
683
|
+
assert.deepEqual(h.sent, []);
|
|
684
|
+
}, false);
|
|
685
|
+
});
|
|
686
|
+
|
|
523
687
|
test("session_start negotiates the current target identity when RDD is on", async () => {
|
|
524
688
|
const targetIdentity = `sha256:${"2".repeat(64)}`;
|
|
525
689
|
const statusRequests: Array<{ agent?: string }> = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { withWindowsSystemProbeRetry } from "../lib/review-candidate-view-owner.ts";
|
|
3
|
+
import { resolveWindowsSystemProbeTimeoutMs, withWindowsSystemProbeRetry } from "../lib/review-candidate-view-owner.ts";
|
|
4
4
|
|
|
5
5
|
function failingProbe(code: string, attempts: { count: number }): () => string {
|
|
6
6
|
return () => {
|
|
@@ -61,3 +61,24 @@ test("windows system probe does not retry when the first attempt succeeds", () =
|
|
|
61
61
|
assert.equal(value, "S-1-5-18");
|
|
62
62
|
assert.equal(attempts.count, 1);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test("windows system probes default to a cold-runner-safe bounded timeout", () => {
|
|
66
|
+
assert.equal(resolveWindowsSystemProbeTimeoutMs({}), 15_000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("windows system probe timeout honors a bounded environment override", () => {
|
|
70
|
+
const env = (value: string) => ({ GENTLE_PI_CANDIDATE_WINDOWS_PROBE_TIMEOUT_MS: value });
|
|
71
|
+
assert.equal(resolveWindowsSystemProbeTimeoutMs(env("30000")), 30_000);
|
|
72
|
+
assert.equal(resolveWindowsSystemProbeTimeoutMs(env("120000")), 120_000);
|
|
73
|
+
for (const invalid of ["0", "-1", "abc", "1.5", "120001", "", "015000"]) {
|
|
74
|
+
assert.equal(resolveWindowsSystemProbeTimeoutMs(env(invalid)), 15_000, invalid);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("every windows system probe uses the configurable timeout", async () => {
|
|
79
|
+
const { readFileSync } = await import("node:fs");
|
|
80
|
+
const source = readFileSync(new URL("../lib/review-candidate-view-owner.ts", import.meta.url), "utf8");
|
|
81
|
+
const probes = source.match(/windowsSystemExecutable\([^)]*\), \[[^\]]*\], \{[^}]*\}/g) ?? [];
|
|
82
|
+
assert.equal(probes.length, 5);
|
|
83
|
+
for (const probe of probes) assert.match(probe, /timeout: resolveWindowsSystemProbeTimeoutMs\(\)/);
|
|
84
|
+
});
|