gentle-pi 2.3.0 → 2.5.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 +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
|
+
import type { ExecFileAdapter, ExecFileResult } from "../lib/native-review-cli.ts";
|
|
6
|
+
import {
|
|
7
|
+
decodeTelemetryTriggerDecision,
|
|
8
|
+
shouldTriggerTelemetry,
|
|
9
|
+
spawnTelemetryTrigger,
|
|
10
|
+
TELEMETRY_TRIGGER_CONTRACT,
|
|
11
|
+
TELEMETRY_TRIGGER_KILL_TIMEOUT_MS,
|
|
12
|
+
type TelemetryTriggerChildLike,
|
|
13
|
+
type TelemetryTriggerSpawn,
|
|
14
|
+
type TelemetryTriggerSpawnOptions,
|
|
15
|
+
} from "../lib/telemetry-trigger.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// gentle-pi#677: gentle-ai owns telemetry end to end (gentle-ai#4309); Pi only
|
|
19
|
+
// nudges the local binary once per process. These tests cover the pure
|
|
20
|
+
// predicate/spawn/decode core in lib/telemetry-trigger.ts and the two ways
|
|
21
|
+
// extensions/gentle-ai.ts wires it in: the once-per-process activation nudge
|
|
22
|
+
// and the foreground /gentle:telemetry slash command relay.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
interface FakeSpawnRecord {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
options: TelemetryTriggerSpawnOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fakeSpawn(): { spawn: TelemetryTriggerSpawn; calls: FakeSpawnRecord[]; killCalls: number } {
|
|
32
|
+
const calls: FakeSpawnRecord[] = [];
|
|
33
|
+
const state = { killCalls: 0 };
|
|
34
|
+
const spawn: TelemetryTriggerSpawn = (command, args, options) => {
|
|
35
|
+
calls.push({ command, args: [...args], options });
|
|
36
|
+
const child: TelemetryTriggerChildLike = {
|
|
37
|
+
unref() {
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
kill() {
|
|
41
|
+
state.killCalls += 1;
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
on() {
|
|
45
|
+
return undefined;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return child;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
spawn,
|
|
52
|
+
calls,
|
|
53
|
+
get killCalls() {
|
|
54
|
+
return state.killCalls;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("shouldTriggerTelemetry: pure predicate table", () => {
|
|
60
|
+
const cases: Array<[Record<string, string | undefined>, boolean]> = [
|
|
61
|
+
[{}, true],
|
|
62
|
+
[{ DO_NOT_TRACK: "1" }, false],
|
|
63
|
+
[{ DO_NOT_TRACK: "0" }, true],
|
|
64
|
+
[{ GENTLE_AI_TELEMETRY: "0" }, false],
|
|
65
|
+
[{ GENTLE_AI_TELEMETRY: "1" }, true],
|
|
66
|
+
[{ CI: "true" }, false],
|
|
67
|
+
[{ CI: "false" }, true],
|
|
68
|
+
[{ CI: undefined }, true],
|
|
69
|
+
[{ DO_NOT_TRACK: "1", GENTLE_AI_TELEMETRY: "0", CI: "true" }, false],
|
|
70
|
+
];
|
|
71
|
+
for (const [env, expected] of cases) {
|
|
72
|
+
assert.equal(shouldTriggerTelemetry(env), expected, JSON.stringify(env));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("spawnTelemetryTrigger: launches the expected argv, detached and stdio-ignored", () => {
|
|
77
|
+
const fake = fakeSpawn();
|
|
78
|
+
const result = spawnTelemetryTrigger({
|
|
79
|
+
executable: "/opt/gentle-ai/gentle-ai",
|
|
80
|
+
cwd: "/work/project",
|
|
81
|
+
env: {},
|
|
82
|
+
spawn: fake.spawn,
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(result, { spawned: true, reason: "spawned" });
|
|
85
|
+
assert.equal(fake.calls.length, 1);
|
|
86
|
+
const [call] = fake.calls;
|
|
87
|
+
assert.equal(call.command, "/opt/gentle-ai/gentle-ai");
|
|
88
|
+
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
89
|
+
assert.equal(call.options.cwd, "/work/project");
|
|
90
|
+
assert.equal(call.options.detached, true);
|
|
91
|
+
assert.equal(call.options.stdio, "ignore");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("spawnTelemetryTrigger: never spawns under each kill switch", () => {
|
|
95
|
+
const switches: Array<[Record<string, string | undefined>, string]> = [
|
|
96
|
+
[{ DO_NOT_TRACK: "1" }, "do-not-track"],
|
|
97
|
+
[{ GENTLE_AI_TELEMETRY: "0" }, "telemetry-disabled"],
|
|
98
|
+
[{ CI: "true" }, "ci"],
|
|
99
|
+
];
|
|
100
|
+
for (const [env, reason] of switches) {
|
|
101
|
+
const fake = fakeSpawn();
|
|
102
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env, spawn: fake.spawn });
|
|
103
|
+
assert.deepEqual(result, { spawned: false, reason }, JSON.stringify(env));
|
|
104
|
+
assert.equal(fake.calls.length, 0, JSON.stringify(env));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("spawnTelemetryTrigger: a throwing spawn is swallowed, never thrown", () => {
|
|
109
|
+
const throwingSpawn: TelemetryTriggerSpawn = () => {
|
|
110
|
+
throw new Error("spawn EAGAIN");
|
|
111
|
+
};
|
|
112
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env: {}, spawn: throwingSpawn });
|
|
113
|
+
assert.deepEqual(result, { spawned: false, reason: "spawn-error" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("spawnTelemetryTrigger: the 3 s kill timer fires exactly once and is unref'd", (t) => {
|
|
117
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
118
|
+
const fake = fakeSpawn();
|
|
119
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env: {}, spawn: fake.spawn });
|
|
120
|
+
assert.equal(result.spawned, true);
|
|
121
|
+
assert.equal(fake.killCalls, 0);
|
|
122
|
+
t.mock.timers.tick(TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
|
|
123
|
+
assert.equal(fake.killCalls, 1);
|
|
124
|
+
t.mock.timers.tick(TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
|
|
125
|
+
assert.equal(fake.killCalls, 1, "the timer must not fire a second time");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("decodeTelemetryTriggerDecision: decodes a well-formed v1 payload", () => {
|
|
129
|
+
const stdout = JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "sent_heartbeat", source: "local-state" });
|
|
130
|
+
assert.deepEqual(decodeTelemetryTriggerDecision(stdout), {
|
|
131
|
+
schema: TELEMETRY_TRIGGER_CONTRACT,
|
|
132
|
+
decision: "sent_heartbeat",
|
|
133
|
+
source: "local-state",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("decodeTelemetryTriggerDecision: every documented decision round-trips", () => {
|
|
138
|
+
for (const decision of ["enrolled", "sent_install", "sent_heartbeat", "rate_limited", "backoff", "disabled"]) {
|
|
139
|
+
const stdout = JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision, source: "env" });
|
|
140
|
+
assert.equal(decodeTelemetryTriggerDecision(stdout)?.decision, decision);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("decodeTelemetryTriggerDecision: an old binary's output decodes as nothing to do", () => {
|
|
145
|
+
assert.equal(decodeTelemetryTriggerDecision("unknown telemetry command\n"), undefined);
|
|
146
|
+
assert.equal(decodeTelemetryTriggerDecision(""), undefined);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("decodeTelemetryTriggerDecision: rejects a mismatched schema, unknown decision, or missing source", () => {
|
|
150
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: "other/v1", decision: "enrolled", source: "cli" })), undefined);
|
|
151
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "unheard-of", source: "cli" })), undefined);
|
|
152
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "enrolled" })), undefined);
|
|
153
|
+
assert.equal(decodeTelemetryTriggerDecision("[]"), undefined);
|
|
154
|
+
assert.equal(decodeTelemetryTriggerDecision("null"), undefined);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Extension wiring: the once-per-process activation nudge.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function buildExtensionHarness(overrides: Parameters<typeof createGentleAiExtension>[0] = {}) {
|
|
162
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
|
|
163
|
+
const commands = new Map<string, { handler: (args: string, ctx: ExtensionContext) => Promise<void> }>();
|
|
164
|
+
const pi = {
|
|
165
|
+
on(name: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<unknown>) {
|
|
166
|
+
handlers.set(name, handler);
|
|
167
|
+
},
|
|
168
|
+
registerCommand(name: string, command: { handler: (args: string, ctx: ExtensionContext) => Promise<void> }) {
|
|
169
|
+
commands.set(name, command);
|
|
170
|
+
},
|
|
171
|
+
registerTool() {},
|
|
172
|
+
events: { emit() {} },
|
|
173
|
+
} as unknown as ExtensionAPI;
|
|
174
|
+
createGentleAiExtension({ nativeReviewCli: null, ...overrides })(pi);
|
|
175
|
+
return { handlers, commands };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function fakeContext(cwd: string, notifications: Array<{ message: string; severity: string }>): ExtensionContext {
|
|
179
|
+
return {
|
|
180
|
+
cwd,
|
|
181
|
+
hasUI: true,
|
|
182
|
+
ui: {
|
|
183
|
+
notify(message: string, severity: string) {
|
|
184
|
+
notifications.push({ message, severity });
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
} as unknown as ExtensionContext;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test("activation: spawns the telemetry trigger exactly once for a primary session", async (t) => {
|
|
191
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
192
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
193
|
+
const fake = fakeSpawn();
|
|
194
|
+
const { handlers } = buildExtensionHarness({
|
|
195
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
196
|
+
telemetryTriggerSpawn: fake.spawn,
|
|
197
|
+
// Pin the environment: the ambient shell or CI may carry a kill switch
|
|
198
|
+
// (CI=true, DO_NOT_TRACK=1, GENTLE_AI_TELEMETRY=0) that would suppress the spawn.
|
|
199
|
+
processEnv: { PATH: "/bin" },
|
|
200
|
+
});
|
|
201
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
202
|
+
assert.equal(typeof beforeAgentStart, "function");
|
|
203
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
204
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
205
|
+
|
|
206
|
+
// A primary-session event carries no agent name at all.
|
|
207
|
+
await beforeAgentStart!({ systemPrompt: "" }, ctx);
|
|
208
|
+
await beforeAgentStart!({ systemPrompt: "" }, ctx);
|
|
209
|
+
|
|
210
|
+
assert.equal(fake.calls.length, 1, "the trigger must be attempted at most once per process");
|
|
211
|
+
const [call] = fake.calls;
|
|
212
|
+
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
213
|
+
assert.equal(call.options.cwd, "/work/project");
|
|
214
|
+
assert.equal(call.options.detached, true);
|
|
215
|
+
assert.equal(call.options.stdio, "ignore");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("activation: never spawns for a named or SDD agent event", async (t) => {
|
|
219
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
220
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
221
|
+
const fake = fakeSpawn();
|
|
222
|
+
const { handlers } = buildExtensionHarness({
|
|
223
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
224
|
+
telemetryTriggerSpawn: fake.spawn,
|
|
225
|
+
});
|
|
226
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
227
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
228
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
229
|
+
|
|
230
|
+
await beforeAgentStart!({ agentName: "review-risk", systemPrompt: "" }, ctx);
|
|
231
|
+
await beforeAgentStart!({ systemPrompt: "SDD apply executor" }, ctx);
|
|
232
|
+
|
|
233
|
+
assert.equal(fake.calls.length, 0, "named/SDD agents must never trigger the nudge");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("activation: a missing binary or spawn error never affects activation", async (t) => {
|
|
237
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
238
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
239
|
+
const { handlers } = buildExtensionHarness({
|
|
240
|
+
resolveTelemetryTriggerBinary: () => {
|
|
241
|
+
throw new Error("package-local-binary-missing: not installed");
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
245
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
246
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
247
|
+
|
|
248
|
+
// Must resolve cleanly and produce the ordinary orchestrator prompt fields,
|
|
249
|
+
// never throw or notify about the missing binary.
|
|
250
|
+
const outcome = await beforeAgentStart!({ systemPrompt: "base" }, ctx);
|
|
251
|
+
assert.equal(typeof outcome, "object");
|
|
252
|
+
assert.equal(notifications.length, 0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Extension wiring: the foreground /gentle:telemetry slash command.
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
function fakeAdapter(result: ExecFileResult): { adapter: ExecFileAdapter; requests: Array<{ file: string; arguments: readonly string[] }> } {
|
|
260
|
+
const requests: Array<{ file: string; arguments: readonly string[] }> = [];
|
|
261
|
+
const adapter: ExecFileAdapter = async (request) => {
|
|
262
|
+
requests.push({ file: request.file, arguments: request.arguments });
|
|
263
|
+
return result;
|
|
264
|
+
};
|
|
265
|
+
return { adapter, requests };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
test("/gentle:telemetry relays a status --json payload", async () => {
|
|
269
|
+
const payload = { schema: "gentle-ai.telemetry-status/v1", enabled: true, source: "default" };
|
|
270
|
+
const { adapter, requests } = fakeAdapter({
|
|
271
|
+
stdout: `${JSON.stringify(payload)}\n`,
|
|
272
|
+
stderr: "",
|
|
273
|
+
exitCode: 0,
|
|
274
|
+
signal: null,
|
|
275
|
+
timedOut: false,
|
|
276
|
+
outputLimitExceeded: false,
|
|
277
|
+
});
|
|
278
|
+
const { commands } = buildExtensionHarness({
|
|
279
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
280
|
+
telemetryExecFileAdapter: adapter,
|
|
281
|
+
});
|
|
282
|
+
const command = commands.get("gentle:telemetry");
|
|
283
|
+
assert.ok(command, "gentle:telemetry must be registered");
|
|
284
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
285
|
+
await command!.handler("", fakeContext("/work/project", notifications));
|
|
286
|
+
|
|
287
|
+
assert.equal(requests.length, 1);
|
|
288
|
+
assert.deepEqual(requests[0].arguments, ["telemetry", "status", "--json"]);
|
|
289
|
+
assert.equal(notifications.length, 1);
|
|
290
|
+
assert.equal(notifications[0].severity, "info");
|
|
291
|
+
assert.deepEqual(JSON.parse(notifications[0].message), payload);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("/gentle:telemetry disable prints a one-line confirmation", async () => {
|
|
295
|
+
const { adapter } = fakeAdapter({
|
|
296
|
+
stdout: `${JSON.stringify({ schema: "gentle-ai.telemetry-status/v1", enabled: false, source: "user" })}\n`,
|
|
297
|
+
stderr: "",
|
|
298
|
+
exitCode: 0,
|
|
299
|
+
signal: null,
|
|
300
|
+
timedOut: false,
|
|
301
|
+
outputLimitExceeded: false,
|
|
302
|
+
});
|
|
303
|
+
const { commands } = buildExtensionHarness({
|
|
304
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
305
|
+
telemetryExecFileAdapter: adapter,
|
|
306
|
+
});
|
|
307
|
+
const command = commands.get("gentle:telemetry");
|
|
308
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
309
|
+
await command!.handler("disable", fakeContext("/work/project", notifications));
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(notifications, [{ message: "Gentle AI telemetry disabled.", severity: "info" }]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("/gentle:telemetry relays a typed non-zero failure", async () => {
|
|
315
|
+
const { adapter } = fakeAdapter({
|
|
316
|
+
stdout: "",
|
|
317
|
+
stderr: "unknown telemetry command\n",
|
|
318
|
+
exitCode: 1,
|
|
319
|
+
signal: null,
|
|
320
|
+
timedOut: false,
|
|
321
|
+
outputLimitExceeded: false,
|
|
322
|
+
});
|
|
323
|
+
const { commands } = buildExtensionHarness({
|
|
324
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
325
|
+
telemetryExecFileAdapter: adapter,
|
|
326
|
+
});
|
|
327
|
+
const command = commands.get("gentle:telemetry");
|
|
328
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
329
|
+
await command!.handler("preview", fakeContext("/work/project", notifications));
|
|
330
|
+
|
|
331
|
+
assert.equal(notifications.length, 1);
|
|
332
|
+
assert.equal(notifications[0].severity, "error");
|
|
333
|
+
assert.match(notifications[0].message, /unknown telemetry command/);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("/gentle:telemetry rejects an unknown sub-action without calling the binary", async () => {
|
|
337
|
+
const { adapter, requests } = fakeAdapter({ stdout: "{}", stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false });
|
|
338
|
+
const { commands } = buildExtensionHarness({
|
|
339
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
340
|
+
telemetryExecFileAdapter: adapter,
|
|
341
|
+
});
|
|
342
|
+
const command = commands.get("gentle:telemetry");
|
|
343
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
344
|
+
await command!.handler("frobnicate", fakeContext("/work/project", notifications));
|
|
345
|
+
|
|
346
|
+
assert.equal(requests.length, 0);
|
|
347
|
+
assert.equal(notifications.length, 1);
|
|
348
|
+
assert.equal(notifications[0].severity, "warning");
|
|
349
|
+
});
|
|
@@ -15,18 +15,18 @@ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
|
15
15
|
// The guard reads the `## Allowed edit surfaces` section out of the delegated
|
|
16
16
|
// `subagent_run` prompt. A delegated task is a full prompt: the surfaces list
|
|
17
17
|
// is followed by deeper headings (`### Validation`, `#### Return`) and by
|
|
18
|
-
// ordinary prose. The section
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// nothing following it, which is why the same
|
|
22
|
-
// rejected in `task`.
|
|
18
|
+
// ordinary prose. The section remains fail-closed through ordinary prose until
|
|
19
|
+
// the next canonical Markdown heading, so following content is always parsed as
|
|
20
|
+
// a surface entry rather than silently discarded. A `context` value carrying
|
|
21
|
+
// only the heading and its lines had nothing following it, which is why the same
|
|
22
|
+
// surfaces were accepted there and rejected in `task`.
|
|
23
23
|
//
|
|
24
24
|
// Every case below runs through the real `tool_call` hook with the exact
|
|
25
25
|
// `subagent_run` input shape, because that is the boundary that rejected.
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
const REJECTION =
|
|
29
|
-
"Writer tasks must include the exact Markdown heading `## Allowed edit surfaces` with narrow repository-relative paths or narrow globs, one per line. The parent must derive or map that canonical block from the delegated task and relaunch the writer; do not accept aliases, and do not ask the human to author paths or globs.";
|
|
29
|
+
"Writer tasks must include the exact Markdown heading `## Allowed edit surfaces` with narrow repository-relative paths or narrow globs, one per line. Every non-empty line belongs to the section until the next canonical Markdown heading and must be a valid surface entry. Paths containing whitespace require whole-entry backticks; begin explanatory prose under the next Markdown heading. The parent must derive or map that canonical block from the delegated task and relaunch the writer; do not accept aliases, and do not ask the human to author paths or globs.";
|
|
30
30
|
|
|
31
31
|
type ToolCallHandler = (
|
|
32
32
|
event: { toolName: string; input: unknown },
|
|
@@ -89,8 +89,8 @@ test("task-scoped surfaces are accepted ahead of a deeper heading", async () =>
|
|
|
89
89
|
}, "a delegated task ends its surfaces list at the next heading of any level");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
test("task-scoped surfaces
|
|
93
|
-
await
|
|
92
|
+
test("task-scoped surfaces reject trailing prose before the next heading", async () => {
|
|
93
|
+
await assertRejected({
|
|
94
94
|
agent: "gentle-ai-worker",
|
|
95
95
|
mode: "task",
|
|
96
96
|
task: [
|
|
@@ -99,7 +99,86 @@ test("task-scoped surfaces are accepted ahead of trailing prose", async () => {
|
|
|
99
99
|
"",
|
|
100
100
|
"Then run the focused test file and report the outcome.",
|
|
101
101
|
].join("\n"),
|
|
102
|
-
}, "prose
|
|
102
|
+
}, "trailing prose must begin under a following Markdown heading");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("canonical headings close the surface section with up to three ASCII spaces", async () => {
|
|
106
|
+
for (const indentation of ["", " ", " ", " "]) {
|
|
107
|
+
await assertAccepted({
|
|
108
|
+
agent: "gentle-ai-worker",
|
|
109
|
+
mode: "task",
|
|
110
|
+
task: [
|
|
111
|
+
"## Allowed edit surfaces",
|
|
112
|
+
"- `lib/sdd-status.ts`",
|
|
113
|
+
`${indentation}### Validation`,
|
|
114
|
+
"node --test",
|
|
115
|
+
].join("\n"),
|
|
116
|
+
}, `${JSON.stringify(indentation)} indentation closes the section`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("canonical empty headings with trailing spaces close the surface section", async () => {
|
|
121
|
+
for (const indentation of ["", " "]) {
|
|
122
|
+
await assertAccepted({
|
|
123
|
+
agent: "gentle-ai-worker",
|
|
124
|
+
mode: "task",
|
|
125
|
+
task: [
|
|
126
|
+
"## Allowed edit surfaces",
|
|
127
|
+
"- `lib/sdd-status.ts`",
|
|
128
|
+
`${indentation}### `,
|
|
129
|
+
"Ordinary prose after an empty heading is not a surface entry.",
|
|
130
|
+
].join("\n"),
|
|
131
|
+
}, `${JSON.stringify(indentation)} indentation closes at an empty heading`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("pseudo-headings remain inside the surface section and reject dangerous paths", async () => {
|
|
136
|
+
for (const [label, heading] of [
|
|
137
|
+
["four-space indentation", " ### Validation"],
|
|
138
|
+
["seven hashes", "####### Validation"],
|
|
139
|
+
["bare marker", "###"],
|
|
140
|
+
["missing heading separator", "###Validation"],
|
|
141
|
+
["tab", "###\tValidation"],
|
|
142
|
+
["vertical tab", "###\vValidation"],
|
|
143
|
+
["form feed", "###\fValidation"],
|
|
144
|
+
["bare carriage return", "###\rValidation"],
|
|
145
|
+
["NBSP", "###\u00a0Validation"],
|
|
146
|
+
["em space", "###\u2003Validation"],
|
|
147
|
+
["Ogham space", "###\u1680Validation"],
|
|
148
|
+
["line separator", "###\u2028Validation"],
|
|
149
|
+
["paragraph separator", "###\u2029Validation"],
|
|
150
|
+
] as const) {
|
|
151
|
+
await assertRejected({
|
|
152
|
+
agent: "gentle-ai-worker",
|
|
153
|
+
mode: "task",
|
|
154
|
+
task: [
|
|
155
|
+
"## Allowed edit surfaces",
|
|
156
|
+
"- `lib/sdd-status.ts`",
|
|
157
|
+
heading,
|
|
158
|
+
"/etc/passwd",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
}, `${label} pseudo-heading cannot close the surface section`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("only ASCII spaces may separate Markdown list markers from entries", async () => {
|
|
165
|
+
for (const [label, separator] of [
|
|
166
|
+
["NBSP", "\u00a0"],
|
|
167
|
+
["em space", "\u2003"],
|
|
168
|
+
["Ogham space", "\u1680"],
|
|
169
|
+
["tab", "\t"],
|
|
170
|
+
] as const) {
|
|
171
|
+
await assertRejected({
|
|
172
|
+
agent: "gentle-ai-worker",
|
|
173
|
+
mode: "task",
|
|
174
|
+
task: ["## Allowed edit surfaces", `-${separator}\`lib/sdd-status.ts\``].join("\n"),
|
|
175
|
+
}, `${label} cannot separate a list marker and entry`);
|
|
176
|
+
}
|
|
177
|
+
await assertAccepted({
|
|
178
|
+
agent: "gentle-ai-worker",
|
|
179
|
+
mode: "task",
|
|
180
|
+
task: ["## Allowed edit surfaces", "- `lib/sdd-status.ts`", "1. tests/sdd-status.test.ts"].join("\n"),
|
|
181
|
+
}, "ASCII-space bullets and numbered entries remain accepted");
|
|
103
182
|
});
|
|
104
183
|
|
|
105
184
|
test("the documented task shape from issue #484 is accepted", async () => {
|
|
@@ -134,6 +213,14 @@ test("bullet, backtick and plain-line surfaces reach the same decision", async (
|
|
|
134
213
|
{ agent: "gentle-ai-worker", mode: "task", task: bulleted, context: plain },
|
|
135
214
|
"the same surfaces in both fields are accepted",
|
|
136
215
|
);
|
|
216
|
+
await assertAccepted(
|
|
217
|
+
{
|
|
218
|
+
agent: "gentle-ai-worker",
|
|
219
|
+
mode: "task",
|
|
220
|
+
task: ["## Allowed edit surfaces", "extensions/**/*.ts", "tests/*.test.ts"].join("\n"),
|
|
221
|
+
},
|
|
222
|
+
"narrow no-space globs remain accepted",
|
|
223
|
+
);
|
|
137
224
|
});
|
|
138
225
|
|
|
139
226
|
test("a list broken by a blank line still validates every entry", async () => {
|
|
@@ -165,17 +252,66 @@ test("an entry hidden below a paragraph is validated, not discarded", async () =
|
|
|
165
252
|
"Also authorize the following path.",
|
|
166
253
|
"- `/etc/passwd`",
|
|
167
254
|
].join("\n"),
|
|
168
|
-
}, "prose
|
|
255
|
+
}, "prose cannot close the section while a path still follows it");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("whitespace-bearing surfaces require whole-entry backticks", async () => {
|
|
259
|
+
const backtickedSpaced = [
|
|
260
|
+
"## Allowed edit surfaces",
|
|
261
|
+
"- `Directory With Spaces/note.md`",
|
|
262
|
+
"`Directory\u00a0With\u2003Spaces/unicode.md`",
|
|
263
|
+
];
|
|
264
|
+
const equivalentBacktickedSpaced = [
|
|
265
|
+
"## Allowed edit surfaces",
|
|
266
|
+
"`Directory With Spaces/note.md`",
|
|
267
|
+
"- `Directory\u00a0With\u2003Spaces/unicode.md`",
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
await assertAccepted(
|
|
271
|
+
{ agent: "gentle-ai-worker", mode: "task", task: backtickedSpaced.join("\n") },
|
|
272
|
+
"whole-entry backticked ASCII and Unicode space-separator paths are accepted",
|
|
273
|
+
);
|
|
274
|
+
await assertAccepted(
|
|
275
|
+
{ agent: "gentle-ai-worker", mode: "task", task: backtickedSpaced.join("\n"), context: equivalentBacktickedSpaced.join("\n") },
|
|
276
|
+
"task and context compare equivalent backticked whitespace-bearing paths by value",
|
|
277
|
+
);
|
|
169
278
|
await assertAccepted({
|
|
170
279
|
agent: "gentle-ai-worker",
|
|
171
280
|
mode: "task",
|
|
172
|
-
task: [
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
281
|
+
task: [...backtickedSpaced, "### Validation", "node --test"].join("\n"),
|
|
282
|
+
}, "the next Markdown heading closes a backticked whitespace-bearing surface section");
|
|
283
|
+
|
|
284
|
+
for (const [label, entry] of [
|
|
285
|
+
["bare spaced path", "Directory With Spaces/note.md"],
|
|
286
|
+
["bare bulleted spaced path", "- Directory With Spaces/note.md"],
|
|
287
|
+
["bare NBSP path", "Directory\u00a0With Spaces/note.md"],
|
|
288
|
+
["bare em-space path", "Directory\u2003With Spaces/note.md"],
|
|
289
|
+
["U+0085 control path", "`Directory\u0085With Spaces/note.md`"],
|
|
290
|
+
["U+0085 list separator", "-\u0085`lib/sdd-status.ts`"],
|
|
291
|
+
["U+2028 line-separator path", "`Directory\u2028With Spaces/note.md`"],
|
|
292
|
+
["U+2029 paragraph-separator path", "`Directory\u2029With Spaces/note.md`"],
|
|
293
|
+
["list-marked prose", "- Then run the focused test file."],
|
|
294
|
+
["marker-only hyphen", "-"],
|
|
295
|
+
["empty backticks", "``"],
|
|
296
|
+
["unclosed backticks", "`lib/sdd-status.ts"],
|
|
297
|
+
["partially backticked path", "`lib/sdd-status.ts` trailing"],
|
|
298
|
+
["absolute spaced path", "/tmp/Outside Directory/file.md"],
|
|
299
|
+
["parent spaced path", "../Other Directory/file.md"],
|
|
300
|
+
["home spaced path", "~/Private Directory/file.md"],
|
|
301
|
+
["Windows spaced path", "C:\\Outside Directory\\file.md"],
|
|
302
|
+
["dangerous backticked bullet", "- `/tmp/Outside Directory/file.md`"],
|
|
303
|
+
] as const) {
|
|
304
|
+
await assertRejected({
|
|
305
|
+
agent: "gentle-ai-worker",
|
|
306
|
+
mode: "task",
|
|
307
|
+
task: ["## Allowed edit surfaces", "lib/sdd-status.ts", entry].join("\n"),
|
|
308
|
+
}, `${label} after a valid entry is rejected`);
|
|
309
|
+
await assertRejected({
|
|
310
|
+
agent: "gentle-ai-worker",
|
|
311
|
+
mode: "task",
|
|
312
|
+
task: ["## Allowed edit surfaces", entry, "lib/sdd-status.ts"].join("\n"),
|
|
313
|
+
}, `${label} before a valid entry is rejected`);
|
|
314
|
+
}
|
|
179
315
|
});
|
|
180
316
|
|
|
181
317
|
test("out-of-scope and empty surfaces stay rejected", async () => {
|
/package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
/package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json
RENAMED
|
File without changes
|
/package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json
RENAMED
|
File without changes
|
|
File without changes
|