gentle-pi 2.2.0 → 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 +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- 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/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- 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/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- 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/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -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/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- 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 +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- 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/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- 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 -530
|
@@ -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,8 +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 { resolveGentleAiBinary } from "../lib/gentle-ai-binary.ts";
|
|
15
|
-
import { NativeReviewCliV216, NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "../lib/native-review-cli.ts";
|
|
16
14
|
|
|
17
15
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
16
|
const EXTENSIONS = [
|
|
@@ -33,18 +31,23 @@ const EXPECTED_BANNER_COMMANDS = [
|
|
|
33
31
|
const EXPECTED_COMMANDS = [
|
|
34
32
|
"gentle:install-sdd",
|
|
35
33
|
"gentle:sdd-preflight",
|
|
36
|
-
"sdd-status",
|
|
37
|
-
"sdd-continue",
|
|
34
|
+
"gentle-sdd-status",
|
|
35
|
+
"gentle-sdd-continue",
|
|
38
36
|
"gentle:models",
|
|
39
37
|
"gentle:persona",
|
|
40
38
|
"gentle:status",
|
|
41
39
|
"gentle:doctor",
|
|
42
|
-
"sdd-init",
|
|
40
|
+
"gentle-sdd-init",
|
|
43
41
|
"skill-registry:refresh",
|
|
44
42
|
...EXPECTED_BANNER_COMMANDS,
|
|
45
43
|
];
|
|
46
44
|
|
|
47
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",
|
|
48
51
|
"gentle-ai:install-sdd",
|
|
49
52
|
"gentle-ai:sdd-preflight",
|
|
50
53
|
"gentle-ai:sdd-status",
|
|
@@ -66,10 +69,28 @@ function createPi() {
|
|
|
66
69
|
const commands = new Map();
|
|
67
70
|
const flags = new Map();
|
|
68
71
|
const tools = new Map();
|
|
72
|
+
const eventHandlers = new Map();
|
|
73
|
+
const emittedEvents = [];
|
|
69
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
|
+
};
|
|
70
90
|
let activeTools = ["read", "bash", "edit", "write"];
|
|
71
91
|
|
|
72
92
|
const pi = {
|
|
93
|
+
events,
|
|
73
94
|
on(name, handler) {
|
|
74
95
|
const list = hooks.get(name) ?? [];
|
|
75
96
|
list.push(handler);
|
|
@@ -110,7 +131,7 @@ function createPi() {
|
|
|
110
131
|
},
|
|
111
132
|
};
|
|
112
133
|
|
|
113
|
-
return { pi, hooks, commands, flags, tools };
|
|
134
|
+
return { pi, hooks, commands, flags, tools, emittedEvents };
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
function createUi() {
|
|
@@ -182,6 +203,15 @@ async function tempWorkspace() {
|
|
|
182
203
|
return mkdtemp(join(tmpdir(), "gentle-pi-runtime-"));
|
|
183
204
|
}
|
|
184
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
|
+
|
|
185
215
|
async function loadExtensions(pi) {
|
|
186
216
|
for (const [index, rel] of EXTENSIONS.entries()) {
|
|
187
217
|
const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
|
|
@@ -197,28 +227,124 @@ async function run() {
|
|
|
197
227
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
198
228
|
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
199
229
|
process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
|
|
200
|
-
// The harness already sandboxes its config and agent homes; the review kill
|
|
201
|
-
// switch is the one piece of machine state it was still inheriting. Several
|
|
202
|
-
// lifecycle-gate assertions below only reach their subject while
|
|
203
|
-
// receipt-driven development is on — with it off, gateLifecycleCommand
|
|
204
|
-
// returns undefined before deriving any target, which is correct behavior
|
|
205
|
-
// but not what those assertions are about.
|
|
206
|
-
//
|
|
207
|
-
// gentle-ai v2.4.0 made the switch opt-in, so an unconfigured machine now
|
|
208
|
-
// resolves to off. Until then the harness passed or failed on whatever the
|
|
209
|
-
// operator happened to have set, which made it green locally and red on a
|
|
210
|
-
// fresh CI runner from identical bytes. It now owns a sandbox HOME and opts
|
|
211
|
-
// in explicitly, the same way a user does, so the gates are what is under
|
|
212
|
-
// test rather than the machine.
|
|
213
|
-
const reviewHome = await tempWorkspace();
|
|
214
|
-
process.env.HOME = reviewHome;
|
|
215
|
-
const reviewModeEnable = execFileSync(resolveGentleAiBinary(ROOT, process.platform), ["review", "mode", "enable", "--scope", "global", "--json"], { cwd: ROOT, encoding: "utf8", env: { ...process.env, HOME: reviewHome } });
|
|
216
|
-
assert.match(reviewModeEnable, /"effective": "on"/, "the harness sandbox HOME must have receipt-driven development explicitly enabled");
|
|
217
230
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
218
231
|
const globalSubagentsPath = join(globalAgentHome, "subagents.json");
|
|
219
|
-
const { pi, hooks, commands, flags, tools } = createPi();
|
|
232
|
+
const { pi, hooks, commands, flags, tools, emittedEvents } = createPi();
|
|
220
233
|
await loadExtensions(pi);
|
|
221
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
|
+
|
|
222
348
|
for (const name of EXPECTED_COMMANDS) {
|
|
223
349
|
assert.ok(commands.has(name), `missing command ${name}`);
|
|
224
350
|
}
|
|
@@ -281,10 +407,13 @@ async function run() {
|
|
|
281
407
|
assert.doesNotMatch(promptResult.systemPrompt, /default\s*\|\s*sonnet\s*\|\s*Non-SDD general delegation/);
|
|
282
408
|
assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
|
|
283
409
|
assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
|
|
284
|
-
assert.
|
|
285
|
-
promptResult.systemPrompt
|
|
286
|
-
|
|
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",
|
|
287
415
|
);
|
|
416
|
+
assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
|
|
288
417
|
assert.doesNotMatch(
|
|
289
418
|
promptResult.systemPrompt,
|
|
290
419
|
new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
@@ -358,10 +487,10 @@ async function run() {
|
|
|
358
487
|
assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
|
|
359
488
|
assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
|
|
360
489
|
const statusCtx = createCtx(promptCwd, true);
|
|
361
|
-
await commands.get("sdd-status").handler("status-demo --json", statusCtx);
|
|
490
|
+
await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
|
|
362
491
|
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-pi\.sdd-status"/);
|
|
363
492
|
const continueCtx = createCtx(promptCwd, true);
|
|
364
|
-
await commands.get("sdd-continue").handler("status-demo", continueCtx);
|
|
493
|
+
await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
|
|
365
494
|
assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Dispatcher/);
|
|
366
495
|
assert.match(continueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
|
|
367
496
|
const { execFileSync } = await import("node:child_process");
|
|
@@ -373,7 +502,7 @@ async function run() {
|
|
|
373
502
|
'{"schema":"gentle-ai.recovery-required/v1","change_name":"status-demo"}',
|
|
374
503
|
);
|
|
375
504
|
const blockedContinueCtx = createCtx(promptCwd, true);
|
|
376
|
-
await commands.get("sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
505
|
+
await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
377
506
|
assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
|
|
378
507
|
assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
|
|
379
508
|
} finally {
|
|
@@ -386,12 +515,11 @@ async function run() {
|
|
|
386
515
|
const ghPrCwd = await tempWorkspace();
|
|
387
516
|
try {
|
|
388
517
|
execFileSync("git", ["init"], { cwd: ghPrCwd, stdio: "ignore" });
|
|
389
|
-
const
|
|
518
|
+
const deliveryResult = await toolHook(
|
|
390
519
|
{ toolName: "bash", input: { command: "gh pr create --draft" } },
|
|
391
|
-
createCtx(ghPrCwd, true, "
|
|
520
|
+
createCtx(ghPrCwd, true, "ordinary-delivery-session"),
|
|
392
521
|
);
|
|
393
|
-
assert.equal(
|
|
394
|
-
assert.match(receiptGate.reason, /exactly derive.*--base/i);
|
|
522
|
+
assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs pull-request creation");
|
|
395
523
|
} finally {
|
|
396
524
|
await rm(ghPrCwd, { recursive: true, force: true });
|
|
397
525
|
}
|
|
@@ -404,6 +532,110 @@ async function run() {
|
|
|
404
532
|
assert.equal(missingReviewView.block, true);
|
|
405
533
|
assert.match(missingReviewView.reason, /candidate view/i);
|
|
406
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
|
+
);
|
|
407
639
|
const sensitiveRead = await toolHook({ toolName: "read", input: { path: join(toolCwd, ".env.local") } }, createCtx(toolCwd));
|
|
408
640
|
assert.equal(sensitiveRead.block, true);
|
|
409
641
|
assert.match(sensitiveRead.reason, /sensitive path/);
|
|
@@ -419,20 +651,74 @@ async function run() {
|
|
|
419
651
|
);
|
|
420
652
|
assert.equal(needsConfirm.block, true);
|
|
421
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;
|
|
422
709
|
assert.equal(
|
|
423
710
|
dangerousReviewCtx.ui.notifications.length,
|
|
424
711
|
0,
|
|
425
|
-
"
|
|
712
|
+
"dangerous-command confirmation must not launch or announce review actors",
|
|
426
713
|
);
|
|
427
714
|
const commitCwd = await tempWorkspace();
|
|
428
715
|
try {
|
|
429
716
|
execFileSync("git", ["init"], { cwd: commitCwd, stdio: "ignore" });
|
|
430
|
-
const
|
|
717
|
+
const deliveryResult = await toolHook(
|
|
431
718
|
{ toolName: "bash", input: { command: "git commit -m bounded tracked.txt" } },
|
|
432
719
|
createCtx(commitCwd),
|
|
433
720
|
);
|
|
434
|
-
assert.equal(
|
|
435
|
-
assert.match(commitGate.reason, /exactly derive/i);
|
|
721
|
+
assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs commits");
|
|
436
722
|
} finally {
|
|
437
723
|
await rm(commitCwd, { recursive: true, force: true });
|
|
438
724
|
}
|
|
@@ -496,131 +782,13 @@ async function run() {
|
|
|
496
782
|
/Controller-owned review lineage/,
|
|
497
783
|
"a failed-closed dispatch must never inject a substituted candidate view into the lens sub-agent's task",
|
|
498
784
|
);
|
|
785
|
+
await chmod(view.root, 0o700);
|
|
499
786
|
registry.cleanup(view.token);
|
|
500
787
|
} finally {
|
|
788
|
+
restoreWorkspaceWritePermissions(candidateDriftCwd);
|
|
501
789
|
await rm(candidateDriftCwd, { recursive: true, force: true });
|
|
502
790
|
}
|
|
503
791
|
|
|
504
|
-
// Corrective v2 lifecycle settling test: the real controller must call the
|
|
505
|
-
// native evidence surface before it accepts a targeted-validation document.
|
|
506
|
-
// This is intentionally controller-level rather than a pure resolver test;
|
|
507
|
-
// the ordered call trace proves production wiring and the fail-closed gate.
|
|
508
|
-
const correctionCwd = await tempWorkspace();
|
|
509
|
-
try {
|
|
510
|
-
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
511
|
-
const { CandidateViewRegistry } = await import(pathToFileURL(join(ROOT, "lib/review-candidate-view.ts")).href);
|
|
512
|
-
gitSync(correctionCwd, "init", "-b", "main");
|
|
513
|
-
await writeFile(join(correctionCwd, "app.ts"), "export const value = 1;\n");
|
|
514
|
-
gitSync(correctionCwd, "add", "app.ts");
|
|
515
|
-
gitSync(correctionCwd, "-c", "user.name=Runtime Harness", "-c", "user.email=runtime-harness@example.invalid", "commit", "-m", "base");
|
|
516
|
-
await writeFile(join(correctionCwd, "app.ts"), "export const value = 2;\n");
|
|
517
|
-
const frozen = new CandidateViewRegistry().create({ contributorRoot: correctionCwd });
|
|
518
|
-
const sha = (digit) => `sha256:${digit.repeat(64)}`;
|
|
519
|
-
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" };
|
|
520
|
-
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") };
|
|
521
|
-
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: {} };
|
|
522
|
-
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") };
|
|
523
|
-
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 }] } } };
|
|
524
|
-
frozen.cleanup();
|
|
525
|
-
const calls = [];
|
|
526
|
-
let statuses = 0;
|
|
527
|
-
const nativeReviewCli = {
|
|
528
|
-
async targetStatus() { calls.push("status"); statuses += 1; return statuses === 1 ? status : afterCapture; },
|
|
529
|
-
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") }; },
|
|
530
|
-
async finalize() { calls.push("finalize"); return { lineageId: "runtime-correction", state: "approved", action: "approved", storeRevision: sha("3") }; },
|
|
531
|
-
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"); },
|
|
532
|
-
};
|
|
533
|
-
const correctionPi = createPi();
|
|
534
|
-
createGentleAiExtension({ nativeReviewCli, candidateViews: new CandidateViewRegistry() })(correctionPi.pi);
|
|
535
|
-
const controller = correctionPi.tools.get("gentle_review");
|
|
536
|
-
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));
|
|
537
|
-
assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize"]);
|
|
538
|
-
assert.equal(response.details.result.state, "approved");
|
|
539
|
-
} finally {
|
|
540
|
-
await rm(correctionCwd, { recursive: true, force: true });
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Task 11.2 (migrate-review-integration-v2): an unimplemented
|
|
544
|
-
// next_transition.execute.operation (e.g. a future "dispose-result") must
|
|
545
|
-
// raise a typed, named unsupported-transition-operation refusal and stop —
|
|
546
|
-
// never fall through to a generic schema-incompatible error, and never
|
|
547
|
-
// synthesize an invocation for it. Drives the real negotiated STATUS decode
|
|
548
|
-
// path (NativeReviewCliV216.targetStatus) against a stubbed adapter, so no
|
|
549
|
-
// live binary needs to advertise the unimplemented operation to prove this.
|
|
550
|
-
{
|
|
551
|
-
const capabilitiesFixturePath = join(
|
|
552
|
-
ROOT, "contracts", "review-integration", "v2", "fixtures", "capabilities.fixture.json",
|
|
553
|
-
);
|
|
554
|
-
const capabilitiesFixture = JSON.parse(await readFile(capabilitiesFixturePath, "utf8"));
|
|
555
|
-
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
556
|
-
const capabilitiesBody = {
|
|
557
|
-
...capabilitiesFixture,
|
|
558
|
-
package: { ...capabilitiesFixture.package, version: "2.4.0" },
|
|
559
|
-
};
|
|
560
|
-
const statusBody = {
|
|
561
|
-
schema: "gentle-ai.review-integration.status/v3",
|
|
562
|
-
contract: "gentle-ai.review-integration/v2",
|
|
563
|
-
operation: "review.status",
|
|
564
|
-
applicability: "unrelated",
|
|
565
|
-
receipt: { status: "not_applicable" },
|
|
566
|
-
action: "start",
|
|
567
|
-
replayability: "not_replayable",
|
|
568
|
-
target_identity: `sha256:${"a".repeat(64)}`,
|
|
569
|
-
repair: {
|
|
570
|
-
schema: "gentle-ai.review-authority-repair-assessment/v1",
|
|
571
|
-
status: "unsupported",
|
|
572
|
-
counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
|
|
573
|
-
supported_operations: ["review/complete-fix", "review/validate-fix"],
|
|
574
|
-
authorization_schema: "gentle-ai.review-repair-authorization/v1",
|
|
575
|
-
},
|
|
576
|
-
projection: {
|
|
577
|
-
schema: "gentle-ai.review-integration.projection/v1",
|
|
578
|
-
kind: "current-changes",
|
|
579
|
-
projection: "workspace",
|
|
580
|
-
base_tree: "b".repeat(40),
|
|
581
|
-
initial_review_tree: "b".repeat(40),
|
|
582
|
-
current_candidate_tree: "b".repeat(40),
|
|
583
|
-
paths_digest: `sha256:${"a".repeat(64)}`,
|
|
584
|
-
paths: [],
|
|
585
|
-
intended_untracked: [],
|
|
586
|
-
intended_untracked_proof: `sha256:${"a".repeat(64)}`,
|
|
587
|
-
initial_snapshot_identity: `sha256:${"a".repeat(64)}`,
|
|
588
|
-
current_snapshot_identity: `sha256:${"a".repeat(64)}`,
|
|
589
|
-
},
|
|
590
|
-
candidates: [],
|
|
591
|
-
next_transition: {
|
|
592
|
-
kind: "execute",
|
|
593
|
-
reason_code: "disposable_result_pending",
|
|
594
|
-
execute: {
|
|
595
|
-
operation: "review.dispose-result",
|
|
596
|
-
arguments: [],
|
|
597
|
-
preconditions: [],
|
|
598
|
-
binding: { target_identity: `sha256:${"a".repeat(64)}` },
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
const calls = [];
|
|
603
|
-
const adapter = async (request) => {
|
|
604
|
-
calls.push(request.arguments);
|
|
605
|
-
const body = calls.length === 1 ? capabilitiesBody : statusBody;
|
|
606
|
-
return { stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
607
|
-
};
|
|
608
|
-
const client = new NativeReviewCliV216(
|
|
609
|
-
adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024,
|
|
610
|
-
async () => undefined, () => executableDigest,
|
|
611
|
-
);
|
|
612
|
-
let thrown;
|
|
613
|
-
try {
|
|
614
|
-
await client.targetStatus({ cwd: "/repo" });
|
|
615
|
-
} catch (error) {
|
|
616
|
-
thrown = error;
|
|
617
|
-
}
|
|
618
|
-
assert.ok(thrown instanceof NativeReviewCliError, "an unimplemented next_transition.execute.operation must raise a typed NativeReviewCliError");
|
|
619
|
-
assert.equal(thrown.code, NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION);
|
|
620
|
-
assert.match(thrown.message, /review\.dispose-result/, "the refusal must name the specific unimplemented operation");
|
|
621
|
-
assert.equal(calls.length, 2, "capabilities negotiation plus one STATUS call — never a third call synthesizing the unimplemented operation");
|
|
622
|
-
}
|
|
623
|
-
|
|
624
792
|
const bannerCwd = await tempWorkspace();
|
|
625
793
|
try {
|
|
626
794
|
const ctx = createCtx(bannerCwd, true);
|
|
@@ -925,17 +1093,13 @@ async function run() {
|
|
|
925
1093
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
926
1094
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
927
1095
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
928
|
-
assert.equal(ctx.ui.selections.length,
|
|
929
|
-
assert.
|
|
930
|
-
assert.equal(ctx.ui.selections[1].label, "SDD artifact store");
|
|
931
|
-
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
932
|
-
assert.equal(ctx.ui.selections[2].label, "SDD PR chaining");
|
|
933
|
-
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/);
|
|
934
1098
|
assert.deepEqual(
|
|
935
1099
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
936
1100
|
{ action: "continue" },
|
|
937
1101
|
);
|
|
938
|
-
assert.equal(ctx.ui.selections.length,
|
|
1102
|
+
assert.equal(ctx.ui.selections.length, 0, "natural SDD triggers reuse preferences without prompts");
|
|
939
1103
|
assert.deepEqual(
|
|
940
1104
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
941
1105
|
{ action: "continue" },
|
|
@@ -948,7 +1112,7 @@ async function run() {
|
|
|
948
1112
|
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
949
1113
|
{ action: "continue" },
|
|
950
1114
|
);
|
|
951
|
-
assert.equal(ctx.ui.selections.length,
|
|
1115
|
+
assert.equal(ctx.ui.selections.length, 0, "slash SDD triggers use automatic defaults without prompts");
|
|
952
1116
|
|
|
953
1117
|
assert.deepEqual(
|
|
954
1118
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
@@ -976,19 +1140,18 @@ async function run() {
|
|
|
976
1140
|
"global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
|
|
977
1141
|
);
|
|
978
1142
|
}
|
|
979
|
-
assert.equal(ctx.ui.selections.length,
|
|
980
|
-
assert.
|
|
981
|
-
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/);
|
|
982
1145
|
await commands.get("gentle:status").handler("", ctx);
|
|
983
1146
|
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
|
|
984
1147
|
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
985
1148
|
|
|
986
1149
|
await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
|
|
987
|
-
assert.equal(ctx.ui.selections.length,
|
|
1150
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic preflight should remain prompt-free for the session");
|
|
988
1151
|
const promptHook = hooks.get("before_agent_start")[0];
|
|
989
1152
|
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
990
1153
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
991
|
-
assert.match(promptResult.systemPrompt, /Execution mode:
|
|
1154
|
+
assert.match(promptResult.systemPrompt, /Execution mode: auto/);
|
|
992
1155
|
const workerPromptResult = await promptHook(
|
|
993
1156
|
{ agentName: "worker", systemPrompt: "worker base" },
|
|
994
1157
|
ctx,
|
|
@@ -1003,7 +1166,7 @@ async function run() {
|
|
|
1003
1166
|
await rm(globalModelsPath, { force: true });
|
|
1004
1167
|
}
|
|
1005
1168
|
|
|
1006
|
-
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()) {
|
|
1007
1170
|
const slashSddCwd = await tempWorkspace();
|
|
1008
1171
|
try {
|
|
1009
1172
|
const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
|
|
@@ -1013,7 +1176,7 @@ async function run() {
|
|
|
1013
1176
|
});
|
|
1014
1177
|
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1015
1178
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1016
|
-
assert.equal(ctx.ui.selections.length,
|
|
1179
|
+
assert.equal(ctx.ui.selections.length, 0, `${text} should use automatic preflight without selectors`);
|
|
1017
1180
|
} finally {
|
|
1018
1181
|
await rm(slashSddCwd, { recursive: true, force: true });
|
|
1019
1182
|
}
|
|
@@ -1025,9 +1188,11 @@ async function run() {
|
|
|
1025
1188
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1026
1189
|
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1027
1190
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1028
|
-
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/);
|
|
1029
1194
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1030
|
-
assert.equal(ctx.ui.selections.length,
|
|
1195
|
+
assert.equal(ctx.ui.selections.length, 4, "explicit preflight remains an intentional re-prompt");
|
|
1031
1196
|
} finally {
|
|
1032
1197
|
await rm(commandSddCwd, { recursive: true, force: true });
|
|
1033
1198
|
}
|
|
@@ -1046,7 +1211,7 @@ async function run() {
|
|
|
1046
1211
|
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1047
1212
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1048
1213
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1049
|
-
assert.equal(ctx.ui.selections.length,
|
|
1214
|
+
assert.equal(ctx.ui.selections.length, 0, "automatic SDD-agent startup must not render selectors");
|
|
1050
1215
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1051
1216
|
assert.doesNotMatch(
|
|
1052
1217
|
promptResult.systemPrompt,
|
|
@@ -1067,7 +1232,7 @@ async function run() {
|
|
|
1067
1232
|
},
|
|
1068
1233
|
ctx,
|
|
1069
1234
|
);
|
|
1070
|
-
assert.equal(ctx.ui.selections.length,
|
|
1235
|
+
assert.equal(ctx.ui.selections.length, 0, "SDD-agent startup should reuse automatic preferences");
|
|
1071
1236
|
assert.doesNotMatch(
|
|
1072
1237
|
reusedPromptResult.systemPrompt,
|
|
1073
1238
|
/el Gentleman Identity and Harness/,
|
|
@@ -1089,7 +1254,7 @@ async function run() {
|
|
|
1089
1254
|
ctx,
|
|
1090
1255
|
);
|
|
1091
1256
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1092
|
-
assert.match(promptResult.systemPrompt, /
|
|
1257
|
+
assert.match(promptResult.systemPrompt, /canonical defaults or persisted choices/);
|
|
1093
1258
|
assert.equal(ctx.ui.selections.length, 0);
|
|
1094
1259
|
assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1095
1260
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
@@ -1115,14 +1280,14 @@ async function run() {
|
|
|
1115
1280
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1116
1281
|
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
1117
1282
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1118
|
-
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "
|
|
1283
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
|
|
1119
1284
|
} finally {
|
|
1120
1285
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1121
1286
|
await rm(engramSddCwd, { recursive: true, force: true });
|
|
1122
1287
|
}
|
|
1123
1288
|
|
|
1124
1289
|
// Issue #64: selecting engram as the artifact store must not cause
|
|
1125
|
-
// /sdd-init to create openspec/ or openspec/config.yaml.
|
|
1290
|
+
// /gentle-sdd-init to create openspec/ or openspec/config.yaml.
|
|
1126
1291
|
const engramSddInitCwd = await tempWorkspace();
|
|
1127
1292
|
try {
|
|
1128
1293
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
@@ -1132,21 +1297,21 @@ async function run() {
|
|
|
1132
1297
|
return options[0];
|
|
1133
1298
|
};
|
|
1134
1299
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1135
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1300
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1136
1301
|
assert.equal(
|
|
1137
1302
|
existsSync(join(engramSddInitCwd, "openspec")),
|
|
1138
1303
|
false,
|
|
1139
|
-
"/sdd-init must not create openspec/ when artifactStore is engram",
|
|
1304
|
+
"/gentle-sdd-init must not create openspec/ when artifactStore is engram",
|
|
1140
1305
|
);
|
|
1141
1306
|
assert.equal(
|
|
1142
1307
|
existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
|
|
1143
1308
|
false,
|
|
1144
|
-
"/sdd-init must not write openspec/config.yaml when artifactStore is engram",
|
|
1309
|
+
"/gentle-sdd-init must not write openspec/config.yaml when artifactStore is engram",
|
|
1145
1310
|
);
|
|
1146
1311
|
assert.doesNotMatch(
|
|
1147
1312
|
ctx.ui.notifications.at(-1).message,
|
|
1148
1313
|
/Wrote openspec\/config\.yaml/,
|
|
1149
|
-
"/sdd-init must not announce openspec/config.yaml when artifactStore is engram",
|
|
1314
|
+
"/gentle-sdd-init must not announce openspec/config.yaml when artifactStore is engram",
|
|
1150
1315
|
);
|
|
1151
1316
|
assert.match(
|
|
1152
1317
|
ctx.ui.notifications.at(-1).message,
|
|
@@ -1166,30 +1331,30 @@ async function run() {
|
|
|
1166
1331
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1167
1332
|
const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
|
|
1168
1333
|
ctx.ui.select = async (label, options) => {
|
|
1169
|
-
if (label === "SDD artifact store") return "
|
|
1334
|
+
if (label === "SDD artifact store") return "hybrid";
|
|
1170
1335
|
return options[0];
|
|
1171
1336
|
};
|
|
1172
1337
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1173
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1338
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1174
1339
|
assert.equal(
|
|
1175
1340
|
existsSync(join(bothSddInitCwd, "openspec", "specs")),
|
|
1176
1341
|
true,
|
|
1177
|
-
"/sdd-init must create openspec/specs when artifactStore is both",
|
|
1342
|
+
"/gentle-sdd-init must create openspec/specs when artifactStore is both",
|
|
1178
1343
|
);
|
|
1179
1344
|
assert.equal(
|
|
1180
1345
|
existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
|
|
1181
1346
|
true,
|
|
1182
|
-
"/sdd-init must create openspec/changes/archive when artifactStore is both",
|
|
1347
|
+
"/gentle-sdd-init must create openspec/changes/archive when artifactStore is both",
|
|
1183
1348
|
);
|
|
1184
1349
|
assert.equal(
|
|
1185
1350
|
existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
|
|
1186
1351
|
true,
|
|
1187
|
-
"/sdd-init must write openspec/config.yaml when artifactStore is both",
|
|
1352
|
+
"/gentle-sdd-init must write openspec/config.yaml when artifactStore is both",
|
|
1188
1353
|
);
|
|
1189
1354
|
assert.match(
|
|
1190
1355
|
ctx.ui.notifications.at(-1).message,
|
|
1191
1356
|
/Wrote openspec\/config\.yaml/,
|
|
1192
|
-
"/sdd-init must announce openspec/config.yaml when artifactStore is both",
|
|
1357
|
+
"/gentle-sdd-init must announce openspec/config.yaml when artifactStore is both",
|
|
1193
1358
|
);
|
|
1194
1359
|
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1195
1360
|
} finally {
|
|
@@ -1202,7 +1367,7 @@ async function run() {
|
|
|
1202
1367
|
pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
|
|
1203
1368
|
const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
|
|
1204
1369
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1205
|
-
assert.
|
|
1370
|
+
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "unrecognized Engram capability must elide the artifact selector");
|
|
1206
1371
|
} finally {
|
|
1207
1372
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1208
1373
|
await rm(directEngramToolCwd, { recursive: true, force: true });
|
|
@@ -1252,7 +1417,7 @@ async function run() {
|
|
|
1252
1417
|
const sddCwd = await tempWorkspace();
|
|
1253
1418
|
try {
|
|
1254
1419
|
const ctx = createCtx(sddCwd, true);
|
|
1255
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1420
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1256
1421
|
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1257
1422
|
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1258
1423
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
@@ -1260,12 +1425,12 @@ async function run() {
|
|
|
1260
1425
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
1261
1426
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1262
1427
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1263
|
-
assert.equal(ctx.ui.selections.length,
|
|
1428
|
+
assert.equal(ctx.ui.selections.length, 0, "sdd-init uses automatic preflight defaults");
|
|
1264
1429
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
1265
1430
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1266
1431
|
|
|
1267
1432
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1268
|
-
assert.equal(ctx.ui.selections.length,
|
|
1433
|
+
assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts after automatic sdd-init");
|
|
1269
1434
|
} finally {
|
|
1270
1435
|
await rm(sddCwd, { recursive: true, force: true });
|
|
1271
1436
|
}
|
|
@@ -1279,7 +1444,7 @@ async function run() {
|
|
|
1279
1444
|
);
|
|
1280
1445
|
await writeFile(globalModelsPath, "{ invalid json");
|
|
1281
1446
|
const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
|
|
1282
|
-
await commands.get("sdd-init").handler("", ctx);
|
|
1447
|
+
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1283
1448
|
assert.equal(ctx.ui.notifications[0].level, "warning");
|
|
1284
1449
|
assert.match(ctx.ui.notifications[0].message, /Model routing skipped:/);
|
|
1285
1450
|
assert.match(ctx.ui.notifications[0].message, /models\.json/);
|