gentle-pi 2.4.0 → 2.6.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- 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 +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -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 +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -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 +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- 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 +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -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 +656 -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 +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
async function proveLazyDiscovery(): Promise<void> {
|
|
10
|
+
const {
|
|
11
|
+
createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices,
|
|
12
|
+
ModelRuntime, SessionManager, SettingsManager,
|
|
13
|
+
} = await import("@earendil-works/pi-coding-agent");
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR!;
|
|
16
|
+
const shim = join(cwd, "extensions.ts");
|
|
17
|
+
const source = (name: string) => JSON.stringify(new URL(`../extensions/${name}.ts`, import.meta.url).href);
|
|
18
|
+
writeFileSync(shim, `
|
|
19
|
+
import { createGentleAiExtension } from ${source("gentle-ai")};
|
|
20
|
+
import gentleAgents from ${source("gentle-agents")};
|
|
21
|
+
import sddInit from ${source("sdd-init")};
|
|
22
|
+
export default function (pi) {
|
|
23
|
+
createGentleAiExtension({ nativeReviewCli: null, candidateViews: null, processEnv: {} })(pi);
|
|
24
|
+
gentleAgents(pi);
|
|
25
|
+
sddInit(pi);
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
const modelRuntime = await ModelRuntime.create({
|
|
29
|
+
authPath: join(agentDir, "empty-auth.json"),
|
|
30
|
+
modelsPath: null,
|
|
31
|
+
modelsStorePath: join(agentDir, "models-store.json"),
|
|
32
|
+
allowModelNetwork: false,
|
|
33
|
+
});
|
|
34
|
+
const runtime = await createAgentSessionRuntime(async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
35
|
+
const services = await createAgentSessionServices({
|
|
36
|
+
cwd, agentDir, modelRuntime,
|
|
37
|
+
settingsManager: SettingsManager.inMemory({ retry: { enabled: false }, compaction: { enabled: false } }),
|
|
38
|
+
resourceLoaderOptions: {
|
|
39
|
+
noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true,
|
|
40
|
+
additionalExtensionPaths: [shim],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
assert.deepEqual(services.resourceLoader.getExtensions().errors, []);
|
|
44
|
+
assert.deepEqual(services.diagnostics, []);
|
|
45
|
+
return {
|
|
46
|
+
...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, tools: ["subagent_list_agents"] })),
|
|
47
|
+
services, diagnostics: services.diagnostics,
|
|
48
|
+
};
|
|
49
|
+
}, { cwd, agentDir, sessionManager: SessionManager.inMemory(cwd) });
|
|
50
|
+
try {
|
|
51
|
+
await runtime.session.bindExtensions({ mode: "print" });
|
|
52
|
+
const session = runtime.session;
|
|
53
|
+
const list = session.agent.state.tools.find(tool => tool.name === "subagent_list_agents");
|
|
54
|
+
assert.ok(list, "actual SDK must expose the wrapped discovery tool");
|
|
55
|
+
const names = async () => {
|
|
56
|
+
const result = await list.execute("asset-discovery", {});
|
|
57
|
+
return result.content.filter(part => part.type === "text").map(part => part.text).join("\n");
|
|
58
|
+
};
|
|
59
|
+
const before = await names();
|
|
60
|
+
assert.match(before, /- gentle-ai-explore \(global\)/);
|
|
61
|
+
assert.match(before, /- review-risk \(global\)/);
|
|
62
|
+
assert.doesNotMatch(before, /- sdd-/, "fresh startup must not install SDD definitions");
|
|
63
|
+
assert.equal(existsSync(join(agentDir, "chains", "sdd-full.chain.md")), false);
|
|
64
|
+
assert.equal(existsSync(join(agentDir, "gentle-ai", "support")), false);
|
|
65
|
+
await session.prompt("/gentle:install-sdd");
|
|
66
|
+
assert.strictEqual(runtime.session, session);
|
|
67
|
+
const after = await names();
|
|
68
|
+
for (const name of ["gentle-ai-explore", "review-risk", "sdd-init", "sdd-apply"]) {
|
|
69
|
+
assert.ok(after.includes(`- ${name} (global)`), `same-session discovery must include ${name}`);
|
|
70
|
+
}
|
|
71
|
+
for (const file of ["sdd-status-contract.md", "strict-tdd.md", "strict-tdd-verify.md"]) {
|
|
72
|
+
assert.ok(existsSync(join(agentDir, "gentle-ai", "support", file)), `missing support: ${file}`);
|
|
73
|
+
}
|
|
74
|
+
assert.ok(existsSync(join(agentDir, "chains", "sdd-full.chain.md")));
|
|
75
|
+
assert.equal(session.messages.length, 0, "slash activation must not start a model turn");
|
|
76
|
+
console.log("SDK discovery: delegation/review only -> /gentle:install-sdd -> same-session sdd-init/sdd-apply and support");
|
|
77
|
+
} finally {
|
|
78
|
+
await runtime.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.env.GENTLE_PI_ASSET_PROOF_CHILD === "1") {
|
|
83
|
+
await proveLazyDiscovery();
|
|
84
|
+
} else {
|
|
85
|
+
test("actual SDK discovers SDD only after explicit activation in the same session", () => {
|
|
86
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-pi-assets-sdk-"));
|
|
87
|
+
try {
|
|
88
|
+
const home = join(root, "home");
|
|
89
|
+
const cwd = join(root, "project");
|
|
90
|
+
const agentDir = join(home, ".pi", "agent");
|
|
91
|
+
for (const path of [cwd, agentDir]) mkdirSync(path, { recursive: true });
|
|
92
|
+
const result = spawnSync(process.execPath, ["--experimental-strip-types", fileURLToPath(import.meta.url)], {
|
|
93
|
+
cwd, encoding: "utf8", timeout: 30_000,
|
|
94
|
+
env: {
|
|
95
|
+
PATH: `${dirname(process.execPath)}:/usr/bin:/bin`, HOME: home, TMPDIR: root,
|
|
96
|
+
PI_CODING_AGENT_DIR: agentDir, GENTLE_PI_AGENT_HOME: agentDir,
|
|
97
|
+
GENTLE_PI_CONFIG_HOME: join(home, "config"), XDG_CONFIG_HOME: join(home, ".config"),
|
|
98
|
+
PI_OFFLINE: "1", GENTLE_PI_ASSET_PROOF_CHILD: "1",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
assert.ifError(result.error);
|
|
102
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
103
|
+
assert.match(result.stdout, /SDK discovery: delegation\/review only/);
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(root, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
7
7
|
|
|
8
|
-
const { classifyGuardedCommand } = __testing;
|
|
8
|
+
const { classifyGuardedCommand, evaluateGuardedCommand, guardedCommandPreview, guardedCommandTitle } = __testing;
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Helpers
|
|
@@ -129,6 +129,121 @@ test("classifyGuardedCommand: git push plain allowed when autonomousMode=true an
|
|
|
129
129
|
assert.equal(result, "allow");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("classifyGuardedCommand: configured push allow is remote-agnostic and unchanged", () => {
|
|
133
|
+
const config = { autonomousMode: true, guardedCommands: { gitPush: "allow" as const } };
|
|
134
|
+
for (const command of [
|
|
135
|
+
"git push origin feature/test",
|
|
136
|
+
"git push upstream feature/test",
|
|
137
|
+
"git push fork feature/test",
|
|
138
|
+
"git push",
|
|
139
|
+
"git push feature/test && git status",
|
|
140
|
+
"echo done || git push origin feature/test",
|
|
141
|
+
]) {
|
|
142
|
+
assert.equal(classifyGuardedCommand(command, config), "allow", command);
|
|
143
|
+
}
|
|
144
|
+
assert.equal(
|
|
145
|
+
classifyGuardedCommand("git push origin feature/test", { autonomousMode: true, guardedCommands: { gitPush: "block" } }),
|
|
146
|
+
"block",
|
|
147
|
+
);
|
|
148
|
+
for (const command of [
|
|
149
|
+
"git push -f origin feature/test",
|
|
150
|
+
"git push --force origin main",
|
|
151
|
+
"git push --force-with-lease origin feature/test",
|
|
152
|
+
]) {
|
|
153
|
+
assert.equal(classifyGuardedCommand(command, config), "block", command);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Guarded command preview — presentation-only centering
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
test("guardedCommandPreview: long late matched action stays visible in preview", () => {
|
|
162
|
+
const prefix = "noise ".repeat(80);
|
|
163
|
+
const command = `${prefix}git push origin feature/test`;
|
|
164
|
+
// Contract: the caller owns the trigger index. confirmCommand hands over
|
|
165
|
+
// evaluation.triggerIndex, so the preview always centers on the matched
|
|
166
|
+
// action supplied by the evaluator.
|
|
167
|
+
const triggerIndex = command.indexOf("git push");
|
|
168
|
+
const preview = guardedCommandPreview(command, triggerIndex);
|
|
169
|
+
assert.match(preview, /git push origin feature\/test/);
|
|
170
|
+
assert.ok(preview.startsWith("…"), "preview elides leading context near a late match");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("guardedCommandPreview: late non-push action visible, elided, and width-bounded", () => {
|
|
174
|
+
const prefix = "noise ".repeat(80);
|
|
175
|
+
const command = `${prefix}npm publish --tag beta`;
|
|
176
|
+
const triggerIndex = command.indexOf("npm publish");
|
|
177
|
+
const preview = guardedCommandPreview(command, triggerIndex);
|
|
178
|
+
assert.match(preview, /npm publish --tag beta/);
|
|
179
|
+
assert.ok(preview.startsWith("…"), "preview elides leading context near a late match");
|
|
180
|
+
// Bounded visible width: 1 leading ellipsis + up to 179 content chars (budget 180).
|
|
181
|
+
assert.ok(preview.length <= 180, `preview exceeds bounded width: ${preview.length}`);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Guarded command title — presentation-only, action-specific
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
test("guardedCommandTitle: exact action-specific titles per guarded key", () => {
|
|
189
|
+
assert.equal(guardedCommandTitle("gitPush"), "Allow guarded git push?");
|
|
190
|
+
assert.equal(guardedCommandTitle("gitRebase"), "Allow guarded git rebase?");
|
|
191
|
+
assert.equal(
|
|
192
|
+
guardedCommandTitle("gitBranchDeleteForce"),
|
|
193
|
+
"Allow guarded forced git branch deletion?",
|
|
194
|
+
);
|
|
195
|
+
assert.equal(guardedCommandTitle("npmPublish"), "Allow guarded npm publish?");
|
|
196
|
+
assert.equal(guardedCommandTitle("piRemove"), "Allow guarded pi remove?");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("guardedCommandTitle: unkeyed guarded command falls back to generic title", () => {
|
|
200
|
+
assert.equal(guardedCommandTitle(undefined), "Allow guarded command?");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("evaluateGuardedCommand: collects guarded matches in command order and applies full-command precedence", () => {
|
|
204
|
+
const command = "npm publish && git push origin main && git rebase main";
|
|
205
|
+
const evaluation = evaluateGuardedCommand(command, {
|
|
206
|
+
autonomousMode: true,
|
|
207
|
+
guardedCommands: { gitPush: "allow", gitRebase: "confirm", npmPublish: "block" },
|
|
208
|
+
});
|
|
209
|
+
assert.deepEqual(evaluation.matches.map((match) => [match.key, match.action]), [
|
|
210
|
+
["npmPublish", "block"],
|
|
211
|
+
["gitPush", "allow"],
|
|
212
|
+
["gitRebase", "confirm"],
|
|
213
|
+
]);
|
|
214
|
+
assert.deepEqual(evaluation.matches.map((match) => command.slice(match.triggerIndex).split(/\s/, 1)[0]), ["publish", "push", "rebase"]);
|
|
215
|
+
assert.equal(evaluation.action, "block");
|
|
216
|
+
|
|
217
|
+
assert.equal(evaluateGuardedCommand("git rebase main && git push --force origin main", {
|
|
218
|
+
autonomousMode: true,
|
|
219
|
+
guardedCommands: { gitRebase: "allow", gitPush: "allow" },
|
|
220
|
+
}).action, "block", "a later hard deny overrides an earlier allowed action");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("evaluateGuardedCommand: git -C stores the push token offset for bounded previews", () => {
|
|
224
|
+
const command = `git -C /${"very-long-path/".repeat(30)} push origin main`;
|
|
225
|
+
const evaluation = evaluateGuardedCommand(command, { autonomousMode: false, guardedCommands: {} });
|
|
226
|
+
assert.equal(command.slice(evaluation.triggerIndex, evaluation.triggerIndex + 4), "push");
|
|
227
|
+
assert.match(guardedCommandPreview(command, evaluation.triggerIndex), /push origin main/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("evaluateGuardedCommand: git -C path containing push still offsets the push action", () => {
|
|
231
|
+
const command = `git -C /${"push-path/".repeat(30)} push origin main`;
|
|
232
|
+
const evaluation = evaluateGuardedCommand(command, { autonomousMode: false, guardedCommands: {} });
|
|
233
|
+
assert.equal(evaluation.triggerIndex, command.lastIndexOf("push"));
|
|
234
|
+
assert.match(guardedCommandPreview(command, evaluation.triggerIndex), /push origin main/);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("evaluateGuardedCommand: repeated forced branch deletions remain separate matches", () => {
|
|
238
|
+
const command = "git branch --delete --force old && git branch --delete --force older";
|
|
239
|
+
const evaluation = evaluateGuardedCommand(command, { autonomousMode: false, guardedCommands: {} });
|
|
240
|
+
assert.deepEqual(evaluation.matches.map((match) => match.key), ["gitBranchDeleteForce", "gitBranchDeleteForce"]);
|
|
241
|
+
assert.equal(
|
|
242
|
+
guardedCommandTitle(evaluation.key, evaluation.matches),
|
|
243
|
+
"Allow guarded actions: forced git branch deletion; forced git branch deletion?",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
132
247
|
test("classifyGuardedCommand: git push plain still confirm when autonomousMode=false even with gitPush=allow in config", () => {
|
|
133
248
|
const result = classifyGuardedCommand("git push origin feature/test", {
|
|
134
249
|
autonomousMode: false,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import codeGraphTools, {
|
|
9
9
|
createCodeGraphTool,
|
|
10
|
+
codeGraphNodeScript,
|
|
11
|
+
findCodeGraphNodeScriptOnPath,
|
|
10
12
|
type CodeGraphRunner,
|
|
11
13
|
} from "../extensions/codegraph-tools.ts";
|
|
12
14
|
|
|
@@ -189,7 +191,7 @@ test("CodeGraph tool rejects incomplete or oversized query requests before runni
|
|
|
189
191
|
});
|
|
190
192
|
|
|
191
193
|
test("CodeGraph tool registration exposes a single constrained custom tool", () => {
|
|
192
|
-
const tools: Array<{ name: string; parameters: Record<string, unknown> }> = [];
|
|
194
|
+
const tools: Array<{ name: string; renderShell?: string; parameters: Record<string, unknown> }> = [];
|
|
193
195
|
const pi = {
|
|
194
196
|
registerTool(tool: { name: string; parameters: Record<string, unknown> }) {
|
|
195
197
|
tools.push(tool);
|
|
@@ -200,6 +202,7 @@ test("CodeGraph tool registration exposes a single constrained custom tool", ()
|
|
|
200
202
|
|
|
201
203
|
assert.equal(tools.length, 1);
|
|
202
204
|
assert.equal(tools[0]?.name, "codegraph");
|
|
205
|
+
assert.equal(tools[0]?.renderShell, "self", "CodeGraph must not inherit Pi's painted Box");
|
|
203
206
|
assert.deepEqual(tools[0]?.parameters, {
|
|
204
207
|
type: "object",
|
|
205
208
|
additionalProperties: false,
|
|
@@ -211,3 +214,110 @@ test("CodeGraph tool registration exposes a single constrained custom tool", ()
|
|
|
211
214
|
},
|
|
212
215
|
});
|
|
213
216
|
});
|
|
217
|
+
|
|
218
|
+
function writeCodeGraphPackage(binDir: string, scriptSource = "#!/usr/bin/env node\n"): string {
|
|
219
|
+
const pkgRoot = join(binDir, "node_modules", "@colbymchenry", "codegraph");
|
|
220
|
+
mkdirSync(pkgRoot, { recursive: true });
|
|
221
|
+
const scriptTarget = join(pkgRoot, "npm-shim.js");
|
|
222
|
+
writeFileSync(scriptTarget, scriptSource);
|
|
223
|
+
writeFileSync(
|
|
224
|
+
join(pkgRoot, "package.json"),
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
name: "@colbymchenry/codegraph",
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
bin: { codegraph: "npm-shim.js" },
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
writeFileSync(
|
|
232
|
+
join(binDir, "codegraph.cmd"),
|
|
233
|
+
"@ECHO off\nnode \"%~dp0\\node_modules\\@colbymchenry\\codegraph\\npm-shim.js\" %*\n",
|
|
234
|
+
);
|
|
235
|
+
return scriptTarget;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function withWindowsPath(t: test.TestContext, path: string): void {
|
|
239
|
+
const previousPath = process.env.PATH;
|
|
240
|
+
const previousWindowsPath = process.env.Path;
|
|
241
|
+
const windowsPath = process.platform === "win32"
|
|
242
|
+
? `${path};${previousWindowsPath ?? previousPath ?? ""}`
|
|
243
|
+
: path;
|
|
244
|
+
process.env.Path = windowsPath;
|
|
245
|
+
process.env.PATH = windowsPath;
|
|
246
|
+
t.after(() => {
|
|
247
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
248
|
+
else process.env.PATH = previousPath;
|
|
249
|
+
if (previousWindowsPath === undefined) delete process.env.Path;
|
|
250
|
+
else process.env.Path = previousWindowsPath;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
test("Windows shim helpers skip stale PATH shims before resolving the npm script", (t) => {
|
|
255
|
+
const staleBinDir = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-stale-bin-")));
|
|
256
|
+
const validBinDir = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-valid-bin-")));
|
|
257
|
+
t.after(() => rmSync(staleBinDir, { recursive: true, force: true }));
|
|
258
|
+
t.after(() => rmSync(validBinDir, { recursive: true, force: true }));
|
|
259
|
+
writeFileSync(join(staleBinDir, "codegraph.cmd"), "@ECHO off\n");
|
|
260
|
+
const stalePkgRoot = join(staleBinDir, "node_modules", "@colbymchenry", "codegraph");
|
|
261
|
+
mkdirSync(stalePkgRoot, { recursive: true });
|
|
262
|
+
writeFileSync(join(stalePkgRoot, "package.json"), JSON.stringify({ bin: { codegraph: "." } }));
|
|
263
|
+
const scriptTarget = writeCodeGraphPackage(validBinDir);
|
|
264
|
+
|
|
265
|
+
withWindowsPath(t, `${staleBinDir};${validBinDir}`);
|
|
266
|
+
|
|
267
|
+
assert.equal(codeGraphNodeScript(join(staleBinDir, "codegraph.cmd")), undefined);
|
|
268
|
+
assert.equal(findCodeGraphNodeScriptOnPath(), scriptTarget);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("Windows shim helpers resolve quoted PATH entries in order without stripping unmatched quotes", (t) => {
|
|
272
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-quoted-bin-")));
|
|
273
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
274
|
+
const firstBin = join(root, "Program Files", "nodejs");
|
|
275
|
+
const secondBin = join(root, "other-bin");
|
|
276
|
+
const firstScript = writeCodeGraphPackage(firstBin);
|
|
277
|
+
const secondScript = writeCodeGraphPackage(secondBin);
|
|
278
|
+
withWindowsPath(t, `"${firstBin}";${secondBin}`);
|
|
279
|
+
|
|
280
|
+
assert.equal(findCodeGraphNodeScriptOnPath(), firstScript);
|
|
281
|
+
for (const [path, expected] of [
|
|
282
|
+
[`${firstBin};"${secondBin}"`, firstScript],
|
|
283
|
+
[`"${firstBin};${secondBin}`, secondScript],
|
|
284
|
+
[`${firstBin}";${secondBin}`, secondScript],
|
|
285
|
+
[`${secondBin};"${firstBin}"`, secondScript],
|
|
286
|
+
]) {
|
|
287
|
+
process.env.Path = path;
|
|
288
|
+
process.env.PATH = path;
|
|
289
|
+
assert.equal(findCodeGraphNodeScriptOnPath(), expected);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("Windows launcher passes command metacharacters as one literal argv value", async (t) => {
|
|
294
|
+
const binDir = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-bin-")));
|
|
295
|
+
t.after(() => rmSync(binDir, { recursive: true, force: true }));
|
|
296
|
+
writeCodeGraphPackage(binDir, "process.stdout.write(JSON.stringify(process.argv.slice(2)));\n");
|
|
297
|
+
const cwd = workspace(t);
|
|
298
|
+
if (process.platform !== "win32") {
|
|
299
|
+
const gitShim = join(binDir, "git");
|
|
300
|
+
writeFileSync(gitShim, "#!/bin/sh\nprintf '%s\\n' \"$PWD\"\n");
|
|
301
|
+
chmodSync(gitShim, 0o755);
|
|
302
|
+
}
|
|
303
|
+
withWindowsPath(t, binDir);
|
|
304
|
+
|
|
305
|
+
const previousPlatform = process.platform;
|
|
306
|
+
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
|
|
307
|
+
t.after(() => Object.defineProperty(process, "platform", { configurable: true, value: previousPlatform }));
|
|
308
|
+
|
|
309
|
+
const query = "literal & echo injected > shell-injection.txt";
|
|
310
|
+
const result = await createCodeGraphTool().execute(
|
|
311
|
+
"test",
|
|
312
|
+
{ operation: "query", query },
|
|
313
|
+
undefined,
|
|
314
|
+
undefined,
|
|
315
|
+
{ cwd } as ExtensionContext,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
assert.deepEqual(result.content, [{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: JSON.stringify(["query", "--path", cwd, "--limit", "10", "--", query]),
|
|
321
|
+
}]);
|
|
322
|
+
assert.equal(existsSync(join(cwd, "shell-injection.txt")), false);
|
|
323
|
+
});
|
|
@@ -92,7 +92,7 @@ test("SDD executor coverage is exhaustive against actual agent files", () => {
|
|
|
92
92
|
// the allowlist matches reality so coverage cannot silently drift.
|
|
93
93
|
const expected = [
|
|
94
94
|
"sdd-apply.md", "sdd-archive.md", "sdd-design.md", "sdd-explore.md",
|
|
95
|
-
"sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-research.md",
|
|
95
|
+
"sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-remediate.md", "sdd-research.md",
|
|
96
96
|
"sdd-spec.md", "sdd-status.md", "sdd-sync.md", "sdd-tasks.md",
|
|
97
97
|
"sdd-verify.md",
|
|
98
98
|
];
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { __testing } from "../../extensions/gentle-ai.ts";
|
|
8
|
+
import { CandidateViewRegistry } from "../../lib/review-candidate-view.ts";
|
|
8
9
|
import {
|
|
9
10
|
NATIVE_REVIEW_ERROR_CODE,
|
|
10
11
|
NativeReviewCliError,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
createNodeExecFileAdapter,
|
|
14
15
|
type ExecFileAdapter,
|
|
15
16
|
} from "../../lib/native-review-cli.ts";
|
|
17
|
+
import type { ReviewStatusV3 } from "../../lib/review-integration-v2.ts";
|
|
16
18
|
import { requireDevBinary } from "../support/native-binary-gate.ts";
|
|
17
19
|
|
|
18
20
|
// Organic RDD Parity: candidate-bound dev-binary journeys.
|
|
@@ -43,6 +45,35 @@ function bridgeAdapter(binary: string, calls: NativeCall[]): ExecFileAdapter {
|
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function selectedStatusFieldNames(stdout: string): string {
|
|
49
|
+
try {
|
|
50
|
+
const status = JSON.parse(stdout) as Record<string, unknown>;
|
|
51
|
+
const projection = status.projection as Record<string, unknown> | undefined;
|
|
52
|
+
const transition = status.next_transition as Record<string, unknown> | undefined;
|
|
53
|
+
const execute = transition?.execute as Record<string, unknown> | undefined;
|
|
54
|
+
const argument = Array.isArray(execute?.arguments) ? execute.arguments[0] as Record<string, unknown> | undefined : undefined;
|
|
55
|
+
const names = (prefix: string, value?: Record<string, unknown>) => value === undefined ? [] : Object.keys(value).sort().map((key) => `${prefix}.${key}`);
|
|
56
|
+
return [...names("status", status), ...names("status.projection", projection), ...names("status.next_transition", transition), ...names("status.next_transition.execute", execute), ...names("status.next_transition.execute.arguments[]", argument)].join(", ");
|
|
57
|
+
} catch {
|
|
58
|
+
return "status JSON";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function controllerResultStructure(result: Record<string, unknown>): string {
|
|
63
|
+
const diagnostics = typeof result.diagnostics === "object" && result.diagnostics !== null && !Array.isArray(result.diagnostics)
|
|
64
|
+
? result.diagnostics as Record<string, unknown>
|
|
65
|
+
: undefined;
|
|
66
|
+
const safeCode = (name: string, value: unknown) => typeof value === "string" && /^[a-z0-9][a-z0-9._-]*$/.test(value) ? `${name}=${value}` : undefined;
|
|
67
|
+
const safeBooleanNames = ["candidate_view_created", "candidate_view_pre_native", "native_invocation_attempted", "lineage_created", "mutation_performed", "reset_eligible"];
|
|
68
|
+
return [
|
|
69
|
+
`keys=[${Object.keys(result).sort().join(",")}]`,
|
|
70
|
+
...(diagnostics === undefined ? [] : [`diagnostics.keys=[${Object.keys(diagnostics).sort().join(",")}]`]),
|
|
71
|
+
safeCode("operation", result.operation), safeCode("status", result.status), safeCode("outcome", result.outcome), safeCode("reason", result.reason),
|
|
72
|
+
safeCode("diagnostics.error_code", diagnostics?.error_code), safeCode("diagnostics.code", diagnostics?.code), safeCode("diagnostics.reason", diagnostics?.reason),
|
|
73
|
+
...safeBooleanNames.flatMap((name) => typeof result[name] === "boolean" ? [`${name}=${String(result[name])}`] : []),
|
|
74
|
+
].filter((value): value is string => value !== undefined).join("; ");
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
function journeyNative(binary: string): { native: NativeReviewCliV216; calls: NativeCall[] } {
|
|
47
78
|
const calls: NativeCall[] = [];
|
|
48
79
|
return { native: new NativeReviewCliV216(bridgeAdapter(binary, calls), binary), calls };
|
|
@@ -202,6 +233,85 @@ test("dev-binary: an empty candidate exposes the current STATUS refusal and neve
|
|
|
202
233
|
assert.equal(calls.filter((arguments_) => arguments_.at(0) === "review" && arguments_.at(1) === "start").length, 0);
|
|
203
234
|
});
|
|
204
235
|
|
|
236
|
+
test("dev-binary: v7 intended-untracked selection decodes both selected statuses before START", { skip: !RUNNABLE }, async (t) => {
|
|
237
|
+
const cwd = repository(t);
|
|
238
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|
|
239
|
+
git(cwd, "add", "app.ts");
|
|
240
|
+
git(cwd, "commit", "-qm", "test: base candidate");
|
|
241
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
242
|
+
writeFileSync(join(cwd, "chosen.md"), "# Chosen\n");
|
|
243
|
+
|
|
244
|
+
const calls: NativeCall[] = [], selectedWireFields: string[] = [];
|
|
245
|
+
let interceptedStarts = 0;
|
|
246
|
+
let delegatedStarts = 0;
|
|
247
|
+
const startIntercept = new Error("test-only native START intercept");
|
|
248
|
+
const real = createNodeExecFileAdapter();
|
|
249
|
+
const adapter: ExecFileAdapter = async (request) => {
|
|
250
|
+
calls.push([...request.arguments]);
|
|
251
|
+
if (request.arguments.at(0) === "review" && request.arguments.at(1) === "start") {
|
|
252
|
+
interceptedStarts += 1;
|
|
253
|
+
throw startIntercept;
|
|
254
|
+
}
|
|
255
|
+
if (request.arguments.at(0) === "review" && request.arguments.at(1) === "start") delegatedStarts += 1;
|
|
256
|
+
const result = await real({ ...request, file: DEV_BINARY! });
|
|
257
|
+
if (request.arguments.some((argument) => argument.startsWith("--intended-untracked-selection="))) selectedWireFields.push(selectedStatusFieldNames(result.stdout));
|
|
258
|
+
return result;
|
|
259
|
+
};
|
|
260
|
+
const native = new NativeReviewCliV216(adapter, DEV_BINARY!);
|
|
261
|
+
const targetStatus = native.targetStatus.bind(native);
|
|
262
|
+
const selectedStatuses: ReviewStatusV3[] = [];
|
|
263
|
+
const selectedFailures: Array<{ call: number; code: string }> = [];
|
|
264
|
+
let selectedStatusCalls = 0;
|
|
265
|
+
native.targetStatus = async (request) => {
|
|
266
|
+
if (request.intendedUntrackedSelection === undefined) return targetStatus(request);
|
|
267
|
+
selectedStatusCalls += 1;
|
|
268
|
+
try {
|
|
269
|
+
const status = await targetStatus(request);
|
|
270
|
+
selectedStatuses.push(status);
|
|
271
|
+
return status;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
selectedFailures.push({ call: selectedStatusCalls, code: error instanceof NativeReviewCliError ? error.code : "unexpected-error" });
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const candidateViews = new CandidateViewRegistry(), retainedSelections = new Map();
|
|
279
|
+
await enableReview(native, cwd);
|
|
280
|
+
const initial = await __testing.executeReviewControllerOperation({ operation: "status" }, cwd, native, undefined, candidateViews, undefined, retainedSelections);
|
|
281
|
+
assert.equal((initial.result as Record<string, unknown> | undefined)?.schema, "gentle-ai.review-integration.status/v7", "initial status.schema");
|
|
282
|
+
assert.equal(typeof initial.selectionBinding, "string", "initial selectionBinding");
|
|
283
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: initial.selectionBinding as string, intendedUntracked: ["chosen.md"] } as never, cwd, native, undefined, candidateViews, undefined, retainedSelections);
|
|
284
|
+
|
|
285
|
+
if (selectedStatusCalls !== 2) assert.fail(`intended-selection STATUS call count=${selectedStatusCalls}; ${controllerResultStructure(result)}`);
|
|
286
|
+
if (selectedFailures.length > 0) {
|
|
287
|
+
const failure = selectedFailures[0]!;
|
|
288
|
+
assert.fail(`selected STATUS call ${failure.call} decode failed at fields: ${selectedWireFields[failure.call - 1] ?? "status"}; error=${failure.code}`);
|
|
289
|
+
}
|
|
290
|
+
assert.equal(selectedStatusCalls, 2, "intended-selection STATUS call count");
|
|
291
|
+
assert.equal(selectedStatuses.length, 2, "decoded intended-selection STATUS count");
|
|
292
|
+
for (const [index, status] of selectedStatuses.entries()) {
|
|
293
|
+
assert.equal(status.raw.schema, "gentle-ai.review-integration.status/v7", `selected STATUS call ${index + 1} status.schema`);
|
|
294
|
+
assert.deepEqual(status.projection.intendedUntracked, ["chosen.md"], `selected STATUS call ${index + 1} status.projection.intended_untracked`);
|
|
295
|
+
assert.equal(status.nextTransition?.kind, "execute", `selected STATUS call ${index + 1} status.next_transition.kind`);
|
|
296
|
+
assert.equal(status.nextTransition?.execute?.operation, "review.start", `selected STATUS call ${index + 1} status.next_transition.execute.operation`);
|
|
297
|
+
assert.ok((status.nextTransition?.execute?.arguments.length ?? 0) > 0, `selected STATUS call ${index + 1} status.next_transition.execute.arguments`);
|
|
298
|
+
assert.equal(status.nextTransition?.execute?.arguments.every((argument) => typeof argument.token === "string" && argument.token.length > 0), true, `selected STATUS call ${index + 1} status.next_transition.execute.arguments[].token`);
|
|
299
|
+
}
|
|
300
|
+
assert.equal(interceptedStarts, 1, "adapter must intercept the one native review.start attempt");
|
|
301
|
+
assert.equal(delegatedStarts, 0, "intercepted native START must not reach the real adapter");
|
|
302
|
+
assert.equal(calls.filter((arguments_) => arguments_.at(0) === "review" && arguments_.at(1) === "start").length, 1, "native review.start adapter attempt count");
|
|
303
|
+
assert.equal(result.outcome, "native-mutation-status-reconciled", "controller result after intentional START intercept");
|
|
304
|
+
assert.equal("lineage_id" in result, false, "intercepted START must report no lineage binding");
|
|
305
|
+
assert.equal("authority" in result, false, "intercepted START must report no authority binding");
|
|
306
|
+
assert.equal("lineage_created" in result, false, "reconciled unknown mutation must not claim lineage creation");
|
|
307
|
+
assert.equal("mutation_performed" in result, false, "reconciled unknown mutation must not claim a mutation result");
|
|
308
|
+
assert.equal(result.mutation_outcome, "unknown", "the controller cannot prove mutation status after an intercepted launch");
|
|
309
|
+
assert.match(String(result.required_status_action), /Run target-scoped review\.status/, "ambiguous START must retain its executable-safe STATUS instruction");
|
|
310
|
+
assert.equal(result.next_action, "start", "the reconciled provider action remains visible without authoring a replay");
|
|
311
|
+
const after = await native.targetStatus({ cwd, agent: "pi" });
|
|
312
|
+
assert.equal(after.authority, undefined, "post-intercept STATUS must not expose authority");
|
|
313
|
+
});
|
|
314
|
+
|
|
205
315
|
test("dev-binary: a low-risk START closes the review with no receipt or follow-up capture", { skip: !RUNNABLE }, async (t) => {
|
|
206
316
|
const cwd = repository(t);
|
|
207
317
|
writeFileSync(join(cwd, "guide.md"), "# Guide\n\nBase\n");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// Reuse Pi's existing YAML dependency without adding a package dependency.
|
|
7
|
+
const piRequire = createRequire(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
8
|
+
const { parseDocument } = piRequire("yaml");
|
|
9
|
+
const formUrl = new URL("../.github/ISSUE_TEMPLATE/feature_request.yml", import.meta.url);
|
|
10
|
+
|
|
11
|
+
function readForm() {
|
|
12
|
+
const document = parseDocument(readFileSync(formUrl, "utf8"), { uniqueKeys: true });
|
|
13
|
+
assert.deepEqual(document.errors, [], "the issue form must be valid, unambiguous YAML");
|
|
14
|
+
return document.toJS();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("feature request form has supported structure and only intake labels", () => {
|
|
18
|
+
const form = readForm();
|
|
19
|
+
assert.deepEqual(Object.keys(form).sort(), ["body", "description", "labels", "name"]);
|
|
20
|
+
assert.equal(form.name, "Feature Request");
|
|
21
|
+
assert.equal(typeof form.description, "string");
|
|
22
|
+
assert.ok(form.description.trim().length > 0);
|
|
23
|
+
// Exact allowlist also rejects approval, exception, and other privileged labels.
|
|
24
|
+
assert.deepEqual(form.labels, ["enhancement", "status:needs-review"]);
|
|
25
|
+
assert.ok(Array.isArray(form.body));
|
|
26
|
+
assert.deepEqual(form.body.map((field) => field.id), [
|
|
27
|
+
"preflight", "problem", "outcome", "alternatives", "context",
|
|
28
|
+
]);
|
|
29
|
+
for (const field of form.body) {
|
|
30
|
+
assert.ok(["checkboxes", "textarea"].includes(field.type));
|
|
31
|
+
assert.match(field.id, /^[a-zA-Z0-9_-]+$/);
|
|
32
|
+
assert.equal(typeof field.attributes.label, "string");
|
|
33
|
+
assert.ok(field.attributes.label.trim().length > 0);
|
|
34
|
+
assert.deepEqual(Object.keys(field).sort(), field.type === "checkboxes"
|
|
35
|
+
? ["attributes", "id", "type"]
|
|
36
|
+
: ["attributes", "id", "type", "validations"]);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("duplicate and privacy affirmations are individually required", () => {
|
|
41
|
+
const [preflight] = readForm().body;
|
|
42
|
+
assert.equal(preflight.type, "checkboxes");
|
|
43
|
+
assert.deepEqual(preflight.attributes.options, [
|
|
44
|
+
{
|
|
45
|
+
label: "I searched open and closed issues and did not find a request for this feature.",
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "I reviewed this request and removed credentials, tokens, private paths, hostnames, and other sensitive data.",
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("feature questions require problem and outcome but keep alternatives and context optional", () => {
|
|
56
|
+
const questions = readForm().body.slice(1);
|
|
57
|
+
assert.deepEqual(questions.map((field) => ({
|
|
58
|
+
type: field.type,
|
|
59
|
+
label: field.attributes.label,
|
|
60
|
+
validations: field.validations,
|
|
61
|
+
})), [
|
|
62
|
+
{ type: "textarea", label: "Problem or opportunity", validations: { required: true } },
|
|
63
|
+
{ type: "textarea", label: "Proposed outcome", validations: { required: true } },
|
|
64
|
+
{ type: "textarea", label: "Alternatives considered", validations: { required: false } },
|
|
65
|
+
{ type: "textarea", label: "Additional context", validations: { required: false } },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const descendant = spawn(process.execPath, ["-e", "process.on('SIGTERM', () => {}); process.stdout.write('ready'); setInterval(() => {}, 1000);"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
4
|
+
descendant.stdout.once("data", () => {
|
|
5
|
+
console.log(`DESCENDANT:${descendant.pid}`);
|
|
6
|
+
if (process.env.AGENTS_PROCESS_CHILD_EXIT_AFTER_READY === "1") process.exit(0);
|
|
7
|
+
});
|
|
8
|
+
process.on("SIGTERM", () => {
|
|
9
|
+
if (process.env.AGENTS_PROCESS_CHILD_EXIT_ON_TERM === "1") process.exit(0);
|
|
10
|
+
});
|
|
11
|
+
process.stdin.setEncoding("utf8");
|
|
12
|
+
process.stdin.on("data", (chunk) => {
|
|
13
|
+
for (const line of chunk.split("\n")) {
|
|
14
|
+
if (!line) continue;
|
|
15
|
+
try {
|
|
16
|
+
const command = JSON.parse(line);
|
|
17
|
+
process.stdout.write(`${JSON.stringify({ type: "response", id: command.id, success: true })}\n`);
|
|
18
|
+
} catch {
|
|
19
|
+
// Ignore malformed input; the runner owns the RPC protocol.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
setInterval(() => {}, 1000);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_sha256": "26be905d39fe8274e049e1270553695b7540a3b24571d42eb7d96c5a7deb48b5",
|
|
3
|
+
"batches": [
|
|
4
|
+
"{\"schema\":\"gentle-ai.telemetry-runtime-aggregate/v1\",\"registry\":1,\"host\":\"pi\",\"rows\":[{\"model\":{\"provider\":\"openai\",\"id\":\"gpt-5.4\"},\"model_evidence\":\"response\",\"agent_kind\":\"built_in\",\"agent_class\":\"worker\",\"selected_effort\":\"high\",\"effective_effort\":\"low\",\"launches\":null,\"responses\":1,\"input_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"output_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"cache_read_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"cache_creation_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"reasoning_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"total_tokens\":{\"reported\":1,\"unavailable\":0,\"unsupported\":0,\"sum\":7},\"error_category\":\"none\",\"duration\":{\"kind\":\"unavailable\",\"measured_count\":0,\"sum_ms\":null}}]}"
|
|
5
|
+
]
|
|
6
|
+
}
|