gentle-pi 2.6.1 → 2.6.2

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 (38) hide show
  1. package/README.md +183 -943
  2. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
  3. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
  4. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
  5. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
  6. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
  7. package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
  8. package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
  9. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  10. package/docs/assets/brand/gentle-pi-banner.svg +33 -0
  11. package/docs/assets/brand/terminal-divider.svg +17 -0
  12. package/docs/assets/diagrams/agent-orchestration.svg +19 -0
  13. package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
  14. package/docs/assets/diagrams/native-review.svg +16 -0
  15. package/docs/assets/diagrams/sdd-cycle.svg +14 -0
  16. package/docs/assets/features/gentle-shell.png +0 -0
  17. package/docs/gentle-shell.md +151 -0
  18. package/docs/readme-reference.md +868 -0
  19. package/extensions/gentle-agents.ts +4 -1
  20. package/extensions/gentle-ai.ts +65 -22
  21. package/lib/agents-history.ts +7 -1
  22. package/lib/native-review-cli.ts +9 -0
  23. package/package.json +1 -1
  24. package/runtime/native-review-cli.mjs +9 -0
  25. package/scripts/gentle-ai-installer.mjs +10 -10
  26. package/scripts/verify-package-files.mjs +2 -2
  27. package/tests/gentle-agents.test.ts +22 -0
  28. package/tests/gentle-ai-binary.test.ts +1 -1
  29. package/tests/gentle-ai-installer.test.ts +47 -47
  30. package/tests/gentle-ai.test.ts +3 -2
  31. package/tests/native-review-capability-contract.test.ts +13 -1
  32. package/tests/package-manifest.test.ts +19 -18
  33. package/tests/review-authority-recovery-docs.test.ts +13 -13
  34. package/tests/review-controller-native-routing.test.ts +49 -0
  35. package/tests/review-ledger-contract.test.ts +7 -5
  36. package/tests/sdd-managed-runtime-settlement.test.ts +37 -0
  37. package/tests/sdd-selection-transport.test.ts +57 -0
  38. package/tests/skill-collision-prefixes.test.ts +2 -2
@@ -197,9 +197,12 @@ export async function admitManagedRemediation(request: TaskRequest, input: unkno
197
197
  task.error = "Managed remediation lacks complete passing planned-command evidence";
198
198
  }
199
199
  if (payload.outcome === "passed" && state.settlement && state.settlement.state !== "blocked") task.status = TASK_STATUS.COMPLETED;
200
- if (!state.settlement || state.settlement.state === "blocked") {
200
+ if (!state.settlement) {
201
201
  task.status = TASK_STATUS.FAILED;
202
202
  task.error = "Native remediation settlement unresolved; retain exact history for reconciliation";
203
+ } else if (state.settlement.state === "blocked") {
204
+ task.status = TASK_STATUS.FAILED;
205
+ task.error = `Native remediation settlement blocked(${state.settlement.reason ?? "unspecified"}); current native admission decides any later attempt`;
203
206
  }
204
207
  await persist(task);
205
208
  },
@@ -1839,7 +1839,15 @@ async function resolveSelectedNativeSddChangeStartup(
1839
1839
  }
1840
1840
  if (selection.phase === "remediate") {
1841
1841
  if (status.nextRecommended !== "remediate" || status.remediationState?.failedEvidenceRevision !== selection.failedEvidenceRevision) throw new Error("Stale remediation selection");
1842
- } else if (status.nextRecommended !== selection.phase || status.dependencies[selection.phase] !== "ready" || status.blockedReasons.length > 0) {
1842
+ } else if (status.nextRecommended !== selection.phase || status.dependencies[selection.phase] !== "ready" || (status.blockedReasons.length > 0 && selection.phase !== "verify")) {
1843
+ // Native's contract gates terminal, archive, and apply work on a
1844
+ // non-empty `blockedReasons`, and it deliberately keeps the `verify`
1845
+ // route runnable, because the blocker can name the evidence refresh
1846
+ // that is its own remedy ("failed verification evidence is incomplete;
1847
+ // rerun SDD verification", gentle-ai#3538). Vetoing that route made the
1848
+ // native-recommended phase unreachable (gentle-pi#972). The other
1849
+ // phases still fail closed, and every blocker stays in the injected
1850
+ // status for reporting.
1843
1851
  throw new Error(`SDD selection native status blocks phase ${selection.phase}; it cannot execute.`);
1844
1852
  }
1845
1853
  return { selection, status };
@@ -5601,6 +5609,33 @@ function completeNativeStart(
5601
5609
  };
5602
5610
  }
5603
5611
 
5612
+ // gentle-ai#4003: every Pi-side teardown step that fails after the native
5613
+ // burn is deferred cleanup, not a failed acknowledgement. Only the
5614
+ // already-sanitized CandidateViewError surface is relayed; anything else is
5615
+ // reduced to the step's fixed code so no path or command text reaches the
5616
+ // caller. The candidate-view hint is out-of-band on purpose: no controller
5617
+ // operation exposes a cleanup-only retry, and replaying acknowledge-approved
5618
+ // would hit the already-burned lineage.
5619
+ const POST_BURN_CLEANUP = {
5620
+ candidateView: { code: "candidate-view-cleanup-failed", nextAction: "retry-candidate-view-cleanup-or-remove-the-view-out-of-band" },
5621
+ retainedSelection: { code: "retained-selection-cleanup-failed", nextAction: "retained-selection-clears-on-the-next-terminal-status" },
5622
+ } as const;
5623
+
5624
+ function deferredPostBurnCleanup(step: (typeof POST_BURN_CLEANUP)[keyof typeof POST_BURN_CLEANUP], cleanup: () => void): Record<string, unknown> | undefined {
5625
+ try {
5626
+ cleanup();
5627
+ return undefined;
5628
+ } catch (error) {
5629
+ return {
5630
+ status: "deferred",
5631
+ diagnostics: error instanceof CandidateViewError
5632
+ ? { code: error.reason, message: error.message }
5633
+ : { code: step.code },
5634
+ next_action: step.nextAction,
5635
+ };
5636
+ }
5637
+ }
5638
+
5604
5639
  function nativeOperationFailure(operation: ReviewControllerOperation | "gentle_review_capture", error: unknown): Record<string, unknown> {
5605
5640
  const value = error as { mutationOutcome?: unknown; nextAction?: unknown; diagnostics?: unknown; auditRecord?: unknown; launchAttempted?: unknown; candidateViewPreNative?: unknown; failureEnvelope?: { raw?: unknown; mutationOutcome?: unknown; replayability?: unknown; nextAction?: unknown; code?: unknown; continuation?: { command?: unknown } } };
5606
5641
  if (isRecord(value.failureEnvelope) && isRecord(value.failureEnvelope.raw)) {
@@ -7335,42 +7370,50 @@ async function executeReviewControllerOperation(
7335
7370
  } catch (error) {
7336
7371
  return nativeOperationFailure(parameters.operation, error);
7337
7372
  }
7373
+ let acknowledged: NativeReviewAcknowledgeApprovedOutcome | void;
7338
7374
  try {
7339
7375
  // gentle-ai #3947: the burn answers with one review-acknowledged/v1
7340
7376
  // envelope bound to exactly this lineage, target, and revision, and
7341
7377
  // the burn is reported from that envelope, never from a later
7342
7378
  // STATUS. Every published release up to v2.5.0-rc.3 still burns in
7343
7379
  // silence, and that result stays byte-identical.
7344
- const acknowledged = await acknowledgementCli.acknowledgeApproved({
7380
+ acknowledged = await acknowledgementCli.acknowledgeApproved({
7345
7381
  argumentTokens,
7346
7382
  cwd: defaultCwd,
7347
7383
  binding: { lineageId: parameters.lineageId, targetIdentity: status.targetIdentity, revision: status.authority.revision },
7348
7384
  ...(signal === undefined ? {} : { signal }),
7349
7385
  });
7350
- clearRetainedNativeUntrackedSelection(retainedUntrackedSelections, defaultCwd, parameters.lineageId);
7351
- // The registry owns restoring writability of its 0555 views before
7352
- // removal; a terminal approved cleanup keeps the lineage projection.
7353
- candidateViews?.cleanupTerminal(parameters.lineageId, "approved", defaultCwd);
7354
- // gentle-pi#668: `closed` is never auto-derived or recorded here --
7355
- // a parent that wants the on-path passes nativeReviewOutcome:
7356
- // "closed" explicitly on its next assess call for this candidate.
7357
- return {
7358
- operation: parameters.operation,
7359
- status: "closed",
7360
- outcome: "native-approved-acknowledgement-completed",
7361
- lineage_id: parameters.lineageId,
7362
- target_identity: status.targetIdentity,
7363
- ...(acknowledged === undefined ? {} : { consumed_revision: acknowledged.consumedRevision }),
7364
- authority: "burned",
7365
- ...(acknowledged === undefined ? {} : { burn_evidence: acknowledged.schema }),
7366
- delivery: "ordinary-repository-policy",
7367
- mutation_performed: true,
7368
- mutation_outcome: "committed",
7369
- };
7370
7386
  } catch (error) {
7371
7387
  if (!nativeMutationRequiresStatus(error)) return nativeOperationFailure(parameters.operation, error);
7372
7388
  return await reconcileNativeMutationFailure(parameters.operation, error, acknowledgementCli, target, retainedUntrackedSelections);
7373
7389
  }
7390
+ // gentle-ai#4003: from here the native burn is the committed authority
7391
+ // outcome. Both Pi-side teardown steps run outside the mutation-result
7392
+ // try/catch and each one is guarded on its own, so a cleanup failure is
7393
+ // reported as deferred cleanup and never as a failed acknowledgement
7394
+ // that would invite a replay of a burned operation.
7395
+ const retainedSelectionCleanup = deferredPostBurnCleanup(POST_BURN_CLEANUP.retainedSelection, () => clearRetainedNativeUntrackedSelection(retainedUntrackedSelections, defaultCwd, parameters.lineageId));
7396
+ // The registry owns restoring writability of its 0555 views before
7397
+ // removal; a terminal approved cleanup keeps the lineage projection.
7398
+ const candidateViewCleanup = deferredPostBurnCleanup(POST_BURN_CLEANUP.candidateView, () => candidateViews?.cleanupTerminal(parameters.lineageId, "approved", defaultCwd));
7399
+ // gentle-pi#668: `closed` is never auto-derived or recorded here --
7400
+ // a parent that wants the on-path passes nativeReviewOutcome:
7401
+ // "closed" explicitly on its next assess call for this candidate.
7402
+ return {
7403
+ operation: parameters.operation,
7404
+ status: "closed",
7405
+ outcome: "native-approved-acknowledgement-completed",
7406
+ lineage_id: parameters.lineageId,
7407
+ target_identity: status.targetIdentity,
7408
+ ...(acknowledged === undefined ? {} : { consumed_revision: acknowledged.consumedRevision }),
7409
+ authority: "burned",
7410
+ ...(acknowledged === undefined ? {} : { burn_evidence: acknowledged.schema }),
7411
+ delivery: "ordinary-repository-policy",
7412
+ mutation_performed: true,
7413
+ mutation_outcome: "committed",
7414
+ ...(retainedSelectionCleanup === undefined ? {} : { retained_selection_cleanup: retainedSelectionCleanup }),
7415
+ ...(candidateViewCleanup === undefined ? {} : { candidate_view_cleanup: candidateViewCleanup }),
7416
+ };
7374
7417
  }
7375
7418
  if (parameters.operation === REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT) {
7376
7419
  const input = parseControllerJson(requiredControllerString(parameters, "input"), parameters.operation);
@@ -16,7 +16,13 @@ export function remediationUnresolved(task: TaskRecord): boolean {
16
16
  const state = task.sddRemediation;
17
17
  if (!state) return false;
18
18
  if (state.acquireUncertain || state.settlementUncertain) return true;
19
- if (state.settlement) return state.settlement.state === "blocked";
19
+ // A received settlement is a definite native outcome, whatever its state
20
+ // (including "blocked"): it is terminal task history, never local
21
+ // ambiguity. Native admission is the sole authority over any later
22
+ // attempt for the same cwd/change; only genuinely uncertain outcomes, or
23
+ // no settlement at all with a still-retained token/claimed actor, are
24
+ // unresolved.
25
+ if (state.settlement) return false;
20
26
  return !!state.token || !!state.actorClaimed || !["blocked", "complete"].includes(state.acquireResult?.state ?? "");
21
27
  }
22
28
 
@@ -971,6 +971,15 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
971
971
  // this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
972
972
  // because neither is proven to reach the negotiated START path Pi consumes.
973
973
  "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 }),
974
+ // v2.8.2 shipped OpenCode SDD preflight plugin fixes, community-tools RTK
975
+ // acquisition, and Claude Code Stop telemetry. The provider contract semver
976
+ // stays 1.2.0; the same pin re-mirrors bundle bytes that had drifted under
977
+ // that semver (lens inspection.status "unavailable", targeted-validator
978
+ // regressions/inspection members, seven Pi stop reason codes). None of
979
+ // those touch the closed START/STATUS fields this row negotiates, so it
980
+ // repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
981
+ // is proven to reach the negotiated START path Pi consumes.
982
+ "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 }),
974
983
  });
975
984
 
976
985
  export interface NativeReviewProcessDiagnostics {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -972,6 +972,15 @@ 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 }),
975
984
  });
976
985
 
977
986
 
@@ -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.8.1";
39
+ export const INSTALLER_VERSION = "2.8.2";
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.8.1`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.8.2`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit 7aad6a51, the published v2.8.1 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:EixFRy7P3JLVi1ovr18pMxQIrtjDib9i7VEH9g4XRVE=";
50
+ // tag resolves to commit e3f53de6, the published v2.8.2 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:54VJ0ruRiPc6MbNw4iFmlcBkQFOyF5D8iHY0A54Ivug=";
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.8.1 digests are pinned from the published release:
70
+ // still holds it. The v2.8.2 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.8.1 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v2.8.2 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.8.1_darwin_amd64.tar.gz", "cbfba54a72ec0a28ce5e42c3057a03cc3e809c28b1ff38d080ef42a242027897", "77784169c58b7f84d21c2df538031de2a9582cc1ea43242103d9487f3278c85f", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_2.8.1_darwin_arm64.tar.gz", "a5aca61d4e98fed0e0a8d1f2d6e1ae5d48746bd6baa541c91f629ade25f47839", "1c4b63db9aba22f02b6cf53b4b61cb750c2a7d8bf429c5aae82993227ca1e613", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_2.8.1_linux_amd64.tar.gz", "609190fa9e8ef1896a8b4fcbb5a44e1ad5ac64aae1d17eb0459735c5bbef199b", "c11a6d627a81741d00abc7bc9071522e9279de6f95219d810469a448615e618e", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_2.8.1_linux_arm64.tar.gz", "919232dcf7ba1b0cd228f808166146cce8f2ea5d691a59bda9cd42a294432ea4", "f4eb76ea6f466ba991e530b7fc6dce57b39da273d8b3013570edeb17dfe6908d", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_2.8.2_darwin_amd64.tar.gz", "0daa28897e6e54ce584f12ccefebf0d0df84e4ea8fbbd3a07e596ca82b079fa2", "17069156869ceda8e23eaa4fe5d7565cd57c1d78528f121dabba7301b82a0c14", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_2.8.2_darwin_arm64.tar.gz", "12265017e0fb6d5dd1ddb1751f188fa8c47d95a95d7c7515ae174394da5f6e97", "491542e4b60e432048d4074f21c1b04417b980cf77eb34cd0a2e7447ca57dd77", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_2.8.2_linux_amd64.tar.gz", "5b95b184606168685a4ff576c103b051ceb23346a7f81ba557e047fa4d744146", "a55f4d2e114128866810c25195e5efecdded4502660e9a9c35c0b7a41c909dd8", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_2.8.2_linux_arm64.tar.gz", "a33c91ca0f5c5c85a4fd69f5169138089eb53e0a92c6ab80de5094d0688debb7", "e2759aca09ffe97638e984491319e43c9477d1ae117726ecbdf20d29d44c8950", "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.8.1 runtime's vendored Gentle AI contract artifacts:");
343
+ console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.8.2 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.8.1 runtime).`);
388
+ console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.8.2 runtime).`);
389
389
  }
390
390
 
391
391
  const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
@@ -2160,3 +2160,25 @@ test("R3/R4 host reload refuses retained acquire/actor uncertainty without anoth
2160
2160
  await h.fire("session_shutdown", ctx);
2161
2161
  }
2162
2162
  });
2163
+
2164
+ test("R3/R4 a known native-blocked settlement lets native admission decide the next attempt; an uncertain one still refuses locally", async () => {
2165
+ for (const uncertain of [false, true]) {
2166
+ const h = fakePi(), runtime = deps(), fixtureHome = join(root, `remediation-settlement-${uncertain}`);
2167
+ mkdirSync(join(fixtureHome, ".pi", "agent", "agents"), { recursive: true });
2168
+ writeFileSync(join(fixtureHome, ".pi", "agent", "agents", "sdd-remediate.md"), readFileSync("assets/agents/sdd-remediate.md"));
2169
+ const revision = `sha256:${"a".repeat(64)}`;
2170
+ const acquire = { workspaceRoot: cwd, changeName: "alpha", requestId: "retained", workUnit: "correct", evidenceGoal: "Observed correction", remediatesEvidenceRevision: revision };
2171
+ await saveTask(historyDir(fixtureHome), { id: "retained", agent: "sdd-remediate", cwd, status: "failed", createdAt: 1, sddRemediation: uncertain
2172
+ ? { acquire, settlementUncertain: true, settle: { requestId: "exact" } }
2173
+ : { acquire, settlement: { state: "blocked", reason: "maintainer_decision" } } } as never, emptyThread());
2174
+ let acquisitions = 0, confirmations = 0;
2175
+ 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 });
2176
+ const { ctx } = fakeContext(fakeTui, async () => { confirmations++; return true; });
2177
+ await h.fire("session_start", ctx);
2178
+ 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/);
2179
+ assert.equal(acquisitions, uncertain ? 0 : 1, "native admission is consulted once local unresolved history is not uncertain");
2180
+ assert.equal(confirmations, uncertain ? 0 : 1);
2181
+ assert.equal(runtime.spawned.length, 0);
2182
+ await h.fire("session_shutdown", ctx);
2183
+ }
2184
+ });
@@ -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.8.1",
100
+ tag: "v2.8.2",
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,