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
|
@@ -9,10 +9,6 @@ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
9
9
|
const AGENTS = join(ROOT, "assets", "agents");
|
|
10
10
|
const ASSETS = join(ROOT, "assets");
|
|
11
11
|
|
|
12
|
-
// SDD phase executors installed to ~/.pi/agent/agents/ by installSddAssets.
|
|
13
|
-
// Each carries its own effective prompt; none inherits the parent workflow.
|
|
14
|
-
const SDD_PREFIX = "sdd-";
|
|
15
|
-
|
|
16
12
|
// Strict-output agents that must remain untouched by Key Learnings: the
|
|
17
13
|
// Judgment Day roles (`jd-*`) and the review lens roles (`review-*`). Derived
|
|
18
14
|
// from the agents actually on disk rather than hardcoded, so retiring one
|
|
@@ -52,53 +48,10 @@ function agentFiles(): string[] {
|
|
|
52
48
|
return readdirSync(AGENTS).filter((f) => f.endsWith(".md")).sort();
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
function sddAgents(): string[] {
|
|
56
|
-
return agentFiles().filter((f) => f.startsWith(SDD_PREFIX));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
51
|
function strictJsonAgents(): string[] {
|
|
60
52
|
return agentFiles().filter((f) => STRICT_JSON_PREFIX.test(f));
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
test("every SDD phase executor carries an effective `## Key Learnings Closing` section with full semantics", () => {
|
|
64
|
-
const agents = sddAgents();
|
|
65
|
-
assert.ok(agents.length >= 12, `expected >=12 SDD agents, found ${agents.length}`);
|
|
66
|
-
const missing: string[] = [];
|
|
67
|
-
const failed: string[] = [];
|
|
68
|
-
for (const file of agents) {
|
|
69
|
-
const source = readFileSync(join(AGENTS, file), "utf8");
|
|
70
|
-
const section = readSection(source, "Key Learnings Closing");
|
|
71
|
-
if (section === null) { missing.push(file); continue; }
|
|
72
|
-
for (const [label, regex] of KL_SEMANTICS) {
|
|
73
|
-
if (!regex.test(section)) failed.push(`${file}: ${label}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
assert.deepEqual(missing, [], "every SDD agent must carry a `## Key Learnings Closing` section");
|
|
77
|
-
assert.deepEqual(failed, [], "every section must encode all canonical semantics");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("no SDD phase executor infers Key Learnings through `standard phase envelope` alone", () => {
|
|
81
|
-
for (const file of sddAgents()) {
|
|
82
|
-
const source = readFileSync(join(AGENTS, file), "utf8");
|
|
83
|
-
const section = readSection(source, "Key Learnings Closing");
|
|
84
|
-
assert.ok(section, `${file} must have a direct Key Learnings Closing section`);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("SDD executor coverage is exhaustive against actual agent files", () => {
|
|
89
|
-
const actual = sddAgents();
|
|
90
|
-
// Allowlist: the 13 known phase executors. A new sdd-*.md without a
|
|
91
|
-
// Key Learnings Closing section fails the first test; this test proves
|
|
92
|
-
// the allowlist matches reality so coverage cannot silently drift.
|
|
93
|
-
const expected = [
|
|
94
|
-
"sdd-apply.md", "sdd-archive.md", "sdd-design.md", "sdd-explore.md",
|
|
95
|
-
"sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-remediate.md", "sdd-research.md",
|
|
96
|
-
"sdd-spec.md", "sdd-status.md", "sdd-tasks.md",
|
|
97
|
-
"sdd-verify.md",
|
|
98
|
-
];
|
|
99
|
-
assert.deepEqual(actual, expected, "SDD agent set must match the known allowlist");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
55
|
test("generic delegation contract instructs the same `## Key Learnings` closing block", () => {
|
|
103
56
|
const delegation = readFileSync(join(ASSETS, "orchestrator-delegation.md"), "utf8");
|
|
104
57
|
const section = readSection(delegation, "Key Learnings closing block");
|
|
@@ -111,14 +64,6 @@ test("generic delegation contract instructs the same `## Key Learnings` closing
|
|
|
111
64
|
assert.match(section, /layers on after/, "must state the block layers on after the envelope");
|
|
112
65
|
});
|
|
113
66
|
|
|
114
|
-
test("sdd-orchestrator-workflow documents routing, not executor authority", () => {
|
|
115
|
-
const workflow = readFileSync(join(ASSETS, "sdd-orchestrator-workflow.md"), "utf8");
|
|
116
|
-
const section = readSection(workflow, "Key Learnings closing block (routing)");
|
|
117
|
-
assert.ok(section, "workflow must document Key Learnings routing");
|
|
118
|
-
assert.match(section, /installed SDD phase executor agent.*carries the effective.*contract/i);
|
|
119
|
-
assert.match(section, /documents routing only and is not the executor authority/);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
67
|
test("provider ownership: no Pi TypeScript runtime parses Key Learnings or invokes passive-capture tools", () => {
|
|
123
68
|
const roots = ["lib", "extensions", "scripts", "runtime"];
|
|
124
69
|
const forbidden: Array<[string, RegExp]> = [
|
|
@@ -214,27 +159,6 @@ test("strict review and Judgment Day agents do not gain Key Learnings or trailin
|
|
|
214
159
|
});
|
|
215
160
|
|
|
216
161
|
test("the canonical Key Learnings heading has no trailing colon in any asset", () => {
|
|
217
|
-
for (const file of sddAgents()) {
|
|
218
|
-
const source = readFileSync(join(AGENTS, file), "utf8");
|
|
219
|
-
assert.match(source, /`## Key Learnings`/, `${file} must reference the canonical heading`);
|
|
220
|
-
assert.doesNotMatch(source, /`## Key Learnings:`/, `${file} must not use a trailing colon`);
|
|
221
|
-
}
|
|
222
162
|
const delegation = readFileSync(join(ASSETS, "orchestrator-delegation.md"), "utf8");
|
|
223
163
|
assert.doesNotMatch(delegation, /`## Key Learnings:`/);
|
|
224
164
|
});
|
|
225
|
-
|
|
226
|
-
test("modified SDD agents are packaged and installed by the existing installer", () => {
|
|
227
|
-
const verifier = readFileSync(join(ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
228
|
-
const expected = [
|
|
229
|
-
"assets/agents/sdd-apply.md", "assets/agents/sdd-archive.md",
|
|
230
|
-
"assets/agents/sdd-design.md", "assets/agents/sdd-explore.md",
|
|
231
|
-
"assets/agents/sdd-init.md", "assets/agents/sdd-onboard.md",
|
|
232
|
-
"assets/agents/sdd-proposal.md", "assets/agents/sdd-research.md",
|
|
233
|
-
"assets/agents/sdd-spec.md", "assets/agents/sdd-status.md",
|
|
234
|
-
"assets/agents/sdd-tasks.md",
|
|
235
|
-
"assets/agents/sdd-verify.md",
|
|
236
|
-
];
|
|
237
|
-
for (const path of expected) {
|
|
238
|
-
assert.match(verifier, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), `${path} must be in the package verifier`);
|
|
239
|
-
}
|
|
240
|
-
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
|
|
4
|
+
|
|
5
|
+
const destructive = [
|
|
6
|
+
`psql -c 'DROP TABLE users'`,
|
|
7
|
+
`mysql --execute='TRUNCATE TABLE users'`,
|
|
8
|
+
`sqlite3 app.db 'DELETE FROM users;'`,
|
|
9
|
+
`psql -c 'DELETE FROM users WHERE 1=1'`,
|
|
10
|
+
`echo 'DROP DATABASE app' | psql`,
|
|
11
|
+
`printf '%s' 'TRUNCATE users' | mysql`,
|
|
12
|
+
`env MODE=test sudo -u postgres /usr/bin/psql -c 'DROP SCHEMA public CASCADE'`,
|
|
13
|
+
`sh -c "echo ready; psql -c 'DROP TABLE users'"`,
|
|
14
|
+
`echo ready && rm -fr -- ./data`,
|
|
15
|
+
`find ./data -type f -delete`,
|
|
16
|
+
`find ./data -type f -exec rm -rf {} +`,
|
|
17
|
+
`find ./data -print0 | xargs -0 rm -rf`,
|
|
18
|
+
`psql <<'SQL'\nDROP TABLE users;\nSQL`,
|
|
19
|
+
`sqlite3 app.db 'SELECT 1; /* maintenance */ DROP TABLE users'`,
|
|
20
|
+
`timeout 30 bash -lc 'rm -r ./data'`,
|
|
21
|
+
`echo ready || (rm -r ./data)`,
|
|
22
|
+
`rm -r /var/app/data`,
|
|
23
|
+
`git -C /repo reset --hard HEAD`,
|
|
24
|
+
`git clean -fd`,
|
|
25
|
+
`git push --force-with-lease origin main`,
|
|
26
|
+
`git branch -D old`,
|
|
27
|
+
`git restore .`,
|
|
28
|
+
String.raw`sh -c "psql -c \"DROP TABLE users\""`,
|
|
29
|
+
String.raw`bash -lc "rm -r \"./data\""`,
|
|
30
|
+
`psql -c 'DELETE FROM "where"'`,
|
|
31
|
+
`mysql -e 'DELETE FROM \`where\`'`,
|
|
32
|
+
`sqlite3 app.db 'DELETE FROM [where]'`,
|
|
33
|
+
`psql -c 'DROP TABLE "where"'`,
|
|
34
|
+
String.raw`find cache -exec echo {} \; -exec rm -r {} \;`,
|
|
35
|
+
String.raw`find cache -execdir echo {} \; -execdir rm -r {} \;`,
|
|
36
|
+
`sudo -n rm -r ./data`,
|
|
37
|
+
`command -p psql -c 'DROP TABLE users'`,
|
|
38
|
+
`env -u MODE sudo -u postgres psql -c 'DROP TABLE users'`,
|
|
39
|
+
];
|
|
40
|
+
for (const command of destructive) {
|
|
41
|
+
test(`recognizes executable destruction: ${command}`, () => {
|
|
42
|
+
const matches = recognizeDestructiveCommands(command);
|
|
43
|
+
assert.ok(matches.length > 0);
|
|
44
|
+
assert.ok(matches.every((match) => match.triggerIndex >= 0 && match.triggerIndex < command.length));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const command of [
|
|
48
|
+
`pnpm test && pnpm run build`, `git status`, `git push origin main`,
|
|
49
|
+
`psql -c 'SELECT * FROM users'`, `sqlite3 app.db 'DELETE FROM users WHERE id=7'`,
|
|
50
|
+
`psql -c "SELECT 'DROP TABLE users'"`,
|
|
51
|
+
`echo 'rm -rf ./data; DROP TABLE users'`, `printf '%s' 'TRUNCATE TABLE users'`,
|
|
52
|
+
`rg 'DROP TABLE' docs`, `rm ./one-file`, `find . -name '*.ts'`,
|
|
53
|
+
`psql <<'SQL'\nSELECT 'DROP TABLE users';\nSQL`,
|
|
54
|
+
`git log --oneline`, `env X=1 echo 'rm -r ./data'`,
|
|
55
|
+
`find . -exec echo 'DROP TABLE users' {} +`,
|
|
56
|
+
`git push origin main && printf '%s' '-f'`,
|
|
57
|
+
`echo ';' rm -r cache`,
|
|
58
|
+
String.raw`echo \; rm -r cache`,
|
|
59
|
+
`printf '%s ' ';' rm -r ./data`,
|
|
60
|
+
`echo '|' rm -r cache`,
|
|
61
|
+
`echo '&&' rm -r cache`,
|
|
62
|
+
`echo ')' rm -r cache`,
|
|
63
|
+
`psql -c 'DELETE FROM "where" WHERE id=7'`,
|
|
64
|
+
String.raw`psql -c "SELECT \"where\", 'DROP TABLE users'"`,
|
|
65
|
+
String.raw`find cache -exec echo {} \; -exec echo 'rm -r data' {} \;`,
|
|
66
|
+
]) {
|
|
67
|
+
test(`leaves ordinary commands and prose unchanged: ${command}`, () => {
|
|
68
|
+
assert.deepEqual(recognizeDestructiveCommands(command), []);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("recognizes later compound destruction and shell wrappers without matching quoted prose", () => {
|
|
73
|
+
const command = `echo 'rm -rf /'; env X=1 sh -c 'rm --recursive ./data'`;
|
|
74
|
+
const matches = recognizeDestructiveCommands(command);
|
|
75
|
+
assert.equal(matches.length, 1);
|
|
76
|
+
assert.equal(matches[0].kind, "filesystem");
|
|
77
|
+
assert.equal(matches[0].hardDeny, false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("root/home recursive removal and destructive git retain hard denial", () => {
|
|
81
|
+
for (const command of [`rm -fr /`, `rm -r "$HOME"`, `git -C /repo reset --hard`, `git push -f`, `git push ';' -f main`, `rm -r ';' /`]) {
|
|
82
|
+
assert.equal(recognizeDestructiveCommands(command)[0]?.hardDeny, true, command);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
|
-
import {
|
|
7
|
+
import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai";
|
|
8
|
+
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { __testing, createGentleAiExtension } from "../../extensions/gentle-ai.ts";
|
|
10
|
+
import { resolveSessionWorktree } from "../../lib/session-worktree-registry.ts";
|
|
8
11
|
import { CandidateViewRegistry } from "../../lib/review-candidate-view.ts";
|
|
9
12
|
import {
|
|
10
13
|
NATIVE_REVIEW_ERROR_CODE,
|
|
@@ -138,6 +141,171 @@ function consentAnswerCall(calls: NativeCall[], answer: "granted" | "declined"):
|
|
|
138
141
|
return matching[0]!;
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
// Development-only counterpart of the published-pin SDK proof. A fixture-only
|
|
145
|
+
// provider drives an actual SDK write; no external model, START or download.
|
|
146
|
+
test("dev-binary: SDK non-Git bootstrap entry preserves files and metadata", { skip: !RUNNABLE }, async (t) => {
|
|
147
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-dev-bootstrap-")));
|
|
148
|
+
const home = join(root, "home");
|
|
149
|
+
const agentDir = join(home, "agent");
|
|
150
|
+
mkdirSync(agentDir, { recursive: true });
|
|
151
|
+
const previous: Record<string, string | undefined> = {};
|
|
152
|
+
const environment = {
|
|
153
|
+
HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: join(home, ".config"),
|
|
154
|
+
XDG_DATA_HOME: join(home, ".local", "share"), XDG_CACHE_HOME: join(home, ".cache"),
|
|
155
|
+
GENTLE_PI_AGENT_HOME: agentDir, PI_CODING_AGENT_DIR: agentDir,
|
|
156
|
+
GENTLE_PI_CONFIG_HOME: join(home, "pi-config"), PI_OFFLINE: "1",
|
|
157
|
+
// A poisoned ambient declaration must be replaced by the production
|
|
158
|
+
// adapter rather than accidentally furnishing the fixture's handshake.
|
|
159
|
+
GENTLE_PI_REVIEW_RELAY_CONTRACT: "invalid-ambient-relay",
|
|
160
|
+
};
|
|
161
|
+
// Disposable Git commands must not follow an ambient repository selector.
|
|
162
|
+
for (const key of Object.keys(process.env).filter((key) => key.startsWith("GIT_"))) {
|
|
163
|
+
previous[key] = process.env[key];
|
|
164
|
+
delete process.env[key];
|
|
165
|
+
}
|
|
166
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
167
|
+
previous[key] = process.env[key];
|
|
168
|
+
process.env[key] = value;
|
|
169
|
+
}
|
|
170
|
+
t.after(() => {
|
|
171
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
172
|
+
if (value === undefined) delete process.env[key]; else process.env[key] = value;
|
|
173
|
+
}
|
|
174
|
+
rmSync(root, { recursive: true, force: true });
|
|
175
|
+
});
|
|
176
|
+
const version = execFileSync(DEV_BINARY!, ["version"], { cwd: root, encoding: "utf8" }).trim();
|
|
177
|
+
t.diagnostic(`development binary=${DEV_BINARY}; version=${version}; published-pin parity is not established`);
|
|
178
|
+
const fixture = (name: string) => {
|
|
179
|
+
const cwd = join(root, name);
|
|
180
|
+
mkdirSync(cwd, { recursive: true });
|
|
181
|
+
writeFileSync(join(cwd, "candidate.txt"), "unchanged candidate bytes\n");
|
|
182
|
+
return cwd;
|
|
183
|
+
};
|
|
184
|
+
const modeRoot = fixture("mode-root");
|
|
185
|
+
git(modeRoot, "init", "--quiet");
|
|
186
|
+
enableGlobalReview(modeRoot);
|
|
187
|
+
const modelRuntime = await ModelRuntime.create({
|
|
188
|
+
authPath: join(agentDir, "empty-auth.json"), modelsPath: null,
|
|
189
|
+
modelsStorePath: join(agentDir, "models-store.json"), allowModelNetwork: false,
|
|
190
|
+
});
|
|
191
|
+
const invocations: Array<{ cwd: string; args: string[] }> = [];
|
|
192
|
+
const entry = async (cwd: string, operation: "startup" | "source" | "inspect" = "source", workspaceRoot?: string, enabled = true, inspectAfterStartup = false) => {
|
|
193
|
+
const before = invocations.length;
|
|
194
|
+
const wasVersioned = resolveSessionWorktree(workspaceRoot ?? cwd, cwd) !== undefined;
|
|
195
|
+
// A new native client per entry prevents a previous transport refusal
|
|
196
|
+
// cache from substituting for this scenario's actual invocation.
|
|
197
|
+
const adapter = createNodeExecFileAdapter();
|
|
198
|
+
const native = new NativeReviewCliV216(async (request) => {
|
|
199
|
+
invocations.push({ cwd: request.cwd, args: [...request.arguments] });
|
|
200
|
+
return adapter(request);
|
|
201
|
+
}, DEV_BINARY!);
|
|
202
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
203
|
+
cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true,
|
|
204
|
+
extensionFactories: [pi => pi.registerProvider("fixture-source", {
|
|
205
|
+
baseUrl: "http://invalid.invalid", apiKey: "fixture-only", api: "fixture-source" as never,
|
|
206
|
+
models: [{ id: "write", name: "Offline source writer", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 1024 }],
|
|
207
|
+
streamSimple(model, context) {
|
|
208
|
+
const stream = createAssistantMessageEventStream();
|
|
209
|
+
queueMicrotask(() => {
|
|
210
|
+
const write = context.messages.at(-1)?.role === "user";
|
|
211
|
+
const message: AssistantMessage = { role: "assistant", api: model.api, provider: model.provider, model: model.id, timestamp: Date.now(), content: [], stopReason: write ? "toolUse" : "stop", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } };
|
|
212
|
+
stream.push({ type: "start", partial: message });
|
|
213
|
+
if (write) {
|
|
214
|
+
const call = { type: "toolCall" as const, id: "fixture-source-write", name: "write", arguments: { path: "app.ts", content: "export const fixture = true;\n" } };
|
|
215
|
+
message.content.push(call);
|
|
216
|
+
stream.push({ type: "toolcall_start", contentIndex: 0, partial: message });
|
|
217
|
+
stream.push({ type: "toolcall_end", contentIndex: 0, toolCall: call, partial: message });
|
|
218
|
+
} else message.content.push({ type: "text", text: "source written" });
|
|
219
|
+
stream.push({ type: "done", reason: write ? "toolUse" : "stop", message });
|
|
220
|
+
stream.end(message);
|
|
221
|
+
});
|
|
222
|
+
return stream;
|
|
223
|
+
},
|
|
224
|
+
}), createGentleAiExtension({ nativeReviewCli: native, candidateViews: null, processEnv: {} })],
|
|
225
|
+
});
|
|
226
|
+
await resourceLoader.reload();
|
|
227
|
+
assert.deepEqual(resourceLoader.getExtensions().errors, []);
|
|
228
|
+
const { session } = await createAgentSession({
|
|
229
|
+
cwd, agentDir, resourceLoader, modelRuntime, sessionManager: SessionManager.inMemory(cwd), tools: ["write", "gentle_review"],
|
|
230
|
+
settingsManager: SettingsManager.inMemory({ retry: { enabled: false }, compaction: { enabled: false } }),
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
await session.bindExtensions({ mode: "print" });
|
|
234
|
+
const beforeAction = invocations.slice(before);
|
|
235
|
+
assert.equal(beforeAction.some(call => call.args[0] === "review" && call.args[1] === "status"), false, "mere binding cannot prepare non-Git");
|
|
236
|
+
if (operation === "source") {
|
|
237
|
+
await session.setModel(modelRuntime.getModel("fixture-source", "write")!);
|
|
238
|
+
session.setActiveToolsByName(["write"]);
|
|
239
|
+
await session.prompt("Write the authorized fixture source app.ts.");
|
|
240
|
+
assert.ok(session.messages.some(message => message.role === "toolResult" && message.toolName === "write" && !message.isError), "actual SDK successful write result");
|
|
241
|
+
assert.equal(readFileSync(join(cwd, "app.ts"), "utf8"), "export const fixture = true;\n");
|
|
242
|
+
}
|
|
243
|
+
assert.ok(session.getAllTools().some((tool) => tool.name === "gentle_review"), "actual SDK registers the facade");
|
|
244
|
+
session.setActiveToolsByName(["gentle_review"]);
|
|
245
|
+
const tool = session.agent.state.tools.find((tool) => tool.name === "gentle_review");
|
|
246
|
+
assert.ok(tool, "actual SDK registers the facade");
|
|
247
|
+
if (operation === "inspect" || inspectAfterStartup) {
|
|
248
|
+
const result = await tool.execute("bootstrap-inspect", { operation: "inspect", ...(workspaceRoot === undefined ? {} : { workspaceRoot }) });
|
|
249
|
+
assert.notEqual((result.details as Record<string, unknown>).outcome, "pi-host-relay-transport-unavailable", "entry must negotiate production-compatible transport");
|
|
250
|
+
}
|
|
251
|
+
if (operation !== "source") assert.equal(session.messages.length, 0, "passive/review entry does not invoke a model");
|
|
252
|
+
} finally { await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" }); session.dispose(); }
|
|
253
|
+
const calls = invocations.slice(before);
|
|
254
|
+
if (operation !== "startup" && !wasVersioned) assert.ok(calls.some(({ args }) => args[0] === "review" && args[1] === "mode"), "every admitted scenario really queries native mode");
|
|
255
|
+
const statuses = calls.filter(({ args }) => args[0] === "review" && args[1] === "status");
|
|
256
|
+
assert.equal(statuses.length > 0, enabled, "each on scenario must really invoke native STATUS; off must not");
|
|
257
|
+
if (enabled) assert.equal(statuses[0]!.cwd, resolveSessionWorktree(workspaceRoot ?? cwd, cwd)?.root ?? workspaceRoot ?? cwd);
|
|
258
|
+
if (inspectAfterStartup) assert.equal(statuses.length, 2, "the same SDK session recognizes native bootstrap for subsequent facade inspect, without restart");
|
|
259
|
+
t.diagnostic(`${operation} target=${workspaceRoot ?? cwd}; mode calls=${calls.length - statuses.length}; STATUS calls=${statuses.length}`);
|
|
260
|
+
};
|
|
261
|
+
const pristine = (cwd: string) => {
|
|
262
|
+
assert.equal(readFileSync(join(cwd, "candidate.txt"), "utf8"), "unchanged candidate bytes\n");
|
|
263
|
+
assert.equal(git(cwd, "ls-files", "--others", "--exclude-standard"), existsSync(join(cwd, "app.ts")) ? "app.ts\ncandidate.txt" : "candidate.txt");
|
|
264
|
+
assert.throws(() => git(cwd, "rev-parse", "--verify", "HEAD"), "HEAD stays unborn");
|
|
265
|
+
assert.equal(git(cwd, "remote"), "");
|
|
266
|
+
assert.equal(existsSync(join(cwd, ".git", "gentle-ai", "reviews")), false, "entry creates no review lineage");
|
|
267
|
+
};
|
|
268
|
+
const startupRoot = fixture("startup");
|
|
269
|
+
await entry(startupRoot, "startup", undefined, false);
|
|
270
|
+
assert.equal(existsSync(join(startupRoot, ".git")), false);
|
|
271
|
+
await entry(startupRoot, "source", undefined, true, true);
|
|
272
|
+
assert.equal(existsSync(join(startupRoot, ".git")), true);
|
|
273
|
+
pristine(startupRoot);
|
|
274
|
+
const metadata = statSync(join(startupRoot, ".git"));
|
|
275
|
+
await entry(startupRoot, "inspect");
|
|
276
|
+
assert.equal(statSync(join(startupRoot, ".git")).ino, metadata.ino, "repeat entry reuses metadata");
|
|
277
|
+
pristine(startupRoot);
|
|
278
|
+
for (const explicit of [false, true]) {
|
|
279
|
+
const target = fixture(explicit ? "explicit-inspect" : "default-inspect");
|
|
280
|
+
await entry(explicit ? modeRoot : target, "inspect", explicit ? target : undefined);
|
|
281
|
+
assert.equal(existsSync(join(target, ".git")), true);
|
|
282
|
+
pristine(target);
|
|
283
|
+
}
|
|
284
|
+
const nested = join(startupRoot, "nested");
|
|
285
|
+
mkdirSync(nested);
|
|
286
|
+
await entry(nested, "inspect");
|
|
287
|
+
assert.equal(existsSync(join(nested, ".git")), false, "valid parent is reused");
|
|
288
|
+
const broken = fixture("broken");
|
|
289
|
+
writeFileSync(join(broken, ".git"), "gitdir: missing-metadata\n");
|
|
290
|
+
await entry(broken);
|
|
291
|
+
assert.equal(readFileSync(join(broken, ".git"), "utf8"), "gitdir: missing-metadata\n");
|
|
292
|
+
const brokenChild = join(broken, "child");
|
|
293
|
+
mkdirSync(brokenChild);
|
|
294
|
+
await entry(brokenChild);
|
|
295
|
+
assert.equal(existsSync(join(brokenChild, ".git")), false, "broken ancestor is not bypassed");
|
|
296
|
+
assert.equal(readFileSync(join(broken, ".git"), "utf8"), "gitdir: missing-metadata\n");
|
|
297
|
+
assert.equal(readFileSync(join(broken, "candidate.txt"), "utf8"), "unchanged candidate bytes\n");
|
|
298
|
+
const disabled = JSON.parse(execFileSync(DEV_BINARY!, ["review", "mode", "disable", "--scope", "global", "--cwd", modeRoot, "--json"], { cwd: modeRoot, encoding: "utf8" }));
|
|
299
|
+
assert.equal(disabled.status.effective, "off");
|
|
300
|
+
for (const operation of ["startup", "source", "inspect"] as const) {
|
|
301
|
+
const off = fixture(`off-${operation}`);
|
|
302
|
+
await entry(off, operation, undefined, false);
|
|
303
|
+
assert.equal(existsSync(join(off, ".git")), false);
|
|
304
|
+
assert.equal(readFileSync(join(off, "candidate.txt"), "utf8"), "unchanged candidate bytes\n");
|
|
305
|
+
}
|
|
306
|
+
assert.equal(invocations.some(({ args }) => args[0] === "review" && args[1] === "start"), false, "no START authority is requested");
|
|
307
|
+
});
|
|
308
|
+
|
|
141
309
|
// ---------------------------------------------------------------------------
|
|
142
310
|
// Kill switch round trip.
|
|
143
311
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import gentleAgents from "../../extensions/gentle-agents.ts";
|
|
10
|
+
import gentleShell from "../../extensions/gentle-shell.ts";
|
|
11
|
+
import { createGentleAiExtension } from "../../extensions/gentle-ai.ts";
|
|
12
|
+
import { createNodeExecFileAdapter, NativeReviewCliV216 } from "../../lib/native-review-cli.ts";
|
|
13
|
+
import { SESSION_WORKTREE_ENTRY } from "../../lib/session-worktree-registry.ts";
|
|
14
|
+
import { requireDevBinary } from "../support/native-binary-gate.ts";
|
|
15
|
+
|
|
16
|
+
const binary = process.env.GENTLE_AI_DEV_BINARY;
|
|
17
|
+
const gate = requireDevBinary({ devBinaryPath: binary, exists: !!binary && binary.startsWith("/") && existsSync(binary), env: process.env });
|
|
18
|
+
if ("reason" in gate) console.log(`non-git-subagent-bootstrap: ${gate.reason}`);
|
|
19
|
+
|
|
20
|
+
// This is a fixture-only provider, not a replacement RPC child. The installed
|
|
21
|
+
// Pi CLI still loads the provider, accepts RPC, runs its agent loop and settles.
|
|
22
|
+
// No HTTP transport, credentials, or paid model are involved.
|
|
23
|
+
function offlineProvider(aiModule: string): string {
|
|
24
|
+
return `import { createAssistantMessageEventStream } from ${JSON.stringify(aiModule)};
|
|
25
|
+
export default function (pi) {
|
|
26
|
+
globalThis.fetch = async () => { throw new Error("Network forbidden in bootstrap fixture"); };
|
|
27
|
+
pi.registerProvider("bootstrap-fixture", {
|
|
28
|
+
baseUrl: "http://invalid.invalid", apiKey: "fixture-only", api: "bootstrap-fixture",
|
|
29
|
+
models: [{ id: "deterministic", name: "Offline bootstrap fixture", reasoning: false,
|
|
30
|
+
input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
31
|
+
contextWindow: 32000, maxTokens: 1024 }],
|
|
32
|
+
streamSimple(model, _context, options) {
|
|
33
|
+
const stream = createAssistantMessageEventStream();
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
const message = { role: "assistant", api: model.api, provider: model.provider,
|
|
36
|
+
model: model.id, timestamp: Date.now(), content: [], stopReason: "stop",
|
|
37
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
38
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } };
|
|
39
|
+
if (options?.signal?.aborted) {
|
|
40
|
+
message.stopReason = "aborted"; message.errorMessage = "Fixture aborted";
|
|
41
|
+
stream.push({ type: "error", reason: "aborted", error: message }); stream.end(message); return;
|
|
42
|
+
}
|
|
43
|
+
const text = "OFFLINE_BOOTSTRAP_CHILD " + JSON.stringify({ pid: process.pid, cwd: process.cwd() });
|
|
44
|
+
stream.push({ type: "start", partial: message });
|
|
45
|
+
message.content.push({ type: "text", text: "" });
|
|
46
|
+
stream.push({ type: "text_start", contentIndex: 0, partial: message });
|
|
47
|
+
message.content[0].text = text;
|
|
48
|
+
stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: message });
|
|
49
|
+
stream.push({ type: "text_end", contentIndex: 0, content: text, partial: message });
|
|
50
|
+
stream.push({ type: "done", reason: "stop", message }); stream.end(message);
|
|
51
|
+
});
|
|
52
|
+
return stream;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const explicit of [false, true]) {
|
|
60
|
+
test(`dev-binary: pre-bootstrap SDK session first ${explicit ? "explicit worker" : "implicit gentle-ai-worker"} dispatch completes a real Pi child`, { skip: !gate.run, timeout: 90000 }, async (t) => {
|
|
61
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-subagent-bootstrap-")));
|
|
62
|
+
const home = join(root, "home");
|
|
63
|
+
const agentDir = join(home, "agent");
|
|
64
|
+
const cwd = join(root, "project");
|
|
65
|
+
mkdirSync(join(agentDir, "agents"), { recursive: true });
|
|
66
|
+
mkdirSync(cwd);
|
|
67
|
+
writeFileSync(join(cwd, "candidate.txt"), "unchanged candidate bytes\n");
|
|
68
|
+
const role = explicit ? "worker" : "gentle-ai-worker";
|
|
69
|
+
mkdirSync(join(cwd, ".pi", "agents"), { recursive: true });
|
|
70
|
+
writeFileSync(join(cwd, ".pi", "agents", `${role}.md`), "---\ndescription: Offline bootstrap proof\nmodel: bootstrap-fixture/deterministic\ntools: [read]\n---\nReturn the offline fixture answer.\n");
|
|
71
|
+
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ retry: { enabled: false }, compaction: { enabled: false }, packages: [] }));
|
|
72
|
+
const env: NodeJS.ProcessEnv = {
|
|
73
|
+
PATH: process.env.PATH, HOME: home, USERPROFILE: home,
|
|
74
|
+
XDG_CONFIG_HOME: join(home, ".config"), XDG_DATA_HOME: join(home, ".local", "share"), XDG_CACHE_HOME: join(home, ".cache"),
|
|
75
|
+
GENTLE_PI_AGENT_HOME: agentDir, PI_CODING_AGENT_DIR: agentDir,
|
|
76
|
+
GENTLE_PI_CONFIG_HOME: join(home, "pi-config"), PI_OFFLINE: "1",
|
|
77
|
+
GENTLE_PI_SHELL: "1", GENTLE_PI_SHELL_CHANGES_WATCH_MS: "off",
|
|
78
|
+
GENTLE_PI_RUNTIME_METRICS: "off",
|
|
79
|
+
};
|
|
80
|
+
const previous = new Map<string, string | undefined>();
|
|
81
|
+
for (const key of new Set([...Object.keys(env), ...Object.keys(process.env).filter(key => key.startsWith("GIT_"))])) {
|
|
82
|
+
previous.set(key, process.env[key]);
|
|
83
|
+
if (env[key] === undefined) delete process.env[key]; else process.env[key] = env[key];
|
|
84
|
+
}
|
|
85
|
+
const children: ChildProcess[] = [];
|
|
86
|
+
t.after(async () => {
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
89
|
+
const exited = new Promise<void>(resolve => child.once("exit", () => resolve()));
|
|
90
|
+
child.kill("SIGTERM");
|
|
91
|
+
await exited;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const [key, value] of previous) {
|
|
95
|
+
if (value === undefined) delete process.env[key]; else process.env[key] = value;
|
|
96
|
+
}
|
|
97
|
+
rmSync(root, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
const sdkPackage = join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..", "package.json");
|
|
100
|
+
const installed = JSON.parse(readFileSync(sdkPackage, "utf8"));
|
|
101
|
+
const cli = realpathSync(join(dirname(sdkPackage), installed.bin.pi));
|
|
102
|
+
const provider = join(root, "offline-provider.mjs");
|
|
103
|
+
writeFileSync(provider, offlineProvider(import.meta.resolve("@earendil-works/pi-ai")));
|
|
104
|
+
t.diagnostic(`installed Pi SDK/CLI=${installed.version}; cli=${cli}; native=${binary}`);
|
|
105
|
+
const modeRoot = join(root, "mode-root");
|
|
106
|
+
mkdirSync(modeRoot);
|
|
107
|
+
execFileSync("git", ["init", "--quiet", modeRoot], { env, stdio: "pipe" });
|
|
108
|
+
const enabled = JSON.parse(execFileSync(binary!, ["review", "mode", "enable", "--scope", "global", "--cwd", modeRoot, "--json"], { env, encoding: "utf8" }));
|
|
109
|
+
assert.equal(enabled.status.effective, "on");
|
|
110
|
+
const nativeCalls: Array<{ cwd: string; args: string[] }> = [];
|
|
111
|
+
let preparationCompleted = false;
|
|
112
|
+
const adapter = createNodeExecFileAdapter();
|
|
113
|
+
const native = new NativeReviewCliV216(async request => {
|
|
114
|
+
nativeCalls.push({ cwd: request.cwd, args: [...request.arguments] });
|
|
115
|
+
const result = await adapter(request);
|
|
116
|
+
if (request.arguments[0] === "review" && request.arguments[1] === "status") preparationCompleted = existsSync(join(cwd, ".git"));
|
|
117
|
+
return result;
|
|
118
|
+
}, binary!);
|
|
119
|
+
const frames: Array<Array<Record<string, unknown>>> = [];
|
|
120
|
+
const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "empty-auth.json"), modelsPath: null, modelsStorePath: join(agentDir, "models-store.json"), allowModelNetwork: false });
|
|
121
|
+
const manager = SessionManager.inMemory(cwd);
|
|
122
|
+
const id = manager.getSessionId();
|
|
123
|
+
const loader = new DefaultResourceLoader({
|
|
124
|
+
cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true,
|
|
125
|
+
extensionFactories: [
|
|
126
|
+
(await import(pathToFileURL(provider).href)).default,
|
|
127
|
+
pi => gentleAgents(pi, env, {
|
|
128
|
+
home, agentHome: agentDir, env,
|
|
129
|
+
pi: { command: process.execPath, args: [cli, "--offline", "--no-extensions", "--extension", provider, "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files"] },
|
|
130
|
+
// Observe real OS spawn and production RPC traffic without replacing it.
|
|
131
|
+
spawn(command, args, options) {
|
|
132
|
+
assert.equal(preparationCompleted, true, "native preparation completed BEFORE OS spawn");
|
|
133
|
+
const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: options.stdio ?? ["pipe", "pipe", "pipe"], windowsHide: true, detached: options.detached });
|
|
134
|
+
children.push(child);
|
|
135
|
+
const records: Array<Record<string, unknown>> = [];
|
|
136
|
+
frames.push(records);
|
|
137
|
+
let buffer = "";
|
|
138
|
+
child.stdout!.on("data", chunk => {
|
|
139
|
+
buffer += chunk.toString();
|
|
140
|
+
const lines = buffer.split("\n"); buffer = lines.pop()!;
|
|
141
|
+
for (const line of lines) { try { records.push(JSON.parse(line)); } catch { /* production parser also ignores non-JSON lines */ } }
|
|
142
|
+
});
|
|
143
|
+
return child as never;
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
pi => gentleShell(pi, env),
|
|
147
|
+
createGentleAiExtension({ nativeReviewCli: native, candidateViews: null, processEnv: {} }),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
await loader.reload();
|
|
151
|
+
assert.deepEqual(loader.getExtensions().errors, []);
|
|
152
|
+
assert.equal(existsSync(join(cwd, ".git")), false, "extensions load before bootstrap");
|
|
153
|
+
const { session } = await createAgentSession({ cwd, agentDir, resourceLoader: loader, modelRuntime, sessionManager: manager, noTools: "builtin", settingsManager: SettingsManager.inMemory({ retry: { enabled: false }, compaction: { enabled: false } }) });
|
|
154
|
+
try {
|
|
155
|
+
await session.bindExtensions({ mode: "print" });
|
|
156
|
+
assert.equal(existsSync(join(cwd, ".git")), false, "registries are bound while the project is still non-Git");
|
|
157
|
+
assert.equal(nativeCalls.some(call => call.args[0] === "review" && call.args[1] === "status"), false, "binding never prepares");
|
|
158
|
+
session.setActiveToolsByName(["session_worktree_register", "subagent_run", "subagent_status"]);
|
|
159
|
+
const tool = (name: string) => {
|
|
160
|
+
const selected = session.agent.state.tools.find(tool => tool.name === name);
|
|
161
|
+
assert.ok(selected, `SDK tool registered: ${name}`);
|
|
162
|
+
return selected;
|
|
163
|
+
};
|
|
164
|
+
const registrations = () => manager.getEntries().filter(entry => entry.type === "custom" && entry.customType === SESSION_WORKTREE_ENTRY);
|
|
165
|
+
assert.equal(registrations().length, 0);
|
|
166
|
+
{
|
|
167
|
+
const result = await tool("subagent_run").execute(explicit ? "explicit" : "implicit", { agent: role, task: "Return offline bootstrap evidence\n## Allowed edit surfaces\nsrc/app.ts\n## Return\nReport", mode: "task", ...(explicit ? { workspace_root: cwd } : {}) });
|
|
168
|
+
const index = 0;
|
|
169
|
+
const child = children[index]!;
|
|
170
|
+
assert.ok(child.pid && child.pid !== process.pid, "real installed Pi child PID");
|
|
171
|
+
const details = (result.details as { gentleAgents: { status: string; cwd: string; taskId: string } }).gentleAgents;
|
|
172
|
+
assert.equal(details.status, "completed", JSON.stringify(result));
|
|
173
|
+
assert.equal(details.cwd, cwd);
|
|
174
|
+
const answer = result.content.find(part => part.type === "text") as { text: string };
|
|
175
|
+
assert.equal(answer.text, `OFFLINE_BOOTSTRAP_CHILD ${JSON.stringify({ pid: child.pid, cwd })}`);
|
|
176
|
+
assert.ok(frames[index]!.some(frame => frame.type === "response" && frame.command === "get_state" && frame.success), "production RPC readiness handshake");
|
|
177
|
+
assert.ok(frames[index]!.some(frame => frame.type === "agent_settled"), "actual CLI task settled");
|
|
178
|
+
assert.equal(manager.getSessionId(), id);
|
|
179
|
+
assert.equal(session.sessionManager, manager, "no manager replacement or Pi reload");
|
|
180
|
+
assert.equal(registrations().length, 1, "spawn registration dedupes durable entry");
|
|
181
|
+
t.diagnostic(`${explicit ? "explicit" : "implicit"} child pid=${child.pid}; RPC ready; task=${details.taskId} completed; canonical cwd=${details.cwd}; same session=${id}`);
|
|
182
|
+
}
|
|
183
|
+
assert.equal(nativeCalls.filter(call => call.cwd === cwd && call.args[0] === "review" && call.args[1] === "status").length, 1);
|
|
184
|
+
assert.equal(nativeCalls.some(call => call.args[0] === "review" && call.args[1] === "start"), false);
|
|
185
|
+
assert.equal(readFileSync(join(cwd, "candidate.txt"), "utf8"), "unchanged candidate bytes\n");
|
|
186
|
+
assert.equal(session.messages.length, 0, "parent never calls a model");
|
|
187
|
+
assert.equal(manager.getEntries().some(entry => entry.type === "custom" && /session-change|review-reminder/.test(entry.customType)), false, "registration is not Changes evidence");
|
|
188
|
+
} finally {
|
|
189
|
+
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
|
190
|
+
session.dispose();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"aaa","timestamp":"2026-09-28T10:00:00.000Z","cwd":"/work/alpha"}
|
|
2
|
+
{"type":"model_change","id":"m1","parentId":null,"timestamp":"2026-09-28T10:00:01.000Z","provider":"claude-bridge","modelId":"claude-opus-5-5"}
|
|
3
|
+
{"type":"message","id":"u1","parentId":"m1","timestamp":"2026-09-28T10:00:30.000Z","message":{"role":"user","content":"hello"}}
|
|
4
|
+
{"type":"message","id":"a1","parentId":"u1","timestamp":"2026-09-28T10:01:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":100,"output":50,"cacheRead":1000,"cacheWrite":200,"totalTokens":1350,"cost":{"total":0.5}}}}
|
|
5
|
+
{not json at all
|
|
6
|
+
{"type":"message","id":"a2","parentId":"a1","timestamp":"2026-09-28T10:30:00.000Z","message":{"role":"assistant","provider":"openai-codex","model":"gpt-6.1-sol","usage":{"input":200,"output":100,"cacheRead":0,"cacheWrite":0,"totalTokens":300,"cost":{"total":0.25}}}}
|
|
7
|
+
{"type":"custom","customType":"gentle-pi.session-change/v1","id":"c1","parentId":"a2","timestamp":"2026-09-28T10:30:01.000Z","data":{}}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"bbb","timestamp":"2026-09-29T23:00:00.000Z","cwd":"/work/alpha"}
|
|
2
|
+
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-09-29T23:10:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":10,"output":20,"cacheRead":0,"cacheWrite":0,"totalTokens":30,"cost":{"total":0.1}}}}
|
|
3
|
+
{"type":"message","id":"a2","parentId":"a1","timestamp":"2026-09-30T01:00:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":0,"output":70,"cacheRead":0,"cacheWrite":0,"totalTokens":70,"cost":{"total":0.2}}}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"nested","timestamp":"2026-09-30T09:00:00.000Z","cwd":"/work/alpha"}
|
|
2
|
+
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-09-30T09:01:00.000Z","message":{"role":"assistant","provider":"x","model":"nested-model","usage":{"input":999999,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":999999,"cost":{"total":99}}}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"ccc","timestamp":"2026-09-01T12:00:00.000Z","cwd":"/work/beta"}
|
|
2
|
+
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-09-01T12:05:00.000Z","message":{"role":"assistant","provider":"openai-codex","model":"gpt-5.5","usage":{"input":500,"output":500,"cacheRead":0,"cacheWrite":0,"cost":{"total":1}}}}
|
package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"aaa","timestamp":"2026-09-28T10:00:00.000Z","cwd":"/work/alpha"}
|
|
2
|
+
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-09-28T10:01:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":100,"output":50,"cacheRead":1000,"cacheWrite":200,"totalTokens":1350,"cost":{"total":0.5}}}}
|
package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"bbb","timestamp":"2026-09-29T23:00:00.000Z","cwd":"/work/alpha"}
|
|
2
|
+
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-09-29T23:10:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":10,"output":20,"cacheRead":0,"cacheWrite":0,"totalTokens":30,"cost":{"total":0.1}}}}
|
|
3
|
+
{"type":"message","id":"a2","parentId":"a1","timestamp":"2026-09-30T01:00:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":0,"output":70,"cacheRead":0,"cacheWrite":0,"totalTokens":70,"cost":{"total":0.2}}}}
|
|
4
|
+
{"type":"message","id":"a3","parentId":"a2","timestamp":"2026-09-30T02:00:00.000Z","message":{"role":"assistant","provider":"claude-bridge","model":"claude-opus-5-5","usage":{"input":0,"output":100,"cacheRead":0,"cacheWrite":0,"totalTokens":100,"cost":{"total":0.3}}}}
|
package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type":"session","version":3,"id":"fff","timestamp":"2026-09-20T09:00:00.000Z","cwd":"/work/gamma"}
|
|
2
|
+
{"type":"model_change","id":"m1","parentId":null,"timestamp":"2026-09-20T09:00:01.000Z","provider":"openai-codex","modelId":"gpt-5.3-codex"}
|
|
3
|
+
{"type":"message","id":"a1","parentId":"m1","timestamp":"2026-09-20T09:05:00.000Z","message":{"role":"assistant","provider":"openai-codex","model":"gpt-5.3-codex","usage":{"input":2000,"output":1000,"cacheRead":0,"cacheWrite":0,"totalTokens":3000,"cost":{"total":1.5}}}}
|