gentle-pi 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -85,7 +85,7 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
|
|
|
85
85
|
reviewStatus: async () => ({ schema: "gentle-ai.review-authority-status/v1", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [], raw: { schema: "gentle-ai.review-authority-status/v1", operation: "review/status", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [] } }),
|
|
86
86
|
targetStatus: async (request) => request.lineageId === undefined
|
|
87
87
|
? candidateStartTargetStatus(request)
|
|
88
|
-
:
|
|
88
|
+
: candidateFinalizeTargetStatus(request, request.lineageId),
|
|
89
89
|
...overrides,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
@@ -202,6 +202,21 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function candidateFinalizeTargetStatus(request: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0], lineageId: string): ReviewStatusV3 {
|
|
206
|
+
let candidate: ReturnType<CandidateViewRegistry["create"]> | undefined;
|
|
207
|
+
try {
|
|
208
|
+
candidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd });
|
|
209
|
+
return targetStatusFixture({
|
|
210
|
+
lineageId,
|
|
211
|
+
baseTree: candidate.baseTree,
|
|
212
|
+
currentCandidateTree: candidate.candidateTree,
|
|
213
|
+
paths: candidate.paths,
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
candidate?.cleanup();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
test("INSPECT and STATUS operate on the explicit workspace root while the session cwd stays elsewhere", async (t) => {
|
|
206
221
|
const sessionCwd = repository(t);
|
|
207
222
|
const worktree = addWorktree(t, sessionCwd, "feat-binding");
|
|
@@ -351,7 +366,7 @@ test("FINALIZE fails closed when the frozen projection belongs to a different wo
|
|
|
351
366
|
},
|
|
352
367
|
}), candidateViews);
|
|
353
368
|
await controller.execute("start-finalize-b", { operation: "start", workspaceRoot: worktree, input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
|
|
354
|
-
const input = JSON.stringify({
|
|
369
|
+
const input = JSON.stringify({});
|
|
355
370
|
const mismatch = await controller.execute("finalize-wrong-root", { operation: "finalize", lineageId: "finalize-lineage", input }, undefined, undefined, context(sessionCwd));
|
|
356
371
|
assert.equal((mismatch.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
357
372
|
assert.equal(finalizes, 0);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
+
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
10
|
+
import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
11
|
+
|
|
12
|
+
// Field defect (Engram #12547, lineage review-43762fca1bba0bf3): a MEDIUM
|
|
13
|
+
// lineage detected a reliability defect, corrected it inside budget, captured
|
|
14
|
+
// evidence and the native targeted validator. Negotiated STATUS then returned
|
|
15
|
+
// `captured_provider_targeted_validation_ready` with an execute
|
|
16
|
+
// `review.finalize` transition, and running that exact transition failed twice
|
|
17
|
+
// with `candidate-target-projection-drift`, mutation none, so no receipt was
|
|
18
|
+
// ever minted.
|
|
19
|
+
//
|
|
20
|
+
// Measured on a faithful live reproduction (medium candidate, real binary,
|
|
21
|
+
// START through the controller so the registry holds the START-time view):
|
|
22
|
+
//
|
|
23
|
+
// START candidate tree: 4c24487f850050af0a0944d37d4dbb15287ab3da
|
|
24
|
+
// corrected candidate tree: 777f48fdc4e6ed9abd325d6b4c49e6d06036c906
|
|
25
|
+
// FINALIZE (no documents) -> candidate-target-projection-drift, mutation none
|
|
26
|
+
// registry branch taken: ["resolveForFinalize"]
|
|
27
|
+
//
|
|
28
|
+
// Root cause: the corrected tree is tolerated only while
|
|
29
|
+
// `correctionCompletion` (validation AND final_evidence) or `validationAttempt`
|
|
30
|
+
// (final_evidence without a forecast) hold. A FINALIZE that merely follows the
|
|
31
|
+
// provider's own execute transition carries no documents, so both are false,
|
|
32
|
+
// the START-time view is resolved, and it is compared against the corrected
|
|
33
|
+
// candidate the provider itself authorized.
|
|
34
|
+
|
|
35
|
+
const SHA = `sha256:${"1".repeat(64)}`;
|
|
36
|
+
const DEFECT = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 2];\n}\n";
|
|
37
|
+
const CORRECTED = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 1];\n}\n";
|
|
38
|
+
|
|
39
|
+
function git(cwd: string, ...arguments_: string[]): string {
|
|
40
|
+
return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function repository(t: test.TestContext): string {
|
|
44
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-corrected-finalize-"));
|
|
45
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
46
|
+
git(cwd, "init", "-b", "main");
|
|
47
|
+
git(cwd, "config", "user.name", "Corrected Test");
|
|
48
|
+
git(cwd, "config", "user.email", "corrected@example.invalid");
|
|
49
|
+
mkdirSync(join(cwd, "src"), { recursive: true });
|
|
50
|
+
writeFileSync(join(cwd, "src", "parse.js"), "export function parsePath(input) {\n return input.split(\"/\");\n}\n");
|
|
51
|
+
git(cwd, "add", "-A");
|
|
52
|
+
git(cwd, "commit", "-m", "base");
|
|
53
|
+
writeFileSync(join(cwd, "src", "parse.js"), DEFECT);
|
|
54
|
+
return cwd;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The live frozen identity of the current workspace candidate. */
|
|
58
|
+
function liveIdentity(cwd: string): { baseTree: string; candidateTree: string; paths: readonly string[] } {
|
|
59
|
+
const probe = new CandidateViewRegistry();
|
|
60
|
+
const view = probe.create({ contributorRoot: cwd });
|
|
61
|
+
try {
|
|
62
|
+
return { baseTree: view.baseTree, candidateTree: view.candidateTree, paths: view.paths };
|
|
63
|
+
} finally {
|
|
64
|
+
probe.cleanup(view.token);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function correctedStatus(lineageId: string, identity: { baseTree: string; candidateTree: string; paths: readonly string[] }): ReviewStatusV3 {
|
|
69
|
+
return {
|
|
70
|
+
contract: "gentle-ai.review-integration/v2",
|
|
71
|
+
applicability: "current_target",
|
|
72
|
+
authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: SHA },
|
|
73
|
+
receipt: { status: "expected_missing" },
|
|
74
|
+
action: "finalize",
|
|
75
|
+
replayability: "not_replayable",
|
|
76
|
+
frozen: { tier: "medium", originalChangedLines: 4, correctionBudget: 2 },
|
|
77
|
+
targetIdentity: SHA,
|
|
78
|
+
projection: {
|
|
79
|
+
schema: "gentle-ai.review-integration.projection/v1",
|
|
80
|
+
kind: "current-changes",
|
|
81
|
+
projection: "workspace",
|
|
82
|
+
baseTree: identity.baseTree,
|
|
83
|
+
initialReviewTree: identity.candidateTree,
|
|
84
|
+
currentCandidateTree: identity.candidateTree,
|
|
85
|
+
pathsDigest: SHA,
|
|
86
|
+
paths: [...identity.paths],
|
|
87
|
+
intendedUntracked: [],
|
|
88
|
+
intendedUntrackedProof: SHA,
|
|
89
|
+
initialSnapshotIdentity: SHA,
|
|
90
|
+
currentSnapshotIdentity: SHA,
|
|
91
|
+
},
|
|
92
|
+
candidates: [],
|
|
93
|
+
// The provider's own answer after it admitted the captured validator.
|
|
94
|
+
nextTransition: {
|
|
95
|
+
kind: "execute",
|
|
96
|
+
reasonCode: "captured_provider_targeted_validation_ready",
|
|
97
|
+
execute: {
|
|
98
|
+
operation: "review.finalize",
|
|
99
|
+
arguments: [
|
|
100
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
101
|
+
{ name: "captured-evidence", value: "true", token: "--captured-evidence=true" },
|
|
102
|
+
],
|
|
103
|
+
binding: { lineageId },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
|
|
107
|
+
} as unknown as ReviewStatusV3;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function approvingNative(status: ReviewStatusV3): { native: NativeReviewCli; transitions: number } {
|
|
111
|
+
const state = { transitions: 0 };
|
|
112
|
+
const native = {
|
|
113
|
+
targetStatus: async () => status,
|
|
114
|
+
finalizeTransition: async () => {
|
|
115
|
+
state.transitions += 1;
|
|
116
|
+
return { lineageId: status.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r2" };
|
|
117
|
+
},
|
|
118
|
+
finalize: async () => { throw new Error("the provider transition must be executed, not raw finalize"); },
|
|
119
|
+
} as unknown as NativeReviewCli;
|
|
120
|
+
return { native, get transitions() { return state.transitions; } } as never;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Binds the START-time immutable reviewer view, as a live session does. */
|
|
124
|
+
function sessionWithStartBinding(cwd: string, lineageId: string): { registry: CandidateViewRegistry; startTree: string } {
|
|
125
|
+
const registry = new CandidateViewRegistry();
|
|
126
|
+
const view = registry.createOrReuse({ contributorRoot: cwd });
|
|
127
|
+
registry.bindCurrent({ token: view.token, lineageId, selectedLenses: ["review-reliability"] });
|
|
128
|
+
return { registry, startTree: view.candidateTree };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test("FINALIZE follows the provider transition after a correction instead of drifting on the START view", async (t) => {
|
|
132
|
+
const cwd = repository(t);
|
|
133
|
+
const lineageId = "review-corrected-lineage";
|
|
134
|
+
const { registry, startTree } = sessionWithStartBinding(cwd, lineageId);
|
|
135
|
+
t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
|
|
136
|
+
|
|
137
|
+
// The bounded correction lands: the candidate identity legitimately moves.
|
|
138
|
+
writeFileSync(join(cwd, "src", "parse.js"), CORRECTED);
|
|
139
|
+
const corrected = liveIdentity(cwd);
|
|
140
|
+
assert.notEqual(corrected.candidateTree, startTree, "the correction must move the candidate tree");
|
|
141
|
+
|
|
142
|
+
const harness = approvingNative(correctedStatus(lineageId, corrected));
|
|
143
|
+
const result = await __testing.executeReviewControllerOperation(
|
|
144
|
+
// Exactly the reporter's call: follow the provider transition, no documents.
|
|
145
|
+
{ operation: "finalize", lineageId, input: JSON.stringify({}) },
|
|
146
|
+
cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
|
|
147
|
+
) as Record<string, unknown>;
|
|
148
|
+
|
|
149
|
+
assert.notEqual(
|
|
150
|
+
(result.diagnostics as { code?: string } | undefined)?.code,
|
|
151
|
+
"candidate-target-projection-drift",
|
|
152
|
+
"a corrected candidate the provider authorized must not read as reviewer-view drift",
|
|
153
|
+
);
|
|
154
|
+
assert.equal(harness.transitions, 1, "the provider's own finalize transition must run");
|
|
155
|
+
assert.equal((result.result as { state?: string } | undefined)?.state, "approved");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("FINALIZE still fails closed when the live candidate does not match the provider projection", async (t) => {
|
|
159
|
+
const cwd = repository(t);
|
|
160
|
+
const lineageId = "review-drifted-lineage";
|
|
161
|
+
const { registry } = sessionWithStartBinding(cwd, lineageId);
|
|
162
|
+
t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
|
|
163
|
+
|
|
164
|
+
// The provider describes a candidate this workspace cannot reproduce.
|
|
165
|
+
const identity = liveIdentity(cwd);
|
|
166
|
+
const harness = approvingNative(correctedStatus(lineageId, { ...identity, candidateTree: identity.baseTree }));
|
|
167
|
+
const result = await __testing.executeReviewControllerOperation(
|
|
168
|
+
{ operation: "finalize", lineageId, input: JSON.stringify({}) },
|
|
169
|
+
cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
|
|
170
|
+
) as Record<string, unknown>;
|
|
171
|
+
|
|
172
|
+
assert.equal(result.status, "blocked", "an unverifiable candidate must never mint a receipt");
|
|
173
|
+
assert.equal(result.mutation_outcome, "none");
|
|
174
|
+
assert.equal(harness.transitions, 0, "no provider transition may run for an unverifiable candidate");
|
|
175
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
+
import { CandidateViewError, CandidateViewRegistry, injectReviewCandidateView } from "../lib/review-candidate-view.ts";
|
|
10
|
+
import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
11
|
+
|
|
12
|
+
// Field report (2026-08-16, gentle-pi main 402f9f77 + gentle-ai
|
|
13
|
+
// 2.4.0-main.20278905): after #340 the FINALIZE routing defect was fixed but
|
|
14
|
+
// the Pi relay's candidate-view registration still refused for an externally
|
|
15
|
+
// recovered successor. Reproduced against the live binary: hydration itself
|
|
16
|
+
// works for that shape (a dirty LINKED worktree hydrates fine) — the gap is
|
|
17
|
+
// that hydration was wired into the STATUS controller operation ONLY, while
|
|
18
|
+
// the maintainer's flow is `finalize` (now correctly blocked with
|
|
19
|
+
// review.capture-result) followed by a reviewer dispatch. The FINALIZE lane
|
|
20
|
+
// decodes the same authoritative status and never hydrated from it.
|
|
21
|
+
//
|
|
22
|
+
// Second defect in the same area: every hydration failure was swallowed by a
|
|
23
|
+
// bare catch, so the later dispatch refusal claimed no binding existed
|
|
24
|
+
// without ever admitting hydration had been attempted and why it failed.
|
|
25
|
+
|
|
26
|
+
const SHA = `sha256:${"1".repeat(64)}`;
|
|
27
|
+
|
|
28
|
+
function git(cwd: string, ...arguments_: string[]): string {
|
|
29
|
+
return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A LINKED worktree (not the primary checkout) with dirty tracked files. */
|
|
33
|
+
function linkedDirtyWorktree(t: test.TestContext): string {
|
|
34
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-pi-hydration-gap-"));
|
|
35
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
36
|
+
const primary = join(root, "primary");
|
|
37
|
+
const linked = join(root, "linked");
|
|
38
|
+
mkdirSync(primary, { recursive: true });
|
|
39
|
+
git(primary, "init", "-b", "main");
|
|
40
|
+
git(primary, "config", "user.name", "Hydration Test");
|
|
41
|
+
git(primary, "config", "user.email", "hydration@example.invalid");
|
|
42
|
+
mkdirSync(join(primary, "internal"), { recursive: true });
|
|
43
|
+
writeFileSync(join(primary, "internal", "parser.go"), "package internal\n\nfunc Parse(s string) string {\n\treturn s\n}\n");
|
|
44
|
+
git(primary, "add", "-A");
|
|
45
|
+
git(primary, "commit", "-m", "base");
|
|
46
|
+
git(primary, "worktree", "add", linked, "-b", "feature/fence-info");
|
|
47
|
+
// Uncommitted tracked modification, exactly the field shape.
|
|
48
|
+
writeFileSync(join(linked, "internal", "parser.go"), "package internal\n\nimport \"strings\"\n\nfunc Parse(s string) string {\n\treturn strings.TrimSpace(s)\n}\n");
|
|
49
|
+
return linked;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reviewerResultCollectInput(lineageId: string, lens: string): ReviewCollectInputV3 {
|
|
53
|
+
return {
|
|
54
|
+
name: "reviewer_result",
|
|
55
|
+
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
56
|
+
captureOperation: "review.capture-result",
|
|
57
|
+
arguments: [
|
|
58
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
59
|
+
{ name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
|
|
60
|
+
{ name: "lens", value: lens, token: `--lens=${lens}` },
|
|
61
|
+
{ name: "order", value: "0", token: "--order=0" },
|
|
62
|
+
],
|
|
63
|
+
artifactSubject: {
|
|
64
|
+
schema: "gentle-ai.review-artifact-subject/v2",
|
|
65
|
+
subjectHash: SHA,
|
|
66
|
+
lineageId,
|
|
67
|
+
authorityRevision: SHA,
|
|
68
|
+
targetIdentity: SHA,
|
|
69
|
+
baseTree: "3".repeat(40),
|
|
70
|
+
candidateTree: "4".repeat(40),
|
|
71
|
+
changedPathManifestSha256: SHA,
|
|
72
|
+
lens,
|
|
73
|
+
selectedOrder: 0,
|
|
74
|
+
},
|
|
75
|
+
} as unknown as ReviewCollectInputV3;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function successorStatus(lineageId: string, projection: { baseTree: string; currentCandidateTree: string; paths: readonly string[] }): ReviewStatusV3 {
|
|
79
|
+
return {
|
|
80
|
+
contract: "gentle-ai.review-integration/v2",
|
|
81
|
+
applicability: "current_target",
|
|
82
|
+
authority: { version: "compact-v2", lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
|
|
83
|
+
receipt: { status: "expected_missing" },
|
|
84
|
+
action: "finalize",
|
|
85
|
+
replayability: "not_replayable",
|
|
86
|
+
targetIdentity: SHA,
|
|
87
|
+
projection: {
|
|
88
|
+
schema: "gentle-ai.review-integration.projection/v1",
|
|
89
|
+
kind: "current-changes",
|
|
90
|
+
projection: "workspace",
|
|
91
|
+
baseTree: projection.baseTree,
|
|
92
|
+
initialReviewTree: projection.currentCandidateTree,
|
|
93
|
+
currentCandidateTree: projection.currentCandidateTree,
|
|
94
|
+
pathsDigest: SHA,
|
|
95
|
+
paths: [...projection.paths],
|
|
96
|
+
intendedUntracked: [],
|
|
97
|
+
intendedUntrackedProof: SHA,
|
|
98
|
+
initialSnapshotIdentity: SHA,
|
|
99
|
+
currentSnapshotIdentity: SHA,
|
|
100
|
+
},
|
|
101
|
+
candidates: [],
|
|
102
|
+
nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [reviewerResultCollectInput(lineageId, "review-reliability")] } },
|
|
103
|
+
raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
|
|
104
|
+
} as unknown as ReviewStatusV3;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The live frozen identity of the dirty linked worktree candidate. */
|
|
108
|
+
function liveProjection(cwd: string): { baseTree: string; currentCandidateTree: string; paths: readonly string[] } {
|
|
109
|
+
const probe = new CandidateViewRegistry();
|
|
110
|
+
const view = probe.create({ contributorRoot: cwd });
|
|
111
|
+
try {
|
|
112
|
+
return { baseTree: view.baseTree, currentCandidateTree: view.candidateTree, paths: view.paths };
|
|
113
|
+
} finally {
|
|
114
|
+
probe.cleanup(view.token);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function statusNative(status: ReviewStatusV3): NativeReviewCli {
|
|
119
|
+
return {
|
|
120
|
+
targetStatus: async () => status,
|
|
121
|
+
finalize: async () => { throw new Error("native finalize must not run while reviewer results are outstanding"); },
|
|
122
|
+
} as unknown as NativeReviewCli;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runController(parameters: Record<string, unknown>, cwd: string, native: NativeReviewCli, candidateViews: CandidateViewRegistry): Promise<Record<string, unknown>> {
|
|
126
|
+
return await __testing.executeReviewControllerOperation(
|
|
127
|
+
parameters, cwd, new Map(), native, undefined, undefined, undefined, candidateViews,
|
|
128
|
+
) as Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test("FINALIZE hydrates the dispatch binding for an externally recovered successor in a dirty linked worktree", async (t) => {
|
|
132
|
+
const cwd = linkedDirtyWorktree(t);
|
|
133
|
+
const lineageId = "recovered-dirty-successor";
|
|
134
|
+
const registry = new CandidateViewRegistry();
|
|
135
|
+
const native = statusNative(successorStatus(lineageId, liveProjection(cwd)));
|
|
136
|
+
|
|
137
|
+
// The maintainer's flow: FINALIZE first (correctly blocked since #340),
|
|
138
|
+
// then a reviewer dispatch. FINALIZE never went through the STATUS
|
|
139
|
+
// controller operation, so nothing had hydrated the registry.
|
|
140
|
+
const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, native, registry);
|
|
141
|
+
assert.equal(finalize.outcome, "reviewer-results-required");
|
|
142
|
+
assert.equal(registry.hasCurrentBinding(), true, "FINALIZE must hydrate the dispatch binding from the status it decoded");
|
|
143
|
+
assert.deepEqual(finalize.dispatch_binding, { hydrated: true, lineage_id: lineageId, lenses: ["review-reliability"] });
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const dispatch: Record<string, unknown> = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
|
|
147
|
+
assert.doesNotThrow(() => injectReviewCandidateView(dispatch, registry));
|
|
148
|
+
assert.match(String(dispatch.task), new RegExp(lineageId));
|
|
149
|
+
} finally {
|
|
150
|
+
registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("a swallowed hydration failure becomes an observable typed dispatch refusal", async (t) => {
|
|
155
|
+
const cwd = linkedDirtyWorktree(t);
|
|
156
|
+
const lineageId = "drifted-successor";
|
|
157
|
+
const registry = new CandidateViewRegistry();
|
|
158
|
+
// Realistic drift: the frozen candidate names the pristine HEAD tree while
|
|
159
|
+
// the live worktree carries the uncommitted modification. Hydration
|
|
160
|
+
// legitimately fails, and that failure must stop being invisible.
|
|
161
|
+
const live = liveProjection(cwd);
|
|
162
|
+
const drifted = { ...live, currentCandidateTree: live.baseTree };
|
|
163
|
+
const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, statusNative(successorStatus(lineageId, drifted)), registry);
|
|
164
|
+
|
|
165
|
+
assert.equal(finalize.outcome, "reviewer-results-required");
|
|
166
|
+
assert.equal(registry.hasCurrentBinding(), false);
|
|
167
|
+
const binding = finalize.dispatch_binding as { hydrated: boolean; lineage_id: string; reason: string; message: string };
|
|
168
|
+
assert.equal(binding.hydrated, false, "a failed hydration must be reported, never swallowed");
|
|
169
|
+
assert.equal(binding.lineage_id, lineageId);
|
|
170
|
+
assert.ok(typeof binding.reason === "string" && binding.reason.length > 0, "the failure carries a typed reason");
|
|
171
|
+
assert.match(binding.message, /projection|live candidate/i);
|
|
172
|
+
|
|
173
|
+
// The dispatch refusal must now admit that hydration was attempted.
|
|
174
|
+
assert.throws(
|
|
175
|
+
() => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
|
|
176
|
+
(error: unknown) => error instanceof CandidateViewError
|
|
177
|
+
&& error.reason === "current-binding-hydration-failed"
|
|
178
|
+
&& /hydrat/i.test(error.message)
|
|
179
|
+
&& error.message.includes(lineageId),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("STATUS keeps hydrating and stays read-only when hydration fails", async (t) => {
|
|
184
|
+
const cwd = linkedDirtyWorktree(t);
|
|
185
|
+
const lineageId = "status-drifted-successor";
|
|
186
|
+
const registry = new CandidateViewRegistry();
|
|
187
|
+
const statusLive = liveProjection(cwd);
|
|
188
|
+
const drifted = { ...statusLive, currentCandidateTree: statusLive.baseTree };
|
|
189
|
+
// STATUS must never fail because hydration failed.
|
|
190
|
+
const envelope = await runController({ operation: "status", lineageId }, cwd, statusNative(successorStatus(lineageId, drifted)), registry);
|
|
191
|
+
assert.equal(envelope.operation, "status");
|
|
192
|
+
assert.equal(registry.hasCurrentBinding(), false);
|
|
193
|
+
assert.throws(
|
|
194
|
+
() => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
|
|
195
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed",
|
|
196
|
+
);
|
|
197
|
+
});
|