gentle-pi 2.2.0 → 2.4.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 +76 -79
- 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-explore.md +1 -0
- 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 +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- 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/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/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 +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- 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 +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- 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 +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -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/issue-creation/SKILL.md +53 -93
- 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/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-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 +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -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 +45 -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 +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- 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-contract-prompt.test.ts +139 -0
- 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 +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -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 +362 -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 +366 -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
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
OPAQUE_PI_REVIEWER_ARGV,
|
|
9
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE,
|
|
10
|
+
OpaquePiReviewerTransportError,
|
|
11
|
+
resolvePiLaunch,
|
|
12
|
+
runOpaquePiReviewer,
|
|
13
|
+
} from "../lib/opaque-pi-reviewer-adapter.ts";
|
|
14
|
+
|
|
15
|
+
const FAKE_PI = `#!/usr/bin/env node
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const path = require("node:path");
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
const chunks = [];
|
|
20
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
21
|
+
process.stdin.on("end", () => {
|
|
22
|
+
const stdin = Buffer.concat(chunks);
|
|
23
|
+
if (process.env.OPAQUE_PI_STDIN_CAPTURE) fs.writeFileSync(process.env.OPAQUE_PI_STDIN_CAPTURE, stdin);
|
|
24
|
+
const mode = process.env.OPAQUE_PI_MODE || "ok";
|
|
25
|
+
const log = {
|
|
26
|
+
argv,
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
entries_before: fs.readdirSync(process.cwd()),
|
|
29
|
+
entries_after: fs.readdirSync(process.cwd()),
|
|
30
|
+
};
|
|
31
|
+
if (process.env.OPAQUE_PI_LOG) fs.appendFileSync(process.env.OPAQUE_PI_LOG, JSON.stringify(log) + "\\n");
|
|
32
|
+
if (mode === "break-cleanup" || process.env.OPAQUE_PI_BREAK_CLEANUP === "true") {
|
|
33
|
+
fs.chmodSync(path.dirname(process.cwd()), 0o500);
|
|
34
|
+
}
|
|
35
|
+
if (mode === "empty") process.exit(0);
|
|
36
|
+
if (mode === "hang") { setTimeout(() => process.exit(0), 10_000); return; }
|
|
37
|
+
if (mode === "nonzero") { process.stderr.write("opaque pi failed\\n"); process.exit(7); }
|
|
38
|
+
process.stdout.write(Buffer.from(process.env.OPAQUE_PI_OUTPUT_B64 || "", "base64"));
|
|
39
|
+
});
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const PROMPT_BYTES = Buffer.concat([
|
|
43
|
+
Buffer.from("opaque prompt\r\n\u0000", "utf8"),
|
|
44
|
+
Buffer.from([0x01, 0xff, 0xfe, 0x00]),
|
|
45
|
+
]);
|
|
46
|
+
const OUTPUT_BYTES = Buffer.concat([
|
|
47
|
+
Buffer.from("opaque output\r\n\u0000", "utf8"),
|
|
48
|
+
Buffer.from([0x07, 0xff, 0xfe, 0x00]),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
interface OpaqueHarness {
|
|
52
|
+
directory: string;
|
|
53
|
+
pi: string;
|
|
54
|
+
logPath: string;
|
|
55
|
+
stdinCapturePath: string;
|
|
56
|
+
environment: NodeJS.ProcessEnv;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface OpaquePiLog {
|
|
60
|
+
argv: string[];
|
|
61
|
+
cwd: string;
|
|
62
|
+
entries_before: string[];
|
|
63
|
+
entries_after: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function harness(t: test.TestContext, overrides: Record<string, string> = {}): OpaqueHarness {
|
|
67
|
+
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-"));
|
|
68
|
+
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
69
|
+
const pi = join(directory, "pi");
|
|
70
|
+
writeFileSync(pi, FAKE_PI);
|
|
71
|
+
chmodSync(pi, 0o755);
|
|
72
|
+
const logPath = join(directory, "pi.log");
|
|
73
|
+
const stdinCapturePath = join(directory, "stdin.bin");
|
|
74
|
+
return {
|
|
75
|
+
directory,
|
|
76
|
+
pi,
|
|
77
|
+
logPath,
|
|
78
|
+
stdinCapturePath,
|
|
79
|
+
environment: {
|
|
80
|
+
...process.env,
|
|
81
|
+
OPAQUE_PI_LOG: logPath,
|
|
82
|
+
OPAQUE_PI_STDIN_CAPTURE: stdinCapturePath,
|
|
83
|
+
OPAQUE_PI_OUTPUT_B64: OUTPUT_BYTES.toString("base64"),
|
|
84
|
+
...overrides,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readLog(path: string): OpaquePiLog[] {
|
|
90
|
+
if (!existsSync(path)) return [];
|
|
91
|
+
return readFileSync(path, "utf8")
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter((line) => line.length > 0)
|
|
94
|
+
.map((line) => JSON.parse(line) as OpaquePiLog);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function rejectsWithTransportError(
|
|
98
|
+
promise: Promise<unknown>,
|
|
99
|
+
kind: (typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE)[keyof typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE],
|
|
100
|
+
): Promise<OpaquePiReviewerTransportError> {
|
|
101
|
+
let caught: OpaquePiReviewerTransportError | undefined;
|
|
102
|
+
await assert.rejects(promise, (error: unknown) => {
|
|
103
|
+
assert.ok(error instanceof OpaquePiReviewerTransportError, `expected OpaquePiReviewerTransportError, received ${String(error)}`);
|
|
104
|
+
caught = error;
|
|
105
|
+
return error.kind === kind;
|
|
106
|
+
});
|
|
107
|
+
return caught!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test("the opaque adapter streams arbitrary prompt and stdout bytes verbatim through the fixed Pi subprocess", async (t) => {
|
|
111
|
+
const fixture = harness(t);
|
|
112
|
+
const result = await runOpaquePiReviewer(PROMPT_BYTES, {
|
|
113
|
+
piExecutable: fixture.pi,
|
|
114
|
+
environment: fixture.environment,
|
|
115
|
+
timeoutMs: 10_000,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.equal(result.promptByteLength, PROMPT_BYTES.length);
|
|
119
|
+
assert.equal(result.stdoutByteLength, OUTPUT_BYTES.length);
|
|
120
|
+
assert.deepEqual(result.stdout, OUTPUT_BYTES);
|
|
121
|
+
assert.deepEqual(readFileSync(fixture.stdinCapturePath), PROMPT_BYTES);
|
|
122
|
+
|
|
123
|
+
const calls = readLog(fixture.logPath);
|
|
124
|
+
assert.equal(calls.length, 1);
|
|
125
|
+
assert.deepEqual(calls[0]!.argv, [...OPAQUE_PI_REVIEWER_ARGV]);
|
|
126
|
+
assert.deepEqual(calls[0]!.entries_before, []);
|
|
127
|
+
assert.deepEqual(calls[0]!.entries_after, []);
|
|
128
|
+
assert.notEqual(calls[0]!.cwd, process.cwd());
|
|
129
|
+
assert.equal(existsSync(calls[0]!.cwd), false, "the empty scratch directory is removed after success");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the opaque adapter returns typed transport errors for launch, nonzero, empty, timeout, and cancellation", async (t) => {
|
|
133
|
+
const fixture = harness(t);
|
|
134
|
+
await rejectsWithTransportError(
|
|
135
|
+
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: join(fixture.directory, "missing-pi"), environment: fixture.environment, timeoutMs: 10_000 }),
|
|
136
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.LAUNCH_FAILED,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const nonzero = await rejectsWithTransportError(
|
|
140
|
+
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero" }, timeoutMs: 10_000 }),
|
|
141
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
|
|
142
|
+
);
|
|
143
|
+
assert.equal(nonzero.exitCode, 7);
|
|
144
|
+
assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
|
|
145
|
+
|
|
146
|
+
await rejectsWithTransportError(
|
|
147
|
+
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "empty" }, timeoutMs: 10_000 }),
|
|
148
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.EMPTY_OUTPUT,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const timedOut = await rejectsWithTransportError(
|
|
152
|
+
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" }, timeoutMs: 300 }),
|
|
153
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
|
|
154
|
+
);
|
|
155
|
+
assert.equal(timedOut.timedOut, true);
|
|
156
|
+
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const cancellation = runOpaquePiReviewer(PROMPT_BYTES, {
|
|
159
|
+
piExecutable: fixture.pi,
|
|
160
|
+
environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" },
|
|
161
|
+
timeoutMs: 10_000,
|
|
162
|
+
signal: controller.signal,
|
|
163
|
+
});
|
|
164
|
+
setTimeout(() => controller.abort(), 100).unref();
|
|
165
|
+
const cancelled = await rejectsWithTransportError(cancellation, OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CANCELLED);
|
|
166
|
+
assert.equal(cancelled.cancelled, true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("the opaque adapter leaves no prompt or result file in scratch and reports cleanup failure as transport-only", async (t) => {
|
|
170
|
+
const fixture = harness(t);
|
|
171
|
+
const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-cleanup-"));
|
|
172
|
+
const originalTmpdir = process.env.TMPDIR;
|
|
173
|
+
process.env.TMPDIR = scratchParent;
|
|
174
|
+
try {
|
|
175
|
+
const error = await rejectsWithTransportError(
|
|
176
|
+
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
177
|
+
piExecutable: fixture.pi,
|
|
178
|
+
environment: { ...fixture.environment, OPAQUE_PI_MODE: "break-cleanup" },
|
|
179
|
+
timeoutMs: 10_000,
|
|
180
|
+
}),
|
|
181
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CLEANUP_FAILED,
|
|
182
|
+
);
|
|
183
|
+
assert.match(error.message, /scratch directory/i);
|
|
184
|
+
} finally {
|
|
185
|
+
if (originalTmpdir === undefined) delete process.env.TMPDIR;
|
|
186
|
+
else process.env.TMPDIR = originalTmpdir;
|
|
187
|
+
chmodSync(scratchParent, 0o700);
|
|
188
|
+
rmSync(scratchParent, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("the opaque adapter preserves primary nonzero and timeout errors when scratch cleanup also fails", async (t) => {
|
|
193
|
+
const fixture = harness(t);
|
|
194
|
+
const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-primary-failure-"));
|
|
195
|
+
const originalTmpdir = process.env.TMPDIR;
|
|
196
|
+
process.env.TMPDIR = scratchParent;
|
|
197
|
+
try {
|
|
198
|
+
const nonzero = await rejectsWithTransportError(
|
|
199
|
+
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
200
|
+
piExecutable: fixture.pi,
|
|
201
|
+
environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero", OPAQUE_PI_BREAK_CLEANUP: "true" },
|
|
202
|
+
timeoutMs: 10_000,
|
|
203
|
+
}),
|
|
204
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
|
|
205
|
+
);
|
|
206
|
+
assert.equal(nonzero.exitCode, 7);
|
|
207
|
+
assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
|
|
208
|
+
chmodSync(scratchParent, 0o700);
|
|
209
|
+
|
|
210
|
+
const timedOut = await rejectsWithTransportError(
|
|
211
|
+
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
212
|
+
piExecutable: fixture.pi,
|
|
213
|
+
environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang", OPAQUE_PI_BREAK_CLEANUP: "true" },
|
|
214
|
+
timeoutMs: 300,
|
|
215
|
+
}),
|
|
216
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
|
|
217
|
+
);
|
|
218
|
+
assert.equal(timedOut.timedOut, true);
|
|
219
|
+
} finally {
|
|
220
|
+
if (originalTmpdir === undefined) delete process.env.TMPDIR;
|
|
221
|
+
else process.env.TMPDIR = originalTmpdir;
|
|
222
|
+
chmodSync(scratchParent, 0o700);
|
|
223
|
+
rmSync(scratchParent, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("the opaque adapter has no review lifecycle imports or identifiers", () => {
|
|
228
|
+
const adapterPath = fileURLToPath(new URL("../lib/opaque-pi-reviewer-adapter.ts", import.meta.url));
|
|
229
|
+
const source = readFileSync(adapterPath, "utf8");
|
|
230
|
+
assert.doesNotMatch(source, /review-integration|gentle-ai|materialize|submit/i);
|
|
231
|
+
for (const identifier of ["lineage", "target", "revision", "receipt", "lens", "order", "subject", "schema", "capture", "submission", "status", "model", "provider", "profile"]) {
|
|
232
|
+
assert.doesNotMatch(source, new RegExp(`\\b${identifier}\\b`, "i"), `adapter must not contain lifecycle identifier ${identifier}`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// #468 / #519: on Windows a bare `pi` resolves to pi.cmd, pi.ps1, or a POSIX
|
|
237
|
+
// shim, none of which Node can spawn with shell:false (EINVAL or ENOENT). The
|
|
238
|
+
// relay already runs inside Pi, so the host's own JavaScript entry is spawned
|
|
239
|
+
// through process.execPath instead. Other platforms keep today's exact shape.
|
|
240
|
+
test("the Pi launch shape spawns the host entry through process.execPath on win32 and stays byte-identical elsewhere", () => {
|
|
241
|
+
const host = {
|
|
242
|
+
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
|
243
|
+
entry: "C:\\Users\\dev\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\bundle\\cli.js",
|
|
244
|
+
};
|
|
245
|
+
const windows = resolvePiLaunch(undefined, "win32", host);
|
|
246
|
+
assert.deepEqual(windows, { file: host.execPath, arguments: [host.entry, ...OPAQUE_PI_REVIEWER_ARGV] });
|
|
247
|
+
assert.notEqual(windows.file, "pi");
|
|
248
|
+
assert.equal(windows.arguments.includes("pi"), false);
|
|
249
|
+
for (const platform of ["linux", "darwin", "freebsd"] as const) {
|
|
250
|
+
assert.deepEqual(resolvePiLaunch(undefined, platform, host), { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
|
|
251
|
+
assert.deepEqual(resolvePiLaunch("/opt/pi/bin/pi", platform, host), { file: "/opt/pi/bin/pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
|
|
252
|
+
}
|
|
253
|
+
assert.deepEqual(resolvePiLaunch("C:\\tools\\pi.exe", "win32", host), { file: "C:\\tools\\pi.exe", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
|
|
254
|
+
for (const entry of [undefined, "", "cli.js", "dist/bundle/cli.js"]) {
|
|
255
|
+
assert.throws(() => resolvePiLaunch(undefined, "win32", { execPath: host.execPath, entry }), /host entry/, `entry ${JSON.stringify(entry)} must fail closed instead of spawning a bare pi`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the default Pi launch resolves from the running host process", () => {
|
|
260
|
+
const launch = resolvePiLaunch(undefined);
|
|
261
|
+
if (process.platform === "win32") {
|
|
262
|
+
assert.deepEqual(launch, { file: process.execPath, arguments: [process.argv[1]!, ...OPAQUE_PI_REVIEWER_ARGV] });
|
|
263
|
+
} else {
|
|
264
|
+
assert.deepEqual(launch, { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test, { after } from "node:test";
|
|
@@ -9,8 +9,8 @@ import test, { after } from "node:test";
|
|
|
9
9
|
// orchestrator-lazy-diet migration tests
|
|
10
10
|
//
|
|
11
11
|
// Locks the split of the always-on `assets/orchestrator.md` into a thin core
|
|
12
|
-
// plus
|
|
13
|
-
//
|
|
12
|
+
// plus lazy reference files (see design.md "Core budget rebuilt from measured
|
|
13
|
+
// drafts" and "Appendix: drafted core texts").
|
|
14
14
|
//
|
|
15
15
|
// `getOrchestratorPrompt`'s rendered return value is memoized in a
|
|
16
16
|
// module-level cache (first-read-wins for the process lifetime — see design.md
|
|
@@ -19,17 +19,19 @@ import test, { after } from "node:test";
|
|
|
19
19
|
// ambient environment variables, so production runtime asset resolution stays
|
|
20
20
|
// deterministic. The representative production assets directory below is
|
|
21
21
|
// populated by COPYING the real repo assets (dynamically, at test-run time)
|
|
22
|
-
// into
|
|
23
|
-
// real repo's absolute path length while keeping content
|
|
24
|
-
// production. Tests that need to inspect the real repo files
|
|
25
|
-
// disposition-mapped union sweep, the core-alone token
|
|
26
|
-
// `assets/*.md` directly via `readFileSync`, sidestepping the
|
|
22
|
+
// into short and deliberately long tmpdir paths. This isolates byte-budget
|
|
23
|
+
// measurement from the real repo's absolute path length while keeping content
|
|
24
|
+
// representative of production. Tests that need to inspect the real repo files
|
|
25
|
+
// directly (the disposition-mapped union sweep, the core-alone token
|
|
26
|
+
// assertions) read `assets/*.md` directly via `readFileSync`, sidestepping the
|
|
27
|
+
// cache entirely.
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
|
|
29
30
|
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
30
31
|
const REAL_ASSETS_DIR = join(REPO_ROOT, "assets");
|
|
31
32
|
const FIXTURE_PATH = join(import.meta.dirname, "fixtures", "orchestrator.pre-diet.md");
|
|
32
|
-
const BUDGET_BYTES =
|
|
33
|
+
const BUDGET_BYTES = 8192;
|
|
34
|
+
const MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS = 93;
|
|
33
35
|
|
|
34
36
|
const LAZY_ASSET_NAMES = [
|
|
35
37
|
"orchestrator.md",
|
|
@@ -38,42 +40,56 @@ const LAZY_ASSET_NAMES = [
|
|
|
38
40
|
"orchestrator-memory.md",
|
|
39
41
|
"orchestrator-skills.md",
|
|
40
42
|
] as const;
|
|
43
|
+
const LAZY_REFERENCE_FILE_NAMES = LAZY_ASSET_NAMES.slice(1);
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
for (const name of LAZY_ASSET_NAMES) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
join(
|
|
47
|
-
|
|
48
|
-
);
|
|
45
|
+
function copyRequiredLazyAssets(destination: string): void {
|
|
46
|
+
for (const name of LAZY_ASSET_NAMES) {
|
|
47
|
+
const source = join(REAL_ASSETS_DIR, name);
|
|
48
|
+
assert.ok(existsSync(source), `missing packaged lazy asset: ${name}`);
|
|
49
|
+
copyFileSync(source, join(destination, name));
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
|
-
const { __testing } = await import("../extensions/gentle-ai.ts");
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const representativeProductionAssetsDir = mkdtempSync(join(tmpdir(), "gp-b-"));
|
|
54
|
+
copyRequiredLazyAssets(representativeProductionAssetsDir);
|
|
55
|
+
const { __testing } = await import("../extensions/gentle-ai.ts");
|
|
53
56
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// to
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// A controlled long assets root proves the parent prompt remains within the
|
|
58
|
+
// canonical budget independently of the checkout or installed-package path.
|
|
59
|
+
// The child-process measurement keeps production cache behavior separate from
|
|
60
|
+
// fixture measurements.
|
|
61
|
+
// The root is built to one exact length rather than "tmpdir plus a long
|
|
62
|
+
// suffix". The prompt declares the absolute root once, so it grows one byte
|
|
63
|
+
// per root character, and a 48-character macOS tmpdir pushed the previous
|
|
64
|
+
// construction to 164 characters where a 4-character Linux /tmp gave 121.
|
|
65
|
+
// A fixed length makes every platform measure the same budget claim.
|
|
66
|
+
const CONTROLLED_LONG_ASSETS_ROOT_CHARS = 128;
|
|
67
|
+
const controlledLongBaseDir = mkdtempSync(join(tmpdir(), "gp-long-"));
|
|
68
|
+
const controlledLongSegmentChars = CONTROLLED_LONG_ASSETS_ROOT_CHARS - join(controlledLongBaseDir, "x", "assets").length + 1;
|
|
62
69
|
assert.ok(
|
|
63
|
-
|
|
64
|
-
`
|
|
70
|
+
controlledLongSegmentChars >= 1,
|
|
71
|
+
`tmpdir ${tmpdir()} is too long to build a ${CONTROLLED_LONG_ASSETS_ROOT_CHARS}-char controlled assets root`,
|
|
65
72
|
);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const controlledLongAssetsDir = join(
|
|
74
|
+
controlledLongBaseDir,
|
|
75
|
+
"path-independent-prompt-budget-".repeat(Math.ceil(controlledLongSegmentChars / 31)).slice(0, controlledLongSegmentChars),
|
|
76
|
+
"assets",
|
|
77
|
+
);
|
|
78
|
+
mkdirSync(controlledLongAssetsDir, { recursive: true });
|
|
79
|
+
assert.equal(
|
|
80
|
+
controlledLongAssetsDir.length,
|
|
81
|
+
CONTROLLED_LONG_ASSETS_ROOT_CHARS,
|
|
82
|
+
`controlled long assets root is ${controlledLongAssetsDir.length} chars, want exactly ${CONTROLLED_LONG_ASSETS_ROOT_CHARS}`,
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
controlledLongAssetsDir.length >= MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS,
|
|
86
|
+
`controlled long assets root is only ${controlledLongAssetsDir.length} chars, need >= ${MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS}`,
|
|
87
|
+
);
|
|
88
|
+
copyRequiredLazyAssets(controlledLongAssetsDir);
|
|
73
89
|
|
|
74
90
|
after(() => {
|
|
75
91
|
rmSync(representativeProductionAssetsDir, { recursive: true, force: true });
|
|
76
|
-
rmSync(
|
|
92
|
+
rmSync(controlledLongBaseDir, { recursive: true, force: true });
|
|
77
93
|
});
|
|
78
94
|
|
|
79
95
|
function readRealAsset(name: string): string {
|
|
@@ -98,7 +114,7 @@ function measureOrchestratorPromptBytes(assetsDir: string): number {
|
|
|
98
114
|
// 2.2 — Byte budget (Spec: Always-On Injection Byte Budget)
|
|
99
115
|
// ---------------------------------------------------------------------------
|
|
100
116
|
|
|
101
|
-
test("getOrchestratorPrompt return value stays within the
|
|
117
|
+
test("getOrchestratorPrompt return value stays within the canonical 8,192 B budget at a short assets root", () => {
|
|
102
118
|
const rendered = __testing.renderOrchestratorPrompt(representativeProductionAssetsDir);
|
|
103
119
|
const bytes = Buffer.byteLength(rendered, "utf8");
|
|
104
120
|
assert.ok(
|
|
@@ -107,33 +123,46 @@ test("getOrchestratorPrompt return value stays within the 10,240 B budget (short
|
|
|
107
123
|
);
|
|
108
124
|
});
|
|
109
125
|
|
|
110
|
-
test(`getOrchestratorPrompt
|
|
111
|
-
const
|
|
126
|
+
test(`getOrchestratorPrompt keeps a controlled long (>= ${MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS} char) assets root within the canonical budget`, () => {
|
|
127
|
+
const rendered = __testing.renderOrchestratorPrompt(controlledLongAssetsDir);
|
|
128
|
+
const bytes = measureOrchestratorPromptBytes(controlledLongAssetsDir);
|
|
129
|
+
assert.equal(bytes, Buffer.byteLength(rendered, "utf8"), "child-process and direct render byte counts must match");
|
|
112
130
|
assert.ok(
|
|
113
131
|
bytes <= BUDGET_BYTES,
|
|
114
|
-
`getOrchestratorPrompt() returned ${bytes} B at
|
|
132
|
+
`getOrchestratorPrompt() returned ${bytes} B at controlled ${controlledLongAssetsDir.length}-char assets root, exceeds the ${BUDGET_BYTES} B budget`,
|
|
133
|
+
);
|
|
134
|
+
assert.equal(
|
|
135
|
+
rendered.split(controlledLongAssetsDir).length - 1,
|
|
136
|
+
1,
|
|
137
|
+
"the absolute assets root must be declared exactly once",
|
|
138
|
+
);
|
|
139
|
+
assert.ok(
|
|
140
|
+
rendered.includes(`Package assets root: \`${controlledLongAssetsDir}\`. Lazy asset paths below are relative to this root.`),
|
|
141
|
+
"the parent prompt must declare how to resolve relative lazy asset paths",
|
|
115
142
|
);
|
|
143
|
+
for (const name of LAZY_REFERENCE_FILE_NAMES) {
|
|
144
|
+
assert.ok(rendered.includes(`\`${name}\``), `lazy asset filename is missing: ${name}`);
|
|
145
|
+
}
|
|
146
|
+
assert.doesNotMatch(rendered, /\{\{/, "unresolved {{...}} placeholder leaked into the rendered prompt");
|
|
116
147
|
});
|
|
117
148
|
|
|
118
149
|
// ---------------------------------------------------------------------------
|
|
119
150
|
// 2.3 — Disposition-mapped union sweep (Spec: No Normative Content Loss +
|
|
120
151
|
// Pointer reachability)
|
|
121
152
|
//
|
|
122
|
-
// Every normative line of the frozen pre-diet fixture is assigned to
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// (intentionally absent
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// LAZY_VERBATIM against their one target lazy file — never a blanket union
|
|
129
|
-
// across all three.
|
|
153
|
+
// Every normative line of the frozen pre-diet fixture is assigned to a
|
|
154
|
+
// documented disposition: CORE_VERBATIM (byte-identical in the core),
|
|
155
|
+
// LAZY_VERBATIM (byte-identical in one specific lazy file), OBSOLETE
|
|
156
|
+
// (intentionally absent), or REPLACED (superseded by the focused #3417 asset
|
|
157
|
+
// policy ratchets below). REPLACED preserves the historical fixture without
|
|
158
|
+
// treating a retired prompt mirror as a current normative source.
|
|
130
159
|
// ---------------------------------------------------------------------------
|
|
131
160
|
|
|
132
161
|
type Target = "core" | "delegation" | "memory" | "skills";
|
|
133
162
|
|
|
134
163
|
interface DispositionRange {
|
|
135
164
|
lines: [number, number];
|
|
136
|
-
target: Target | "obsolete";
|
|
165
|
+
target: Target | "obsolete" | "replaced";
|
|
137
166
|
label: string;
|
|
138
167
|
}
|
|
139
168
|
|
|
@@ -158,13 +187,21 @@ const DISPOSITION_MAP: DispositionRange[] = [
|
|
|
158
187
|
{ lines: [25, 29], target: "delegation", label: "Language Boundary LB5 (exceptions)" },
|
|
159
188
|
{ lines: [31, 40], target: "core", label: "Mental Model" },
|
|
160
189
|
{ lines: [42, 42], target: "core", label: "Work Routing Ladder heading" },
|
|
161
|
-
{
|
|
190
|
+
{
|
|
191
|
+
lines: [44, 97],
|
|
192
|
+
target: "replaced",
|
|
193
|
+
label: "Pre-RDD routing detail replaced by focused direct-delegation guidance (#3417)",
|
|
194
|
+
},
|
|
162
195
|
{
|
|
163
196
|
lines: [98, 107],
|
|
164
197
|
target: "obsolete",
|
|
165
198
|
label: "Size/risk-selected SDD tier replaced by explicit-request/accepted-proposal selection (#312)",
|
|
166
199
|
},
|
|
167
|
-
{
|
|
200
|
+
{
|
|
201
|
+
lines: [108, 108],
|
|
202
|
+
target: "replaced",
|
|
203
|
+
label: "Earlier SDD trigger wording replaced by the focused SDD boundary (#3417)",
|
|
204
|
+
},
|
|
168
205
|
{
|
|
169
206
|
lines: [109, 110],
|
|
170
207
|
target: "obsolete",
|
|
@@ -179,8 +216,8 @@ const DISPOSITION_MAP: DispositionRange[] = [
|
|
|
179
216
|
},
|
|
180
217
|
{
|
|
181
218
|
lines: [128, 132],
|
|
182
|
-
target: "
|
|
183
|
-
label: "
|
|
219
|
+
target: "replaced",
|
|
220
|
+
label: "Pre-RDD trigger wording replaced by focused direct-delegation guidance (#3417)",
|
|
184
221
|
},
|
|
185
222
|
{
|
|
186
223
|
lines: [133, 133],
|
|
@@ -189,8 +226,8 @@ const DISPOSITION_MAP: DispositionRange[] = [
|
|
|
189
226
|
},
|
|
190
227
|
{
|
|
191
228
|
lines: [134, 167],
|
|
192
|
-
target: "
|
|
193
|
-
label: "
|
|
229
|
+
target: "replaced",
|
|
230
|
+
label: "Pre-RDD trigger and workflow wording replaced by focused delegation guidance (#3417)",
|
|
194
231
|
},
|
|
195
232
|
{
|
|
196
233
|
lines: [169, 181],
|
|
@@ -199,8 +236,16 @@ const DISPOSITION_MAP: DispositionRange[] = [
|
|
|
199
236
|
},
|
|
200
237
|
{ lines: [183, 191], target: "core", label: "SDD Workflow pointer" },
|
|
201
238
|
{ lines: [193, 193], target: "core", label: "Memory Contract heading" },
|
|
202
|
-
{
|
|
203
|
-
|
|
239
|
+
{
|
|
240
|
+
lines: [195, 195],
|
|
241
|
+
target: "replaced",
|
|
242
|
+
label: "Verbose memory introduction replaced by compact parent/subagent ownership (#3417)",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
lines: [197, 201],
|
|
246
|
+
target: "replaced",
|
|
247
|
+
label: "Verbose non-SDD memory forwarding replaced by compact ownership (#3417)",
|
|
248
|
+
},
|
|
204
249
|
{ lines: [203, 230], target: "memory", label: "Memory Contract SDD phases table + artifact keys + lifecycle rule" },
|
|
205
250
|
{ lines: [232, 232], target: "core", label: "Skill Registry Protocol heading" },
|
|
206
251
|
{ lines: [234, 253], target: "skills", label: "Skill Registry Protocol detail" },
|
|
@@ -219,6 +264,11 @@ function isNormativeLine(line: string): boolean {
|
|
|
219
264
|
}
|
|
220
265
|
|
|
221
266
|
const fixtureLines = readFileSync(FIXTURE_PATH, "utf8").split("\n");
|
|
267
|
+
// Fixture lines 187 and 191 predate the root-relative lazy-asset contract and
|
|
268
|
+
// canonical-authority resolution. Keep their coverage by asserting the
|
|
269
|
+
// intentionally updated production wording instead of weakening the range.
|
|
270
|
+
const CURRENT_SDD_WORKFLOW_PATH = "`sdd-orchestrator-workflow.md`";
|
|
271
|
+
const CURRENT_HARD_PREFLIGHT_INVARIANT = "Hard preflight invariant: `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named \"preflight\" are not session preflight. Do not mark SDD preflight complete, start `sdd-init`, launch SDD subagents/chains, or move to explore/proposal/spec/design/tasks until this session has an injected `## SDD Session Preflight` block or a canonical-authority resolution. Defaults and capability constraints may resolve fields without confirmation prompts; preserve unresolved-choice and safety gates.";
|
|
222
272
|
const SUPERSEDED_LIFECYCLE_REVIEW_LINES = new Set([
|
|
223
273
|
70,
|
|
224
274
|
// 74/77: the loose mode-choice background lines were replaced by the
|
|
@@ -240,6 +290,7 @@ const SUPERSEDED_LIFECYCLE_REVIEW_LINES = new Set([
|
|
|
240
290
|
]);
|
|
241
291
|
|
|
242
292
|
for (const range of DISPOSITION_MAP) {
|
|
293
|
+
if (range.target === "replaced") continue;
|
|
243
294
|
test(
|
|
244
295
|
`disposition-mapped union: ${range.label} (fixture:${range.lines[0]}-${range.lines[1]}) -> ${range.target}`,
|
|
245
296
|
() => {
|
|
@@ -251,6 +302,12 @@ for (const range of DISPOSITION_MAP) {
|
|
|
251
302
|
const raw = fixtureLines[ln - 1];
|
|
252
303
|
if (raw === undefined || !isNormativeLine(raw)) continue;
|
|
253
304
|
const trimmed = raw.trim();
|
|
305
|
+
const expected =
|
|
306
|
+
ln === 187
|
|
307
|
+
? CURRENT_SDD_WORKFLOW_PATH
|
|
308
|
+
: ln === 191
|
|
309
|
+
? CURRENT_HARD_PREFLIGHT_INVARIANT
|
|
310
|
+
: trimmed;
|
|
254
311
|
if (SUPERSEDED_LIFECYCLE_REVIEW_LINES.has(ln)) {
|
|
255
312
|
assert.ok(
|
|
256
313
|
!targetContent.includes(trimmed),
|
|
@@ -266,8 +323,8 @@ for (const range of DISPOSITION_MAP) {
|
|
|
266
323
|
continue;
|
|
267
324
|
}
|
|
268
325
|
assert.ok(
|
|
269
|
-
targetContent.includes(
|
|
270
|
-
`normative line lost: fixture:${ln} "${
|
|
326
|
+
targetContent.includes(expected),
|
|
327
|
+
`normative line lost: fixture:${ln} "${expected}" not found verbatim in ${TARGET_FILE[range.target]} (disposition: ${range.target}, section: ${range.label})`,
|
|
271
328
|
);
|
|
272
329
|
}
|
|
273
330
|
},
|
|
@@ -279,32 +336,28 @@ for (const range of DISPOSITION_MAP) {
|
|
|
279
336
|
// string alone, no lazy union.
|
|
280
337
|
// ---------------------------------------------------------------------------
|
|
281
338
|
|
|
282
|
-
test("core-alone: load-bearing delegation tokens
|
|
339
|
+
test("core-alone: load-bearing direct-delegation tokens remain without lazy union", () => {
|
|
283
340
|
const core = readRealAsset("orchestrator.md");
|
|
284
341
|
assert.match(core, /4-file rule/);
|
|
285
342
|
assert.match(core, /Multi-file write rule/);
|
|
286
|
-
assert.match(core, /Lifecycle gate rule/);
|
|
287
343
|
assert.match(core, /Incident rule/);
|
|
344
|
+
assert.match(core, /Verification rule/);
|
|
288
345
|
assert.match(core, /Long-session rule/);
|
|
289
|
-
assert.match(core, /Review actor rule/);
|
|
290
346
|
});
|
|
291
347
|
|
|
292
|
-
test("core-alone:
|
|
348
|
+
test("core-alone: dynamic Gentle AI ownership replaces package lifecycle instructions", () => {
|
|
293
349
|
const core = readRealAsset("orchestrator.md");
|
|
294
|
-
assert.match(core, /
|
|
295
|
-
assert.match(core, /
|
|
296
|
-
assert.
|
|
297
|
-
assert.
|
|
298
|
-
assert.match(core, /Dangerous-command safety remains independent and authoritative/i);
|
|
299
|
-
assert.match(core, /SDD completion adds no review or Judgment Day pass/i);
|
|
350
|
+
assert.match(core, /injects the mirrored provider-bundle review execution contract into this session's system prompt at start/);
|
|
351
|
+
assert.match(core, /Absent that mirrored contract, this package invents no lifecycle instructions/);
|
|
352
|
+
assert.doesNotMatch(core, /start -> finalize -> validate/i);
|
|
353
|
+
assert.doesNotMatch(core, /receipt validation/i);
|
|
300
354
|
});
|
|
301
355
|
|
|
302
|
-
test("lazy
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
assert.match(content, /review-readability/);
|
|
356
|
+
test("lazy delegation detail has no native RDD controller markers", () => {
|
|
357
|
+
const delegation = readRealAsset("orchestrator-delegation.md");
|
|
358
|
+
for (const marker of ["next_transition", "review.capture-result", "reconcile-terminal-mirrors"]) {
|
|
359
|
+
assert.ok(!delegation.includes(marker), `stale RDD marker retained: ${marker}`);
|
|
360
|
+
}
|
|
308
361
|
});
|
|
309
362
|
|
|
310
363
|
test("live orchestrator assets remove the stale strong-gate retry contract", () => {
|
|
@@ -345,20 +398,11 @@ test("relocated lazy bodies are not double-delivered in the always-on core", ()
|
|
|
345
398
|
);
|
|
346
399
|
});
|
|
347
400
|
|
|
348
|
-
test("relocated lazy files are reachable via in-core
|
|
401
|
+
test("relocated lazy files are reachable via root-relative in-core filenames", () => {
|
|
349
402
|
const rendered = __testing.getOrchestratorPrompt();
|
|
350
|
-
|
|
351
|
-
rendered.includes(
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
assert.ok(
|
|
355
|
-
rendered.includes(join(REAL_ASSETS_DIR, "orchestrator-memory.md")),
|
|
356
|
-
"core is missing a reachable pointer to orchestrator-memory.md",
|
|
357
|
-
);
|
|
358
|
-
assert.ok(
|
|
359
|
-
rendered.includes(join(REAL_ASSETS_DIR, "orchestrator-skills.md")),
|
|
360
|
-
"core is missing a reachable pointer to orchestrator-skills.md",
|
|
361
|
-
);
|
|
403
|
+
for (const name of LAZY_REFERENCE_FILE_NAMES) {
|
|
404
|
+
assert.ok(rendered.includes(`\`${name}\``), `core is missing a reachable pointer to ${name}`);
|
|
405
|
+
}
|
|
362
406
|
});
|
|
363
407
|
|
|
364
408
|
// ---------------------------------------------------------------------------
|