gentle-pi 2.7.0 → 3.0.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 +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -2,6 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { PassThrough, Writable } from "node:stream";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
|
|
6
|
+
import { AgentRunner, type ChildLike, type TaskRequest } from "../lib/agents-runner.ts";
|
|
7
|
+
import { TaskStore } from "../lib/agents-protocol.ts";
|
|
5
8
|
|
|
6
9
|
const REPOSITORY_ID = `sha256:${"a".repeat(64)}`;
|
|
7
10
|
const OTHER_REPOSITORY_ID = `sha256:${"b".repeat(64)}`;
|
|
@@ -26,37 +29,253 @@ function pair() {
|
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
type Fd3StdioMode = "pipe" | "overlapped";
|
|
33
|
+
|
|
34
|
+
type WindowsFd3DiagnosticTrace = {
|
|
35
|
+
stdioMode: Fd3StdioMode;
|
|
36
|
+
childClientAttached: boolean | "unknown";
|
|
37
|
+
childRequestAttemptCount: number;
|
|
38
|
+
childSettlement: "not-attempted" | "settled-true" | "settled-false" | "unknown";
|
|
39
|
+
childResponseObserved: "unknown-without-production-instrumentation";
|
|
40
|
+
childTimeoutObserved: "unknown-without-production-instrumentation";
|
|
41
|
+
parentRawDataChunkCount: number;
|
|
42
|
+
parentAuthorizationCallbackCount: number;
|
|
43
|
+
parentWriteCallback: "unknown-without-production-instrumentation";
|
|
44
|
+
parentWriteErrorCode: "unknown-without-production-instrumentation";
|
|
45
|
+
parentFd3Cleanup: "not-needed" | "deadline-termination-requested" | "deadline-confirmed-close" | "deadline-unconfirmed-after-grace";
|
|
46
|
+
childClose: "pending" | "observed" | "unconfirmed-after-grace";
|
|
47
|
+
fd3Close: "pending" | "observed" | "unconfirmed-after-grace";
|
|
48
|
+
probeResult: "pending" | "completed" | "spawn-failure" | "child-error" | "child-exit-nonzero" | "parse-failure" | "stream-error" | "deadline-unconfirmed";
|
|
49
|
+
capturedOutput: "within-4096-byte-bound" | "truncated-at-4096-bytes";
|
|
50
|
+
childExit: "unknown" | "zero" | "nonzero" | "error";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ProductionChildOptions = {
|
|
54
|
+
maxRequests?: number;
|
|
55
|
+
closeChannel?: boolean;
|
|
56
|
+
withoutChannel?: boolean;
|
|
57
|
+
stdioMode?: Fd3StdioMode;
|
|
58
|
+
childDeadlineMs?: number;
|
|
59
|
+
childCloseGraceMs?: number;
|
|
60
|
+
diagnosticTrace?: WindowsFd3DiagnosticTrace;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const productionFd3StdioMode: Fd3StdioMode = process.platform === "win32" ? "overlapped" : "pipe";
|
|
64
|
+
|
|
65
|
+
function windowsFd3DiagnosticTrace(stdioMode: Fd3StdioMode): WindowsFd3DiagnosticTrace {
|
|
66
|
+
return {
|
|
67
|
+
stdioMode,
|
|
68
|
+
childClientAttached: "unknown",
|
|
69
|
+
childRequestAttemptCount: 0,
|
|
70
|
+
childSettlement: "unknown",
|
|
71
|
+
childResponseObserved: "unknown-without-production-instrumentation",
|
|
72
|
+
childTimeoutObserved: "unknown-without-production-instrumentation",
|
|
73
|
+
parentRawDataChunkCount: 0,
|
|
74
|
+
parentAuthorizationCallbackCount: 0,
|
|
75
|
+
parentWriteCallback: "unknown-without-production-instrumentation",
|
|
76
|
+
parentWriteErrorCode: "unknown-without-production-instrumentation",
|
|
77
|
+
parentFd3Cleanup: "not-needed",
|
|
78
|
+
childClose: "pending",
|
|
79
|
+
fd3Close: "pending",
|
|
80
|
+
probeResult: "pending",
|
|
81
|
+
capturedOutput: "within-4096-byte-bound",
|
|
82
|
+
childExit: "unknown",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function productionChild(requests: number, authorize: () => boolean, options: ProductionChildOptions = {}): Promise<boolean[]> {
|
|
30
87
|
const moduleUrl = new URL("../lib/review-session-standing-permission-ipc.ts", import.meta.url).href;
|
|
88
|
+
const diagnostic = options.diagnosticTrace !== undefined;
|
|
31
89
|
const source = `
|
|
32
90
|
import { createChildStandingReviewPermissionClient } from ${JSON.stringify(moduleUrl)};
|
|
33
91
|
const client = createChildStandingReviewPermissionClient();
|
|
34
92
|
const answers = [];
|
|
35
|
-
|
|
93
|
+
const trace = {
|
|
94
|
+
childClientAttached: client !== undefined,
|
|
95
|
+
childRequestAttemptCount: 0,
|
|
96
|
+
childSettlement: "not-attempted",
|
|
97
|
+
childResponseObserved: "unknown-without-production-instrumentation",
|
|
98
|
+
childTimeoutObserved: "unknown-without-production-instrumentation",
|
|
99
|
+
};
|
|
100
|
+
for (let index = 0; index < ${requests}; index += 1) {
|
|
101
|
+
trace.childRequestAttemptCount += 1;
|
|
102
|
+
const answer = await client?.requestAuthorization(${JSON.stringify(REPOSITORY_ID)}) ?? false;
|
|
103
|
+
answers.push(answer);
|
|
104
|
+
trace.childSettlement = answer ? "settled-true" : "settled-false";
|
|
105
|
+
}
|
|
36
106
|
client?.close();
|
|
37
|
-
process.stdout.write(JSON.stringify(answers));
|
|
107
|
+
process.stdout.write(JSON.stringify(${diagnostic} ? { answers, trace } : answers));
|
|
38
108
|
`;
|
|
39
109
|
const child = spawn(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", source], {
|
|
40
110
|
env: { ...process.env, GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3" },
|
|
41
|
-
stdio: options.withoutChannel ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "pipe",
|
|
111
|
+
stdio: options.withoutChannel ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "pipe", options.stdioMode ?? productionFd3StdioMode],
|
|
42
112
|
});
|
|
43
113
|
const pipe = child.stdio[3];
|
|
114
|
+
const trace = options.diagnosticTrace;
|
|
115
|
+
const onParentData = () => { if (trace !== undefined) trace.parentRawDataChunkCount += 1; };
|
|
116
|
+
if (trace !== undefined && pipe !== null && pipe !== undefined) pipe.on("data", onParentData);
|
|
117
|
+
const parentAuthorize = trace === undefined
|
|
118
|
+
? authorize
|
|
119
|
+
: () => {
|
|
120
|
+
trace.parentAuthorizationCallbackCount += 1;
|
|
121
|
+
return authorize();
|
|
122
|
+
};
|
|
44
123
|
const broker = pipe === null || pipe === undefined
|
|
45
124
|
? undefined
|
|
46
|
-
: new ParentStandingReviewPermissionBroker({ readable: pipe, writable: pipe },
|
|
125
|
+
: new ParentStandingReviewPermissionBroker({ readable: pipe, writable: pipe }, parentAuthorize, options);
|
|
47
126
|
if (options.closeChannel && pipe !== null && pipe !== undefined) {
|
|
48
127
|
broker?.close();
|
|
49
128
|
pipe.destroy();
|
|
50
129
|
}
|
|
51
130
|
let stdout = "";
|
|
52
131
|
let stderr = "";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
132
|
+
if (trace === undefined) {
|
|
133
|
+
child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); });
|
|
134
|
+
child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); });
|
|
135
|
+
return new Promise((resolve, reject) => child.once("exit", (code) => {
|
|
136
|
+
broker?.close();
|
|
137
|
+
if (code !== 0) reject(new Error(`child exited ${code}: ${stderr}`));
|
|
138
|
+
else resolve(JSON.parse(stdout) as boolean[]);
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const diagnosticTrace = trace;
|
|
143
|
+
const outputLimitBytes = 4_096;
|
|
144
|
+
let stdoutBytes = 0;
|
|
145
|
+
let stderrBytes = 0;
|
|
146
|
+
let cleanupStarted = false;
|
|
147
|
+
let settled = false;
|
|
148
|
+
let deadline: NodeJS.Timeout | undefined;
|
|
149
|
+
let closeGrace: NodeJS.Timeout | undefined;
|
|
150
|
+
const appendBounded = (current: string, chunk: Buffer, currentBytes: number): { value: string; bytes: number } => {
|
|
151
|
+
if (currentBytes >= outputLimitBytes) {
|
|
152
|
+
diagnosticTrace.capturedOutput = "truncated-at-4096-bytes";
|
|
153
|
+
return { value: current, bytes: currentBytes };
|
|
154
|
+
}
|
|
155
|
+
const accepted = chunk.subarray(0, outputLimitBytes - currentBytes);
|
|
156
|
+
if (accepted.length !== chunk.length) diagnosticTrace.capturedOutput = "truncated-at-4096-bytes";
|
|
157
|
+
return { value: current + accepted.toString("utf8"), bytes: currentBytes + accepted.length };
|
|
158
|
+
};
|
|
159
|
+
const onStdoutData = (chunk: Buffer) => {
|
|
160
|
+
const captured = appendBounded(stdout, chunk, stdoutBytes);
|
|
161
|
+
stdout = captured.value;
|
|
162
|
+
stdoutBytes = captured.bytes;
|
|
163
|
+
};
|
|
164
|
+
const onStderrData = (chunk: Buffer) => {
|
|
165
|
+
const captured = appendBounded(stderr, chunk, stderrBytes);
|
|
166
|
+
stderr = captured.value;
|
|
167
|
+
stderrBytes = captured.bytes;
|
|
168
|
+
};
|
|
169
|
+
child.stdout.on("data", onStdoutData);
|
|
170
|
+
child.stderr.on("data", onStderrData);
|
|
171
|
+
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const removeOperationalListeners = () => {
|
|
174
|
+
child.off("exit", onChildExit);
|
|
175
|
+
child.off("close", onChildClose);
|
|
176
|
+
child.stdout.off("data", onStdoutData);
|
|
177
|
+
child.stderr.off("data", onStderrData);
|
|
178
|
+
pipe?.off("close", onFd3Close);
|
|
179
|
+
if (pipe !== null && pipe !== undefined) pipe.off("data", onParentData);
|
|
180
|
+
};
|
|
181
|
+
const removeInertErrorGuards = () => {
|
|
182
|
+
child.off("error", onChildError);
|
|
183
|
+
child.stdout.off("error", onStreamError);
|
|
184
|
+
child.stderr.off("error", onStreamError);
|
|
185
|
+
pipe?.off("error", onStreamError);
|
|
186
|
+
};
|
|
187
|
+
const finish = (answers: boolean[], retainInertErrorGuards = false) => {
|
|
188
|
+
if (settled) return;
|
|
189
|
+
settled = true;
|
|
190
|
+
if (deadline !== undefined) clearTimeout(deadline);
|
|
191
|
+
if (closeGrace !== undefined) clearTimeout(closeGrace);
|
|
192
|
+
removeOperationalListeners();
|
|
193
|
+
if (retainInertErrorGuards) child.once("close", removeInertErrorGuards);
|
|
194
|
+
else removeInertErrorGuards();
|
|
195
|
+
broker?.close();
|
|
196
|
+
resolve(answers);
|
|
197
|
+
};
|
|
198
|
+
const parseCompletedAnswers = (): boolean[] | undefined => {
|
|
199
|
+
if (diagnosticTrace.capturedOutput !== "within-4096-byte-bound") return undefined;
|
|
200
|
+
try {
|
|
201
|
+
const result = JSON.parse(stdout) as { answers?: unknown; trace?: Partial<WindowsFd3DiagnosticTrace> };
|
|
202
|
+
if (!Array.isArray(result.answers) || !result.answers.every((answer) => typeof answer === "boolean")) return undefined;
|
|
203
|
+
diagnosticTrace.childClientAttached = result.trace?.childClientAttached === true;
|
|
204
|
+
diagnosticTrace.childRequestAttemptCount = typeof result.trace?.childRequestAttemptCount === "number" ? result.trace.childRequestAttemptCount : 0;
|
|
205
|
+
diagnosticTrace.childSettlement = result.trace?.childSettlement === "settled-true" || result.trace?.childSettlement === "settled-false"
|
|
206
|
+
? result.trace.childSettlement
|
|
207
|
+
: "unknown";
|
|
208
|
+
return result.answers;
|
|
209
|
+
} catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const finishFromChildClose = () => {
|
|
214
|
+
if (settled) return;
|
|
215
|
+
if (diagnosticTrace.childExit === "nonzero" || diagnosticTrace.childExit === "error") {
|
|
216
|
+
diagnosticTrace.probeResult = diagnosticTrace.childExit === "error" ? "child-error" : "child-exit-nonzero";
|
|
217
|
+
finish([]);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const answers = parseCompletedAnswers();
|
|
221
|
+
if (answers === undefined) {
|
|
222
|
+
diagnosticTrace.probeResult = "parse-failure";
|
|
223
|
+
finish([]);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (diagnosticTrace.probeResult === "pending") diagnosticTrace.probeResult = "completed";
|
|
227
|
+
finish(answers);
|
|
228
|
+
};
|
|
229
|
+
const finishIfPhysicalCleanupConfirmed = () => {
|
|
230
|
+
if (!cleanupStarted || diagnosticTrace.childClose !== "observed" || diagnosticTrace.fd3Close !== "observed") return;
|
|
231
|
+
diagnosticTrace.parentFd3Cleanup = "deadline-confirmed-close";
|
|
232
|
+
finishFromChildClose();
|
|
233
|
+
};
|
|
234
|
+
const onChildExit = (code: number | null) => {
|
|
235
|
+
diagnosticTrace.childExit = code === 0 ? "zero" : "nonzero";
|
|
236
|
+
};
|
|
237
|
+
const onChildClose = (code: number | null) => {
|
|
238
|
+
diagnosticTrace.childClose = "observed";
|
|
239
|
+
if (diagnosticTrace.childExit === "unknown") diagnosticTrace.childExit = code === 0 ? "zero" : "nonzero";
|
|
240
|
+
if (cleanupStarted) finishIfPhysicalCleanupConfirmed();
|
|
241
|
+
else finishFromChildClose();
|
|
242
|
+
};
|
|
243
|
+
const onFd3Close = () => {
|
|
244
|
+
diagnosticTrace.fd3Close = "observed";
|
|
245
|
+
finishIfPhysicalCleanupConfirmed();
|
|
246
|
+
};
|
|
247
|
+
const onChildError = () => {
|
|
248
|
+
diagnosticTrace.childExit = "error";
|
|
249
|
+
if (diagnosticTrace.probeResult === "pending") diagnosticTrace.probeResult = "child-error";
|
|
250
|
+
};
|
|
251
|
+
const onStreamError = () => {
|
|
252
|
+
if (diagnosticTrace.probeResult === "pending") diagnosticTrace.probeResult = "stream-error";
|
|
253
|
+
};
|
|
254
|
+
const startDeadlineCleanup = () => {
|
|
255
|
+
if (settled || cleanupStarted) return;
|
|
256
|
+
cleanupStarted = true;
|
|
257
|
+
diagnosticTrace.parentFd3Cleanup = "deadline-termination-requested";
|
|
258
|
+
broker?.close();
|
|
259
|
+
pipe?.destroy();
|
|
260
|
+
try { child.kill(); } catch { /* The owned child may already have exited. */ }
|
|
261
|
+
closeGrace = setTimeout(() => {
|
|
262
|
+
if (diagnosticTrace.childClose === "pending") diagnosticTrace.childClose = "unconfirmed-after-grace";
|
|
263
|
+
if (diagnosticTrace.fd3Close === "pending") diagnosticTrace.fd3Close = "unconfirmed-after-grace";
|
|
264
|
+
if (diagnosticTrace.fd3Close !== "observed") diagnosticTrace.parentFd3Cleanup = "deadline-unconfirmed-after-grace";
|
|
265
|
+
diagnosticTrace.probeResult = "deadline-unconfirmed";
|
|
266
|
+
finish([], true);
|
|
267
|
+
}, options.childCloseGraceMs ?? 250);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
child.once("exit", onChildExit);
|
|
271
|
+
child.once("close", onChildClose);
|
|
272
|
+
child.on("error", onChildError);
|
|
273
|
+
child.stdout.on("error", onStreamError);
|
|
274
|
+
child.stderr.on("error", onStreamError);
|
|
275
|
+
pipe?.once("close", onFd3Close);
|
|
276
|
+
pipe?.on("error", onStreamError);
|
|
277
|
+
deadline = setTimeout(startDeadlineCleanup, options.childDeadlineMs ?? 6_000);
|
|
278
|
+
});
|
|
60
279
|
}
|
|
61
280
|
|
|
62
281
|
test("the production child fd3 client gets grant, revocation, closure, denial, and request bounds from its parent broker", async () => {
|
|
@@ -101,7 +320,7 @@ test("fresh Jiti moduleCache:false reloads share fd3 structurally and reject sta
|
|
|
101
320
|
`;
|
|
102
321
|
const child = spawn(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", source], {
|
|
103
322
|
env: { ...process.env, GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3" },
|
|
104
|
-
stdio: ["ignore", "pipe", "pipe",
|
|
323
|
+
stdio: ["ignore", "pipe", "pipe", productionFd3StdioMode],
|
|
105
324
|
});
|
|
106
325
|
const pipe = child.stdio[3];
|
|
107
326
|
assert.ok(pipe);
|
|
@@ -231,3 +450,198 @@ test("a child disconnect during a pending parent response cannot crash the broke
|
|
|
231
450
|
parentToChild.destroy();
|
|
232
451
|
}
|
|
233
452
|
});
|
|
453
|
+
|
|
454
|
+
test("AgentRunner delivers real fd3 authorization rechecks through its permission launch", { timeout: 15_000 }, async () => {
|
|
455
|
+
const PROBE_DEADLINE_MS = 12_000;
|
|
456
|
+
const CLOSE_GRACE_MS = 750;
|
|
457
|
+
const moduleUrl = new URL("../lib/review-session-standing-permission-ipc.ts", import.meta.url).href;
|
|
458
|
+
const source = `
|
|
459
|
+
import { createChildStandingReviewPermissionClient } from ${JSON.stringify(moduleUrl)};
|
|
460
|
+
const client = createChildStandingReviewPermissionClient();
|
|
461
|
+
const answers = [];
|
|
462
|
+
for (let index = 0; index < 2; index += 1) answers.push(await client?.requestAuthorization(${JSON.stringify(REPOSITORY_ID)}) ?? false);
|
|
463
|
+
client?.close();
|
|
464
|
+
process.stdout.write(JSON.stringify({ answers, fixtureArgs: process.argv.slice(1) }));
|
|
465
|
+
`;
|
|
466
|
+
const agent: AgentDefinition = { name: "permission-fixture", description: "test", filePath: "/test.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
|
|
467
|
+
const store = new TaskStore();
|
|
468
|
+
let authorizationCalls = 0;
|
|
469
|
+
let child: ReturnType<typeof spawn> | undefined;
|
|
470
|
+
let childStdout: ReturnType<typeof spawn>["stdout"] | undefined;
|
|
471
|
+
let taskId: string | undefined;
|
|
472
|
+
let closeObserved = false;
|
|
473
|
+
let terminating = false;
|
|
474
|
+
let launchFailure: Error | undefined;
|
|
475
|
+
let childFailure: Error | undefined;
|
|
476
|
+
let terminationFailure: Error | undefined;
|
|
477
|
+
let deadline: NodeJS.Timeout | undefined;
|
|
478
|
+
let grace: NodeJS.Timeout | undefined;
|
|
479
|
+
let stdout = "";
|
|
480
|
+
let stdoutBytes = 0;
|
|
481
|
+
let stdoutTruncated = false;
|
|
482
|
+
let finishProbe!: (result: { closeObserved: boolean; failure: Error | undefined }) => void;
|
|
483
|
+
const probe = new Promise<{ closeObserved: boolean; failure: Error | undefined }>((resolve) => { finishProbe = resolve; });
|
|
484
|
+
let probeFinished = false;
|
|
485
|
+
let runner: AgentRunner;
|
|
486
|
+
const clearTimers = () => {
|
|
487
|
+
if (deadline !== undefined) clearTimeout(deadline);
|
|
488
|
+
if (grace !== undefined) clearTimeout(grace);
|
|
489
|
+
deadline = undefined;
|
|
490
|
+
grace = undefined;
|
|
491
|
+
};
|
|
492
|
+
const releaseOwnedReferences = () => {
|
|
493
|
+
const unref = (value: unknown) => {
|
|
494
|
+
if (value !== null && typeof value === "object" && "unref" in value && typeof value.unref === "function") value.unref();
|
|
495
|
+
};
|
|
496
|
+
child?.unref();
|
|
497
|
+
unref(child?.channel);
|
|
498
|
+
unref(child?.stdout);
|
|
499
|
+
unref(child?.stderr);
|
|
500
|
+
for (const stream of child?.stdio ?? []) unref(stream);
|
|
501
|
+
};
|
|
502
|
+
const finish = (result: { closeObserved: boolean; failure: Error | undefined }) => {
|
|
503
|
+
if (probeFinished) return;
|
|
504
|
+
probeFinished = true;
|
|
505
|
+
clearTimers();
|
|
506
|
+
finishProbe(result);
|
|
507
|
+
};
|
|
508
|
+
const onStdout = (chunk: Buffer | string) => {
|
|
509
|
+
if (stdoutBytes >= 4_096) {
|
|
510
|
+
stdoutTruncated = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
514
|
+
const captured = bytes.subarray(0, 4_096 - stdoutBytes);
|
|
515
|
+
if (captured.length !== bytes.length) stdoutTruncated = true;
|
|
516
|
+
stdout += captured.toString("utf8");
|
|
517
|
+
stdoutBytes += captured.length;
|
|
518
|
+
};
|
|
519
|
+
const removeOperationalListeners = () => {
|
|
520
|
+
child?.off("error", onChildError);
|
|
521
|
+
child?.off("close", onChildClose);
|
|
522
|
+
childStdout?.off("data", onStdout);
|
|
523
|
+
};
|
|
524
|
+
const retainLateCloseGuard = () => {
|
|
525
|
+
const suppressLateStreamError = () => {};
|
|
526
|
+
child?.once("close", () => {
|
|
527
|
+
closeObserved = true;
|
|
528
|
+
releaseOwnedReferences();
|
|
529
|
+
});
|
|
530
|
+
child?.once("error", suppressLateStreamError);
|
|
531
|
+
childStdout?.once("error", suppressLateStreamError);
|
|
532
|
+
child?.stderr?.once("error", suppressLateStreamError);
|
|
533
|
+
for (const stream of child?.stdio ?? []) stream?.once("error", suppressLateStreamError);
|
|
534
|
+
};
|
|
535
|
+
const terminate = (failure: Error) => {
|
|
536
|
+
if (terminating || closeObserved) return;
|
|
537
|
+
terminating = true;
|
|
538
|
+
terminationFailure ??= failure;
|
|
539
|
+
clearTimers();
|
|
540
|
+
if (!taskId || !runner.cancel(taskId)) {
|
|
541
|
+
try { child?.kill(); } catch { /* The owned child may already have exited. */ }
|
|
542
|
+
}
|
|
543
|
+
if (closeObserved) return;
|
|
544
|
+
grace = setTimeout(() => {
|
|
545
|
+
if (closeObserved) return;
|
|
546
|
+
removeOperationalListeners();
|
|
547
|
+
releaseOwnedReferences();
|
|
548
|
+
retainLateCloseGuard();
|
|
549
|
+
finish({ closeObserved: false, failure });
|
|
550
|
+
}, CLOSE_GRACE_MS);
|
|
551
|
+
};
|
|
552
|
+
const onChildError = (error: Error) => {
|
|
553
|
+
childFailure ??= error;
|
|
554
|
+
terminate(error);
|
|
555
|
+
};
|
|
556
|
+
const onChildClose = () => {
|
|
557
|
+
closeObserved = true;
|
|
558
|
+
removeOperationalListeners();
|
|
559
|
+
releaseOwnedReferences();
|
|
560
|
+
finish({ closeObserved: true, failure: childFailure ?? terminationFailure });
|
|
561
|
+
};
|
|
562
|
+
runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 10_000 }, {
|
|
563
|
+
spawn: (command, args, options) => {
|
|
564
|
+
try {
|
|
565
|
+
child = spawn(command, args, { cwd: options.cwd, env: options.env, detached: options.detached, stdio: options.stdio });
|
|
566
|
+
} catch (error) {
|
|
567
|
+
launchFailure = error instanceof Error ? error : new Error(String(error));
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
childStdout = child.stdout;
|
|
571
|
+
if (childStdout === null) {
|
|
572
|
+
launchFailure = new Error("AgentRunner child stdout is required");
|
|
573
|
+
queueMicrotask(() => terminate(launchFailure!));
|
|
574
|
+
} else childStdout.on("data", onStdout);
|
|
575
|
+
child.once("error", onChildError);
|
|
576
|
+
child.once("close", onChildClose);
|
|
577
|
+
deadline = setTimeout(() => terminate(new Error(`owned child startup/handshake probe exceeded ${PROBE_DEADLINE_MS}ms`)), PROBE_DEADLINE_MS);
|
|
578
|
+
return child as unknown as ChildLike;
|
|
579
|
+
},
|
|
580
|
+
now: Date.now,
|
|
581
|
+
schedule: (fn, ms) => {
|
|
582
|
+
const timer = setTimeout(fn, ms);
|
|
583
|
+
return () => clearTimeout(timer);
|
|
584
|
+
},
|
|
585
|
+
pi: { command: process.execPath, args: ["--experimental-strip-types", "--input-type=module", "--eval", source, "--"] },
|
|
586
|
+
}, { askUser: async () => ({ cancelled: true }) });
|
|
587
|
+
const request: TaskRequest = {
|
|
588
|
+
agent,
|
|
589
|
+
prompt: "permission fixture",
|
|
590
|
+
label: undefined,
|
|
591
|
+
context: undefined,
|
|
592
|
+
mode: AGENT_MODE.BACKGROUND,
|
|
593
|
+
cwd: process.cwd(),
|
|
594
|
+
parentSessionId: "test",
|
|
595
|
+
model: undefined,
|
|
596
|
+
thinking: undefined,
|
|
597
|
+
sessionDir: "/tmp",
|
|
598
|
+
resumeSessionPath: undefined,
|
|
599
|
+
env: {},
|
|
600
|
+
authorizeParentStandingReviewPermission: () => ++authorizationCalls === 1,
|
|
601
|
+
};
|
|
602
|
+
try {
|
|
603
|
+
const task = runner.run(request);
|
|
604
|
+
taskId = task.id;
|
|
605
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
606
|
+
assert.ifError(launchFailure);
|
|
607
|
+
assert.ok(child, "AgentRunner must own the real child launch");
|
|
608
|
+
const result = await probe;
|
|
609
|
+
assert.equal(result.closeObserved, true, result.failure?.message ?? "owned child close was unconfirmed after bounded cleanup");
|
|
610
|
+
assert.ifError(result.failure);
|
|
611
|
+
assert.equal(stdoutTruncated, false, "child stdout capture remains within 4096 bytes");
|
|
612
|
+
const output = (() => {
|
|
613
|
+
try { return JSON.parse(stdout) as { answers?: unknown; fixtureArgs?: unknown }; }
|
|
614
|
+
catch (error) { assert.fail(`could not parse bounded child stdout: ${error instanceof Error ? error.message : String(error)}`); }
|
|
615
|
+
})();
|
|
616
|
+
assert.deepEqual(output.answers, [true, false], "the real child receives one grant and one rechecked denial through AgentRunner");
|
|
617
|
+
assert.ok(Array.isArray(output.fixtureArgs) && output.fixtureArgs.includes("--mode") && output.fixtureArgs.includes("rpc"), "the spawn adapter forwards Runner-appended arguments after Node's -- boundary");
|
|
618
|
+
assert.equal(authorizationCalls, 2, "AgentRunner invokes its parent callback for both child requests");
|
|
619
|
+
} finally {
|
|
620
|
+
if (!closeObserved) terminate(new Error("test cleanup requested owned child termination"));
|
|
621
|
+
await probe;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
if (process.platform === "win32") {
|
|
626
|
+
test("diagnostic: fd3 production child compares pipe and overlapped without inferring a pipe denial", { timeout: 15_000 }, async (t) => {
|
|
627
|
+
const results: Array<{ answers: boolean[] | undefined; trace: WindowsFd3DiagnosticTrace }> = [];
|
|
628
|
+
for (const stdioMode of ["pipe", "overlapped"] as const) {
|
|
629
|
+
const trace = windowsFd3DiagnosticTrace(stdioMode);
|
|
630
|
+
let answers: boolean[] | undefined;
|
|
631
|
+
try {
|
|
632
|
+
answers = await productionChild(1, () => true, { stdioMode, childDeadlineMs: 6_000, childCloseGraceMs: 250, diagnosticTrace: trace });
|
|
633
|
+
} catch {
|
|
634
|
+
trace.probeResult = "spawn-failure";
|
|
635
|
+
}
|
|
636
|
+
t.diagnostic(JSON.stringify(trace));
|
|
637
|
+
results.push({ answers, trace });
|
|
638
|
+
}
|
|
639
|
+
const overlapped = results.find((result) => result.trace.stdioMode === "overlapped");
|
|
640
|
+
assert.deepEqual(overlapped?.answers, [true], "overlapped must carry a production-client grant from the direct parent broker");
|
|
641
|
+
assert.equal(overlapped?.trace.childClientAttached, true, "the production child must attach its fd3 client");
|
|
642
|
+
assert.equal(overlapped?.trace.childRequestAttemptCount, 1, "the production child must attempt one synthetic request");
|
|
643
|
+
assert.ok((overlapped?.trace.parentRawDataChunkCount ?? 0) > 0, "the parent must receive child request data without assuming stream chunk framing");
|
|
644
|
+
assert.equal(overlapped?.trace.parentAuthorizationCallbackCount, 1, "the direct parent broker must authorize one request");
|
|
645
|
+
assert.equal(overlapped?.trace.probeResult, "completed", "the overlapped probe must close and parse cleanly");
|
|
646
|
+
});
|
|
647
|
+
}
|
|
@@ -1254,7 +1254,7 @@ async function run() {
|
|
|
1254
1254
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1255
1255
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1256
1256
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1257
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")),
|
|
1257
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1258
1258
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1259
1259
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1260
1260
|
assert.equal(ctx.ui.selections.length, 1, "first interactive SDD trigger confirms session suggestions");
|
|
@@ -1286,7 +1286,7 @@ async function run() {
|
|
|
1286
1286
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1287
1287
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1288
1288
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1289
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")),
|
|
1289
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1290
1290
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1291
1291
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1292
1292
|
const globalSddApply = await readFile(
|
|
@@ -1793,7 +1793,7 @@ async function run() {
|
|
|
1793
1793
|
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
1794
1794
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
1795
1795
|
await commands.get("gentle:status").handler("", ctx);
|
|
1796
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides:
|
|
1796
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides: 5 file\(s\)/);
|
|
1797
1797
|
assert.match(ctx.ui.notifications.at(-1).message, /active non-builtin SDD agents shadow package assets/);
|
|
1798
1798
|
await commands.get("gentle:doctor").handler("", ctx);
|
|
1799
1799
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
@@ -1823,7 +1823,7 @@ async function run() {
|
|
|
1823
1823
|
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1824
1824
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1825
1825
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1826
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")),
|
|
1826
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1827
1827
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1828
1828
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1829
1829
|
assert.equal(ctx.ui.selections.length, 1, "sdd-init confirms session preflight before project initialization");
|
|
@@ -67,10 +67,9 @@ const requiredToolsByAgent: Record<string, string[]> = {
|
|
|
67
67
|
"sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
68
68
|
"sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
69
69
|
"sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
|
-
"sdd-research.md": [
|
|
70
|
+
"sdd-research.md": [ "fetch_content", "web_search", "source_check", "get_search_content"],
|
|
71
71
|
"sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
72
72
|
"sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
|
|
73
|
-
"sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
74
73
|
"sdd-tasks.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
75
74
|
"sdd-verify.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
|
|
76
75
|
};
|
|
@@ -91,13 +90,14 @@ test("SDD package agents declare role-appropriate tools as YAML arrays", () => {
|
|
|
91
90
|
|
|
92
91
|
test("artifact-producing SDD agents can persist OpenSpec files while status remains read-only", () => {
|
|
93
92
|
for (const fileName of Object.keys(requiredToolsByAgent).filter(
|
|
94
|
-
(fileName) =>
|
|
93
|
+
(fileName) => !["sdd-status.md", "sdd-research.md"].includes(fileName),
|
|
95
94
|
)) {
|
|
96
95
|
const tools = readTools(join(assetsAgentsDir, fileName));
|
|
97
96
|
assert.ok(tools.includes("edit"), `${fileName} must include edit`);
|
|
98
97
|
assert.ok(tools.includes("write"), `${fileName} must include write`);
|
|
99
98
|
}
|
|
100
99
|
|
|
100
|
+
assert.deepEqual(readTools(join(assetsAgentsDir, "sdd-research.md")), requiredToolsByAgent["sdd-research.md"]);
|
|
101
101
|
const statusTools = readTools(join(assetsAgentsDir, "sdd-status.md"));
|
|
102
102
|
assert.ok(!statusTools.includes("edit"), "sdd-status.md must remain read-only");
|
|
103
103
|
assert.ok(!statusTools.includes("write"), "sdd-status.md must remain read-only");
|
|
@@ -108,7 +108,7 @@ test("research instructions require executed evidence rather than blanket denial
|
|
|
108
108
|
assert.doesNotMatch(source, /documentation=\[\]; open-web=\[\]/);
|
|
109
109
|
assert.match(source, /Actually call approved tools/);
|
|
110
110
|
assert.match(source, /claim maps to source IDs/);
|
|
111
|
-
assert.match(source, /
|
|
111
|
+
assert.match(source, /partial or unavailable research is not a failed proposal gate/);
|
|
112
112
|
assert.ok(!readTools(join(assetsAgentsDir, "sdd-research.md")).includes("bash"));
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -132,39 +132,18 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
test("
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"blockers:",
|
|
143
|
-
"critical_findings:",
|
|
144
|
-
"requirements:",
|
|
145
|
-
"scenarios:",
|
|
146
|
-
"test_command:",
|
|
147
|
-
"test_exit_code:",
|
|
148
|
-
"test_output_hash: sha256:",
|
|
149
|
-
"build_command:",
|
|
150
|
-
"build_exit_code:",
|
|
151
|
-
"build_output_hash: sha256:",
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
const agentSource = readFileSync(join(assetsAgentsDir, "sdd-verify.md"), "utf8");
|
|
155
|
-
assert.match(agentSource, /```yaml\nschema: gentle-ai\.verify-result\/v1\n/);
|
|
156
|
-
for (const field of envelopeFields) {
|
|
157
|
-
assert.ok(agentSource.includes(field), `sdd-verify.md envelope must carry \`${field}\``);
|
|
135
|
+
test("optional verification retains practical evidence without retired attestation admission", () => {
|
|
136
|
+
const agent = readFileSync(join(assetsAgentsDir, "sdd-verify.md"), "utf8");
|
|
137
|
+
const chain = readFileSync(join(repoRoot, "assets/chains/sdd-verify.chain.md"), "utf8");
|
|
138
|
+
for (const source of [agent, chain]) {
|
|
139
|
+
assert.doesNotMatch(source, /sdd-verify-validate|gentle-ai\.verify-result\/v1/);
|
|
140
|
+
assert.match(source, /commands/);
|
|
141
|
+
assert.match(source, /blockers/);
|
|
158
142
|
}
|
|
159
|
-
assert.match(
|
|
160
|
-
assert.match(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const chainSource = readFileSync(join(repoRoot, "assets", "chains", "sdd-verify.chain.md"), "utf8");
|
|
166
|
-
assert.match(chainSource, /gentle-ai\.verify-result\/v1/);
|
|
167
|
-
assert.match(chainSource, /sdd-verify-validate/);
|
|
143
|
+
assert.match(agent, /strict TDD compliance/);
|
|
144
|
+
assert.match(agent, /exit codes/);
|
|
145
|
+
assert.match(agent, /never fabricate PASS/);
|
|
146
|
+
assert.match(chain, /does not make verification mandatory/);
|
|
168
147
|
});
|
|
169
148
|
|
|
170
149
|
test("the retired Pi adversarial role agents are not packaged", () => {
|