gentle-pi 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -12
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/orchestrator-delegation.md +19 -9
- package/assets/orchestrator.md +5 -5
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +898 -79
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +88 -12
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-integration-v2.ts +58 -8
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +87 -11
- package/runtime/review-integration-v2.mjs +58 -8
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +6 -2
- package/skills/_shared/review-ledger-contract.md +3 -1
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +28 -12
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +17 -6
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +3 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +25 -0
- package/tests/review-host-relay-routing.test.ts +20 -4
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
|
@@ -60,6 +60,15 @@ test("rendered parent prompt keeps the RDD boundary while omitting lifecycle mir
|
|
|
60
60
|
for (const marker of ["Authority-First Terminal Procedure", "reconcile-terminal-mirrors", "next_transition"]) {
|
|
61
61
|
assert.ok(!rendered.includes(marker), `rendered parent prompt leaked: ${marker}`);
|
|
62
62
|
}
|
|
63
|
+
// gentle-pi#661: getOrchestratorPrompt()'s no-argument default renders the
|
|
64
|
+
// "unknown (native status unavailable)" RDD status line -- the longest of
|
|
65
|
+
// the three renderable forms -- so this IS the worst-case render the 8 KiB
|
|
66
|
+
// budget below must cover, not a smaller placeholder production later
|
|
67
|
+
// exceeds.
|
|
68
|
+
assert.ok(
|
|
69
|
+
rendered.includes("Receipt-driven development: unknown (native status unavailable)"),
|
|
70
|
+
"the default render must include the worst-case RDD status line",
|
|
71
|
+
);
|
|
63
72
|
assert.ok(Buffer.byteLength(rendered, "utf8") <= 8192, "the rendered parent prompt must stay below the reduced 8 KiB budget");
|
|
64
73
|
});
|
|
65
74
|
|
|
@@ -80,6 +80,8 @@ interface PackageJson {
|
|
|
80
80
|
files?: string[];
|
|
81
81
|
scripts?: Record<string, string>;
|
|
82
82
|
dependencies?: Record<string, string>;
|
|
83
|
+
peerDependencies?: Record<string, string>;
|
|
84
|
+
devDependencies?: Record<string, string>;
|
|
83
85
|
bundledDependencies?: string[];
|
|
84
86
|
bundleDependencies?: string[];
|
|
85
87
|
repository?: {
|
|
@@ -99,6 +101,15 @@ function readPackageJson(): PackageJson {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
test("package declares the tested Pi minimum required for agent_settled", () => {
|
|
105
|
+
const manifest = readPackageJson();
|
|
106
|
+
assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=0.85.1");
|
|
107
|
+
assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], "0.85.1");
|
|
108
|
+
const readme = readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8");
|
|
109
|
+
assert.match(readme, /Pi 0\.85\.1 or newer/);
|
|
110
|
+
assert.match(readme, /agent_settled/);
|
|
111
|
+
});
|
|
112
|
+
|
|
102
113
|
test("package manifest has no obsolete native activation build surface", () => {
|
|
103
114
|
const packageJson = readPackageJson();
|
|
104
115
|
const manifest = JSON.stringify(packageJson);
|
|
@@ -262,20 +273,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
262
273
|
);
|
|
263
274
|
});
|
|
264
275
|
|
|
265
|
-
test("package verification binds the published Gentle AI v2.
|
|
276
|
+
test("package verification binds the published Gentle AI v2.7.0 runtime pin", () => {
|
|
266
277
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
267
278
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
268
279
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
269
280
|
|
|
270
|
-
assert.match(installer, /INSTALLER_VERSION = "2\.
|
|
281
|
+
assert.match(installer, /INSTALLER_VERSION = "2\.7\.0"/);
|
|
271
282
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
272
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
283
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:SE4KCLo3y1qWaTq\+7HJKZ1DkAAAl\+w\+82YY0\+qQkFwM="/);
|
|
273
284
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
274
285
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
275
286
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
276
287
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
277
288
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
278
|
-
assert.match(verifier, /v2\.
|
|
289
|
+
assert.match(verifier, /v2\.7\.0/);
|
|
279
290
|
});
|
|
280
291
|
|
|
281
292
|
|
|
@@ -1277,9 +1288,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
|
|
|
1277
1288
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1278
1289
|
});
|
|
1279
1290
|
|
|
1280
|
-
test("v2.
|
|
1291
|
+
test("v2.5.0 release package and runtime stop before publication", () => {
|
|
1281
1292
|
const packageJson = readPackageJson();
|
|
1282
|
-
assert.equal(packageJson.version, "2.
|
|
1293
|
+
assert.equal(packageJson.version, "2.5.0", "the release manifest must remain explicitly pinned to v2.5.0");
|
|
1283
1294
|
assert.equal(
|
|
1284
1295
|
packageJson.scripts?.test,
|
|
1285
1296
|
"node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
import { cardBody, cardHint, cardTitle, cardTone } from "./gentle-card-text.ts";
|
|
6
7
|
import piPretty from "../extensions/pi-pretty.ts";
|
|
7
8
|
import quietTools, {
|
|
8
9
|
countNonEmptyLines,
|
|
@@ -180,7 +181,16 @@ test("quiet tool rendering registers noisy built-in tools", () => {
|
|
|
180
181
|
|
|
181
182
|
test("quiet tool execution uses the tool-call cwd", async () => {
|
|
182
183
|
const tool = registeredQuietTools().get("bash");
|
|
183
|
-
const
|
|
184
|
+
const context = {
|
|
185
|
+
cwd: "/tmp",
|
|
186
|
+
sessionManager: {
|
|
187
|
+
getSessionId: () => "quiet-tool-test",
|
|
188
|
+
getSessionFile: () => undefined,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const output = extractTextContent(
|
|
192
|
+
await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, context),
|
|
193
|
+
).trim();
|
|
184
194
|
// pwd prints the physical directory: on macOS /tmp is a symlink to /private/tmp.
|
|
185
195
|
assert.equal(output, realpathSync("/tmp"));
|
|
186
196
|
assert.notEqual(output, process.cwd());
|
|
@@ -472,21 +482,21 @@ test("quiet tool rendering hides every collapsed direct Gentle AI result and pre
|
|
|
472
482
|
const nonText = renderToString(tool.renderResult({ content: [{ type: "image", data: "opaque", mimeType: "image/png" }] }, { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
|
|
473
483
|
|
|
474
484
|
assert.equal([...textRose].length, 2);
|
|
475
|
-
assert.equal(call
|
|
476
|
-
assert.
|
|
477
|
-
assert.
|
|
485
|
+
assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · review status`);
|
|
486
|
+
assert.match(cardBody(collapsed), /\d+ lines?\b/);
|
|
487
|
+
assert.match(cardBody(collapsed), /\d+ lines?\b/);
|
|
478
488
|
assert.doesNotMatch(collapsed, /next_transition|stop/);
|
|
479
|
-
assert.equal(expanded.split("\n")[0], '{"next_transition":"stop"}');
|
|
489
|
+
assert.equal(cardBody(expanded).split("\n")[0], '{"next_transition":"stop"}');
|
|
480
490
|
assert.match(expanded, /"next_transition":"stop"/);
|
|
481
491
|
assert.doesNotMatch(expanded, /to expand/);
|
|
482
|
-
assert.
|
|
492
|
+
assert.match(cardBody(failure), /\d+ lines?\b/);
|
|
483
493
|
assert.doesNotMatch(failure, /review status failed|authority unavailable|lineage=secret/);
|
|
484
494
|
assert.match(expandedFailure, /review status failed: authority unavailable/);
|
|
485
495
|
assert.match(expandedFailure, /lineage=secret/);
|
|
486
496
|
assert.doesNotMatch(expandedFailure, /to expand/);
|
|
487
|
-
assert.doesNotMatch(expandedFailure, /\x1b\[/);
|
|
488
|
-
assert.equal(empty, "");
|
|
489
|
-
assert.equal(nonText, "");
|
|
497
|
+
assert.doesNotMatch(cardBody(expandedFailure), /\x1b\[/);
|
|
498
|
+
assert.equal(cardBody(empty), "");
|
|
499
|
+
assert.equal(cardBody(nonText), "");
|
|
490
500
|
});
|
|
491
501
|
|
|
492
502
|
test("quiet tool rendering transitions one Gentle AI header through lifecycle states", () => {
|
|
@@ -528,16 +538,16 @@ test("quiet tool rendering transitions one Gentle AI header through lifecycle st
|
|
|
528
538
|
assert.strictEqual(initial, running);
|
|
529
539
|
assert.strictEqual(running, completed);
|
|
530
540
|
assert.strictEqual(completed, failed);
|
|
531
|
-
assert.equal(initialText, "
|
|
532
|
-
assert.equal(runningText, "
|
|
533
|
-
assert.equal(completedText, "
|
|
534
|
-
assert.equal(failedText, "
|
|
541
|
+
assert.equal(cardTitle(initialText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(initialText), "warning");
|
|
542
|
+
assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(runningText), "warning");
|
|
543
|
+
assert.equal(cardTitle(completedText), "🌹︎ Gentle AI · completed · review status"); assert.equal(cardTone(completedText), "success");
|
|
544
|
+
assert.equal(cardTitle(failedText), "🌹︎ Gentle AI · failed · review status"); assert.equal(cardTone(failedText), "error");
|
|
535
545
|
assert.doesNotMatch(failedText, /private-change/);
|
|
536
546
|
});
|
|
537
547
|
|
|
538
548
|
test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
539
549
|
const tool = registeredQuietTools().get("bash");
|
|
540
|
-
const rose =
|
|
550
|
+
const rose = /🌹︎ Gentle AI/;
|
|
541
551
|
const render = (command: string, state: Record<string, unknown>, overrides: Record<string, unknown> = {}) => {
|
|
542
552
|
const component = tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, state, argsComplete: false, ...overrides }));
|
|
543
553
|
return [component, renderToString(component)] as const;
|
|
@@ -545,14 +555,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
|
545
555
|
const command = "gentle-ai review status --token 'lineage-secret";
|
|
546
556
|
const state = {};
|
|
547
557
|
const [preparing, collapsed] = render(command, state);
|
|
548
|
-
assert.equal(collapsed, "🌹︎ Gentle AI · preparing · review status");
|
|
558
|
+
assert.equal(cardTitle(collapsed), "🌹︎ Gentle AI · preparing · review status");
|
|
549
559
|
assert.doesNotMatch(collapsed, /token|lineage-secret/);
|
|
550
560
|
const expanded = render(command, state, { expanded: true, lastComponent: preparing })[1];
|
|
551
|
-
assert.
|
|
561
|
+
assert.equal(cardTitle(expanded), "🌹︎ Gentle AI · preparing · review status"); assert.equal(cardBody(expanded), "$ gentle-ai review status --token 'lineage-secret");
|
|
552
562
|
|
|
553
563
|
const direct = "gentle-ai review status";
|
|
554
564
|
const [running, runningText] = render(direct, state, { argsComplete: true, executionStarted: true, lastComponent: preparing });
|
|
555
|
-
assert.strictEqual(running, preparing); assert.equal(runningText, "🌹︎ Gentle AI · running · review status");
|
|
565
|
+
assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status");
|
|
556
566
|
const genericState = {};
|
|
557
567
|
const [generic, genericText] = render("gentle-ai review status | cat", genericState);
|
|
558
568
|
assert.doesNotMatch(genericText, rose); assert.match(genericText, /\| cat/);
|
|
@@ -569,7 +579,7 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
|
569
579
|
assert.equal(genericResult.split("\n")[0], "generic result");
|
|
570
580
|
const directState = {}; render(direct, directState, { argsComplete: true });
|
|
571
581
|
const directResult = renderToolResult(tool, textResult("direct result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: directState, argsComplete: true });
|
|
572
|
-
assert.
|
|
582
|
+
assert.match(cardBody(directResult), /\d+ lines?\b/);
|
|
573
583
|
});
|
|
574
584
|
|
|
575
585
|
test("quiet tool rendering displays only finite safe Gentle AI operation paths", () => {
|
|
@@ -635,7 +645,7 @@ test("quiet tool rendering displays only finite safe Gentle AI operation paths",
|
|
|
635
645
|
routineRenderContext({ args: { command } }),
|
|
636
646
|
),
|
|
637
647
|
);
|
|
638
|
-
assert.equal(rendered
|
|
648
|
+
assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
|
|
639
649
|
assert.doesNotMatch(rendered, /change-123|private|secret|lineage|sha256|result\.json|incident\.json/);
|
|
640
650
|
}
|
|
641
651
|
});
|
|
@@ -658,7 +668,7 @@ test("quiet tool rendering covers version and future standalone Gentle AI comman
|
|
|
658
668
|
const rendered = renderToString(
|
|
659
669
|
tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command } })),
|
|
660
670
|
);
|
|
661
|
-
assert.equal(rendered
|
|
671
|
+
assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
|
|
662
672
|
assert.doesNotMatch(rendered, /authorization-root|secret-change|private|C:\\\\private/);
|
|
663
673
|
}
|
|
664
674
|
});
|
|
@@ -686,7 +696,7 @@ test("quiet tool rendering recognizes exact quoted, escaped, Windows, and comman
|
|
|
686
696
|
|
|
687
697
|
for (const command of routineCommands) {
|
|
688
698
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
689
|
-
assert.equal(call
|
|
699
|
+
assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · version", command);
|
|
690
700
|
}
|
|
691
701
|
|
|
692
702
|
for (const command of ["'gentle-ai-copy' version", '"gentle-ai-copy.exe" version', "'gentle-ai' version && echo done"] as const) {
|
|
@@ -713,7 +723,7 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
|
|
|
713
723
|
] as const;
|
|
714
724
|
for (const [command, operationPath] of cases) {
|
|
715
725
|
const call = renderToString(tool.renderCall({ command }, statusTheme, routineRenderContext({ args: { command } })));
|
|
716
|
-
assert.equal(call
|
|
726
|
+
assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · ${operationPath}`, command); assert.equal(cardTone(call), "warning", command);
|
|
717
727
|
assert.doesNotMatch(call, /gentle-ai-main|private|secret|hidden/);
|
|
718
728
|
}
|
|
719
729
|
const command = `${devPath} review status --prompt hidden-prompt --lineage lineage-secret --body private-body`;
|
|
@@ -728,11 +738,11 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
|
|
|
728
738
|
const expanded = renderToolResult(tool, textResult(text), { expanded: true, isPartial: false, isError: true }, lifecycleContext);
|
|
729
739
|
const refreshed = renderToolResult(tool, textResult("result-secret"), { expanded: false, isPartial: false }, { args: { command: refreshedCommand } });
|
|
730
740
|
const hint = keyHint("app.tools.expand", "to expand");
|
|
731
|
-
assert.
|
|
732
|
-
assert.equal(collapsed.split(hint).length - 1,
|
|
741
|
+
assert.match(cardBody(collapsed), /\d+ lines?\b/);
|
|
742
|
+
assert.equal(cardBody(collapsed).split(hint).length - 1, 0);
|
|
733
743
|
assert.doesNotMatch(collapsed, /private|lineage|secret|hidden|error/);
|
|
734
744
|
assert.match(expanded, /private failure|lineage=secret body=hidden/);
|
|
735
|
-
assert.doesNotMatch(expanded, /to expand|\x1b\[/);
|
|
745
|
+
assert.doesNotMatch(cardBody(expanded), /to expand|\x1b\[/);
|
|
736
746
|
assert.doesNotMatch(refreshed, /result-secret/);
|
|
737
747
|
assert.ok(resolutions > 1);
|
|
738
748
|
});
|
|
@@ -794,12 +804,12 @@ test("quiet tool rendering hides the routine partial result because the header o
|
|
|
794
804
|
),
|
|
795
805
|
);
|
|
796
806
|
|
|
797
|
-
assert.
|
|
798
|
-
assert.equal(partial.split(expandHint).length - 1,
|
|
807
|
+
assert.match(cardBody(partial), /\d+ lines?\b/);
|
|
808
|
+
assert.equal(cardBody(partial).split(expandHint).length - 1, 0);
|
|
799
809
|
assert.match(partialExpanded, /"status":"running"/);
|
|
800
810
|
assert.doesNotMatch(partialExpanded, /to expand/);
|
|
801
|
-
assert.
|
|
802
|
-
assert.
|
|
811
|
+
assert.match(cardBody(partialFailure), /\d+ lines?\b/);
|
|
812
|
+
assert.match(cardBody(completed), /\d+ lines?\b/);
|
|
803
813
|
|
|
804
814
|
const partialExpandedFailure = renderToString(
|
|
805
815
|
tool.renderResult(
|
|
@@ -810,7 +820,7 @@ test("quiet tool rendering hides the routine partial result because the header o
|
|
|
810
820
|
),
|
|
811
821
|
);
|
|
812
822
|
assert.match(partialExpandedFailure, /review status failed: authority unavailable/);
|
|
813
|
-
assert.doesNotMatch(partialExpandedFailure, /\x1b\[/);
|
|
823
|
+
assert.doesNotMatch(cardBody(partialExpandedFailure), /\x1b\[/);
|
|
814
824
|
});
|
|
815
825
|
|
|
816
826
|
test("quiet tool rendering collapses grant calls to action and authorization-root cardinality", () => {
|
|
@@ -844,8 +854,8 @@ test("quiet tool rendering collapses grant calls to action and authorization-roo
|
|
|
844
854
|
assert.strictEqual(running, completed);
|
|
845
855
|
assert.strictEqual(completed, failed);
|
|
846
856
|
const rendered = renderToString(failed);
|
|
847
|
-
assert.equal(rendered, "
|
|
848
|
-
assert.doesNotMatch(rendered, /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
|
|
857
|
+
assert.equal(cardTitle(rendered), "🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots"); assert.equal(cardTone(rendered), "error");
|
|
858
|
+
assert.doesNotMatch(rendered.split(keyHint("app.tools.expand", "to expand")).join(""), /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
|
|
849
859
|
});
|
|
850
860
|
|
|
851
861
|
test("quiet tool rendering counts grant authorization roots without rendering values", () => {
|
|
@@ -863,7 +873,7 @@ test("quiet tool rendering counts grant authorization roots without rendering va
|
|
|
863
873
|
|
|
864
874
|
for (const [command, expected] of cases) {
|
|
865
875
|
const rendered = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
866
|
-
assert.equal(rendered
|
|
876
|
+
assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${expected}`, command);
|
|
867
877
|
assert.doesNotMatch(rendered, /authorization-root|\/repo\/root|\/one|\/two|\/three|change/);
|
|
868
878
|
}
|
|
869
879
|
});
|
|
@@ -883,8 +893,8 @@ test("quiet tool rendering hides invocation secrets from collapsed Gentle AI cal
|
|
|
883
893
|
),
|
|
884
894
|
);
|
|
885
895
|
|
|
886
|
-
assert.equal(call
|
|
887
|
-
assert.
|
|
896
|
+
assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review finalize");
|
|
897
|
+
assert.match(cardBody(collapsed), /\d+ lines?\b/);
|
|
888
898
|
for (const forbidden of ["hidden-prompt", "lineage-secret", "private-body", "/private/root", "audit:"]) {
|
|
889
899
|
assert.doesNotMatch(call, new RegExp(forbidden));
|
|
890
900
|
assert.doesNotMatch(collapsed, new RegExp(forbidden));
|
|
@@ -973,7 +983,7 @@ test("quiet tool rendering sanitizes collapsed output and call rows", () => {
|
|
|
973
983
|
|
|
974
984
|
const carriageReturn = renderToolResult(tools.get("bash"), textResult("prefix\rSECRET\r\nnext"), { expanded: false, isPartial: false }, { args: { command: "printf output" } });
|
|
975
985
|
assert.match(collapsed, /safered/);
|
|
976
|
-
assert.doesNotMatch(collapsed, /\x1b\[31m|\x1b\[0m/);
|
|
986
|
+
assert.doesNotMatch(cardBody(collapsed), /\x1b\[31m|\x1b\[0m/);
|
|
977
987
|
assert.equal(call.trimEnd(), "$ echo red");
|
|
978
988
|
assert.match(carriageReturn, /prefixSECRET\nnext/);
|
|
979
989
|
assert.doesNotMatch(carriageReturn, /\r/);
|
|
@@ -1064,7 +1074,7 @@ test("quiet tool rendering recognizes a quoted exact dev override path containin
|
|
|
1064
1074
|
const command = `"${devPath}" review status "literal \\$|#;"`;
|
|
1065
1075
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1066
1076
|
const collapsed = renderToolResult(tool, textResult("private result"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1067
|
-
assert.equal(call
|
|
1077
|
+
assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review status");
|
|
1068
1078
|
assert.doesNotMatch(call, /\/opt\/Gentle AI\/gentle-ai|literal/);
|
|
1069
1079
|
assert.doesNotMatch(collapsed, /private result/);
|
|
1070
1080
|
});
|
|
@@ -1274,3 +1284,52 @@ test("quiet tool rendering respects command and env wrapper order", () => {
|
|
|
1274
1284
|
assert.equal(gentleAiRoutineCommand({ command }), undefined);
|
|
1275
1285
|
assertGenericBash(bash, command);
|
|
1276
1286
|
});
|
|
1287
|
+
|
|
1288
|
+
test("quiet tool rendering lets a final result promote a replayed call card to its outcome", async () => {
|
|
1289
|
+
// The promotion invalidates after the render returns (a microtask), never inside it.
|
|
1290
|
+
const flush = () => new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
|
|
1291
|
+
const { pi, tools } = createPi();
|
|
1292
|
+
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
1293
|
+
const tool = tools.get("bash");
|
|
1294
|
+
const command = "gentle-ai review status";
|
|
1295
|
+
const state = {};
|
|
1296
|
+
let invalidations = 0;
|
|
1297
|
+
const replayed = routineRenderContext({ args: { command }, state, argsComplete: false, executionStarted: false, isPartial: false, invalidate: () => { invalidations += 1; } });
|
|
1298
|
+
|
|
1299
|
+
const call = tool.renderCall({ command }, statusTheme, replayed);
|
|
1300
|
+
assert.equal(cardTitle(renderToString(call)), "🌹︎ Gentle AI · preparing · review status");
|
|
1301
|
+
renderToString(tool.renderResult(textResult("done"), { expanded: false, isPartial: false }, statusTheme, replayed));
|
|
1302
|
+
assert.equal(invalidations, 0, "never reentrant");
|
|
1303
|
+
await flush();
|
|
1304
|
+
assert.equal(invalidations, 1);
|
|
1305
|
+
const promoted = tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call });
|
|
1306
|
+
assert.strictEqual(promoted, call);
|
|
1307
|
+
assert.equal(cardTitle(renderToString(promoted)), "🌹︎ Gentle AI · completed · review status");
|
|
1308
|
+
assert.equal(cardTone(renderToString(promoted)), "success");
|
|
1309
|
+
|
|
1310
|
+
renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
|
|
1311
|
+
await flush();
|
|
1312
|
+
assert.equal(invalidations, 2);
|
|
1313
|
+
assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "🌹︎ Gentle AI · failed · review status");
|
|
1314
|
+
renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
|
|
1315
|
+
await flush();
|
|
1316
|
+
assert.equal(invalidations, 2, "an unchanged outcome must not request another render");
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test("quiet tool rendering puts the expand key in the finished call card's top rule, not in the result", () => {
|
|
1320
|
+
const { pi, tools } = createPi();
|
|
1321
|
+
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
1322
|
+
const tool = tools.get("bash");
|
|
1323
|
+
const command = "gentle-ai review status";
|
|
1324
|
+
const running = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: true })));
|
|
1325
|
+
assert.equal(cardHint(running), undefined, "a running call has nothing to expand yet");
|
|
1326
|
+
const completed = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: false })));
|
|
1327
|
+
assert.match(cardHint(completed) ?? "", /to expand$/);
|
|
1328
|
+
assert.equal(cardTitle(completed), "🌹︎ Gentle AI · completed · review status");
|
|
1329
|
+
const expanded = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: true })));
|
|
1330
|
+
assert.match(cardHint(expanded) ?? "", /to collapse$/);
|
|
1331
|
+
const collapsedResult = renderToolResult(tool, textResult("{\"next_transition\":\"stop\"}"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1332
|
+
assert.equal(cardBody(collapsedResult), "1 line");
|
|
1333
|
+
assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "3 lines");
|
|
1334
|
+
assert.match(collapsedResult.split("\n").pop() ?? "", /╰─+╯$/);
|
|
1335
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// gentle-pi#661/#662: RDD-aware verification rule for delegated work.
|
|
8
|
+
//
|
|
9
|
+
// The bounded writer always self-verifies: it runs the parent-authorized
|
|
10
|
+
// `## Verification` commands itself and reports observed output. Whether a
|
|
11
|
+
// SEPARATE `gentle-ai-verify` delegation is also required depends on the
|
|
12
|
+
// rendered `Receipt-driven development:` line, stated normatively exactly
|
|
13
|
+
// once in trigger 5 (Verification rule) and referenced -- not restated --
|
|
14
|
+
// everywhere else in this asset:
|
|
15
|
+
// - `on` -> the writer's own report is the verification of
|
|
16
|
+
// record; `gentle-ai-verify` is on-demand, except
|
|
17
|
+
// passive risk, which gets a structural readback.
|
|
18
|
+
// - `off`/`unknown` -> gentle-pi#662: the parent calls `gentle_review` with
|
|
19
|
+
// `{"operation":"assess"}` over the writer's diff and
|
|
20
|
+
// follows the returned plan by native risk tier
|
|
21
|
+
// (passive/medium/high/unassessable), instead of a
|
|
22
|
+
// blanket non-trivial judgment. An unknown RDD line
|
|
23
|
+
// never lowers a tier below `off`.
|
|
24
|
+
// These tests assert the exact distinctive sentences (not bare words like
|
|
25
|
+
// `off`/`unknown`/`partial`/`blocked`), that the tier table is stated exactly
|
|
26
|
+
// once, that the routing ladder paragraph references trigger 5 rather than
|
|
27
|
+
// restating it, and that `## Known environmental failures` has one canonical
|
|
28
|
+
// definition (owned by the worker asset) that the delegation asset
|
|
29
|
+
// references rather than duplicates.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
33
|
+
|
|
34
|
+
function read(relativePath: string): string {
|
|
35
|
+
return readFileSync(join(ROOT, relativePath), "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
39
|
+
return haystack.split(needle).length - 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const delegation = read("assets/orchestrator-delegation.md");
|
|
43
|
+
const worker = read("assets/agents/gentle-ai-worker.md");
|
|
44
|
+
|
|
45
|
+
const ON_SENTENCE =
|
|
46
|
+
"When the line reads `on`, that writer report is the verification of record, and the native review is the independent check the writer cannot influence";
|
|
47
|
+
const OFF_UNKNOWN_SENTENCE =
|
|
48
|
+
'When the line reads `off` or `unknown`, after the writer returns, call `gentle_review` with `{"operation":"assess"}` over the writer\'s diff and follow the returned plan instead of judging non-triviality from the task description: the operation resolves the native risk tier and states exactly who verifies next.';
|
|
49
|
+
const TIER_TABLE_HEADER = "| Native risk tier | Verification when RDD is `off`/`unknown` |";
|
|
50
|
+
const PASSIVE_TIER_ROW = "| passive | structural readback by the parent; no separate verifier, no tests |";
|
|
51
|
+
const MEDIUM_TIER_ROW = "| medium | writer self-verification stands; a separate `gentle-ai-verify` run is added only when the writer profile is a small model (mini or low effort) |";
|
|
52
|
+
const HIGH_TIER_ROW = "| high | writer self-verification plus a separate `gentle-ai-verify` run, always |";
|
|
53
|
+
const UNASSESSABLE_TIER_ROW = "| unknown / assess failed | treated as high |";
|
|
54
|
+
const SMALL_MODEL_BIAS_SENTENCE =
|
|
55
|
+
"The small-model bias raises the tier by one for verification purposes (medium becomes high); an unknown `Receipt-driven development:` line never lowers a tier below `off`.";
|
|
56
|
+
const SPOT_CHECK_SENTENCE =
|
|
57
|
+
"The parent spot check (re-running one reported command before delivery) stays required in every tier.";
|
|
58
|
+
// gentle-pi#668: the `on` branch of trigger 5 holds only while the native
|
|
59
|
+
// review actually reaches a terminal outcome for this candidate -- a decline,
|
|
60
|
+
// a clone-local disable, or a refused START/STATUS all fall back to the exact
|
|
61
|
+
// same risk-gated path as `off`.
|
|
62
|
+
const ON_BRANCH_FALLBACK_SENTENCE =
|
|
63
|
+
'That `on` branch holds only while the native review actually reaches a terminal outcome for this candidate (gentle-pi#668): a human decline of the consent envelope for this candidate (candidate-scoped, never the RDD kill switch), a clone-local RDD disable discovered mid-flow, or a refused START/STATUS all fall back to the risk-gated path exactly as `off` -- call `gentle_review` with `{"operation":"assess"}` (pass `nativeReviewOutcome` when the parent already knows it; the tool derives it from what it itself observed for the candidate otherwise, failing closed to `unknown` when it cannot) and follow the returned plan.';
|
|
64
|
+
|
|
65
|
+
test("trigger 5 (Verification rule) states the exact on-line routing: writer report is the verification of record", () => {
|
|
66
|
+
assert.ok(delegation.includes(ON_SENTENCE), "trigger 5 is missing the exact on-line sentence");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("trigger 5 states the exact off/unknown-line routing: the parent calls gentle_review's assess operation and follows the returned plan", () => {
|
|
70
|
+
assert.ok(delegation.includes(OFF_UNKNOWN_SENTENCE), "trigger 5 is missing the exact off/unknown-line sentence");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("trigger 5 states the native risk tier table exactly once, with all four rows", () => {
|
|
74
|
+
for (const row of [TIER_TABLE_HEADER, PASSIVE_TIER_ROW, MEDIUM_TIER_ROW, HIGH_TIER_ROW, UNASSESSABLE_TIER_ROW]) {
|
|
75
|
+
assert.equal(countOccurrences(delegation, row), 1, `expected exactly one occurrence of tier table row: ${row}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("trigger 5 states the small-model bias and the unknown-never-lowers-a-tier rule", () => {
|
|
80
|
+
assert.ok(delegation.includes(SMALL_MODEL_BIAS_SENTENCE), "trigger 5 is missing the small-model bias sentence");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("trigger 5 keeps the parent spot check requirement in every tier", () => {
|
|
84
|
+
assert.ok(delegation.includes(SPOT_CHECK_SENTENCE), "trigger 5 is missing the parent spot check sentence");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("trigger 5 states that the on branch holds only while the native review closes for this candidate, falling back to the off path exactly once (gentle-pi#668)", () => {
|
|
88
|
+
assert.equal(countOccurrences(delegation, ON_BRANCH_FALLBACK_SENTENCE), 1, "trigger 5 is missing (or duplicates) the on-branch fallback sentence");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("the on-branch fallback sentence names all three non-closed triggers and never introduces a forbidden native RDD marker", () => {
|
|
92
|
+
for (const clause of ["decline", "clone-local", "refused START/STATUS"]) {
|
|
93
|
+
assert.ok(ON_BRANCH_FALLBACK_SENTENCE.includes(clause), `on-branch fallback sentence missing: ${clause}`);
|
|
94
|
+
}
|
|
95
|
+
for (const marker of ["gentle-ai review status", "next_transition", "review.capture-result", "review.validate", "reviewGate.result"]) {
|
|
96
|
+
assert.ok(!delegation.includes(marker), `stale/forbidden RDD marker introduced: ${marker}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the on-line and off/unknown-line routing sentences appear exactly once each (normative statement lives only in trigger 5)", () => {
|
|
101
|
+
for (const sentence of [ON_SENTENCE, OFF_UNKNOWN_SENTENCE]) {
|
|
102
|
+
assert.equal(countOccurrences(delegation, sentence), 1, `expected exactly one occurrence of: ${sentence.slice(0, 60)}...`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("trigger 5 never restates the retired #661 off/unknown non-trivial judgment", () => {
|
|
107
|
+
assert.doesNotMatch(delegation, /non-trivial change, in addition to the writer's own report/);
|
|
108
|
+
assert.doesNotMatch(delegation, /purely passive documentation with no behavior to verify/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("the Simple Delegation paragraph references trigger 5 instead of restating the on/off/unknown routing", () => {
|
|
112
|
+
assert.match(
|
|
113
|
+
delegation,
|
|
114
|
+
/per the RDD-aware Verification rule \(trigger 5 under Mandatory Delegation Triggers, gentle-pi#661\)/,
|
|
115
|
+
);
|
|
116
|
+
assert.match(delegation, /the normative on\/off\/unknown routing lives there, not here/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("delegation overlay's trigger 5 (Verification rule) is RDD-aware", () => {
|
|
120
|
+
assert.match(delegation, /\*\*Verification rule\*\*.*RDD-aware/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("delegation overlay reserves separate exploration for parent routing decisions", () => {
|
|
124
|
+
assert.match(delegation, /exploration stays reserved for when the parent needs the map to decide or route/i);
|
|
125
|
+
assert.match(delegation, /reading that prepares a write belongs with the writer/i);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("delegation overlay keeps the required headings", () => {
|
|
129
|
+
for (const heading of [
|
|
130
|
+
"### Delegation Rules",
|
|
131
|
+
"#### Background Subagent Policy",
|
|
132
|
+
"#### Allowed edit surfaces (MANDATORY)",
|
|
133
|
+
"### 3. SDD (optional)",
|
|
134
|
+
]) {
|
|
135
|
+
assert.ok(delegation.includes(heading), `delegation overlay lost required heading: ${heading}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("worker asset declares the Verification section after Test discipline", () => {
|
|
140
|
+
const testDisciplineIndex = worker.indexOf("## Test discipline");
|
|
141
|
+
const verificationIndex = worker.indexOf("## Verification");
|
|
142
|
+
const interactionIndex = worker.indexOf("## Interaction contract");
|
|
143
|
+
assert.ok(testDisciplineIndex >= 0, "worker asset lost ## Test discipline");
|
|
144
|
+
assert.ok(verificationIndex >= 0, "worker asset is missing ## Verification");
|
|
145
|
+
assert.ok(interactionIndex >= 0, "worker asset lost ## Interaction contract");
|
|
146
|
+
assert.ok(
|
|
147
|
+
testDisciplineIndex < verificationIndex && verificationIndex < interactionIndex,
|
|
148
|
+
"## Verification must sit between ## Test discipline and ## Interaction contract",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("worker asset requires foreground, one-at-a-time verification with nothing left unreported", () => {
|
|
153
|
+
for (const clause of [
|
|
154
|
+
"in the foreground",
|
|
155
|
+
"one at a time",
|
|
156
|
+
"Never launch a verification command in the background",
|
|
157
|
+
"never end the task with a listed command unreported",
|
|
158
|
+
]) {
|
|
159
|
+
assert.ok(worker.includes(clause), `worker asset is missing: ${clause}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("worker asset reports each verification command as <exact command>: <observed result> in validation", () => {
|
|
164
|
+
assert.ok(worker.includes("`<exact command>: <observed result>`"));
|
|
165
|
+
assert.ok(worker.includes("in `validation`"));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Contract consistency (`## Known environmental failures`): defined exactly
|
|
170
|
+
// once, in the worker asset, as "exact test names (or exact command lines)
|
|
171
|
+
// that already fail on the base"; the writer reports those as evidence, but
|
|
172
|
+
// any OTHER failing required command still forces `status: partial`. The
|
|
173
|
+
// delegation asset must reference this same definition, not restate it.
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
test("worker asset owns the canonical Known environmental failures definition", () => {
|
|
177
|
+
assert.ok(worker.includes("## Known environmental failures"));
|
|
178
|
+
assert.match(worker, /this is the canonical definition; other assets reference it, they do not restate it/i);
|
|
179
|
+
assert.match(worker, /lists exact test names or exact command lines that already fail on the base/i);
|
|
180
|
+
assert.match(worker, /Any OTHER required command that fails -- one not named under that heading -- still forces `status: partial`\./);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("delegation asset references the worker's Known environmental failures definition instead of restating it", () => {
|
|
184
|
+
assert.match(
|
|
185
|
+
delegation,
|
|
186
|
+
/`## Known environmental failures` follows the same definition as `gentle-ai-worker`'s Verification contract: exact pre-existing base failures reported as evidence, never blockers -- any other failing required command still forces `status: partial`\./,
|
|
187
|
+
);
|
|
188
|
+
// The full canonical wording ("lists exact test names or exact command
|
|
189
|
+
// lines that already fail on the base") must not be duplicated here.
|
|
190
|
+
assert.doesNotMatch(delegation, /lists exact test names or exact command lines that already fail on the base/i);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("worker asset never claims completion while a required verification command fails under RDD, except a named environmental failure", () => {
|
|
194
|
+
assert.match(worker, /this report is the verification of record/i);
|
|
195
|
+
assert.match(worker, /native review remains the independent check/i);
|
|
196
|
+
assert.match(
|
|
197
|
+
worker,
|
|
198
|
+
/never report `status: completed` while a required command under `## Verification` is failing, unless that exact failure is named under `## Known environmental failures`\./i,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("worker asset keeps the existing Return contract fields", () => {
|
|
203
|
+
for (const field of [
|
|
204
|
+
"status: completed | partial | blocked | interaction_required",
|
|
205
|
+
"summary:",
|
|
206
|
+
"files_changed:",
|
|
207
|
+
"tdd_evidence:",
|
|
208
|
+
"validation:",
|
|
209
|
+
"risks:",
|
|
210
|
+
"review_focus:",
|
|
211
|
+
"skill_resolution:",
|
|
212
|
+
"interaction_required:",
|
|
213
|
+
]) {
|
|
214
|
+
assert.ok(worker.includes(field), `worker asset lost Return contract field: ${field}`);
|
|
215
|
+
}
|
|
216
|
+
});
|