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.
Files changed (122) hide show
  1. package/README.md +24 -6
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +14 -4
  18. package/docs/readme-reference.md +171 -32
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +281 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +74 -30
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +67 -111
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +10 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +2 -1
  47. package/lib/session-changes.ts +8 -1
  48. package/lib/shell-bar.ts +21 -12
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes.ts +3 -2
  51. package/lib/shell-prompt.ts +25 -8
  52. package/lib/shell-sidebar-banner.ts +2 -2
  53. package/lib/shell-sidebar-layout.ts +5 -2
  54. package/lib/windows-session-transport.ts +877 -0
  55. package/package.json +3 -3
  56. package/runtime/native-review-cli.mjs +9 -96
  57. package/runtime/windows-session-transport.ps1 +791 -0
  58. package/scripts/test-packed-runner.mjs +1668 -20
  59. package/scripts/verify-package-files.mjs +0 -1
  60. package/tests/agent-home.test.ts +52 -0
  61. package/tests/agent-profiles.test.ts +30 -1
  62. package/tests/agents-config.test.ts +44 -0
  63. package/tests/agents-history.test.ts +12 -24
  64. package/tests/agents-runner.test.ts +307 -58
  65. package/tests/agents-session-transport-process.test.ts +249 -0
  66. package/tests/agents-session-transport.test.ts +823 -0
  67. package/tests/artifact-language.test.ts +10 -7
  68. package/tests/command-palette.test.ts +378 -0
  69. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  70. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  71. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  72. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  73. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  74. package/tests/gentle-agents.test.ts +849 -356
  75. package/tests/gentle-ai.test.ts +472 -4
  76. package/tests/gentle-shell.test.ts +201 -8
  77. package/tests/native-choice-list.test.ts +13 -0
  78. package/tests/native-review-cli.test.ts +0 -33
  79. package/tests/odd-routing-contract.test.ts +208 -0
  80. package/tests/orchestrator-budget.test.ts +17 -2
  81. package/tests/package-manifest.test.ts +115 -27
  82. package/tests/persona-single-channel.test.ts +3 -3
  83. package/tests/pi-pretty.test.ts +45 -0
  84. package/tests/profile-pin.test.ts +370 -0
  85. package/tests/quiet-tool-rendering.test.ts +32 -5
  86. package/tests/review-contract-prompt.test.ts +9 -0
  87. package/tests/review-controller.test.ts +0 -44
  88. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  89. package/tests/runtime-harness.mjs +4 -4
  90. package/tests/sdd-agent-tools.test.ts +15 -36
  91. package/tests/sdd-archive-replay.test.ts +82 -0
  92. package/tests/sdd-classical-continuation.test.ts +74 -0
  93. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  94. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  95. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  96. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  97. package/tests/sdd-odd-integration.test.ts +33 -0
  98. package/tests/sdd-optional-research.test.ts +124 -0
  99. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  100. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  101. package/tests/sdd-preflight.test.ts +1 -1
  102. package/tests/sdd-research-capabilities.test.ts +20 -162
  103. package/tests/sdd-selection-transport.test.ts +180 -88
  104. package/tests/sdd-status.test.ts +5 -778
  105. package/tests/sdd-task-truth.test.ts +43 -0
  106. package/tests/session-change-capture.test.ts +20 -2
  107. package/tests/session-changes.test.ts +11 -0
  108. package/tests/shell-bar.test.ts +21 -0
  109. package/tests/shell-card.test.ts +8 -6
  110. package/tests/shell-changes.test.ts +8 -0
  111. package/tests/shell-prompt.test.ts +41 -7
  112. package/tests/shell-sidebar-banner.test.ts +4 -4
  113. package/tests/shell-sidebar-layout.test.ts +97 -13
  114. package/tests/startup-banner.test.ts +55 -2
  115. package/tests/windows-hidden-processes.test.ts +303 -0
  116. package/tests/windows-session-bootstrap.test.ts +1772 -0
  117. package/tests/windows-session-compile.test.ts +170 -0
  118. package/tests/windows-session-transport.test.ts +754 -0
  119. package/assets/agents/sdd-sync.md +0 -146
  120. package/lib/openspec-guardrails.ts +0 -99
  121. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  122. package/tests/openspec-guardrails.test.ts +0 -71
@@ -0,0 +1,82 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+ import { applyDeltaSpec, parseRequirementBlocks } from "../lib/openspec-deltas.ts";
5
+
6
+ const archive = readFileSync(new URL("../assets/agents/sdd-archive.md", import.meta.url), "utf8");
7
+
8
+ // These checks exercise the shipped instruction contract, not an autonomous executor.
9
+ test("archive reconciles prior composition per operation before applying remaining deltas", () => {
10
+ assert.match(archive, /### Resume prior composition/);
11
+ assert.match(archive, /Classify each operation as already applied, pending, or unresolved/);
12
+ assert.match(archive, /existing change-specific artifacts.*history/);
13
+ assert.match(archive, /PASS.*absence alone.*not proof/);
14
+ assert.match(archive, /REMOVED.*absent.*same requirement.*removed by this change/);
15
+ assert.match(archive, /ADDED.*MODIFIED.*full current requirement block.*intended delta result/);
16
+ assert.match(archive, /Apply only pending operations/);
17
+ assert.match(archive, /unresolved.*before any canonical write or archive move/);
18
+ assert.match(archive, /Do not create a new report schema.*hash inventory/);
19
+ });
20
+
21
+ test("archive reconciliation preserves safety and backend closure boundaries", () => {
22
+ assert.match(archive, /collision.*order.*still apply/);
23
+ assert.match(archive, /Fail or block.*REMOVED target is missing without corroborating history/);
24
+ assert.match(archive, /differing current content.*unresolved/);
25
+ assert.match(archive, /re-read the persisted tasks artifact/i);
26
+ assert.match(archive, /resolved symlink targets/);
27
+ assert.match(archive, /Block rather than overwrite an existing archive destination/);
28
+ assert.match(archive, /Engram.*do not create or require.*canonical/);
29
+ assert.match(archive, /already-applied.*pending.*unresolved.*supporting artifact/);
30
+ });
31
+
32
+ const original = `# Domain
33
+
34
+ ## Requirements
35
+
36
+ ### Requirement: Retained
37
+
38
+ Keep the original behavior.
39
+
40
+ ### Requirement: Removed
41
+
42
+ Retire this behavior.
43
+ `;
44
+ const removal = "## REMOVED Requirements\n\n### Requirement: Removed\n\nRetired by this change.\n";
45
+ const modification = "## MODIFIED Requirements\n\n### Requirement: Retained\n\nKeep the revised behavior.\n";
46
+ const addition = "## ADDED Requirements\n\n### Requirement: Added\n\nAdd this behavior.\n";
47
+
48
+ // Existing helpers deliberately reject replay; they do not infer historical ownership.
49
+ // Explicit pending inputs below demonstrate composition after instruction-level reconciliation.
50
+ test("an already-applied removal is preserved without reapplying it", () => {
51
+ const synced = applyDeltaSpec(original, removal);
52
+ assert.throws(() => applyDeltaSpec(synced, removal), /Missing canonical requirement.*REMOVED/);
53
+ const unchanged = applyDeltaSpec(synced, "");
54
+ assert.equal(unchanged, synced);
55
+ assert.deepEqual(parseRequirementBlocks(unchanged).map((block) => block.name), ["Retained"]);
56
+ });
57
+
58
+ test("unapplied missing removal cannot become a successful helper operation", () => {
59
+ const neverExisted = removal.replaceAll("Removed", "Never existed");
60
+ assert.throws(() => applyDeltaSpec(original, neverExisted), /Missing canonical requirement.*Never existed/);
61
+ assert.match(original, /### Requirement: Removed/);
62
+ });
63
+
64
+ test("mixed prior removal and pending modification/addition compose without duplicating prior effects", () => {
65
+ const partiallySynced = applyDeltaSpec(original, removal);
66
+ const resumed = applyDeltaSpec(partiallySynced, `${modification}\n${addition}`);
67
+ const uninterrupted = applyDeltaSpec(original, `${removal}\n${modification}\n${addition}`);
68
+ assert.equal(resumed, uninterrupted);
69
+ assert.deepEqual(parseRequirementBlocks(resumed).map((block) => block.name), ["Retained", "Added"]);
70
+ assert.throws(() => applyDeltaSpec(resumed, addition), /Cannot add existing canonical requirement/);
71
+ });
72
+
73
+ test("canonical content comparison distinguishes matching effects from later drift", () => {
74
+ const synced = applyDeltaSpec(original, modification);
75
+ const intended = parseRequirementBlocks(modification)[0].content;
76
+ assert.equal(parseRequirementBlocks(synced)[0].content, intended);
77
+ const drifted = synced.replace("Keep the revised behavior.", "A later change owns this behavior.");
78
+ assert.notEqual(parseRequirementBlocks(drifted)[0].content, intended);
79
+ // The helper is not a drift/consent authority: the executor must stop before calling it.
80
+ assert.match(drifted, /A later change owns this behavior/);
81
+ assert.match(applyDeltaSpec(drifted, modification), /Keep the revised behavior/);
82
+ });
@@ -0,0 +1,74 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { ensureSddPreflight } from "../lib/sdd-preflight.ts";
10
+
11
+ type Hook = (event: unknown, ctx: ExtensionContext) => Promise<{ systemPrompt: string }>;
12
+
13
+ for (const store of ["openspec", "engram", "hybrid"] as const) {
14
+ for (const selected of [true, false]) {
15
+ test(`classical startup preserves native archive and admits optional verify (store=${store}, selected=${selected})`, async (t) => {
16
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "pi-classical-")));
17
+ t.after(() => rmSync(root, { recursive: true, force: true }));
18
+ const status = {
19
+ schemaName: "gentle-ai.sdd-status", schemaVersion: 2, changeName: "alpha", artifactStore: store,
20
+ planningHome: { mode: "repo-local", path: store === "openspec" ? join(root, "openspec") : "engram:sdd" }, changeRoot: store === "openspec" ? join(root, "openspec/changes/alpha") : null,
21
+ actionContext: { mode: "repo-local", workspaceRoot: root, allowedEditRoots: [root] },
22
+ dependencies: { proposal: "all_done", specs: "all_done", design: "all_done", tasks: "all_done", apply: "all_done", verify: "ready", archive: "ready" },
23
+ phaseInstructions: { apply: [], verify: ["Optional functional verification."], archive: ["Compose applicable specs and archive." ] },
24
+ blockedReasons: [] as string[], nextRecommended: "archive",
25
+ };
26
+ let phase = "verify";
27
+ let reply: unknown = status;
28
+ const hooks = new Map<string, Hook>();
29
+ const pi = {
30
+ on(name: string, hook: Hook) { hooks.set(name, hook); }, events: { emit() {} }, registerCommand() {}, registerTool() {},
31
+ getFlag: () => selected ? JSON.stringify({ changeName: "alpha", workspaceRoot: root, phase }) : undefined,
32
+ getActiveTools: () => [],
33
+ } as unknown as ExtensionAPI;
34
+ const ctx = { cwd: root, hasUI: false, sessionManager: { getSessionId: () => root } } as unknown as ExtensionContext;
35
+ await ensureSddPreflight(ctx, { pi, installAssets: () => ({ agents: 0, chains: 0, support: 0, skipped: 0 }) });
36
+ createGentleAiExtension({ nativeReviewCli: { sddStatus: async () => reply } as unknown as NativeReviewCli, processEnv: {} })(pi);
37
+ const start = () => hooks.get("before_agent_start")!({ agentName: `sdd-${phase}`, systemPrompt: `SDD ${phase} executor` }, ctx);
38
+ for (const next of ["verify", "archive"]) {
39
+ phase = next;
40
+ await t.test(`explicit ${next}`, async () => {
41
+ const result = await start();
42
+ assert.doesNotMatch(result.systemPrompt, /SDD selection blocked:/);
43
+ assert.ok(result.systemPrompt.includes(JSON.stringify(status, null, 2)), "never rewrite the native recommended action");
44
+ });
45
+ }
46
+ phase = "verify";
47
+ reply = { ...status, nextRecommended: "apply", dependencies: { ...status.dependencies, apply: "ready" } };
48
+ await t.test("partial verification", async () => {
49
+ assert.doesNotMatch((await start()).systemPrompt, /SDD selection blocked:/, "explicit partial verification preserves native apply selection");
50
+ });
51
+ reply = { ...status, dependencies: { ...status.dependencies, verify: "blocked" } };
52
+ assert.match((await start()).systemPrompt, /SDD selection blocked:/);
53
+ phase = "archive";
54
+ for (const blocked of [
55
+ { ...status, nextRecommended: "apply", dependencies: { ...status.dependencies, apply: "ready", archive: "blocked" } },
56
+ { ...status, blockedReasons: ["edit authority missing"] },
57
+ { ...status, dependencies: { ...status.dependencies, archive: "blocked" } },
58
+ { ...status, nextRecommended: "verify" },
59
+ ]) {
60
+ reply = blocked;
61
+ assert.match((await start()).systemPrompt, /SDD selection blocked:/);
62
+ }
63
+ phase = "sync";
64
+ reply = status;
65
+ await t.test("stale sync is mechanically blocked", async () => {
66
+ assert.match((await start()).systemPrompt, /SDD selection blocked:/, "stale sync executors must not bypass native authority");
67
+ const callTool = hooks.get("tool_call")! as unknown as (event: unknown, ctx: ExtensionContext) => Promise<{ block?: boolean }>;
68
+ for (const toolName of ["write", "edit", "bash"]) {
69
+ assert.equal((await callTool({ toolName, input: { path: "unsafe.ts", command: "echo unsafe" } }, ctx))?.block, true);
70
+ }
71
+ });
72
+ });
73
+ }
74
+ }
@@ -20,9 +20,25 @@ for (const path of guidancePaths) {
20
20
  });
21
21
  }
22
22
 
23
- test("workflow preserves explicit continuation and the manual sync resolver", () => {
23
+ test("workflow preserves explicit continuation and native-only classical completion", () => {
24
24
  const workflow = readFileSync(new URL("../assets/sdd-orchestrator-workflow.md", import.meta.url), "utf8");
25
25
  assert.match(workflow, /only.*sdd-continue|sdd-continue.*only/i);
26
- assert.match(workflow, /manual sdd-sync/i);
26
+ assert.doesNotMatch(workflow, /manual sdd-sync|local resolver/i);
27
+ assert.match(workflow, /verification is optional/);
27
28
  assert.doesNotMatch(workflow, /sdd-(?:apply|verify|archive).*local|local.*sdd-(?:apply|verify|archive)/i);
28
29
  });
30
+
31
+ test("archive owns composition without mandatory verification or a standalone sync receipt", () => {
32
+ const archive = readFileSync(new URL("../assets/agents/sdd-archive.md", import.meta.url), "utf8");
33
+ const full = readFileSync(new URL("../assets/chains/sdd-full.chain.md", import.meta.url), "utf8");
34
+ assert.doesNotMatch(full, /^## sdd-(verify|sync)$/m);
35
+ assert.match(full, /apply -> archive/);
36
+ assert.match(archive, /missing optional report is not a blocker/);
37
+ assert.match(archive, /no separate sync phase or successful sync-report artifact is required/);
38
+ for (const guard of ["Final Task Completion Gate", "allowed edit roots", "resolved symlink targets", "existing archive destination",
39
+ "ADDED Requirements", "MODIFIED Requirements", "REMOVED Requirements", "RENAMED Requirements",
40
+ "explicit composition/archive order", "explicit approval for the destructive sync", "dependsOn", "rules.sync",
41
+ "archive-report", "observation-ID traceability", "Preserve every canonical requirement not mentioned by the delta"]) {
42
+ assert.ok(archive.includes(guard), `archive retains ${guard}`);
43
+ }
44
+ });
@@ -1,40 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { parseRemediationPlan, remediationEvidence, observeRemediationTool, type RemediationObservations, type RemediationPlan, type RemediationScope, type TaskRequest } from "../lib/agents-runner.ts";
5
- import { NATIVE_REVIEW_ERROR_CODE, NATIVE_REVIEW_OPERATION, NativeReviewCliError, type NativeReviewCli } from "../lib/native-review-cli.ts";
6
- import { remediationUnresolved } from "../lib/agents-history.ts";
7
- import type { TaskRecord } from "../lib/agents-protocol.ts";
4
+ import { parseRemediationPlan, type RemediationPlan, type RemediationScope } from "../lib/agents-runner.ts";
8
5
 
9
6
  const testCwd = process.cwd();
10
- const human = { hasUI: true, ui: { confirm: async () => true } } as unknown as Pick<ExtensionContext, "hasUI" | "ui">;
11
7
  const shellScope = (cwd: string, commands: string[]): RemediationScope => ({ cwd, commands, editPaths: [], allowedEditRoots: [cwd] });
12
- const revision = `sha256:${"a".repeat(64)}`;
13
8
  const plan: RemediationPlan = { cwd: testCwd, commands: ["pnpm test"], runtimeHarness: { naReason: "Not applicable because this correction changes only static assets." }, rollback: { boundary: "Revert the changed asset and paired test", command: "git diff --check" } };
14
- function state(): RemediationObservations { return { failedEvidenceRevision: revision, plan: parseRemediationPlan(plan, testCwd), observations: [], pending: {}, invalid: false }; }
15
- function observed(s: RemediationObservations, command: string, id: string, exitCode: number | null = 0, overrides: Record<string, unknown> = {}) {
16
- observeRemediationTool(s, { type: "tool_execution_start", toolName: "bash", toolCallId: id, args: { command } });
17
- observeRemediationTool(s, { type: "tool_execution_end", toolName: "bash", toolCallId: id, isError: false, result: { content: [{ type: "text", text: "concrete command output" }], details: { remediationCommand: { toolCallId: id, command, cwd: testCwd, exitCode, ...overrides } } } });
18
- }
19
- test("remediation evidence requires every exact planned command and rollback observation", () => {
20
- const s = state();
21
- assert.equal(remediationEvidence(s), undefined);
22
- observed(s, "pnpm test", "a");
23
- assert.equal(remediationEvidence(s), undefined);
24
- observed(s, "git diff --check", "b");
25
- const evidence = remediationEvidence(s);
26
- assert.equal(evidence.failed_evidence_revision, revision);
27
- assert.equal(evidence.commands[0].exit_code, 0);
28
- assert.match(evidence.commands[0].result, /sha256:/);
29
- assert.equal(evidence.runtime_harness.status, "not_applicable");
30
- assert.equal(evidence.rollback.boundary, plan.rollback.boundary);
31
- });
32
- for (const fault of ["nonzero", "null", "cwd", "id", "truncated", "unmatched"]) test(`remediation refuses ${fault} without parsing prose`, () => {
33
- const s = state();
34
- observed(s, fault === "unmatched" ? "echo passed" : "pnpm test", "a", fault === "nonzero" ? 1 : fault === "null" ? null : 0, fault === "cwd" ? { cwd: "/other" } : fault === "id" ? { toolCallId: "wrong" } : fault === "truncated" ? { truncated: true } : {});
35
- observed(s, "git diff --check", "b");
36
- assert.equal(remediationEvidence(s), undefined);
37
- });
9
+
38
10
  test("remediation plan is bounded and cannot select another working directory", () => {
39
11
  assert.throws(() => parseRemediationPlan({ ...plan, cwd: "/other" }, testCwd), /plan/);
40
12
  assert.throws(() => parseRemediationPlan({ ...plan, commands: [] }, testCwd), /plan/);
@@ -71,86 +43,6 @@ test("remediation owner and packaged actor are installed through existing owners
71
43
  assert.match(readFileSync("assets/agents/sdd-remediate.md", "utf8"), /SDD remediate executor/);
72
44
  });
73
45
 
74
- test("native remediation admission refuses unsupported owner before acquire", async () => {
75
- const { admitManagedRemediation } = await import("../extensions/gentle-agents.ts");
76
- let acquires = 0;
77
- await assert.rejects(admitManagedRemediation({ agent: { name: "sdd-remediate", filePath: "/absent" }, sddChange: { changeName: "fix", workspaceRoot: testCwd, phase: "remediate", failedEvidenceRevision: revision }, cwd: testCwd } as unknown as TaskRequest, {}, { sddAttemptAcquire: async () => { acquires++; } } as unknown as NativeReviewCli, async () => {}), /unsupported/i);
78
- assert.equal(acquires, 0);
79
- });
80
-
81
-
82
- test("admitted remediation retains exact settlement before one lost-reply replay", async () => {
83
- const { admitManagedRemediation } = await import("../extensions/gentle-agents.ts");
84
- const { resolve } = await import("node:path");
85
- const { readFileSync } = await import("node:fs");
86
- const path = resolve("assets/agents/sdd-remediate.md");
87
- const { parseAgentDefinition } = await import("../lib/agents-config.ts");
88
- const agent = parseAgentDefinition(readFileSync(path, "utf8"), path, "global");
89
- assert.ok("instructions" in agent);
90
- const saved = [], calls = [];
91
- const request = { agent, sddChange: { changeName: "fix", workspaceRoot: testCwd, phase: "remediate", failedEvidenceRevision: revision }, cwd: testCwd } as unknown as TaskRequest;
92
- const native = { sddStatus: async () => ({ schemaName: "gentle-ai.sdd-status", schemaVersion: 2, artifactStore: "openspec", planningHome: { mode: "repo-local", path: `${testCwd}/openspec` }, changeRoot: `${testCwd}/openspec/changes/fix`, dependencies: Object.fromEntries(["proposal", "specs", "design", "tasks", "apply", "verify", "archive"].map(key => [key, "ready"])), phaseInstructions: { apply: [], verify: [], remediate: ["Correct evidence"], archive: [] }, blockedReasons: [], nextRecommended: "remediate", changeName: "fix", actionContext: { mode: "repo-local", workspaceRoot: testCwd, allowedEditRoots: [testCwd] }, remediationState: { required: true, complete: false, failedEvidenceRevision: revision } }), sddAttemptAcquire: async input => { calls.push(["acquire", input]); return { state: "proceed", token: "opaque-admitted" }; }, sddAttemptSettle: async input => { assert.equal(saved.at(-1).sddRemediation.settle.requestId, input.requestId); calls.push(["settle", structuredClone(input)]); if (calls.length === 2) throw new Error("lost reply"); return { state: "complete" as const }; } } as unknown as NativeReviewCli;
93
- const admitted = await admitManagedRemediation(request, { plan, attempt: { requestId: "a", workUnit: "fix", evidenceGoal: "Observed correction", maxAttempts: 1, maxChangedLines: 200 } }, native, async task => { saved.push(structuredClone(task)); }, human, { id: "prepared", cwd: testCwd, agent: "sdd-remediate", status: "queued" } as unknown as TaskRecord);
94
- const task = { id: "task", cwd: testCwd, status: "completed", sddRemediation: admitted.sddRemediation } as unknown as TaskRecord & { sddRemediation: typeof admitted.sddRemediation };
95
- observed(task.sddRemediation, "pnpm test", "one"); observed(task.sddRemediation, "git diff --check", "two");
96
- await admitted.finalizeRemediation(task, { spawned: true, exited: true, cleanupConfirmed: true });
97
- assert.equal(calls.filter(([verb]) => verb === "acquire").length, 1);
98
- assert.equal(calls.filter(([verb]) => verb === "settle").length, 2);
99
- assert.deepEqual(calls[1], calls[2]);
100
- assert.equal(calls[1][1].outcome, "passed");
101
- assert.equal(calls[1][1].remediatesEvidenceRevision, revision);
102
- assert.equal(JSON.parse(calls[1][1].remediationEvidence).failed_evidence_revision, revision);
103
- });
104
-
105
-
106
- async function admissionFixture(overrides = {}, persist: (task: TaskRecord) => Promise<void> = async () => {}, agentPatch = {}, attemptPatch = {}, context: Pick<ExtensionContext, "hasUI" | "ui"> = human) {
107
- const { admitManagedRemediation } = await import("../extensions/gentle-agents.ts");
108
- const { parseAgentDefinition } = await import("../lib/agents-config.ts");
109
- const { readFileSync } = await import("node:fs");
110
- const { resolve } = await import("node:path");
111
- const path = resolve("assets/agents/sdd-remediate.md");
112
- const request = { agent: { ...parseAgentDefinition(readFileSync(path, "utf8"), path, "global"), ...agentPatch }, cwd: testCwd, sddChange: { changeName: "fix", workspaceRoot: testCwd, phase: "remediate", failedEvidenceRevision: revision } } as unknown as TaskRequest;
113
- const calls = [];
114
- const native = { sddStatus: async () => ({ schemaName: "gentle-ai.sdd-status", schemaVersion: 2, artifactStore: "openspec", planningHome: { mode: "repo-local", path: `${testCwd}/openspec` }, changeRoot: `${testCwd}/openspec/changes/fix`, dependencies: Object.fromEntries(["proposal", "specs", "design", "tasks", "apply", "verify", "archive"].map(key => [key, "ready"])), phaseInstructions: { apply: [], verify: [], remediate: ["Correct evidence"], archive: [] }, blockedReasons: [], nextRecommended: "remediate", changeName: "fix", actionContext: { mode: "repo-local", workspaceRoot: testCwd, allowedEditRoots: [testCwd] }, remediationState: { required: true, complete: false, failedEvidenceRevision: revision } }), sddAttemptAcquire: async input => { calls.push(["acquire", input]); return { state: "proceed", token: "opaque" }; }, sddAttemptSettle: async input => { calls.push(["settle", input]); return { state: "proceed" as const }; }, ...overrides } as unknown as NativeReviewCli;
115
- const admitted = await admitManagedRemediation(request, { plan, attempt: { requestId: "a", workUnit: "fix", evidenceGoal: "Observed correction", untrackedScope: "select", expectedUntrackedInventory: revision, intendedUntracked: ["new file.ts"], ...attemptPatch } }, native, persist, context, { id: "prepared", cwd: testCwd, agent: "sdd-remediate", status: "queued" } as unknown as TaskRecord);
116
- return { admitted, calls, task: { id: "one", status: "failed", sddRemediation: admitted.sddRemediation } as unknown as TaskRecord & { sddRemediation: typeof admitted.sddRemediation } };
117
- }
118
- for (const state of ["blocked", "complete"]) test(`native ${state} refuses without settlement`, async () => {
119
- let settles = 0;
120
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => ({ state }), sddAttemptSettle: async () => { settles++; } }), new RegExp(state));
121
- assert.equal(settles, 0);
122
- });
123
- for (const status of ["failed", "cancelled", "timed_out"] as const) test(`terminal ${status} settles without passing evidence and retains exact untracked scope`, async () => {
124
- const { admitted, calls, task } = await admissionFixture(); task.status = status;
125
- await admitted.finalizeRemediation(task, { spawned: true, exited: true, cleanupConfirmed: true });
126
- const payload = calls[1][1];
127
- assert.equal(payload.outcome, status === "failed" ? "failed" : "interrupted");
128
- assert.equal(payload.remediationEvidence, undefined);
129
- assert.equal(typeof payload.evidenceRevision, status === "failed" ? "string" : "undefined");
130
- assert.deepEqual(payload.intendedUntracked, calls[0][1].intendedUntracked);
131
- assert.equal(payload.expectedUntrackedInventory, revision);
132
- });
133
- test("failure to retain settlement never mutates native; two lost replies retain uncertainty", async () => {
134
- const failed = await admissionFixture({}, async task => { if (task.sddRemediation.settle) throw new Error("disk failure"); });
135
- await assert.rejects(failed.admitted.finalizeRemediation(failed.task, { spawned: false, exited: false, cleanupConfirmed: true }), /disk failure/);
136
- assert.equal(failed.calls.length, 1);
137
- let replies = 0;
138
- const lost = await admissionFixture({ sddAttemptSettle: async () => { replies++; throw new Error("lost"); } });
139
- await lost.admitted.finalizeRemediation(lost.task, { spawned: false, exited: false, cleanupConfirmed: true });
140
- assert.equal(replies, 2); assert.equal(lost.task.sddRemediation.settlementUncertain, true);
141
- assert.equal(lost.task.sddRemediation.settle.outcome, "interrupted");
142
- });
143
- test("runtime harness requires its own observed command; malformed content cannot pass", () => {
144
- const s = state(); s.plan.runtimeHarness = { command: "pnpm run harness" };
145
- observed(s, "pnpm test", "test"); observed(s, "git diff --check", "rollback");
146
- assert.equal(remediationEvidence(s), undefined);
147
- observed(s, "pnpm run harness", "harness"); assert.equal(remediationEvidence(s).runtime_harness.status, "passed");
148
- const malformed = state();
149
- observeRemediationTool(malformed, { type: "tool_execution_start", toolName: "bash", toolCallId: "a", args: { command: "pnpm test" } });
150
- observeRemediationTool(malformed, { type: "tool_execution_end", toolName: "bash", toolCallId: "a", isError: false, result: { content: [null], details: { remediationCommand: { toolCallId: "a", command: "pnpm test", cwd: testCwd, exitCode: 0 } } } });
151
- observed(malformed, "git diff --check", "b"); assert.equal(remediationEvidence(malformed), undefined);
152
- });
153
-
154
46
 
155
47
  test("stock local shell wrapper observes real exit zero and preserves cancellation", async () => {
156
48
  const { remediationBash } = await import("../extensions/gentle-agents.ts");
@@ -170,88 +62,6 @@ test("stock local shell wrapper observes real exit zero and preserves cancellati
170
62
  });
171
63
 
172
64
 
173
- test("nonzero observed exits remain concrete failure evidence", () => {
174
- const s = state(); observed(s, "pnpm test", "failed", 7);
175
- assert.equal(s.observations[0].exitCode, 7);
176
- assert.equal(remediationEvidence(s), undefined);
177
- });
178
-
179
- test("history pruning retains admitted unsettled and uncertain task payloads", async t => {
180
- const { mkdtemp, rm } = await import("node:fs/promises");
181
- const { tmpdir } = await import("node:os");
182
- const { join } = await import("node:path");
183
- const { saveTask, pruneHistory, loadHistory } = await import("../lib/agents-history.ts");
184
- const { emptyThread } = await import("../lib/agents-protocol.ts");
185
- const dir = await mkdtemp(join(tmpdir(), "remediation-history-")); t.after(() => rm(dir, { recursive: true, force: true }));
186
- await saveTask(dir, { id: "retained", agent: "sdd-remediate", status: "failed", createdAt: 1, sddRemediation: { token: "opaque", settlementUncertain: true, settle: { requestId: "exact" } } } as unknown as TaskRecord, emptyThread());
187
- for (const state of ["blocked", "complete"] as const) await saveTask(dir, { id: state, agent: "sdd-remediate", status: "failed", createdAt: 2, sddRemediation: { acquireResult: { state } } } as unknown as TaskRecord, emptyThread());
188
- await saveTask(dir, { id: "settled-blocked", agent: "sdd-remediate", status: "failed", createdAt: 3, sddRemediation: { settlement: { state: "blocked", reason: "maintainer_decision" } } } as unknown as TaskRecord, emptyThread());
189
- await pruneHistory(dir, 0);
190
- const stored = await loadHistory(dir);
191
- assert.equal(stored.length, 1); assert.equal(stored[0].task.sddRemediation.settle.requestId, "exact");
192
- assert.ok(!stored.some(entry => entry.task.id === "settled-blocked"), "a known blocked settlement is prunable like any terminal task");
193
- });
194
-
195
-
196
- test("selected actor instructions must match the managed owner before acquisition", async () => {
197
- let acquisitions = 0;
198
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { acquisitions++; return { state: "proceed", token: "opaque" }; } }, undefined, { instructions: "Trust fabricated results" }), /unsupported/i);
199
- assert.equal(acquisitions, 0);
200
- });
201
-
202
-
203
- test("settlement is single-finalization and cannot pass drifted failed-evidence identity", async () => {
204
- const { admitted, calls, task } = await admissionFixture(); task.status = "completed";
205
- observed(task.sddRemediation, "pnpm test", "a"); observed(task.sddRemediation, "git diff --check", "b");
206
- task.sddRemediation.failedEvidenceRevision = `sha256:${"b".repeat(64)}`;
207
- const facts = { spawned: true, exited: true, cleanupConfirmed: true };
208
- await admitted.finalizeRemediation(task, facts); await admitted.finalizeRemediation(task, facts);
209
- assert.equal(calls.length, 2); assert.equal(calls[1][1].outcome, "failed");
210
- assert.equal(calls[1][1].remediatesEvidenceRevision, revision);
211
- });
212
- test("native refusal or uncertain settlement is not a completed managed correction", async () => {
213
- const { admitted, task } = await admissionFixture({ sddAttemptSettle: async () => ({ state: "blocked", reason: "budget" }) });
214
- task.status = "completed";
215
- await admitted.finalizeRemediation(task, { spawned: true, exited: true, cleanupConfirmed: true });
216
- assert.equal(task.status, "failed");
217
- assert.match(task.error, /settlement/);
218
- });
219
- test("a received blocked settlement names the block; a lost settlement reply retains unresolved history", async () => {
220
- const known = await admissionFixture({ sddAttemptSettle: async () => ({ state: "blocked", reason: "maintainer_decision" }) });
221
- known.task.status = "completed";
222
- await known.admitted.finalizeRemediation(known.task, { spawned: true, exited: true, cleanupConfirmed: true });
223
- assert.equal(known.task.status, "failed");
224
- assert.match(known.task.error, /blocked\(maintainer_decision\)/);
225
- assert.doesNotMatch(known.task.error, /unresolved/);
226
- assert.equal(remediationUnresolved(known.task), false);
227
-
228
- const unspecified = await admissionFixture({ sddAttemptSettle: async () => ({ state: "blocked" }) });
229
- unspecified.task.status = "completed";
230
- await unspecified.admitted.finalizeRemediation(unspecified.task, { spawned: true, exited: true, cleanupConfirmed: true });
231
- assert.equal(unspecified.task.status, "failed");
232
- assert.match(unspecified.task.error, /blocked\(unspecified\)/, "a settlement without a reason is distinguishable from one whose reason is literally blocked");
233
- assert.doesNotMatch(unspecified.task.error, /unresolved/);
234
-
235
- let attempts = 0;
236
- const uncertain = await admissionFixture({ sddAttemptSettle: async () => { attempts++; throw new Error("lost reply"); } });
237
- uncertain.task.status = "completed";
238
- await uncertain.admitted.finalizeRemediation(uncertain.task, { spawned: true, exited: true, cleanupConfirmed: true });
239
- assert.equal(attempts, 2);
240
- assert.equal(uncertain.task.status, "failed");
241
- assert.match(uncertain.task.error, /unresolved/);
242
- assert.equal(remediationUnresolved(uncertain.task), true);
243
- });
244
- test("remediationUnresolved treats a received settlement as terminal, regardless of state, unless uncertain", () => {
245
- // A terminal acquire result keeps the final branch false, so the token and
246
- // actor-claim assertions below discriminate on those fields alone.
247
- const base = { acquire: {}, acquireResult: { state: "complete" } } as unknown as TaskRecord["sddRemediation"];
248
- assert.equal(remediationUnresolved({ sddRemediation: { ...base } } as unknown as TaskRecord), false);
249
- assert.equal(remediationUnresolved({ sddRemediation: { ...base, settlement: { state: "blocked", reason: "maintainer_decision" } } } as unknown as TaskRecord), false);
250
- assert.equal(remediationUnresolved({ sddRemediation: { ...base, settlement: { state: "blocked", reason: "maintainer_decision" }, settlementUncertain: true } } as unknown as TaskRecord), true);
251
- assert.equal(remediationUnresolved({ sddRemediation: { ...base, actorClaimed: true } } as unknown as TaskRecord), true);
252
- assert.equal(remediationUnresolved({ sddRemediation: { ...base, token: "retained" } } as unknown as TaskRecord), true);
253
- });
254
-
255
65
 
256
66
  test("remediation actor preserves separately authorized memory artifact tools", async () => {
257
67
  const { parseAgentDefinition } = await import("../lib/agents-config.ts");
@@ -262,25 +72,6 @@ test("remediation actor preserves separately authorized memory artifact tools",
262
72
  });
263
73
 
264
74
 
265
- test("managed parent admission continues an explicitly retained token without a new acquire", async () => {
266
- const { calls } = await admissionFixture({}, undefined, {}, { token: "retained-parent-admission" });
267
- assert.equal(calls.length, 1);
268
- assert.equal(calls[0][1].token, "retained-parent-admission");
269
- });
270
-
271
-
272
- test("R3-duplicate-command-evidence requires a distinct execution for every indexed slot", () => {
273
- const s = state(); s.plan.commands = ["pnpm test", "pnpm test"];
274
- s.plan.runtimeHarness = { command: "pnpm test" }; s.plan.rollback.command = "pnpm test";
275
- for (let index = 0; index < 4; index++) {
276
- observed(s, "pnpm test", `slot-${index}`);
277
- assert.equal(remediationEvidence(s) === undefined, index < 3);
278
- }
279
- assert.deepEqual(s.observations.map(item => item.slot), [0, 1, 2, 3]);
280
- observed(s, "pnpm test", "slot-0");
281
- assert.equal(remediationEvidence(s), undefined, "reused call identity cannot produce evidence");
282
- });
283
-
284
75
 
285
76
  test("R1 confirms exact canonical paths and commands; data, denial and symlinks grant nothing", async t => {
286
77
  const { confirmRemediationScope, remediationToolAllowed } = await import("../extensions/gentle-agents.ts");
@@ -303,32 +94,7 @@ test("R1 confirms exact canonical paths and commands; data, denial and symlinks
303
94
  });
304
95
 
305
96
 
306
- test("R3/R4 acquire request is durable before mutation and token before actor admission", async () => {
307
- const records = [], calls = [];
308
- const { admitted } = await admissionFixture({ sddAttemptAcquire: async request => {
309
- assert.deepEqual(records.at(-1).sddRemediation.acquire, request);
310
- assert.equal(records.at(-1).sddRemediation.token, undefined);
311
- calls.push(structuredClone(request)); if (calls.length === 1) throw new Error("lost acquire reply");
312
- return { state: "proceed", token: "native-returned" };
313
- } }, async task => { records.push(structuredClone(task)); });
314
- assert.equal(calls.length, 2); assert.deepEqual(calls[0], calls[1]);
315
- assert.equal(records.at(-1).sddRemediation.token, "native-returned");
316
- assert.equal(admitted.sddRemediation.token, "native-returned");
317
- });
318
- test("R3/R4 acquire persistence failure refuses before native mutation", async () => {
319
- let mutations = 0;
320
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { mutations++; return { state: "proceed", token: "unsafe" }; } }, async () => { throw new Error("disk full"); }), /disk full/);
321
- assert.equal(mutations, 0);
322
- });
323
-
324
97
 
325
- test("R1 denial/headless/cancellation gives zero acquisition and durable mutation", async () => {
326
- for (const context of [{ hasUI: false }, { hasUI: true, ui: { confirm: async () => false } }, { hasUI: true, ui: { confirm: async () => undefined } }, { hasUI: true, ui: { confirm: async () => { throw new Error("cancelled"); } } }]) {
327
- let acquisitions = 0, persistence = 0;
328
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { acquisitions++; } }, async () => { persistence++; }, {}, {}, context as unknown as Pick<ExtensionContext, "hasUI" | "ui">));
329
- assert.equal(acquisitions, 0); assert.equal(persistence, 0);
330
- }
331
- });
332
98
  test("R1 child invokes only the exact confirmed command/cwd/count with distinct call IDs", async () => {
333
99
  const { remediationBash } = await import("../extensions/gentle-agents.ts");
334
100
  let executions = 0;
@@ -341,103 +107,49 @@ test("R1 child invokes only the exact confirmed command/cwd/count with distinct
341
107
  await execute("two", "pnpm test"); await assert.rejects(execute("three", "pnpm test"), /authorization/);
342
108
  assert.equal(executions, 2);
343
109
  });
344
- test("R3/R4 known non-mutating acquire failures retain retryable blocked history while ambiguity remains unresolved", async () => {
345
- for (const error of [new TypeError("invalid acquire request"), new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.SDD_ATTEMPT, true, false, "native launch unavailable")]) {
346
- let calls = 0; const retained = [];
347
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { calls++; throw error; } }, async task => { retained.push(structuredClone(task)); }), error);
348
- assert.equal(calls, 1);
349
- assert.deepEqual(retained.at(-1).sddRemediation.acquireResult, { state: "blocked" });
350
- assert.equal(remediationUnresolved(retained.at(-1)), false);
351
- }
352
- let calls = 0; const retained = [];
353
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { calls++; throw new Error("lost reply"); } }, async task => { retained.push(structuredClone(task)); }), /Unknown acquire/);
354
- assert.equal(calls, 2); assert.equal(retained.at(-1).sddRemediation.acquireUncertain, true);
355
- assert.equal(remediationUnresolved(retained.at(-1)), true);
356
- assert.deepEqual(retained[0].sddRemediation.acquire, retained.at(-1).sddRemediation.acquire);
357
- calls = 0;
358
- await assert.rejects(admissionFixture({ sddAttemptAcquire: async () => { calls++; return { state: "proceed", token: "returned" }; } }, async task => { if (task.sddRemediation.token) throw new Error("token disk failure"); }), /token disk failure/);
359
- assert.equal(calls, 1);
360
- });
361
-
362
- test("uncertain acquire reconciliation terminalizes blocked/complete exact replays", async () => {
363
- const { reconcileManagedRemediation } = await import("../extensions/gentle-agents.ts");
364
- for (const nativeState of ["blocked", "complete"] as const) {
365
- const acquire = { workspaceRoot: testCwd, changeName: "fix", requestId: `reconcile-${nativeState}`, workUnit: "fix", evidenceGoal: "Observed correction", remediatesEvidenceRevision: revision };
366
- const task = { id: nativeState, agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as unknown as TaskRecord;
367
- const calls = [], saved = [];
368
- const result = await reconcileManagedRemediation(task, { sddAttemptAcquire: async input => { calls.push(structuredClone(input)); return { state: nativeState }; } } as unknown as NativeReviewCli, async current => { saved.push(structuredClone(current)); });
369
- assert.deepEqual(calls, [acquire]);
370
- assert.equal(result.acquireState, nativeState);
371
- assert.equal(task.sddRemediation.acquireUncertain, undefined);
372
- assert.deepEqual(task.sddRemediation.acquireResult, { state: nativeState });
373
- assert.equal(remediationUnresolved(task), false);
374
- assert.ok(saved.length >= 1);
375
- }
376
- });
377
-
378
- test("recovered proceed is durably settled as interrupted without actor replay", async () => {
379
- const { reconcileManagedRemediation } = await import("../extensions/gentle-agents.ts");
380
- const acquire = { workspaceRoot: testCwd, changeName: "fix", requestId: "reconcile-proceed", workUnit: "fix", evidenceGoal: "Observed correction", remediatesEvidenceRevision: revision, untrackedScope: "exclude" as const };
381
- const task = { id: "proceed", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as unknown as TaskRecord;
382
- const saved = [], calls = [];
383
- const result = await reconcileManagedRemediation(task, {
384
- sddAttemptAcquire: async input => { calls.push(["acquire", structuredClone(input)]); return { state: "proceed", token: "secret-token" }; },
385
- sddAttemptSettle: async input => {
386
- calls.push(["settle", structuredClone(input)]);
387
- assert.deepEqual(saved.at(-1).sddRemediation.settle, input, "settle intent is durable before mutation");
388
- return { state: "complete" };
389
- },
390
- } as unknown as NativeReviewCli, async current => { saved.push(structuredClone(current)); });
391
- assert.equal(result.acquireState, "proceed"); assert.equal(result.settlementState, "complete");
392
- assert.deepEqual(calls[0], ["acquire", acquire]);
393
- assert.equal(calls[1][1].outcome, "interrupted");
394
- assert.equal(calls[1][1].token, "secret-token");
395
- assert.match(calls[1][1].requestId, /^reconcile-[a-f0-9]{32}$/);
396
- assert.equal(calls[1][1].remediatesEvidenceRevision, revision);
397
- assert.equal(calls[1][1].untrackedScope, "exclude");
398
- assert.ok(saved.filter(snapshot => snapshot.sddRemediation.token).every(snapshot => snapshot.sddRemediation.settle), "no durable checkpoint may retain recovered authority without its compensating settle request");
399
- assert.equal(task.sddRemediation.actorClaimed, undefined);
400
- assert.equal(task.sddRemediation.acquireUncertain, undefined);
401
- assert.equal(task.sddRemediation.settlementUncertain, undefined);
402
- assert.equal(remediationUnresolved(task), false);
403
- assert.equal(JSON.stringify(result).includes("secret-token"), false);
404
- });
405
-
406
- test("reconciliation retries only exact pending mutations and preserves uncertainty", async () => {
407
- const { reconcileManagedRemediation } = await import("../extensions/gentle-agents.ts");
408
- const acquire = { workspaceRoot: testCwd, changeName: "fix", requestId: "reconcile-lost", workUnit: "fix", evidenceGoal: "Observed correction", remediatesEvidenceRevision: revision };
409
- const acquireTask = { id: "lost-acquire", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true } } as unknown as TaskRecord;
410
- let acquireCalls = 0;
411
- await assert.rejects(reconcileManagedRemediation(acquireTask, { sddAttemptAcquire: async input => { acquireCalls++; assert.deepEqual(input, acquire); throw new Error("lost reply"); } } as unknown as NativeReviewCli, async () => {}), /acquire.*unresolved/i);
412
- assert.equal(acquireCalls, 2); assert.equal(acquireTask.sddRemediation.acquireUncertain, true);
413
-
414
- const settle = { workspaceRoot: testCwd, changeName: "fix", token: "retained", requestId: "settle-exact", outcome: "interrupted" as const, diagnosis: "Interrupted", harnessDisposition: "invalidated" as const, cleanupEvidence: "None", processEvidence: "None", remediatesEvidenceRevision: revision };
415
- const settleTask = { id: "lost-settle", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireResult: { state: "proceed", token: "retained" }, token: "retained", settle, settlementUncertain: true } } as unknown as TaskRecord;
416
- const settleCalls = [];
417
- const result = await reconcileManagedRemediation(settleTask, { sddAttemptSettle: async input => { settleCalls.push(structuredClone(input)); return { state: "blocked", reason: "already_closed" }; } } as unknown as NativeReviewCli, async () => {});
418
- assert.deepEqual(settleCalls, [settle]); assert.equal(result.settlementState, "blocked");
419
- assert.equal(settleTask.sddRemediation.settlementUncertain, undefined);
420
- assert.equal(remediationUnresolved(settleTask), false);
421
-
422
- const ambiguousSettle = { id: "ambiguous-settle", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireResult: { state: "proceed", token: "retained" }, token: "retained", settle, settlementUncertain: true } } as unknown as TaskRecord;
423
- let ambiguousCalls = 0; const ambiguousSaved = [];
424
- await assert.rejects(reconcileManagedRemediation(ambiguousSettle, { sddAttemptSettle: async input => { ambiguousCalls++; assert.deepEqual(input, settle); throw new Error("lost settle reply"); } } as unknown as NativeReviewCli, async current => { ambiguousSaved.push(structuredClone(current)); }), /settlement.*unresolved/i);
425
- assert.equal(ambiguousCalls, 2);
426
- assert.equal(ambiguousSettle.sddRemediation.settlementUncertain, true);
427
- assert.deepEqual(ambiguousSaved.at(-1).sddRemediation.settle, settle);
428
- assert.equal(remediationUnresolved(ambiguousSettle), true);
429
-
430
- const actorTask = { id: "actor", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireUncertain: true, actorClaimed: true } } as unknown as TaskRecord;
431
- await assert.rejects(reconcileManagedRemediation(actorTask, {} as NativeReviewCli, async () => {}), /actor.*uncertain/i);
432
- const mismatchedSettle = structuredClone(settle); mismatchedSettle.changeName = "other";
433
- const corruptTask = { id: "corrupt", agent: "sdd-remediate", cwd: testCwd, status: "failed", createdAt: 1, sddRemediation: { acquire, acquireResult: { state: "proceed", token: "retained" }, token: "retained", settle: mismatchedSettle, settlementUncertain: true } } as unknown as TaskRecord;
434
- await assert.rejects(reconcileManagedRemediation(corruptTask, { sddAttemptSettle: async () => { throw new Error("must not call"); } } as unknown as NativeReviewCli, async () => {}), /does not match/);
435
- });
436
-
437
110
 
438
111
  test("remediation actor explains one-launch human authority and indexed command evidence", async () => {
439
112
  const { readFileSync } = await import("node:fs");
440
113
  const text = readFileSync("assets/agents/sdd-remediate.md", "utf8");
441
114
  assert.match(text, /fresh host UI confirmation/); assert.match(text, /each repeated command.*separate execution/);
442
- assert.match(text, /reconcile.*without.*another actor/);
115
+ assert.match(text, /Inspect prior task\/artifact history/);
116
+ assert.match(text, /No attempt-ledger command is required/);
117
+ });
118
+
119
+
120
+ // Runs only in the explicit producer verification lane; no default-suite skip is added.
121
+ if (process.env.SDD_TEST_PRODUCER) test("real native selection admits only its own remediation route without attempt subprocesses", async (t) => {
122
+ const { execFileSync } = await import("node:child_process");
123
+ const { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, realpathSync } = await import("node:fs");
124
+ const { tmpdir } = await import("node:os");
125
+ const { join, resolve } = await import("node:path");
126
+ const { NativeReviewCliV216, createNodeExecFileAdapter } = await import("../lib/native-review-cli.ts");
127
+ const { admitManagedRemediation } = await import("../extensions/gentle-agents.ts");
128
+ const { parseAgentDefinition } = await import("../lib/agents-config.ts");
129
+ const cwd = realpathSync(mkdtempSync(join(tmpdir(), "no-attempt-producer-")));
130
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
131
+ execFileSync("git", ["init", "--quiet", cwd]);
132
+ const change = join(cwd, "openspec/changes/fix");
133
+ mkdirSync(join(change, "specs/example"), { recursive: true });
134
+ for (const [path, bytes] of Object.entries({ "proposal.md": "# Proposal\n", "design.md": "# Design\n", "specs/example/spec.md": "# Spec\n", "tasks.md": "- [x] Fixture complete\n" })) writeFileSync(join(change, path), bytes);
135
+ const revision = `sha256:${"a".repeat(64)}`;
136
+ writeFileSync(join(change, "verify-report.md"), ["```yaml", "schema: gentle-ai.verify-result/v1", `evidence_revision: ${revision}`, "verdict: fail", "blockers: 1", "critical_findings: 0", "requirements: 1/1", "scenarios: 1/1", "test_command: false", "test_exit_code: 1", `test_output_hash: sha256:${"b".repeat(64)}`, "build_command: true", "build_exit_code: 0", `build_output_hash: sha256:${"b".repeat(64)}`, "```"].join("\n"));
137
+ const invocations: string[] = [], adapter = createNodeExecFileAdapter();
138
+ const native = new NativeReviewCliV216(async (request) => { invocations.push(request.arguments[0]!); return adapter(request); }, process.env.SDD_TEST_PRODUCER!);
139
+ const status = await native.sddStatus({ workspaceRoot: cwd, changeName: "fix" });
140
+ const path = resolve("assets/agents/sdd-remediate.md"), agent = parseAgentDefinition(readFileSync(path, "utf8"), path, "global");
141
+ assert.ok("instructions" in agent);
142
+ let confirmations = 0;
143
+ const context = { hasUI: true, ui: { confirm: async () => { confirmations++; return true; } } } as unknown as Pick<ExtensionContext, "hasUI" | "ui">;
144
+ const request = { agent, cwd, sddChange: { changeName: "fix", workspaceRoot: cwd, phase: "remediate", failedEvidenceRevision: revision } } as import("../lib/agents-runner.ts").TaskRequest;
145
+ const admission = admitManagedRemediation(request, { plan: { ...plan, cwd } }, native, context);
146
+ if (status.nextRecommended === "remediate") {
147
+ const admitted = await admission;
148
+ assert.equal(admitted.sddRemediation?.failedEvidenceRevision, revision);
149
+ assert.equal(confirmations, 1);
150
+ } else {
151
+ await assert.rejects(admission, /Stale remediation selection/);
152
+ assert.equal(confirmations, 0, "a classical provider is not assigned a fabricated remediation route");
153
+ }
154
+ assert.deepEqual(invocations, ["sdd-status", "sdd-status"]);
443
155
  });