gentle-pi 2.6.1 → 2.6.3
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 +183 -943
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/docs/assets/brand/gentle-pi-banner.svg +33 -0
- package/docs/assets/brand/terminal-divider.svg +17 -0
- package/docs/assets/diagrams/agent-orchestration.svg +19 -0
- package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
- package/docs/assets/diagrams/native-review.svg +16 -0
- package/docs/assets/diagrams/sdd-cycle.svg +14 -0
- package/docs/assets/features/gentle-shell.png +0 -0
- package/docs/gentle-shell.md +151 -0
- package/docs/readme-reference.md +868 -0
- package/extensions/gentle-agents.ts +122 -9
- package/extensions/gentle-ai.ts +65 -22
- package/lib/agents-history.ts +89 -2
- package/lib/agents-runner.ts +44 -11
- package/lib/native-review-cli.ts +22 -0
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +22 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/agents-history.test.ts +23 -1
- package/tests/agents-runner.test.ts +154 -2
- package/tests/gentle-agents.test.ts +102 -4
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +3 -2
- package/tests/native-review-capability-contract.test.ts +29 -1
- package/tests/package-manifest.test.ts +19 -18
- package/tests/review-authority-recovery-docs.test.ts +13 -13
- package/tests/review-controller-native-routing.test.ts +49 -0
- package/tests/review-ledger-contract.test.ts +7 -5
- package/tests/sdd-managed-runtime-settlement.test.ts +112 -0
- package/tests/sdd-selection-transport.test.ts +57 -0
- package/tests/skill-collision-prefixes.test.ts +2 -2
|
@@ -972,6 +972,28 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
972
972
|
// this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
|
|
973
973
|
// because neither is proven to reach the negotiated START path Pi consumes.
|
|
974
974
|
"2.8.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
975
|
+
// v2.8.2 shipped OpenCode SDD preflight plugin fixes, community-tools RTK
|
|
976
|
+
// acquisition, and Claude Code Stop telemetry. The provider contract semver
|
|
977
|
+
// stays 1.2.0; the same pin re-mirrors bundle bytes that had drifted under
|
|
978
|
+
// that semver (lens inspection.status "unavailable", targeted-validator
|
|
979
|
+
// regressions/inspection members, seven Pi stop reason codes). None of
|
|
980
|
+
// those touch the closed START/STATUS fields this row negotiates, so it
|
|
981
|
+
// repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
|
|
982
|
+
// is proven to reach the negotiated START path Pi consumes.
|
|
983
|
+
"2.8.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
984
|
+
// v2.9.0 shipped RTK opt-in Community Tool integration (#4560, installer/
|
|
985
|
+
// sync/TUI only), SDD attempt-ledger fixes (#4564, #4567, #4569 — the
|
|
986
|
+
// remediation pointer is now decided by chain equality before shape, and
|
|
987
|
+
// the refusal wording changed), sync telemetry-runtime symlinked root
|
|
988
|
+
// (#4565), OpenCode reviewer Task wrapper decoding (#4545), and Engram
|
|
989
|
+
// protocol asset wording (#4179). Ground-truthed by diffing
|
|
990
|
+
// contracts/review-integration/v2 and contracts/review-provider-contract
|
|
991
|
+
// between the v2.8.2 and v2.9.0 tags in the gentle-ai source tree: zero
|
|
992
|
+
// bytes changed. None of the above touch the closed START/STATUS fields
|
|
993
|
+
// this row negotiates, so it repeats 2.8.2 exactly. riskEvidence and hint
|
|
994
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
995
|
+
// path Pi consumes.
|
|
996
|
+
"2.9.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
975
997
|
});
|
|
976
998
|
|
|
977
999
|
|
|
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "2.
|
|
39
|
+
export const INSTALLER_VERSION = "2.9.0";
|
|
40
40
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai";
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v2";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.9.0`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit be495547, the published v2.9.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:WqmYw1UfVYCbtOxU1uTuiYq9+ROCUIDrmKIOjyYggQI=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The v2.
|
|
70
|
+
// still holds it. The v2.9.0 digests are pinned from the published release:
|
|
71
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
72
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
73
73
|
// executables.
|
|
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
112
|
-
// distributing unsigned Windows builds in c4b764d0, so v2.
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v2.9.0 publishes signed
|
|
113
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
114
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
115
115
|
// upstream ships signed Windows assets.
|
|
116
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
117
|
-
"darwin/amd64": asset("gentle-ai_2.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_2.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_2.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_2.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_2.9.0_darwin_amd64.tar.gz", "0e1ce0b117e6f15b56e05defecb33a33825c2e25eb8306df09401d41e0a176fb", "0dc22de552e0403f318770067adccd82783b1f654ed4d0410d7d969e28e39602", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_2.9.0_darwin_arm64.tar.gz", "0a58d81cd7d76315e1d11ecf6b38abb27ee8e7e709a04c9a786e89636fb559bb", "5e72f1f34667a858fc179504e794847a1407142240361f99effd252d6ad49ee9", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_2.9.0_linux_amd64.tar.gz", "7d414cd8cba8ddc0ab9fa4bc309932638c533217b2f999b2a52e7a4fc0bf6f14", "5c47029fb6520f99b7df1b6841766b09d7184efed2e18694d83795bf4d4a5ad4", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_2.9.0_linux_arm64.tar.gz", "2bdab4684b5d415df9c9423c2022b017c7c1c1d9056f56acc08633fa0d821cfc", "56501a4d91f4e3255726b63c59b96a6d42c6952af2d265f54c8e99b361cda0c9", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -340,7 +340,7 @@ async function main() {
|
|
|
340
340
|
});
|
|
341
341
|
|
|
342
342
|
if (driftedContracts.length > 0) {
|
|
343
|
-
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.
|
|
343
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.9.0 runtime's vendored Gentle AI contract artifacts:");
|
|
344
344
|
for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
|
|
345
345
|
process.exit(1);
|
|
346
346
|
}
|
|
@@ -385,7 +385,7 @@ async function main() {
|
|
|
385
385
|
process.exit(1);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.
|
|
388
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.9.0 runtime).`);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import test, { after } from "node:test";
|
|
6
|
-
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
7
|
+
import { acquireTaskLock, historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
7
8
|
import { applyTaskEvent, emptyThread, TASK_EVENT, TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
8
9
|
|
|
9
10
|
// Gentle Agents history: JSON per task, async, lazy, pruned by count.
|
|
@@ -12,6 +13,13 @@ const root = mkdtempSync(join(tmpdir(), "gentle-agents-history-"));
|
|
|
12
13
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
13
14
|
const dir = join(root, "tasks");
|
|
14
15
|
|
|
16
|
+
function orphanLock(lockDir: string, id: string): void {
|
|
17
|
+
const moduleUrl = new URL("../lib/agents-history.ts", import.meta.url).href;
|
|
18
|
+
const source = `import { acquireTaskLock } from ${JSON.stringify(moduleUrl)}; const [dir, id] = process.argv.slice(-2); acquireTaskLock(dir, id);`;
|
|
19
|
+
const child = spawnSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "-e", source, lockDir, id], { encoding: "utf8" });
|
|
20
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
function task(id: string, createdAt: number): TaskRecord {
|
|
16
24
|
return { id, agent: "explore", mode: "task", prompt: "p", label: "p", cwd: "/r", parentSessionId: "s", status: TASK_STATUS.COMPLETED, createdAt, startedAt: createdAt, endedAt: createdAt + 5, model: "m", thinking: undefined, sessionPath: null, error: null, result: "ok", lastStep: "done", lastActivityAt: createdAt, turns: 1, toolCalls: 0, tokens: 10, cost: 0.01 };
|
|
17
25
|
}
|
|
@@ -33,6 +41,20 @@ test("saveTask writes a task with its thread and loadStoredTask reads it back",
|
|
|
33
41
|
assert.deepEqual(readdirSync(dir), ["a1.json"], "no temp file is left behind");
|
|
34
42
|
});
|
|
35
43
|
|
|
44
|
+
test("task reconciliation elections bypass dead candidates and fail closed for active or ambiguous candidates", () => {
|
|
45
|
+
const lockDir = join(root, "task-locks"), token = "11111111-1111-4111-8111-111111111111";
|
|
46
|
+
const held = acquireTaskLock(lockDir, "busy");
|
|
47
|
+
assert.match(held.path, /busy\.reconcile\.[0-9a-f-]+$/);
|
|
48
|
+
assert.throws(() => acquireTaskLock(lockDir, "busy"), /busy|active|ambiguous/i); held.release();
|
|
49
|
+
orphanLock(lockDir, "dead");
|
|
50
|
+
const deadName = readdirSync(lockDir).find(name => name.startsWith("dead.reconcile."))!;
|
|
51
|
+
const bypassed = acquireTaskLock(lockDir, "dead");
|
|
52
|
+
assert.notEqual(bypassed.path, join(lockDir, deadName)); assert.ok(!readdirSync(lockDir).includes(deadName)); bypassed.release();
|
|
53
|
+
const malformed = join(lockDir, `malformed.reconcile.${token}`); writeFileSync(malformed, "not-json");
|
|
54
|
+
assert.throws(() => acquireTaskLock(lockDir, "malformed"), /busy|active|ambiguous|malformed/i);
|
|
55
|
+
const foreign = join(lockDir, `foreign.reconcile.${token}`); writeFileSync(foreign, JSON.stringify({ schema: "gentle-pi.task-reconciliation-lock/v1", taskId: "foreign", token, pid: process.pid, host: "foreign-host" }));
|
|
56
|
+
assert.throws(() => acquireTaskLock(lockDir, "foreign"), /busy|active|ambiguous|foreign/i);
|
|
57
|
+
});
|
|
36
58
|
test("loadHistory skips broken files, sorts newest first, and pruneHistory keeps the newest N", async () => {
|
|
37
59
|
await saveTask(dir, task("b2", 3000), emptyThread());
|
|
38
60
|
await saveTask(dir, task("c3", 2000), emptyThread());
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
3
4
|
import { AGENT_MODE, parseAgentsConfig, resolveAgentProfile, type AgentDefinition } from "../lib/agents-config.ts";
|
|
4
5
|
import { TASK_STATUS, TaskStore, type RemediationTaskState, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
5
6
|
import { AgentRunner, childArguments, JsonLines, piCommand, abortReasonText, type RemediationPlan, type RemediationTerminalFacts, type RunnerDeps, type RunnerHooks, type TaskRequest } from "../lib/agents-runner.ts";
|
|
@@ -25,7 +26,7 @@ interface Harness {
|
|
|
25
26
|
spawnOptions: Array<{ env: NodeJS.ProcessEnv; stdio?: string[] }>;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function harness(options: { pid?: number; maxConcurrency?: number; answer?: Record<string, unknown>; exitOnKill?: boolean; state?: Record<string, unknown>; stateSuccess?: boolean; onNotification?: RunnerHooks["onNotification"]; onSuccessfulMutation?: RunnerHooks["onSuccessfulMutation"]; onFinish?: RunnerHooks["onFinish"] } = {}): Harness {
|
|
29
|
+
function harness(options: { pid?: number; maxConcurrency?: number; stallTimeoutMs?: number; answer?: Record<string, unknown>; exitOnKill?: boolean; state?: Record<string, unknown>; stateSuccess?: boolean; onNotification?: RunnerHooks["onNotification"]; onSuccessfulMutation?: RunnerHooks["onSuccessfulMutation"]; onFinish?: RunnerHooks["onFinish"] } = {}): Harness {
|
|
29
30
|
const children: FakeChild[] = [];
|
|
30
31
|
const timers: Harness["timers"] = [];
|
|
31
32
|
const asks: Harness["asks"] = [];
|
|
@@ -59,7 +60,7 @@ function harness(options: { pid?: number; maxConcurrency?: number; answer?: Reco
|
|
|
59
60
|
pi: { command: "pi", args: [] },
|
|
60
61
|
};
|
|
61
62
|
const store = new TaskStore();
|
|
62
|
-
const runner = new AgentRunner(store, { maxConcurrency: options.maxConcurrency ?? 2, stallTimeoutMs: 10_000 }, deps, {
|
|
63
|
+
const runner = new AgentRunner(store, { maxConcurrency: options.maxConcurrency ?? 2, stallTimeoutMs: options.stallTimeoutMs ?? 10_000 }, deps, {
|
|
63
64
|
askUser: async (taskId, ask) => {
|
|
64
65
|
asks.push({ taskId, method: ask.method });
|
|
65
66
|
return options.answer ?? { value: "yes" };
|
|
@@ -73,6 +74,157 @@ function harness(options: { pid?: number; maxConcurrency?: number; answer?: Reco
|
|
|
73
74
|
|
|
74
75
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
75
76
|
|
|
77
|
+
const FOUR_MIN_MS = 4 * 60_000;
|
|
78
|
+
|
|
79
|
+
// A child that never answers the launch RPC commands (get_state, prompt), so
|
|
80
|
+
// the task's lastStep never leaves its initial "starting" stage. Used to
|
|
81
|
+
// exercise the stall watchdog before any child response arrives.
|
|
82
|
+
function silentHarness(stallTimeoutMs: number): { store: TaskStore; runner: AgentRunner; timers: Array<{ fn: () => void; ms: number; cancelled: boolean }>; child: () => FakeChild } {
|
|
83
|
+
const timers: Array<{ fn: () => void; ms: number; cancelled: boolean }> = [];
|
|
84
|
+
let clock = 1000;
|
|
85
|
+
let created: FakeChild | undefined;
|
|
86
|
+
const store = new TaskStore();
|
|
87
|
+
const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs }, {
|
|
88
|
+
spawn: () => {
|
|
89
|
+
created = fakeChild();
|
|
90
|
+
created.child.stdin.removeAllListeners("data");
|
|
91
|
+
return created.child;
|
|
92
|
+
},
|
|
93
|
+
now: () => (clock += 1),
|
|
94
|
+
schedule: (fn, ms) => {
|
|
95
|
+
const timer = { fn, ms, cancelled: false };
|
|
96
|
+
timers.push(timer);
|
|
97
|
+
return () => {
|
|
98
|
+
timer.cancelled = true;
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
pi: { command: "pi", args: [] },
|
|
102
|
+
}, { askUser: async () => ({ cancelled: true }) });
|
|
103
|
+
return { store, runner, timers, child: () => created! };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test("stall before any child response records the last completed stage as starting, with the stderr tail", async () => {
|
|
107
|
+
const h = silentHarness(FOUR_MIN_MS);
|
|
108
|
+
const task = h.runner.run(request());
|
|
109
|
+
await tick();
|
|
110
|
+
(h.child().child.stderr as unknown as PassThrough).write("Error: cannot bind\nprovider socket\n");
|
|
111
|
+
await tick();
|
|
112
|
+
const stall = h.timers.filter((timer) => timer.ms === FOUR_MIN_MS && !timer.cancelled).at(-1);
|
|
113
|
+
assert.ok(stall);
|
|
114
|
+
stall!.fn();
|
|
115
|
+
await tick();
|
|
116
|
+
assert.equal(h.store.get(task.id)?.status, TASK_STATUS.TIMED_OUT);
|
|
117
|
+
assert.equal(h.store.get(task.id)?.error, "stalled for 4 min after: starting; stderr: Error: cannot bind provider socket");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("stall after get_state and prompt responses records the prompt accepted stage", async () => {
|
|
121
|
+
const h = harness({ stallTimeoutMs: FOUR_MIN_MS });
|
|
122
|
+
const task = h.runner.run(request());
|
|
123
|
+
await tick();
|
|
124
|
+
assert.deepEqual(h.children[0].written.map((command) => command.type), ["get_state", "prompt"]);
|
|
125
|
+
assert.equal(h.store.get(task.id)?.lastStep, "prompt accepted");
|
|
126
|
+
const stall = h.timers.filter((timer) => timer.ms === FOUR_MIN_MS && !timer.cancelled).at(-1);
|
|
127
|
+
assert.ok(stall);
|
|
128
|
+
stall!.fn();
|
|
129
|
+
await tick();
|
|
130
|
+
assert.equal(h.store.get(task.id)?.error, "stalled for 4 min after: prompt accepted");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("stderr tail bounds the child's raw output to 512 characters before stripping ANSI escapes", async () => {
|
|
134
|
+
const h = silentHarness(FOUR_MIN_MS);
|
|
135
|
+
const task = h.runner.run(request());
|
|
136
|
+
await tick();
|
|
137
|
+
const filler = "x".repeat(508);
|
|
138
|
+
(h.child().child.stderr as unknown as PassThrough).write(`${filler}[31mOK[0m`);
|
|
139
|
+
await tick();
|
|
140
|
+
const stall = h.timers.filter((timer) => timer.ms === FOUR_MIN_MS && !timer.cancelled).at(-1)!;
|
|
141
|
+
stall.fn();
|
|
142
|
+
await tick();
|
|
143
|
+
const expectedTail = `${"x".repeat(501)}OK`;
|
|
144
|
+
assert.equal(h.store.get(task.id)?.error, `stalled for 4 min after: starting; stderr: ${expectedTail}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("child exit before agent_settled includes the stderr tail; a completed task never carries stderr", async () => {
|
|
148
|
+
const h = harness({ maxConcurrency: 2 });
|
|
149
|
+
const crashing = h.runner.run(request());
|
|
150
|
+
const completing = h.runner.run(request({ prompt: "finish clean" }));
|
|
151
|
+
await tick();
|
|
152
|
+
const crashChild = h.children[0];
|
|
153
|
+
const doneChild = h.children[1];
|
|
154
|
+
(crashChild.child.stderr as unknown as PassThrough).write("panic: provider unavailable");
|
|
155
|
+
await tick();
|
|
156
|
+
crashChild.exit(1);
|
|
157
|
+
await tick();
|
|
158
|
+
assert.equal(h.store.get(crashing.id)?.status, TASK_STATUS.FAILED);
|
|
159
|
+
assert.equal(h.store.get(crashing.id)?.error, "pi exited with code 1 before agent_settled; stderr: panic: provider unavailable");
|
|
160
|
+
|
|
161
|
+
(doneChild.child.stderr as unknown as PassThrough).write("noisy but irrelevant");
|
|
162
|
+
await tick();
|
|
163
|
+
doneChild.emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "final report" }], stopReason: "stop" }] });
|
|
164
|
+
doneChild.emit({ type: "agent_settled" });
|
|
165
|
+
await h.runner.waitFor(completing.id);
|
|
166
|
+
assert.equal(h.store.get(completing.id)?.status, TASK_STATUS.COMPLETED);
|
|
167
|
+
assert.equal(h.store.get(completing.id)?.error, null);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("cancel(id, reason) records the given reason for a live and a queued task; cancelAll(reason) threads it", async () => {
|
|
171
|
+
const h = harness({ maxConcurrency: 1 });
|
|
172
|
+
const running = h.runner.run(request());
|
|
173
|
+
const queued = h.runner.run(request({ prompt: "queued work" }));
|
|
174
|
+
await tick();
|
|
175
|
+
assert.equal(h.store.get(queued.id)?.status, TASK_STATUS.QUEUED);
|
|
176
|
+
assert.equal(h.runner.cancel(queued.id, "stopped from the agents panel"), true);
|
|
177
|
+
assert.equal(h.store.get(queued.id)?.status, TASK_STATUS.CANCELLED);
|
|
178
|
+
assert.equal(h.store.get(queued.id)?.error, "stopped from the agents panel before start");
|
|
179
|
+
assert.equal(h.runner.cancel(running.id, "stopped from the agents panel"), true);
|
|
180
|
+
await tick();
|
|
181
|
+
assert.equal(h.store.get(running.id)?.status, TASK_STATUS.CANCELLED);
|
|
182
|
+
assert.equal(h.store.get(running.id)?.error, "stopped from the agents panel");
|
|
183
|
+
|
|
184
|
+
const h2 = harness({ maxConcurrency: 1 });
|
|
185
|
+
const runningTwo = h2.runner.run(request());
|
|
186
|
+
const queuedTwo = h2.runner.run(request({ prompt: "queued work" }));
|
|
187
|
+
await tick();
|
|
188
|
+
assert.equal(h2.runner.cancelAll("cancelled: parent session shut down"), 2);
|
|
189
|
+
await tick();
|
|
190
|
+
assert.equal(h2.store.get(runningTwo.id)?.error, "cancelled: parent session shut down");
|
|
191
|
+
assert.equal(h2.store.get(queuedTwo.id)?.error, "cancelled: parent session shut down before start");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("earlier get_state and prompt responses cannot regress lastStep past a later child event", async () => {
|
|
195
|
+
const store = new TaskStore();
|
|
196
|
+
const timers: Array<{ fn: () => void; ms: number; cancelled: boolean }> = [];
|
|
197
|
+
let clock = 1000;
|
|
198
|
+
const fake = fakeChild();
|
|
199
|
+
fake.child.stdin.removeAllListeners("data"); // respond to get_state/prompt manually, out of order
|
|
200
|
+
const written: Array<Record<string, unknown>> = [];
|
|
201
|
+
fake.child.stdin.on("data", (chunk: Buffer) => written.push(JSON.parse(chunk.toString())));
|
|
202
|
+
const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: FOUR_MIN_MS }, {
|
|
203
|
+
spawn: () => fake.child,
|
|
204
|
+
now: () => (clock += 1),
|
|
205
|
+
schedule: (fn, ms) => {
|
|
206
|
+
const timer = { fn, ms, cancelled: false };
|
|
207
|
+
timers.push(timer);
|
|
208
|
+
return () => {
|
|
209
|
+
timer.cancelled = true;
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
pi: { command: "pi", args: [] },
|
|
213
|
+
}, { askUser: async () => ({ cancelled: true }) });
|
|
214
|
+
const task = runner.run(request());
|
|
215
|
+
await tick();
|
|
216
|
+
assert.deepEqual(written.map((command) => command.type), ["get_state", "prompt"]);
|
|
217
|
+
// A later child event (a tool call) advances lastStep before either launch reply arrives.
|
|
218
|
+
fake.emit({ type: "tool_execution_start", toolCallId: "c1", toolName: "bash", args: {} });
|
|
219
|
+
await tick();
|
|
220
|
+
assert.equal(store.get(task.id)?.lastStep, "bash");
|
|
221
|
+
// The get_state and prompt responses arrive late; they must not regress the stage.
|
|
222
|
+
fake.emit({ type: "response", id: written[0]?.id, command: "get_state", success: true, data: { sessionFile: "/sessions/child.jsonl" } });
|
|
223
|
+
fake.emit({ type: "response", id: written[1]?.id, command: "prompt", success: true });
|
|
224
|
+
await tick();
|
|
225
|
+
assert.equal(store.get(task.id)?.lastStep, "bash", "a late get_state/prompt reply must not regress lastStep");
|
|
226
|
+
});
|
|
227
|
+
|
|
76
228
|
test("synchronous cancellation before dequeue never invokes the policy callback", async () => {
|
|
77
229
|
const h = harness(); let checks = 0;
|
|
78
230
|
const task = h.runner.run(request({ prepareResponseObservations: async () => { checks++; return true; } }));
|
|
@@ -11,7 +11,7 @@ import { visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
|
11
11
|
import gentleAgents, { agentRuntimePaths, agentsCollapseKey, agentsEnabled, agentsStopKey, agentsViewKey, answerThroughUi, completionText, legacySubagentsInstalled, type AgentsDeps } from "../extensions/gentle-agents.ts";
|
|
12
12
|
import { historyDir, loadHistory, saveTask } from "../lib/agents-history.ts";
|
|
13
13
|
import { STALE_COMPLETION_MS } from "../lib/agents-completion-delivery.ts";
|
|
14
|
-
import { emptyThread, TASK_EVENT, TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
14
|
+
import { applyTaskEvent, emptyThread, TASK_EVENT, TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
15
15
|
import { NativePointerScope } from "../lib/native-pointer-region.ts";
|
|
16
16
|
import { PresenceCursor, PresencePublisher, listPresence, readActivity } from "../lib/orchestrator-presence.ts";
|
|
17
17
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
@@ -179,10 +179,11 @@ function deps(): { deps: Partial<AgentsDeps>; children: FakeChild[]; spawned: st
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
test("all
|
|
182
|
+
test("all ten subagent registrations own their transcript shell", () => {
|
|
183
183
|
const { pi, tools } = fakePi();
|
|
184
184
|
gentleAgents(pi, {}, deps().deps);
|
|
185
|
-
assert.equal(tools.size,
|
|
185
|
+
assert.equal(tools.size, 10);
|
|
186
|
+
assert.deepEqual(tools.get("subagent_reconcile")?.parameters, { type: "object", additionalProperties: false, required: ["task_id"], properties: { task_id: { type: "string" } } });
|
|
186
187
|
for (const tool of tools.values()) assert.equal(tool.renderShell, "self", tool.name);
|
|
187
188
|
});
|
|
188
189
|
|
|
@@ -1261,7 +1262,7 @@ test("subagent_list_agents and subagent_run in task mode launch a child with the
|
|
|
1261
1262
|
gentleAgents(pi, {}, harness.deps);
|
|
1262
1263
|
const { ctx, widget } = fakeContext();
|
|
1263
1264
|
await fire("session_start", ctx);
|
|
1264
|
-
assert.deepEqual([...tools.keys()].sort(), ["subagent_cancel", "subagent_continue", "subagent_list_agents", "subagent_list_tasks", "subagent_reply", "subagent_result", "subagent_run", "subagent_send_message", "subagent_status"]);
|
|
1265
|
+
assert.deepEqual([...tools.keys()].sort(), ["subagent_cancel", "subagent_continue", "subagent_list_agents", "subagent_list_tasks", "subagent_reconcile", "subagent_reply", "subagent_result", "subagent_run", "subagent_send_message", "subagent_status"]);
|
|
1265
1266
|
const listed = await tools.get("subagent_list_agents")!.execute("c0", {}, undefined, undefined, ctx);
|
|
1266
1267
|
assert.match(listed.content[0].text, /- explore \(global\): maps things/);
|
|
1267
1268
|
|
|
@@ -2141,6 +2142,81 @@ test("R1 malformed child grant denies tools even before/after failed session ini
|
|
|
2141
2142
|
});
|
|
2142
2143
|
|
|
2143
2144
|
|
|
2145
|
+
test("public reconciliation replays retained authority, persists closure, and never exposes or starts the actor", async () => {
|
|
2146
|
+
const fixtureHome = join(root, "remediation-reconcile");
|
|
2147
|
+
const acquire = { workspaceRoot: cwd, changeName: "alpha", requestId: "retained-acquire", workUnit: "correct", evidenceGoal: "Observed correction", remediatesEvidenceRevision: `sha256:${"a".repeat(64)}` };
|
|
2148
|
+
await saveTask(historyDir(fixtureHome), { id: "retained", agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as never, emptyThread());
|
|
2149
|
+
const h = fakePi(), runtime = deps(), calls = [];
|
|
2150
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: {
|
|
2151
|
+
sddAttemptAcquire: async input => { calls.push(["acquire", structuredClone(input)]); return { state: "proceed", token: "private-token" }; },
|
|
2152
|
+
sddAttemptSettle: async input => { calls.push(["settle", structuredClone(input)]); return { state: "complete" }; },
|
|
2153
|
+
} as unknown as NativeReviewCli });
|
|
2154
|
+
const { ctx } = fakeContext(); await h.fire("session_start", ctx);
|
|
2155
|
+
const output = await h.tools.get("subagent_reconcile").execute("reconcile", { task_id: "retained" }, undefined, undefined, ctx);
|
|
2156
|
+
assert.match(output.content[0].text, /reconciled/i);
|
|
2157
|
+
assert.equal(JSON.stringify(output).includes("private-token"), false);
|
|
2158
|
+
assert.deepEqual(calls[0], ["acquire", acquire]); assert.equal(calls[1][0], "settle");
|
|
2159
|
+
assert.equal(runtime.spawned.length, 0);
|
|
2160
|
+
const retained = (await loadHistory(historyDir(fixtureHome)))[0].task;
|
|
2161
|
+
assert.equal(retained.sddRemediation.acquireUncertain, undefined);
|
|
2162
|
+
assert.deepEqual(retained.sddRemediation.settlement, { state: "complete" });
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
test("durable reconciliation locks serialize independent extension instances sharing one tasksDir", async () => {
|
|
2166
|
+
const fixtureHome = join(root, "remediation-reconcile-independent");
|
|
2167
|
+
const id = "retained-independent";
|
|
2168
|
+
const acquire = { workspaceRoot: cwd, changeName: "alpha", requestId: id, workUnit: "correct", evidenceGoal: "Observed correction" };
|
|
2169
|
+
await saveTask(historyDir(fixtureHome), { id, agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as never, emptyThread());
|
|
2170
|
+
const first = fakePi(), second = fakePi(), runtime = deps();
|
|
2171
|
+
let calls = 0, release!: (result: { state: "blocked" }) => void;
|
|
2172
|
+
const pending = new Promise<{ state: "blocked" }>(resolve => { release = resolve; });
|
|
2173
|
+
const native = { sddAttemptAcquire: async () => { calls++; return calls === 1 ? pending : { state: "blocked" }; } } as unknown as NativeReviewCli;
|
|
2174
|
+
gentleAgents(first.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: native });
|
|
2175
|
+
gentleAgents(second.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: native });
|
|
2176
|
+
const firstContext = fakeContext(), secondContext = fakeContext();
|
|
2177
|
+
await first.fire("session_start", firstContext.ctx); await second.fire("session_start", secondContext.ctx);
|
|
2178
|
+
const running = first.tools.get("subagent_reconcile")!.execute("first", { task_id: id }, undefined, undefined, firstContext.ctx);
|
|
2179
|
+
await eventually(() => calls === 1, "the first instance must reach native acquire while holding the lock");
|
|
2180
|
+
await assert.rejects(second.tools.get("subagent_reconcile")!.execute("second", { task_id: id }, undefined, undefined, secondContext.ctx), /busy|active|already being reconciled/i);
|
|
2181
|
+
assert.equal(calls, 1, "a busy filesystem lock fails before native acquire");
|
|
2182
|
+
release({ state: "blocked" }); await running;
|
|
2183
|
+
await first.fire("session_shutdown", firstContext.ctx); await second.fire("session_shutdown", secondContext.ctx);
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
test("reconciliation reloads a stale local task and preserves the retained disk thread", async () => {
|
|
2187
|
+
const fixtureHome = join(root, "remediation-reconcile-reload");
|
|
2188
|
+
const id = "retained-reload";
|
|
2189
|
+
const oldAcquire = { workspaceRoot: cwd, changeName: "alpha", requestId: "old", workUnit: "old", evidenceGoal: "old" };
|
|
2190
|
+
const freshAcquire = { workspaceRoot: cwd, changeName: "alpha", requestId: "fresh", workUnit: "fresh", evidenceGoal: "fresh" };
|
|
2191
|
+
await saveTask(historyDir(fixtureHome), { id, agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: { acquire: oldAcquire, acquireUncertain: true } } as never, applyTaskEvent(emptyThread(), { type: TASK_EVENT.NOTE, text: "old thread" }));
|
|
2192
|
+
const h = fakePi(), runtime = deps(), seen: unknown[] = [];
|
|
2193
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: { sddAttemptAcquire: async input => { seen.push(structuredClone(input)); return { state: "blocked" }; } } as unknown as NativeReviewCli });
|
|
2194
|
+
const { ctx } = fakeContext(); await h.fire("session_start", ctx);
|
|
2195
|
+
await h.tools.get("subagent_status")!.execute("status", { task_id: id }, undefined, undefined, ctx);
|
|
2196
|
+
const freshThread = applyTaskEvent(emptyThread(), { type: TASK_EVENT.NOTE, text: "fresh thread" });
|
|
2197
|
+
await saveTask(historyDir(fixtureHome), { id, agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: { acquire: freshAcquire, acquireUncertain: true } } as never, freshThread);
|
|
2198
|
+
await h.tools.get("subagent_reconcile")!.execute("reconcile", { task_id: id }, undefined, undefined, ctx);
|
|
2199
|
+
assert.deepEqual(seen, [freshAcquire], "native receives the force-reloaded retained request");
|
|
2200
|
+
const stored = (await loadHistory(historyDir(fixtureHome))).find(entry => entry.task.id === id)!;
|
|
2201
|
+
assert.deepEqual(stored.thread.items, freshThread.items, "persistence retains the exact disk thread, not the stale store thread");
|
|
2202
|
+
await h.fire("session_shutdown", ctx);
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
test("reconciliation releases the durable lock after native failure", async () => {
|
|
2206
|
+
const fixtureHome = join(root, "remediation-reconcile-failure");
|
|
2207
|
+
const id = "retained-failure";
|
|
2208
|
+
const acquire = { workspaceRoot: cwd, changeName: "alpha", requestId: id, workUnit: "correct", evidenceGoal: "Observed correction" };
|
|
2209
|
+
await saveTask(historyDir(fixtureHome), { id, agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as never, emptyThread());
|
|
2210
|
+
const h = fakePi(), runtime = deps(); let calls = 0;
|
|
2211
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: { sddAttemptAcquire: async () => { calls++; if (calls === 1) throw new TypeError("native failure"); return { state: "blocked" }; } } as unknown as NativeReviewCli });
|
|
2212
|
+
const { ctx } = fakeContext(); await h.fire("session_start", ctx);
|
|
2213
|
+
await assert.rejects(h.tools.get("subagent_reconcile")!.execute("failed", { task_id: id }, undefined, undefined, ctx), /native failure/);
|
|
2214
|
+
const recovered = await h.tools.get("subagent_reconcile")!.execute("retry", { task_id: id }, undefined, undefined, ctx);
|
|
2215
|
+
assert.match(recovered.content[0].text, /reconciled/i);
|
|
2216
|
+
assert.equal(calls, 2, "the second attempt acquires after finally released the first lock");
|
|
2217
|
+
await h.fire("session_shutdown", ctx);
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2144
2220
|
test("R3/R4 host reload refuses retained acquire/actor uncertainty without another launch", async () => {
|
|
2145
2221
|
for (const actorClaimed of [false, true, "blocked", "complete"]) {
|
|
2146
2222
|
const normal = typeof actorClaimed === "string";
|
|
@@ -2160,3 +2236,25 @@ test("R3/R4 host reload refuses retained acquire/actor uncertainty without anoth
|
|
|
2160
2236
|
await h.fire("session_shutdown", ctx);
|
|
2161
2237
|
}
|
|
2162
2238
|
});
|
|
2239
|
+
|
|
2240
|
+
test("R3/R4 a known native-blocked settlement lets native admission decide the next attempt; an uncertain one still refuses locally", async () => {
|
|
2241
|
+
for (const uncertain of [false, true]) {
|
|
2242
|
+
const h = fakePi(), runtime = deps(), fixtureHome = join(root, `remediation-settlement-${uncertain}`);
|
|
2243
|
+
mkdirSync(join(fixtureHome, ".pi", "agent", "agents"), { recursive: true });
|
|
2244
|
+
writeFileSync(join(fixtureHome, ".pi", "agent", "agents", "sdd-remediate.md"), readFileSync("assets/agents/sdd-remediate.md"));
|
|
2245
|
+
const revision = `sha256:${"a".repeat(64)}`;
|
|
2246
|
+
const acquire = { workspaceRoot: cwd, changeName: "alpha", requestId: "retained", workUnit: "correct", evidenceGoal: "Observed correction", remediatesEvidenceRevision: revision };
|
|
2247
|
+
await saveTask(historyDir(fixtureHome), { id: "retained", agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: uncertain
|
|
2248
|
+
? { acquire, settlementUncertain: true, settle: { requestId: "exact" } }
|
|
2249
|
+
: { acquire, settlement: { state: "blocked", reason: "maintainer_decision" } } } as never, emptyThread());
|
|
2250
|
+
let acquisitions = 0, confirmations = 0;
|
|
2251
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, home: fixtureHome, nativeSdd: { sddStatus: async () => ({ schemaName: "gentle-ai.sdd-status", schemaVersion: 2, changeName: "alpha", artifactStore: "openspec", planningHome: { mode: "repo-local", path: join(cwd, "openspec") }, changeRoot: join(cwd, "openspec/changes/alpha"), actionContext: { mode: "repo-local", workspaceRoot: cwd, allowedEditRoots: [cwd] }, dependencies: Object.fromEntries(["proposal", "specs", "design", "tasks", "apply", "verify", "archive"].map(key => [key, "ready"])), phaseInstructions: { apply: [], verify: [], remediate: ["Correct evidence"], archive: [] }, blockedReasons: [], nextRecommended: "remediate", remediationState: { required: true, complete: false, failedEvidenceRevision: revision } }), sddAttemptSettle: async () => ({ state: "proceed" as const }), sddAttemptAcquire: async () => { acquisitions++; return { state: "blocked" }; } } as unknown as NativeReviewCli });
|
|
2252
|
+
const { ctx } = fakeContext(fakeTui, async () => { confirmations++; return true; });
|
|
2253
|
+
await h.fire("session_start", ctx);
|
|
2254
|
+
await assert.rejects(h.tools.get("subagent_run").execute("again", { agent: "sdd-remediate", task: "Correct alpha", context: PARENT_CONFIRMED_SDD_CONTEXT, mode: "background", sdd_change: { changeName: "alpha", workspaceRoot: cwd, phase: "remediate", failedEvidenceRevision: revision }, remediation: { attempt: { ...acquire, requestId: "different" }, plan: { cwd, commands: ["pnpm test"], runtimeHarness: { naReason: "Not applicable because this fixture has no runtime boundary." }, rollback: { boundary: "Revert fixture", command: "git diff --check" } } } }, undefined, undefined, ctx), uncertain ? /reconcile exact history without actor replay/ : /no actor started/);
|
|
2255
|
+
assert.equal(acquisitions, uncertain ? 0 : 1, "native admission is consulted once local unresolved history is not uncertain");
|
|
2256
|
+
assert.equal(confirmations, uncertain ? 0 : 1);
|
|
2257
|
+
assert.equal(runtime.spawned.length, 0);
|
|
2258
|
+
await h.fire("session_shutdown", ctx);
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
@@ -97,7 +97,7 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
97
97
|
method: "go-sumdb-source-build",
|
|
98
98
|
package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
99
99
|
module: "github.com/gentleman-programming/gentle-ai/v2",
|
|
100
|
-
tag: "v2.
|
|
100
|
+
tag: "v2.9.0",
|
|
101
101
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
102
102
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
103
103
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|