gentle-pi 2.1.2 → 2.3.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 +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
-
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
@@ -11,7 +11,6 @@ import { matchesKey } from "@earendil-works/pi-tui";
|
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
13
13
|
import { domainHashV1 } from "../lib/review-canonical.ts";
|
|
14
|
-
import { NativeReviewCliV216, NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "../lib/native-review-cli.ts";
|
|
15
14
|
|
|
16
15
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
16
|
const EXTENSIONS = [
|
|
@@ -32,18 +31,23 @@ const EXPECTED_BANNER_COMMANDS = [
|
|
|
32
31
|
const EXPECTED_COMMANDS = [
|
|
33
32
|
"gentle:install-sdd",
|
|
34
33
|
"gentle:sdd-preflight",
|
|
35
|
-
"sdd-status",
|
|
36
|
-
"sdd-continue",
|
|
34
|
+
"gentle-sdd-status",
|
|
35
|
+
"gentle-sdd-continue",
|
|
37
36
|
"gentle:models",
|
|
38
37
|
"gentle:persona",
|
|
39
38
|
"gentle:status",
|
|
40
39
|
"gentle:doctor",
|
|
41
|
-
"sdd-init",
|
|
40
|
+
"gentle-sdd-init",
|
|
42
41
|
"skill-registry:refresh",
|
|
43
42
|
...EXPECTED_BANNER_COMMANDS,
|
|
44
43
|
];
|
|
45
44
|
|
|
46
45
|
const FORBIDDEN_COMPAT_COMMANDS = [
|
|
46
|
+
// The SDD entry points carry the gentle- prefix so they read identically in
|
|
47
|
+
// Claude Code and Pi. The bare names are retired without an alias.
|
|
48
|
+
"sdd-init",
|
|
49
|
+
"sdd-continue",
|
|
50
|
+
"sdd-status",
|
|
47
51
|
"gentle-ai:install-sdd",
|
|
48
52
|
"gentle-ai:sdd-preflight",
|
|
49
53
|
"gentle-ai:sdd-status",
|
|
@@ -65,10 +69,28 @@ function createPi() {
|
|
|
65
69
|
const commands = new Map();
|
|
66
70
|
const flags = new Map();
|
|
67
71
|
const tools = new Map();
|
|
72
|
+
const eventHandlers = new Map();
|
|
73
|
+
const emittedEvents = [];
|
|
68
74
|
const flagValues = new Map([["no-skill-registry", true]]);
|
|
75
|
+
const events = {
|
|
76
|
+
emit(channel, data) {
|
|
77
|
+
emittedEvents.push({ channel, data });
|
|
78
|
+
for (const handler of eventHandlers.get(channel) ?? []) handler(data);
|
|
79
|
+
},
|
|
80
|
+
on(channel, handler) {
|
|
81
|
+
const handlers = eventHandlers.get(channel) ?? new Set();
|
|
82
|
+
handlers.add(handler);
|
|
83
|
+
eventHandlers.set(channel, handlers);
|
|
84
|
+
return () => {
|
|
85
|
+
handlers.delete(handler);
|
|
86
|
+
if (handlers.size === 0) eventHandlers.delete(channel);
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
69
90
|
let activeTools = ["read", "bash", "edit", "write"];
|
|
70
91
|
|
|
71
92
|
const pi = {
|
|
93
|
+
events,
|
|
72
94
|
on(name, handler) {
|
|
73
95
|
const list = hooks.get(name) ?? [];
|
|
74
96
|
list.push(handler);
|
|
@@ -109,7 +131,7 @@ function createPi() {
|
|
|
109
131
|
},
|
|
110
132
|
};
|
|
111
133
|
|
|
112
|
-
return { pi, hooks, commands, flags, tools };
|
|
134
|
+
return { pi, hooks, commands, flags, tools, emittedEvents };
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
function createUi() {
|
|
@@ -181,6 +203,15 @@ async function tempWorkspace() {
|
|
|
181
203
|
return mkdtemp(join(tmpdir(), "gentle-pi-runtime-"));
|
|
182
204
|
}
|
|
183
205
|
|
|
206
|
+
function restoreWorkspaceWritePermissions(cwd) {
|
|
207
|
+
if (process.platform === "win32") return;
|
|
208
|
+
try {
|
|
209
|
+
execFileSync("chmod", ["-R", "u+w", cwd], { stdio: "ignore" });
|
|
210
|
+
} catch {
|
|
211
|
+
// A prior candidate-view cleanup may already have removed the workspace.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
async function loadExtensions(pi) {
|
|
185
216
|
for (const [index, rel] of EXTENSIONS.entries()) {
|
|
186
217
|
const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
|
|
@@ -198,9 +229,122 @@ async function run() {
|
|
|
198
229
|
process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
|
|
199
230
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
200
231
|
const globalSubagentsPath = join(globalAgentHome, "subagents.json");
|
|
201
|
-
const { pi, hooks, commands, flags, tools } = createPi();
|
|
232
|
+
const { pi, hooks, commands, flags, tools, emittedEvents } = createPi();
|
|
202
233
|
await loadExtensions(pi);
|
|
203
234
|
|
|
235
|
+
// gentle-pi#404: a collect binding that returns the native last-event
|
|
236
|
+
// closure must terminate after one capture. It must not re-enter a public
|
|
237
|
+
// lifecycle mutation or synthesize a follow-up transition.
|
|
238
|
+
{
|
|
239
|
+
const lineageId = "runtime-last-event";
|
|
240
|
+
const sha = `sha256:${"a".repeat(64)}`;
|
|
241
|
+
const tree = "b".repeat(40);
|
|
242
|
+
const repositoryContext = `rctx1_${"c".repeat(64)}`;
|
|
243
|
+
const calls = [];
|
|
244
|
+
const arguments_ = [
|
|
245
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
246
|
+
{ name: "expected-revision", value: sha, token: `--expected-revision=${sha}` },
|
|
247
|
+
{ name: "target", value: sha, token: `--target=${sha}` },
|
|
248
|
+
{ name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
|
|
249
|
+
{ name: "lens", value: "review-risk", token: "--lens=review-risk" },
|
|
250
|
+
{ name: "order", value: "0", token: "--order=0" },
|
|
251
|
+
{ name: "subject-hash", value: sha, token: `--subject-hash=${sha}` },
|
|
252
|
+
];
|
|
253
|
+
const input = {
|
|
254
|
+
name: "correction_plan",
|
|
255
|
+
schema: "gentle-ai.review-correction-plan/v1",
|
|
256
|
+
captureOperation: "review.capture-correction-plan",
|
|
257
|
+
arguments: arguments_,
|
|
258
|
+
submission: {
|
|
259
|
+
operationToken: "capture-correction-plan",
|
|
260
|
+
argumentTokens: [...arguments_.map((argument) => argument.token), "--correction-lines={{value}}"],
|
|
261
|
+
values: [{ slot: "correction_lines", domain: "positive_integer", substitutionLocation: 7, minimum: 1, maximum: 1 }],
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
const status = {
|
|
265
|
+
contract: "gentle-ai.review-integration/v2",
|
|
266
|
+
applicability: "current_target",
|
|
267
|
+
authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: sha },
|
|
268
|
+
receipt: { status: "expected_missing" },
|
|
269
|
+
action: "stop",
|
|
270
|
+
replayability: "not_replayable",
|
|
271
|
+
targetIdentity: sha,
|
|
272
|
+
projection: {
|
|
273
|
+
schema: "gentle-ai.review-candidate-projection/v1",
|
|
274
|
+
kind: "current-changes",
|
|
275
|
+
projection: "workspace",
|
|
276
|
+
baseTree: tree,
|
|
277
|
+
initialReviewTree: tree,
|
|
278
|
+
currentCandidateTree: tree,
|
|
279
|
+
pathsDigest: sha,
|
|
280
|
+
paths: ["app.ts"],
|
|
281
|
+
intendedUntracked: [],
|
|
282
|
+
intendedUntrackedProof: sha,
|
|
283
|
+
initialSnapshotIdentity: sha,
|
|
284
|
+
currentSnapshotIdentity: sha,
|
|
285
|
+
},
|
|
286
|
+
repair: { schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported", counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 }, supportedOperations: ["review/complete-fix", "review/validate-fix"], authorizationSchema: "gentle-ai.review-repair-authorization/v1" },
|
|
287
|
+
candidates: [],
|
|
288
|
+
nextTransition: { kind: "collect", reasonCode: "correction_plan_required", collect: { inputs: [input] } },
|
|
289
|
+
raw: { schema: "gentle-ai.review-integration.status/v5" },
|
|
290
|
+
};
|
|
291
|
+
const nativeReviewCli = {
|
|
292
|
+
async targetStatus(request) {
|
|
293
|
+
calls.push({ operation: "status", request });
|
|
294
|
+
return status;
|
|
295
|
+
},
|
|
296
|
+
async captureCorrectionPlan(request) {
|
|
297
|
+
calls.push({ operation: "capture-correction-plan", request });
|
|
298
|
+
return {
|
|
299
|
+
schema: "gentle-ai.review-last-event-closure/v1",
|
|
300
|
+
operation: "review.capture-correction-plan",
|
|
301
|
+
lineageId,
|
|
302
|
+
state: "correction_required",
|
|
303
|
+
targetIdentity: sha,
|
|
304
|
+
requestHash: sha,
|
|
305
|
+
correctionLines: 1,
|
|
306
|
+
storeRevision: sha,
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const lastEventPi = createPi();
|
|
311
|
+
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
312
|
+
createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
|
|
313
|
+
const controller = lastEventPi.tools.get("gentle_review");
|
|
314
|
+
const capture = lastEventPi.tools.get("gentle_review_capture");
|
|
315
|
+
assert.ok(controller, "runtime must register the public status controller");
|
|
316
|
+
assert.ok(capture, "runtime must register the one-slot capture tool");
|
|
317
|
+
assert.equal(controller.parameters.properties.operation.enum.includes("finalize"), false);
|
|
318
|
+
assert.equal(controller.parameters.properties.operation.enum.includes("validate"), false);
|
|
319
|
+
|
|
320
|
+
const publicStatus = await controller.execute(
|
|
321
|
+
"runtime-status",
|
|
322
|
+
{ operation: "status", lineageId },
|
|
323
|
+
undefined,
|
|
324
|
+
undefined,
|
|
325
|
+
createCtx(ROOT, false, lineageId),
|
|
326
|
+
);
|
|
327
|
+
const collectBindings = publicStatus.details.collectBindings;
|
|
328
|
+
assert.equal(publicStatus.details.status, "blocked");
|
|
329
|
+
assert.equal(collectBindings.length, 1);
|
|
330
|
+
|
|
331
|
+
const captured = await capture.execute(
|
|
332
|
+
"runtime-capture",
|
|
333
|
+
{ lineageId, collectBinding: collectBindings[0].collectBinding, correctionLines: 1 },
|
|
334
|
+
undefined,
|
|
335
|
+
undefined,
|
|
336
|
+
createCtx(ROOT, false, lineageId),
|
|
337
|
+
);
|
|
338
|
+
assert.equal(captured.details.status, "closed");
|
|
339
|
+
assert.equal(captured.details.outcome, "native-last-event-closure");
|
|
340
|
+
assert.equal(captured.details.closure.operation, "review.capture-correction-plan");
|
|
341
|
+
assert.deepEqual(
|
|
342
|
+
calls.map(({ operation }) => operation),
|
|
343
|
+
["status", "status", "capture-correction-plan"],
|
|
344
|
+
"last-event closure must make no follow-up lifecycle mutation",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
204
348
|
for (const name of EXPECTED_COMMANDS) {
|
|
205
349
|
assert.ok(commands.has(name), `missing command ${name}`);
|
|
206
350
|
}
|
|
@@ -217,6 +361,7 @@ async function run() {
|
|
|
217
361
|
assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
|
|
218
362
|
}
|
|
219
363
|
assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
|
|
364
|
+
assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
|
|
220
365
|
assert.deepEqual(
|
|
221
366
|
tools.get("gentle_review").parameters.properties.operation.enum.filter((operation) => operation.includes("supersession") || operation === "supersede" || operation === "reconcile-authority"),
|
|
222
367
|
["reconcile-authority"],
|
|
@@ -262,10 +407,13 @@ async function run() {
|
|
|
262
407
|
assert.doesNotMatch(promptResult.systemPrompt, /default\s*\|\s*sonnet\s*\|\s*Non-SDD general delegation/);
|
|
263
408
|
assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
|
|
264
409
|
assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
|
|
265
|
-
assert.
|
|
266
|
-
promptResult.systemPrompt
|
|
267
|
-
|
|
410
|
+
assert.ok(
|
|
411
|
+
promptResult.systemPrompt.includes(
|
|
412
|
+
`Package assets root: \`${join(ROOT, "assets")}\`. Lazy asset paths below are relative to this root.`,
|
|
413
|
+
),
|
|
414
|
+
"parent prompt must declare the one absolute root for relative lazy asset paths",
|
|
268
415
|
);
|
|
416
|
+
assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
|
|
269
417
|
assert.doesNotMatch(
|
|
270
418
|
promptResult.systemPrompt,
|
|
271
419
|
new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
@@ -339,10 +487,10 @@ async function run() {
|
|
|
339
487
|
assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
|
|
340
488
|
assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
|
|
341
489
|
const statusCtx = createCtx(promptCwd, true);
|
|
342
|
-
await commands.get("sdd-status").handler("status-demo --json", statusCtx);
|
|
490
|
+
await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
|
|
343
491
|
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-pi\.sdd-status"/);
|
|
344
492
|
const continueCtx = createCtx(promptCwd, true);
|
|
345
|
-
await commands.get("sdd-continue").handler("status-demo", continueCtx);
|
|
493
|
+
await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
|
|
346
494
|
assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Dispatcher/);
|
|
347
495
|
assert.match(continueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
|
|
348
496
|
const { execFileSync } = await import("node:child_process");
|
|
@@ -354,7 +502,7 @@ async function run() {
|
|
|
354
502
|
'{"schema":"gentle-ai.recovery-required/v1","change_name":"status-demo"}',
|
|
355
503
|
);
|
|
356
504
|
const blockedContinueCtx = createCtx(promptCwd, true);
|
|
357
|
-
await commands.get("sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
505
|
+
await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
358
506
|
assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
|
|
359
507
|
assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
|
|
360
508
|
} finally {
|
|
@@ -367,12 +515,11 @@ async function run() {
|
|
|
367
515
|
const ghPrCwd = await tempWorkspace();
|
|
368
516
|
try {
|
|
369
517
|
execFileSync("git", ["init"], { cwd: ghPrCwd, stdio: "ignore" });
|
|
370
|
-
const
|
|
518
|
+
const deliveryResult = await toolHook(
|
|
371
519
|
{ toolName: "bash", input: { command: "gh pr create --draft" } },
|
|
372
|
-
createCtx(ghPrCwd, true, "
|
|
520
|
+
createCtx(ghPrCwd, true, "ordinary-delivery-session"),
|
|
373
521
|
);
|
|
374
|
-
assert.equal(
|
|
375
|
-
assert.match(receiptGate.reason, /exactly derive.*--base/i);
|
|
522
|
+
assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs pull-request creation");
|
|
376
523
|
} finally {
|
|
377
524
|
await rm(ghPrCwd, { recursive: true, force: true });
|
|
378
525
|
}
|
|
@@ -385,6 +532,110 @@ async function run() {
|
|
|
385
532
|
assert.equal(missingReviewView.block, true);
|
|
386
533
|
assert.match(missingReviewView.reason, /candidate view/i);
|
|
387
534
|
assert.equal(reviewDispatch.task, "review", "blocked review dispatch must not mutate child input");
|
|
535
|
+
|
|
536
|
+
for (const [agent, label, task] of [
|
|
537
|
+
["gentle-ai-worker", "missing", "Implement the requested change."],
|
|
538
|
+
["gentle-ai-worker", "absolute", "## Allowed edit surfaces\n/tmp/outside.ts"],
|
|
539
|
+
["gentle-ai-worker", "Windows absolute", "## Allowed edit surfaces\nC:\\outside.ts"],
|
|
540
|
+
["gentle-ai-worker", "prose instead of paths", "## Allowed edit surfaces\nThe parent will determine the paths."],
|
|
541
|
+
["gentle-ai-worker", "repository root", "## Allowed edit surfaces\n."],
|
|
542
|
+
["gentle-ai-worker", "bare repository root", "## Allowed edit surfaces\n./"],
|
|
543
|
+
["gentle-ai-worker", "normalized bare repository root", "## Allowed edit surfaces\n.//"],
|
|
544
|
+
["gentle-ai-worker", "equivalent normalized bare repository root", "## Allowed edit surfaces\n././/"],
|
|
545
|
+
["worker", "generic writer missing", "Implement the requested change."],
|
|
546
|
+
]) {
|
|
547
|
+
const writerDispatch = { agent, task, mode: "task" };
|
|
548
|
+
const writerResult = await toolHook(
|
|
549
|
+
{ toolName: "subagent_run", input: writerDispatch },
|
|
550
|
+
createCtx(toolCwd),
|
|
551
|
+
);
|
|
552
|
+
assert.equal(writerResult?.block, true, `${label} writer scope must be blocked before dispatch`);
|
|
553
|
+
assert.match(writerResult?.reason ?? "", /derive|map/i);
|
|
554
|
+
assert.match(writerResult?.reason ?? "", /relaunch/i);
|
|
555
|
+
assert.match(writerResult?.reason ?? "", /do not ask.*human.*paths or globs/i);
|
|
556
|
+
assert.equal(writerDispatch.task, task, "writer guard must not mutate child input");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const scopedWriterDispatch = {
|
|
560
|
+
agent: "gentle-ai-worker",
|
|
561
|
+
task: "Implement the requested change.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs",
|
|
562
|
+
mode: "task",
|
|
563
|
+
};
|
|
564
|
+
assert.equal(
|
|
565
|
+
await toolHook({ toolName: "subagent_run", input: scopedWriterDispatch }, createCtx(toolCwd)),
|
|
566
|
+
undefined,
|
|
567
|
+
"a writer may dispatch with narrow task-scoped repository-relative paths",
|
|
568
|
+
);
|
|
569
|
+
assert.equal(
|
|
570
|
+
await toolHook(
|
|
571
|
+
{
|
|
572
|
+
toolName: "subagent_run",
|
|
573
|
+
input: {
|
|
574
|
+
agent: "worker",
|
|
575
|
+
task: "Implement the requested change.",
|
|
576
|
+
context: "## Allowed edit surfaces\n- assets/orchestrator.md",
|
|
577
|
+
mode: "task",
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
createCtx(toolCwd),
|
|
581
|
+
),
|
|
582
|
+
undefined,
|
|
583
|
+
"a writer may dispatch when context carries narrow task-scoped repository-relative paths",
|
|
584
|
+
);
|
|
585
|
+
for (const [label, input] of [
|
|
586
|
+
[
|
|
587
|
+
"valid scope followed by a repository-root scope",
|
|
588
|
+
{
|
|
589
|
+
agent: "gentle-ai-worker",
|
|
590
|
+
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\n\n## Allowed edit surfaces\n.",
|
|
591
|
+
mode: "task",
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
[
|
|
595
|
+
"valid task scope plus invalid context scope",
|
|
596
|
+
{
|
|
597
|
+
agent: "gentle-ai-worker",
|
|
598
|
+
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
599
|
+
context: "## Allowed edit surfaces\n.",
|
|
600
|
+
mode: "task",
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
[
|
|
604
|
+
"conflicting valid task and context scopes",
|
|
605
|
+
{
|
|
606
|
+
agent: "gentle-ai-worker",
|
|
607
|
+
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
608
|
+
context: "## Allowed edit surfaces\ntests/runtime-harness.mjs",
|
|
609
|
+
mode: "task",
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
]) {
|
|
613
|
+
const writerResult = await toolHook({ toolName: "subagent_run", input }, createCtx(toolCwd));
|
|
614
|
+
assert.equal(writerResult?.block, true, `${label} must block writer dispatch`);
|
|
615
|
+
}
|
|
616
|
+
assert.equal(
|
|
617
|
+
await toolHook(
|
|
618
|
+
{
|
|
619
|
+
toolName: "subagent_run",
|
|
620
|
+
input: {
|
|
621
|
+
agent: "gentle-ai-worker",
|
|
622
|
+
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs\n\n## Allowed edit surfaces\n- `tests/runtime-harness.mjs`\n- `extensions/gentle-ai.ts`",
|
|
623
|
+
mode: "task",
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
createCtx(toolCwd),
|
|
627
|
+
),
|
|
628
|
+
undefined,
|
|
629
|
+
"a writer may dispatch when multiple allowed edit surface sections are compatible",
|
|
630
|
+
);
|
|
631
|
+
assert.equal(
|
|
632
|
+
await toolHook(
|
|
633
|
+
{ toolName: "subagent_run", input: { agent: "scout", task: "Map the repository.", mode: "task" } },
|
|
634
|
+
createCtx(toolCwd),
|
|
635
|
+
),
|
|
636
|
+
undefined,
|
|
637
|
+
"non-writer subagents must remain unaffected",
|
|
638
|
+
);
|
|
388
639
|
const sensitiveRead = await toolHook({ toolName: "read", input: { path: join(toolCwd, ".env.local") } }, createCtx(toolCwd));
|
|
389
640
|
assert.equal(sensitiveRead.block, true);
|
|
390
641
|
assert.match(sensitiveRead.reason, /sensitive path/);
|
|
@@ -400,20 +651,74 @@ async function run() {
|
|
|
400
651
|
);
|
|
401
652
|
assert.equal(needsConfirm.block, true);
|
|
402
653
|
assert.match(needsConfirm.reason, /not confirmed/);
|
|
654
|
+
assert.deepEqual(
|
|
655
|
+
emittedEvents.map(({ channel, data }) => ({
|
|
656
|
+
channel,
|
|
657
|
+
state: data.state,
|
|
658
|
+
active: data.active,
|
|
659
|
+
label: data.label,
|
|
660
|
+
})),
|
|
661
|
+
[
|
|
662
|
+
{
|
|
663
|
+
channel: "pi-permission-system:permission-request",
|
|
664
|
+
state: "waiting",
|
|
665
|
+
active: undefined,
|
|
666
|
+
label: undefined,
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
channel: "herdr:blocked",
|
|
670
|
+
state: undefined,
|
|
671
|
+
active: true,
|
|
672
|
+
label: "Guarded command confirmation",
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
channel: "pi-permission-system:permission-request",
|
|
676
|
+
state: "denied",
|
|
677
|
+
active: undefined,
|
|
678
|
+
label: undefined,
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
channel: "herdr:blocked",
|
|
682
|
+
state: undefined,
|
|
683
|
+
active: false,
|
|
684
|
+
label: undefined,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
"guarded confirmation emits both lifecycle channels in order",
|
|
688
|
+
);
|
|
689
|
+
assert.equal(emittedEvents[0].data.requestId, emittedEvents[2].data.requestId);
|
|
690
|
+
emittedEvents.length = 0;
|
|
691
|
+
pi.events.emit("rpiv:ask-user:blocked", {
|
|
692
|
+
active: true,
|
|
693
|
+
question: "private runtime questionnaire",
|
|
694
|
+
answer: "private runtime answer",
|
|
695
|
+
path: "/private/runtime-path",
|
|
696
|
+
command: "private runtime command",
|
|
697
|
+
});
|
|
698
|
+
pi.events.emit("rpiv:ask-user:blocked", { active: true, duplicate: true });
|
|
699
|
+
pi.events.emit("rpiv:ask-user:blocked", { active: "true" });
|
|
700
|
+
pi.events.emit("rpiv:ask-user:other", { active: false });
|
|
701
|
+
pi.events.emit("rpiv:ask-user:blocked", { active: false });
|
|
702
|
+
const rpivHerdrEvents = emittedEvents.filter(({ channel }) => channel === "herdr:blocked");
|
|
703
|
+
assert.deepEqual(rpivHerdrEvents, [
|
|
704
|
+
{ channel: "herdr:blocked", data: { active: true, label: "Questionnaire awaiting input" } },
|
|
705
|
+
{ channel: "herdr:blocked", data: { active: false } },
|
|
706
|
+
]);
|
|
707
|
+
assert.doesNotMatch(JSON.stringify(rpivHerdrEvents), /private runtime/i);
|
|
708
|
+
emittedEvents.length = 0;
|
|
403
709
|
assert.equal(
|
|
404
710
|
dangerousReviewCtx.ui.notifications.length,
|
|
405
711
|
0,
|
|
406
|
-
"
|
|
712
|
+
"dangerous-command confirmation must not launch or announce review actors",
|
|
407
713
|
);
|
|
408
714
|
const commitCwd = await tempWorkspace();
|
|
409
715
|
try {
|
|
410
716
|
execFileSync("git", ["init"], { cwd: commitCwd, stdio: "ignore" });
|
|
411
|
-
const
|
|
717
|
+
const deliveryResult = await toolHook(
|
|
412
718
|
{ toolName: "bash", input: { command: "git commit -m bounded tracked.txt" } },
|
|
413
719
|
createCtx(commitCwd),
|
|
414
720
|
);
|
|
415
|
-
assert.equal(
|
|
416
|
-
assert.match(commitGate.reason, /exactly derive/i);
|
|
721
|
+
assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs commits");
|
|
417
722
|
} finally {
|
|
418
723
|
await rm(commitCwd, { recursive: true, force: true });
|
|
419
724
|
}
|
|
@@ -477,131 +782,13 @@ async function run() {
|
|
|
477
782
|
/Controller-owned review lineage/,
|
|
478
783
|
"a failed-closed dispatch must never inject a substituted candidate view into the lens sub-agent's task",
|
|
479
784
|
);
|
|
785
|
+
await chmod(view.root, 0o700);
|
|
480
786
|
registry.cleanup(view.token);
|
|
481
787
|
} finally {
|
|
788
|
+
restoreWorkspaceWritePermissions(candidateDriftCwd);
|
|
482
789
|
await rm(candidateDriftCwd, { recursive: true, force: true });
|
|
483
790
|
}
|
|
484
791
|
|
|
485
|
-
// Corrective v2 lifecycle settling test: the real controller must call the
|
|
486
|
-
// native evidence surface before it accepts a targeted-validation document.
|
|
487
|
-
// This is intentionally controller-level rather than a pure resolver test;
|
|
488
|
-
// the ordered call trace proves production wiring and the fail-closed gate.
|
|
489
|
-
const correctionCwd = await tempWorkspace();
|
|
490
|
-
try {
|
|
491
|
-
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
492
|
-
const { CandidateViewRegistry } = await import(pathToFileURL(join(ROOT, "lib/review-candidate-view.ts")).href);
|
|
493
|
-
gitSync(correctionCwd, "init", "-b", "main");
|
|
494
|
-
await writeFile(join(correctionCwd, "app.ts"), "export const value = 1;\n");
|
|
495
|
-
gitSync(correctionCwd, "add", "app.ts");
|
|
496
|
-
gitSync(correctionCwd, "-c", "user.name=Runtime Harness", "-c", "user.email=runtime-harness@example.invalid", "commit", "-m", "base");
|
|
497
|
-
await writeFile(join(correctionCwd, "app.ts"), "export const value = 2;\n");
|
|
498
|
-
const frozen = new CandidateViewRegistry().create({ contributorRoot: correctionCwd });
|
|
499
|
-
const sha = (digit) => `sha256:${digit.repeat(64)}`;
|
|
500
|
-
const repair = { schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported", counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 }, supportedOperations: ["review/complete-fix", "review/validate-fix"], authorizationSchema: "gentle-ai.review-repair-authorization/v1" };
|
|
501
|
-
const projection = { schema: "gentle-ai.review-integration.projection/v1", kind: "current-changes", projection: "workspace", baseTree: frozen.baseTree, initialReviewTree: frozen.candidateTree, currentCandidateTree: frozen.candidateTree, pathsDigest: sha("a"), paths: frozen.paths, intendedUntracked: [], intendedUntrackedProof: sha("b"), initialSnapshotIdentity: sha("c"), currentSnapshotIdentity: sha("c") };
|
|
502
|
-
const status = { contract: "gentle-ai.review-integration/v2", applicability: "current_target", authority: { version: "compact-v2", lineageId: "runtime-correction", state: "correction_required", generation: 1, revision: sha("d") }, receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable", frozen: { tier: "medium", originalChangedLines: 1, correctionBudget: 1 }, targetIdentity: sha("e"), projection, repair, candidates: [], nextTransition: { kind: "collect", reasonCode: "verification_evidence_required", collect: { inputs: [{ name: "verification_evidence", schema: "gentle-ai.review-verification-evidence/v2", captureOperation: "review.capture-evidence", arguments: [{ name: "lineage", value: "runtime-correction" }] }] } }, raw: {} };
|
|
503
|
-
const validationRequest = { schema: "gentle-ai.review-targeted-validation-request/v1", requestHash: sha("f"), lineageId: "runtime-correction", expectedRevision: sha("d"), targetIdentity: sha("e"), fixFindingIds: [], projection: "workspace", correctionCandidateTree: frozen.candidateTree, correctionTargetIdentity: sha("e"), correctionPaths: frozen.paths, correctionPathsDigest: sha("a") };
|
|
504
|
-
const afterCapture = { ...status, authority: { ...status.authority, state: "validating" }, validationRequest, nextTransition: { kind: "collect", reasonCode: "targeted_validation_required", collect: { inputs: [{ name: "targeted_validation", schema: validationRequest.schema, captureOperation: "external.run_targeted_validation", arguments: [{ name: "lineage", value: "runtime-correction" }], validationRequest }] } } };
|
|
505
|
-
frozen.cleanup();
|
|
506
|
-
const calls = [];
|
|
507
|
-
let statuses = 0;
|
|
508
|
-
const nativeReviewCli = {
|
|
509
|
-
async targetStatus() { calls.push("status"); statuses += 1; return statuses === 1 ? status : afterCapture; },
|
|
510
|
-
async captureEvidence(request) { calls.push("capture-evidence"); return { schema: "gentle-ai.review-verification-evidence/v2", version: 2, lineageId: "runtime-correction", authorityRevision: sha("d"), targetIdentity: sha("e"), candidateTree: frozen.candidateTree, pathsDigest: sha("a"), paths: frozen.paths, ledgerIds: [], rawPayloadSha256: sha("1"), rawPayloadBytes: request.evidenceDocument.length, outcome: "passed", recordDigest: sha("2") }; },
|
|
511
|
-
async finalize() { calls.push("finalize"); return { lineageId: "runtime-correction", state: "approved", action: "approved", storeRevision: sha("3") }; },
|
|
512
|
-
async start() { throw new Error("not expected"); }, async validate() { throw new Error("not expected"); }, async bindSdd() { throw new Error("not expected"); }, async sddStatus() { return { ready: false }; }, async reviewStatus() { throw new Error("not expected"); },
|
|
513
|
-
};
|
|
514
|
-
const correctionPi = createPi();
|
|
515
|
-
createGentleAiExtension({ nativeReviewCli, candidateViews: new CandidateViewRegistry() })(correctionPi.pi);
|
|
516
|
-
const controller = correctionPi.tools.get("gentle_review");
|
|
517
|
-
const response = await controller.execute("runtime-correction", { operation: "finalize", lineageId: "runtime-correction", input: JSON.stringify({ final_evidence: "focused verification passed", final_verification_outcome: "passed", validation: { request_hash: "f".repeat(64), correction_ids: [], original_criteria: { passed: true, evidence: ["acceptance passes"] }, correction_regression: { passed: true, evidence: ["regression passes"] }, fix_caused_findings: [], follow_ups: [] } }) }, undefined, undefined, createCtx(correctionCwd));
|
|
518
|
-
assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize"]);
|
|
519
|
-
assert.equal(response.details.result.state, "approved");
|
|
520
|
-
} finally {
|
|
521
|
-
await rm(correctionCwd, { recursive: true, force: true });
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Task 11.2 (migrate-review-integration-v2): an unimplemented
|
|
525
|
-
// next_transition.execute.operation (e.g. a future "dispose-result") must
|
|
526
|
-
// raise a typed, named unsupported-transition-operation refusal and stop —
|
|
527
|
-
// never fall through to a generic schema-incompatible error, and never
|
|
528
|
-
// synthesize an invocation for it. Drives the real negotiated STATUS decode
|
|
529
|
-
// path (NativeReviewCliV216.targetStatus) against a stubbed adapter, so no
|
|
530
|
-
// live binary needs to advertise the unimplemented operation to prove this.
|
|
531
|
-
{
|
|
532
|
-
const capabilitiesFixturePath = join(
|
|
533
|
-
ROOT, "contracts", "review-integration", "v2", "fixtures", "capabilities.fixture.json",
|
|
534
|
-
);
|
|
535
|
-
const capabilitiesFixture = JSON.parse(await readFile(capabilitiesFixturePath, "utf8"));
|
|
536
|
-
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
537
|
-
const capabilitiesBody = {
|
|
538
|
-
...capabilitiesFixture,
|
|
539
|
-
package: { ...capabilitiesFixture.package, version: "2.2.2" },
|
|
540
|
-
};
|
|
541
|
-
const statusBody = {
|
|
542
|
-
schema: "gentle-ai.review-integration.status/v3",
|
|
543
|
-
contract: "gentle-ai.review-integration/v2",
|
|
544
|
-
operation: "review.status",
|
|
545
|
-
applicability: "unrelated",
|
|
546
|
-
receipt: { status: "not_applicable" },
|
|
547
|
-
action: "start",
|
|
548
|
-
replayability: "not_replayable",
|
|
549
|
-
target_identity: `sha256:${"a".repeat(64)}`,
|
|
550
|
-
repair: {
|
|
551
|
-
schema: "gentle-ai.review-authority-repair-assessment/v1",
|
|
552
|
-
status: "unsupported",
|
|
553
|
-
counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
|
|
554
|
-
supported_operations: ["review/complete-fix", "review/validate-fix"],
|
|
555
|
-
authorization_schema: "gentle-ai.review-repair-authorization/v1",
|
|
556
|
-
},
|
|
557
|
-
projection: {
|
|
558
|
-
schema: "gentle-ai.review-integration.projection/v1",
|
|
559
|
-
kind: "current-changes",
|
|
560
|
-
projection: "workspace",
|
|
561
|
-
base_tree: "b".repeat(40),
|
|
562
|
-
initial_review_tree: "b".repeat(40),
|
|
563
|
-
current_candidate_tree: "b".repeat(40),
|
|
564
|
-
paths_digest: `sha256:${"a".repeat(64)}`,
|
|
565
|
-
paths: [],
|
|
566
|
-
intended_untracked: [],
|
|
567
|
-
intended_untracked_proof: `sha256:${"a".repeat(64)}`,
|
|
568
|
-
initial_snapshot_identity: `sha256:${"a".repeat(64)}`,
|
|
569
|
-
current_snapshot_identity: `sha256:${"a".repeat(64)}`,
|
|
570
|
-
},
|
|
571
|
-
candidates: [],
|
|
572
|
-
next_transition: {
|
|
573
|
-
kind: "execute",
|
|
574
|
-
reason_code: "disposable_result_pending",
|
|
575
|
-
execute: {
|
|
576
|
-
operation: "review.dispose-result",
|
|
577
|
-
arguments: [],
|
|
578
|
-
preconditions: [],
|
|
579
|
-
binding: { target_identity: `sha256:${"a".repeat(64)}` },
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
};
|
|
583
|
-
const calls = [];
|
|
584
|
-
const adapter = async (request) => {
|
|
585
|
-
calls.push(request.arguments);
|
|
586
|
-
const body = calls.length === 1 ? capabilitiesBody : statusBody;
|
|
587
|
-
return { stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
588
|
-
};
|
|
589
|
-
const client = new NativeReviewCliV216(
|
|
590
|
-
adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024,
|
|
591
|
-
async () => undefined, () => executableDigest,
|
|
592
|
-
);
|
|
593
|
-
let thrown;
|
|
594
|
-
try {
|
|
595
|
-
await client.targetStatus({ cwd: "/repo" });
|
|
596
|
-
} catch (error) {
|
|
597
|
-
thrown = error;
|
|
598
|
-
}
|
|
599
|
-
assert.ok(thrown instanceof NativeReviewCliError, "an unimplemented next_transition.execute.operation must raise a typed NativeReviewCliError");
|
|
600
|
-
assert.equal(thrown.code, NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION);
|
|
601
|
-
assert.match(thrown.message, /review\.dispose-result/, "the refusal must name the specific unimplemented operation");
|
|
602
|
-
assert.equal(calls.length, 2, "capabilities negotiation plus one STATUS call — never a third call synthesizing the unimplemented operation");
|
|
603
|
-
}
|
|
604
|
-
|
|
605
792
|
const bannerCwd = await tempWorkspace();
|
|
606
793
|
try {
|
|
607
794
|
const ctx = createCtx(bannerCwd, true);
|
|
@@ -624,6 +811,76 @@ async function run() {
|
|
|
624
811
|
await rm(join(globalConfigHome, "banner.json"), { force: true });
|
|
625
812
|
}
|
|
626
813
|
|
|
814
|
+
// issue-301: cancelling the color picker must be a no-op — no write,
|
|
815
|
+
// no notify, and the previously saved color must survive byte/semantically
|
|
816
|
+
// unchanged. Covers both entry points: /gentle:banner-color picker
|
|
817
|
+
// (cancelled), and /gentle:banner -> Color row -> nested picker (cancelled).
|
|
818
|
+
// Also covers an invalid non-empty argument, which must still open the
|
|
819
|
+
// picker and treat its cancellation as a no-op.
|
|
820
|
+
const cancelPickerCwd = await tempWorkspace();
|
|
821
|
+
try {
|
|
822
|
+
const bannerConfigPath = join(globalConfigHome, "banner.json");
|
|
823
|
+
const seeded = {
|
|
824
|
+
showRose: false,
|
|
825
|
+
showTextLogo: false,
|
|
826
|
+
color: "green",
|
|
827
|
+
};
|
|
828
|
+
const seededJson = `${JSON.stringify(seeded, null, 2)}\n`;
|
|
829
|
+
await writeFile(bannerConfigPath, seededJson, "utf8");
|
|
830
|
+
|
|
831
|
+
const cancelCtx = createCtx(cancelPickerCwd, true);
|
|
832
|
+
|
|
833
|
+
// (a) /gentle:banner-color picker cancelled: seeded color unchanged, no notify.
|
|
834
|
+
cancelCtx.ui.notifications.length = 0;
|
|
835
|
+
cancelCtx.ui.selections.length = 0;
|
|
836
|
+
cancelCtx.ui.select = async (label, options) => {
|
|
837
|
+
cancelCtx.ui.selections.push({ label, options });
|
|
838
|
+
return undefined;
|
|
839
|
+
};
|
|
840
|
+
await commands.get("gentle:banner-color").handler("", cancelCtx);
|
|
841
|
+
let afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
842
|
+
assert.equal(afterCancel, seededJson, "banner-color cancel must not rewrite banner.json");
|
|
843
|
+
assert.equal(cancelCtx.ui.selections.length, 1, "banner-color cancel must open the picker once");
|
|
844
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color cancel must not notify");
|
|
845
|
+
|
|
846
|
+
// (d) invalid non-empty /gentle:banner-color input still opens picker;
|
|
847
|
+
// cancelling it is a no-op.
|
|
848
|
+
cancelCtx.ui.notifications.length = 0;
|
|
849
|
+
cancelCtx.ui.selections.length = 0;
|
|
850
|
+
await commands.get("gentle:banner-color").handler("purple", cancelCtx);
|
|
851
|
+
afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
852
|
+
assert.equal(afterCancel, seededJson, "banner-color invalid+cancel must not rewrite banner.json");
|
|
853
|
+
assert.equal(cancelCtx.ui.selections.length, 1, "invalid banner-color arg must still open the picker");
|
|
854
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color invalid+cancel must not notify");
|
|
855
|
+
|
|
856
|
+
// (b) /gentle:banner selects the Color row, then the nested picker is
|
|
857
|
+
// cancelled: seeded color unchanged, no notify. The outer select
|
|
858
|
+
// returns the Color row; the nested select returns undefined.
|
|
859
|
+
cancelCtx.ui.notifications.length = 0;
|
|
860
|
+
cancelCtx.ui.selections.length = 0;
|
|
861
|
+
let selectCall = 0;
|
|
862
|
+
cancelCtx.ui.select = async (label, options) => {
|
|
863
|
+
cancelCtx.ui.selections.push({ label, options });
|
|
864
|
+
selectCall += 1;
|
|
865
|
+
// First call: outer "Startup banner" menu -> pick the Color row.
|
|
866
|
+
// Second call: nested color picker -> cancel (undefined).
|
|
867
|
+
return selectCall === 1 ? options[options.length - 1] : undefined;
|
|
868
|
+
};
|
|
869
|
+
await commands.get("gentle:banner").handler("", cancelCtx);
|
|
870
|
+
afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
871
|
+
assert.equal(afterCancel, seededJson, "banner Color-row cancel must not rewrite banner.json");
|
|
872
|
+
assert.equal(cancelCtx.ui.selections.length, 2, "banner Color-row flow must open outer then nested picker");
|
|
873
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner Color-row cancel must not notify");
|
|
874
|
+
|
|
875
|
+
// Sanity: the seeded config round-trips through normalization unchanged,
|
|
876
|
+
// proving the byte equality above is semantic, not a test artifact.
|
|
877
|
+
const reparsed = JSON.parse(await readFile(bannerConfigPath, "utf8"));
|
|
878
|
+
assert.deepEqual(reparsed, seeded, "seeded non-default color must round-trip semantically");
|
|
879
|
+
} finally {
|
|
880
|
+
await rm(cancelPickerCwd, { recursive: true, force: true });
|
|
881
|
+
await rm(join(globalConfigHome, "banner.json"), { force: true });
|
|
882
|
+
}
|
|
883
|
+
|
|
627
884
|
const noUiCwd = await tempWorkspace();
|
|
628
885
|
try {
|
|
629
886
|
for (const handler of hooks.get("session_start")) {
|
|
@@ -640,22 +897,22 @@ async function run() {
|
|
|
640
897
|
"session_start must not install project-local SDD chains",
|
|
641
898
|
);
|
|
642
899
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
900
|
+
// gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
|
|
901
|
+
// installation must not (re)create them.
|
|
643
902
|
const installedRefuterPath = join(globalAgentHome, "agents", "review-refuter.md");
|
|
644
|
-
assert.equal(existsSync(installedRefuterPath),
|
|
645
|
-
assert.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
"
|
|
903
|
+
assert.equal(existsSync(installedRefuterPath), false, "the retired review-refuter agent must not be installed");
|
|
904
|
+
assert.equal(
|
|
905
|
+
existsSync(join(globalAgentHome, "agents", "review-validator.md")),
|
|
906
|
+
false,
|
|
907
|
+
"the retired review-validator agent must not be installed",
|
|
649
908
|
);
|
|
650
|
-
const
|
|
651
|
-
assert.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
909
|
+
const installedExplorePath = join(globalAgentHome, "agents", "gentle-ai-explore.md");
|
|
910
|
+
assert.equal(existsSync(installedExplorePath), true);
|
|
911
|
+
assert.deepEqual(
|
|
912
|
+
readAgentDefinition(await readFile(installedExplorePath, "utf8")),
|
|
913
|
+
{ name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
|
|
914
|
+
"isolated package installation must activate only the explorer inspection tools",
|
|
655
915
|
);
|
|
656
|
-
assert.match(installedRefuterSource, /complete inferential-severe frozen-row list once/);
|
|
657
|
-
assert.match(installedRefuterSource, /refuted \| corroborated \| inconclusive/);
|
|
658
|
-
assert.doesNotMatch(installedRefuterSource, /general, correctness, impact\/exploitability, or reproducibility/);
|
|
659
916
|
const installedRiskSource = await readFile(
|
|
660
917
|
join(globalAgentHome, "agents", "review-risk.md"),
|
|
661
918
|
"utf8",
|
|
@@ -685,6 +942,13 @@ async function run() {
|
|
|
685
942
|
].join("\n");
|
|
686
943
|
await writeFile(installedRefuterPath, samePathUserRefuter);
|
|
687
944
|
delete managedAssetsManifest.assets["agents/review-refuter.md"];
|
|
945
|
+
// Retirement sweep: a hash-proven package-managed copy of a retired
|
|
946
|
+
// asset is deleted on refresh; the user-authored same-path refuter
|
|
947
|
+
// above must survive because its hash proves nothing.
|
|
948
|
+
const retiredManagedValidatorPath = join(globalAgentHome, "agents", "review-validator.md");
|
|
949
|
+
const retiredManagedValidator = "stale managed validator\n";
|
|
950
|
+
await writeFile(retiredManagedValidatorPath, retiredManagedValidator);
|
|
951
|
+
managedAssetsManifest.assets["agents/review-validator.md"] = sha256(retiredManagedValidator);
|
|
688
952
|
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
689
953
|
const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
|
|
690
954
|
await writeFile(userRefuterOverride, "user refuter override must stay\n");
|
|
@@ -738,6 +1002,24 @@ async function run() {
|
|
|
738
1002
|
"user refuter override must stay\n",
|
|
739
1003
|
"package refresh must not rewrite or certify an explicit user refuter",
|
|
740
1004
|
);
|
|
1005
|
+
assert.equal(
|
|
1006
|
+
existsSync(retiredManagedValidatorPath),
|
|
1007
|
+
false,
|
|
1008
|
+
"session refresh must delete a hash-proven package-managed copy of a retired asset",
|
|
1009
|
+
);
|
|
1010
|
+
const refreshedManagedAssets = JSON.parse(
|
|
1011
|
+
await readFile(managedAssetsManifestPath, "utf8"),
|
|
1012
|
+
);
|
|
1013
|
+
assert.equal(
|
|
1014
|
+
refreshedManagedAssets.assets["agents/review-validator.md"],
|
|
1015
|
+
undefined,
|
|
1016
|
+
"a retired asset must lose package-managed ownership",
|
|
1017
|
+
);
|
|
1018
|
+
assert.equal(
|
|
1019
|
+
refreshedManagedAssets.assets["agents/review-refuter.md"],
|
|
1020
|
+
undefined,
|
|
1021
|
+
"a user-authored same-path retired asset must stay unowned",
|
|
1022
|
+
);
|
|
741
1023
|
} finally {
|
|
742
1024
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
743
1025
|
}
|
|
@@ -811,17 +1093,13 @@ async function run() {
|
|
|
811
1093
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
812
1094
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
813
1095
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
814
|
-
assert.equal(ctx.ui.selections.length,
|
|
815
|
-
assert.
|
|
816
|
-
assert.equal(ctx.ui.selections[1].label, "SDD artifact store");
|
|
817
|
-
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
818
|
-
assert.equal(ctx.ui.selections[2].label, "SDD PR chaining");
|
|
819
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: user prompt/);
|
|
1096
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic SDD triggers must not render confirmation-only selectors");
|
|
1097
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: canonical default or persisted preference/);
|
|
820
1098
|
assert.deepEqual(
|
|
821
1099
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
822
1100
|
{ action: "continue" },
|
|
823
1101
|
);
|
|
824
|
-
assert.equal(ctx.ui.selections.length,
|
|
1102
|
+
assert.equal(ctx.ui.selections.length, 0, "natural SDD triggers reuse preferences without prompts");
|
|
825
1103
|
assert.deepEqual(
|
|
826
1104
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
827
1105
|
{ action: "continue" },
|
|
@@ -834,7 +1112,7 @@ async function run() {
|
|
|
834
1112
|
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
835
1113
|
{ action: "continue" },
|
|
836
1114
|
);
|
|
837
|
-
assert.equal(ctx.ui.selections.length,
|
|
1115
|
+
assert.equal(ctx.ui.selections.length, 0, "slash SDD triggers use automatic defaults without prompts");
|
|
838
1116
|
|
|
839
1117
|
assert.deepEqual(
|
|
840
1118
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
@@ -862,19 +1140,18 @@ async function run() {
|
|
|
862
1140
|
"global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
|
|
863
1141
|
);
|
|
864
1142
|
}
|
|
865
|
-
assert.equal(ctx.ui.selections.length,
|
|
866
|
-
assert.
|
|
867
|
-
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
1143
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic SDD routing must not render confirmation-only selectors");
|
|
1144
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: canonical default or persisted preference/);
|
|
868
1145
|
await commands.get("gentle:status").handler("", ctx);
|
|
869
1146
|
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
|
|
870
1147
|
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
871
1148
|
|
|
872
1149
|
await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
|
|
873
|
-
assert.equal(ctx.ui.selections.length,
|
|
1150
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic preflight should remain prompt-free for the session");
|
|
874
1151
|
const promptHook = hooks.get("before_agent_start")[0];
|
|
875
1152
|
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
876
1153
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
877
|
-
assert.match(promptResult.systemPrompt, /Execution mode:
|
|
1154
|
+
assert.match(promptResult.systemPrompt, /Execution mode: auto/);
|
|
878
1155
|
const workerPromptResult = await promptHook(
|
|
879
1156
|
{ agentName: "worker", systemPrompt: "worker base" },
|
|
880
1157
|
ctx,
|
|
@@ -889,7 +1166,7 @@ async function run() {
|
|
|
889
1166
|
await rm(globalModelsPath, { force: true });
|
|
890
1167
|
}
|
|
891
1168
|
|
|
892
|
-
for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change"].entries()) {
|
|
1169
|
+
for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/gentle-sdd-init"].entries()) {
|
|
893
1170
|
const slashSddCwd = await tempWorkspace();
|
|
894
1171
|
try {
|
|
895
1172
|
const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
|
|
@@ -899,7 +1176,7 @@ async function run() {
|
|
|
899
1176
|
});
|
|
900
1177
|
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
901
1178
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
902
|
-
assert.equal(ctx.ui.selections.length,
|
|
1179
|
+
assert.equal(ctx.ui.selections.length, 0, `${text} should use automatic preflight without selectors`);
|
|
903
1180
|
} finally {
|
|
904
1181
|
await rm(slashSddCwd, { recursive: true, force: true });
|
|
905
1182
|
}
|
|
@@ -911,9 +1188,11 @@ async function run() {
|
|
|
911
1188
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
912
1189
|
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
913
1190
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
914
|
-
assert.equal(ctx.ui.selections.length,
|
|
1191
|
+
assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
|
|
1192
|
+
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
|
|
1193
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
915
1194
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
916
|
-
assert.equal(ctx.ui.selections.length,
|
|
1195
|
+
assert.equal(ctx.ui.selections.length, 4, "explicit preflight remains an intentional re-prompt");
|
|
917
1196
|
} finally {
|
|
918
1197
|
await rm(commandSddCwd, { recursive: true, force: true });
|
|
919
1198
|
}
|
|
@@ -932,7 +1211,7 @@ async function run() {
|
|
|
932
1211
|
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
933
1212
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
934
1213
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
935
|
-
assert.equal(ctx.ui.selections.length,
|
|
1214
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic SDD-agent startup must not render selectors");
|
|
936
1215
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
937
1216
|
assert.doesNotMatch(
|
|
938
1217
|
promptResult.systemPrompt,
|
|
@@ -953,7 +1232,7 @@ async function run() {
|
|
|
953
1232
|
},
|
|
954
1233
|
ctx,
|
|
955
1234
|
);
|
|
956
|
-
assert.equal(ctx.ui.selections.length,
|
|
1235
|
+
assert.equal(ctx.ui.selections.length, 0, "SDD-agent startup should reuse automatic preferences");
|
|
957
1236
|
assert.doesNotMatch(
|
|
958
1237
|
reusedPromptResult.systemPrompt,
|
|
959
1238
|
/el Gentleman Identity and Harness/,
|
|
@@ -975,7 +1254,7 @@ async function run() {
|
|
|
975
1254
|
ctx,
|
|
976
1255
|
);
|
|
977
1256
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
978
|
-
assert.match(promptResult.systemPrompt, /
|
|
1257
|
+
assert.match(promptResult.systemPrompt, /canonical defaults or persisted choices/);
|
|
979
1258
|
assert.equal(ctx.ui.selections.length, 0);
|
|
980
1259
|
assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
981
1260
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
@@ -1001,18 +1280,94 @@ async function run() {
|
|
|
1001
1280
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1002
1281
|
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
1003
1282
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1004
|
-
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "
|
|
1283
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
|
|
1005
1284
|
} finally {
|
|
1006
1285
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1007
1286
|
await rm(engramSddCwd, { recursive: true, force: true });
|
|
1008
1287
|
}
|
|
1009
1288
|
|
|
1289
|
+
// Issue #64: selecting engram as the artifact store must not cause
|
|
1290
|
+
// /gentle-sdd-init to create openspec/ or openspec/config.yaml.
|
|
1291
|
+
const engramSddInitCwd = await tempWorkspace();
|
|
1292
|
+
try {
|
|
1293
|
+
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1294
|
+
const ctx = createCtx(engramSddInitCwd, true, "engram-sdd-init-session");
|
|
1295
|
+
ctx.ui.select = async (label, options) => {
|
|
1296
|
+
if (label === "SDD artifact store") return "engram";
|
|
1297
|
+
return options[0];
|
|
1298
|
+
};
|
|
1299
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1300
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1301
|
+
assert.equal(
|
|
1302
|
+
existsSync(join(engramSddInitCwd, "openspec")),
|
|
1303
|
+
false,
|
|
1304
|
+
"/gentle-sdd-init must not create openspec/ when artifactStore is engram",
|
|
1305
|
+
);
|
|
1306
|
+
assert.equal(
|
|
1307
|
+
existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
|
|
1308
|
+
false,
|
|
1309
|
+
"/gentle-sdd-init must not write openspec/config.yaml when artifactStore is engram",
|
|
1310
|
+
);
|
|
1311
|
+
assert.doesNotMatch(
|
|
1312
|
+
ctx.ui.notifications.at(-1).message,
|
|
1313
|
+
/Wrote openspec\/config\.yaml/,
|
|
1314
|
+
"/gentle-sdd-init must not announce openspec/config.yaml when artifactStore is engram",
|
|
1315
|
+
);
|
|
1316
|
+
assert.match(
|
|
1317
|
+
ctx.ui.notifications.at(-1).message,
|
|
1318
|
+
/SDD initialized for engram:/,
|
|
1319
|
+
);
|
|
1320
|
+
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1321
|
+
} finally {
|
|
1322
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1323
|
+
await rm(engramSddInitCwd, { recursive: true, force: true });
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Issue #64 counterpart: the engram skip must stay narrow. Selecting both
|
|
1327
|
+
// still has to create the full openspec/ scaffold and write config.yaml,
|
|
1328
|
+
// so an over-broad skip is caught here instead of in the field.
|
|
1329
|
+
const bothSddInitCwd = await tempWorkspace();
|
|
1330
|
+
try {
|
|
1331
|
+
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1332
|
+
const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
|
|
1333
|
+
ctx.ui.select = async (label, options) => {
|
|
1334
|
+
if (label === "SDD artifact store") return "hybrid";
|
|
1335
|
+
return options[0];
|
|
1336
|
+
};
|
|
1337
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1338
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1339
|
+
assert.equal(
|
|
1340
|
+
existsSync(join(bothSddInitCwd, "openspec", "specs")),
|
|
1341
|
+
true,
|
|
1342
|
+
"/gentle-sdd-init must create openspec/specs when artifactStore is both",
|
|
1343
|
+
);
|
|
1344
|
+
assert.equal(
|
|
1345
|
+
existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
|
|
1346
|
+
true,
|
|
1347
|
+
"/gentle-sdd-init must create openspec/changes/archive when artifactStore is both",
|
|
1348
|
+
);
|
|
1349
|
+
assert.equal(
|
|
1350
|
+
existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
|
|
1351
|
+
true,
|
|
1352
|
+
"/gentle-sdd-init must write openspec/config.yaml when artifactStore is both",
|
|
1353
|
+
);
|
|
1354
|
+
assert.match(
|
|
1355
|
+
ctx.ui.notifications.at(-1).message,
|
|
1356
|
+
/Wrote openspec\/config\.yaml/,
|
|
1357
|
+
"/gentle-sdd-init must announce openspec/config.yaml when artifactStore is both",
|
|
1358
|
+
);
|
|
1359
|
+
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1360
|
+
} finally {
|
|
1361
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1362
|
+
await rm(bothSddInitCwd, { recursive: true, force: true });
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1010
1365
|
const directEngramToolCwd = await tempWorkspace();
|
|
1011
1366
|
try {
|
|
1012
1367
|
pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
|
|
1013
1368
|
const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
|
|
1014
1369
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1015
|
-
assert.
|
|
1370
|
+
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "unrecognized Engram capability must elide the artifact selector");
|
|
1016
1371
|
} finally {
|
|
1017
1372
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1018
1373
|
await rm(directEngramToolCwd, { recursive: true, force: true });
|
|
@@ -1062,7 +1417,7 @@ async function run() {
|
|
|
1062
1417
|
const sddCwd = await tempWorkspace();
|
|
1063
1418
|
try {
|
|
1064
1419
|
const ctx = createCtx(sddCwd, true);
|
|
1065
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1420
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1066
1421
|
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1067
1422
|
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1068
1423
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
@@ -1070,12 +1425,12 @@ async function run() {
|
|
|
1070
1425
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
1071
1426
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1072
1427
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1073
|
-
assert.equal(ctx.ui.selections.length,
|
|
1428
|
+
assert.equal(ctx.ui.selections.length, 0, "sdd-init uses automatic preflight defaults");
|
|
1074
1429
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
1075
1430
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1076
1431
|
|
|
1077
1432
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1078
|
-
assert.equal(ctx.ui.selections.length,
|
|
1433
|
+
assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts after automatic sdd-init");
|
|
1079
1434
|
} finally {
|
|
1080
1435
|
await rm(sddCwd, { recursive: true, force: true });
|
|
1081
1436
|
}
|
|
@@ -1089,7 +1444,7 @@ async function run() {
|
|
|
1089
1444
|
);
|
|
1090
1445
|
await writeFile(globalModelsPath, "{ invalid json");
|
|
1091
1446
|
const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
|
|
1092
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1447
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1093
1448
|
assert.equal(ctx.ui.notifications[0].level, "warning");
|
|
1094
1449
|
assert.match(ctx.ui.notifications[0].message, /Model routing skipped:/);
|
|
1095
1450
|
assert.match(ctx.ui.notifications[0].message, /models\.json/);
|
|
@@ -1699,6 +2054,44 @@ async function run() {
|
|
|
1699
2054
|
const restoredAgent = await readFile(join(modelsCwd, ".pi", "agents", "sdd-apply.md"), "utf8");
|
|
1700
2055
|
assert.match(restoredAgent, /model: restore\/provider/);
|
|
1701
2056
|
assert.match(restoredAgent, /thinking: high/);
|
|
2057
|
+
|
|
2058
|
+
// issue #286: `thinking: "max"` must survive save normalization and
|
|
2059
|
+
// reach both subagents.json (effort) and agent frontmatter (thinking).
|
|
2060
|
+
ctx.ui.custom = () =>
|
|
2061
|
+
Promise.resolve({
|
|
2062
|
+
type: "save",
|
|
2063
|
+
config: { "sdd-apply": { model: "openai/gpt-5", thinking: "max" } },
|
|
2064
|
+
});
|
|
2065
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
2066
|
+
const maxSavedConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
|
|
2067
|
+
assert.equal(maxSavedConfig["sdd-apply"].thinking, "max");
|
|
2068
|
+
const maxSubagents = JSON.parse(
|
|
2069
|
+
await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
2070
|
+
);
|
|
2071
|
+
assert.equal(maxSubagents.model_profiles["sdd-apply"].effort, "max");
|
|
2072
|
+
const maxApplyAgent = await readFile(
|
|
2073
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
2074
|
+
"utf8",
|
|
2075
|
+
);
|
|
2076
|
+
assert.match(maxApplyAgent, /thinking: max/);
|
|
2077
|
+
|
|
2078
|
+
// issue #286: effort picker must offer `max` after `xhigh` and save it.
|
|
2079
|
+
let maxPickerCalls = 0;
|
|
2080
|
+
ctx.ui.custom = (factory) =>
|
|
2081
|
+
new Promise((resolve) => {
|
|
2082
|
+
maxPickerCalls += 1;
|
|
2083
|
+
const panel = factory(null, null, null, resolve);
|
|
2084
|
+
if (maxPickerCalls === 1) {
|
|
2085
|
+
panel.handleInput(kittyE); // open effort picker (set-all row)
|
|
2086
|
+
for (let i = 0; i < 7; i++) panel.handleInput("j"); // max
|
|
2087
|
+
panel.handleInput("\r");
|
|
2088
|
+
panel.handleInput("\u0013"); // ctrl+s saves the draft
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
2093
|
+
const pickerMaxConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
|
|
2094
|
+
assert.equal(pickerMaxConfig["sdd-apply"].thinking, "max");
|
|
1702
2095
|
} finally {
|
|
1703
2096
|
await rm(modelsCwd, { recursive: true, force: true });
|
|
1704
2097
|
await rm(globalModelsPath, { force: true });
|