gentle-pi 2.1.2 → 2.3.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 +108 -79
- 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 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- 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 +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- 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/v1/schemas/transition-execution.schema.json +42 -0
- 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/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- 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 +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -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/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -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/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -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/start-v3-zero-lens-closed.captured.json +21 -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/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -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 +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- 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 +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- 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/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- 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/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
NativeReviewCliV216,
|
|
8
8
|
NativeReviewConsentBindingError,
|
|
9
9
|
NativeReviewConsentRequiredError,
|
|
10
|
-
clearNativeReviewCapabilitiesCacheForTesting,
|
|
11
10
|
type ExecFileAdapter,
|
|
12
11
|
} from "../lib/native-review-cli.ts";
|
|
12
|
+
import { NativeReviewCliV216 as RuntimeNativeReviewCliV216 } from "../runtime/native-review-cli.mjs";
|
|
13
13
|
import type { ReviewConsentV2 } from "../lib/review-integration-v2.ts";
|
|
14
14
|
|
|
15
15
|
const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
|
|
@@ -25,7 +25,7 @@ function capabilities(): Record<string, unknown> {
|
|
|
25
25
|
function unrelatedStatus(targetIdentity: string): Record<string, unknown> {
|
|
26
26
|
const status = fixture<Record<string, unknown>>("status.fixture.json");
|
|
27
27
|
status.applicability = "unrelated";
|
|
28
|
-
status.receipt
|
|
28
|
+
delete status.receipt;
|
|
29
29
|
status.action = "start";
|
|
30
30
|
status.replayability = "not_replayable";
|
|
31
31
|
status.target_identity = targetIdentity;
|
|
@@ -39,8 +39,41 @@ function unrelatedStatus(targetIdentity: string): Record<string, unknown> {
|
|
|
39
39
|
return status;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function startTransitionTokens(targetIdentity: string): readonly string[] {
|
|
43
|
+
return [
|
|
44
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
45
|
+
"--cwd=/repo",
|
|
46
|
+
`--target=${targetIdentity}`,
|
|
47
|
+
"--projection=workspace",
|
|
48
|
+
"--agent=pi",
|
|
49
|
+
"--consent=relay",
|
|
50
|
+
] as const;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function executableStartStatus(targetIdentity: string): Record<string, unknown> {
|
|
54
|
+
const status = unrelatedStatus(targetIdentity);
|
|
55
|
+
const tokens = startTransitionTokens(targetIdentity);
|
|
56
|
+
status.next_transition = {
|
|
57
|
+
kind: "execute",
|
|
58
|
+
reason_code: "review_start_required",
|
|
59
|
+
execute: {
|
|
60
|
+
operation: "review.start",
|
|
61
|
+
arguments: tokens.map((token) => {
|
|
62
|
+
const separator = token.indexOf("=");
|
|
63
|
+
return { name: token.slice(2, separator), value: token.slice(separator + 1), token };
|
|
64
|
+
}),
|
|
65
|
+
preconditions: [],
|
|
66
|
+
binding: { target_identity: targetIdentity },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
return status;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter: ExecFileAdapter; calls: Array<readonly string[]> } {
|
|
43
|
-
|
|
73
|
+
// V216 no longer probes capabilities before negotiated STATUS. Keep the
|
|
74
|
+
// historical queue construction below, but discard only its obsolete probe
|
|
75
|
+
// response so every remaining entry binds the current invocation.
|
|
76
|
+
const queue = outputs.filter((body) => body.schema !== "gentle-ai.review-integration.capabilities/v2");
|
|
44
77
|
const calls: Array<readonly string[]> = [];
|
|
45
78
|
return {
|
|
46
79
|
calls,
|
|
@@ -54,61 +87,85 @@ function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter:
|
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
57
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
58
90
|
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
|
|
59
91
|
}
|
|
60
92
|
|
|
61
|
-
|
|
93
|
+
function runtimeClient(adapter: ExecFileAdapter): RuntimeNativeReviewCliV216 {
|
|
94
|
+
return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("negotiated ordinary START executes the complete STATUS-rendered relay vector and preserves the consent envelope", async () => {
|
|
62
98
|
const consent = fixture<Record<string, unknown>>("consent.fixture.json");
|
|
63
99
|
const target = String(consent.target_identity);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
assert.equal(queue.calls.some((arguments_) => arguments_[1] === "status"), true);
|
|
100
|
+
const tokens = startTransitionTokens(target);
|
|
101
|
+
for (const createClient of [client, runtimeClient]) {
|
|
102
|
+
const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
|
|
103
|
+
await assert.rejects(
|
|
104
|
+
() => createClient(queue.adapter).start({ cwd: "/repo" }),
|
|
105
|
+
(error: unknown) => {
|
|
106
|
+
assert.equal((error as { name?: string }).name, "NativeReviewConsentRequiredError");
|
|
107
|
+
const required = error as { consent: { raw: Record<string, unknown>; targetIdentity: string } };
|
|
108
|
+
assert.deepEqual(required.consent.raw, consent);
|
|
109
|
+
assert.equal(required.consent.targetIdentity, target);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
assert.deepEqual(queue.calls, [
|
|
114
|
+
["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi", "--next-transition"],
|
|
115
|
+
["review", "start", ...tokens],
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
83
118
|
});
|
|
84
119
|
|
|
85
|
-
test("controller-prebound START target
|
|
120
|
+
test("controller-prebound START target checks STATUS once and executes its exact matching transition", async () => {
|
|
86
121
|
const consent = fixture<Record<string, unknown>>("consent.fixture.json");
|
|
87
122
|
const target = String(consent.target_identity);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
const tokens = startTransitionTokens(target);
|
|
124
|
+
for (const createClient of [client, runtimeClient]) {
|
|
125
|
+
const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
|
|
126
|
+
await assert.rejects(
|
|
127
|
+
() => createClient(queue.adapter).start({ cwd: "/repo", targetIdentity: target, projection: "workspace" }),
|
|
128
|
+
(error: unknown) => (error as { name?: string }).name === "NativeReviewConsentRequiredError",
|
|
129
|
+
);
|
|
130
|
+
assert.equal(queue.calls.filter((arguments_) => arguments_[1] === "status").length, 1, "a prebound target is a drift check, never a STATUS bypass");
|
|
131
|
+
assert.deepEqual(queue.calls.at(-1), ["review", "start", ...tokens]);
|
|
132
|
+
}
|
|
98
133
|
});
|
|
99
134
|
|
|
100
|
-
test("consent follow-up executes the provider-named invocation exactly once and refuses
|
|
135
|
+
test("consent follow-up executes the provider-named invocation exactly once and refuses changed lineage or target bindings", async () => {
|
|
101
136
|
const consent = fixture<ReviewConsentV2 extends never ? never : Record<string, unknown>>("consent.fixture.json");
|
|
102
137
|
const decodedConsent = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(consent);
|
|
103
|
-
const
|
|
104
|
-
|
|
138
|
+
const preboundLineage = "Review.Lineage_42";
|
|
139
|
+
for (const choice of decodedConsent.choices) {
|
|
140
|
+
choice.invocation = choice.invocation.replace("review-consent-fixture", preboundLineage);
|
|
141
|
+
}
|
|
142
|
+
const started = fixture<Record<string, unknown>>("start.fixture.json");
|
|
143
|
+
started.lineage_id = preboundLineage;
|
|
144
|
+
started.artifact_subjects = [];
|
|
105
145
|
const queue = queuedAdapter([capabilities(), started]);
|
|
106
146
|
const native = client(queue.adapter);
|
|
107
147
|
const result = await native.answerConsent!({ cwd: "/repo", consent: decodedConsent, answer: "granted" });
|
|
108
148
|
assert.equal(result.kind, "started");
|
|
149
|
+
assert.ok(result.kind === "started");
|
|
150
|
+
assert.equal(result.start.lineageId, preboundLineage);
|
|
109
151
|
assert.deepEqual(queue.calls.at(-1), decodedConsent.choices[0].invocation.split(" ").slice(1));
|
|
110
152
|
assert.equal(queue.calls.filter((arguments_) => arguments_.includes("granted")).length, 1);
|
|
111
153
|
|
|
154
|
+
const runtimeQueue = queuedAdapter([capabilities(), structuredClone(started)]);
|
|
155
|
+
const runtimeResult = await runtimeClient(runtimeQueue.adapter).answerConsent({ cwd: "/repo", consent: decodedConsent, answer: "granted" });
|
|
156
|
+
assert.equal(runtimeResult.kind, "started");
|
|
157
|
+
assert.ok(runtimeResult.kind === "started");
|
|
158
|
+
assert.equal(runtimeResult.start.lineageId, preboundLineage);
|
|
159
|
+
assert.equal(runtimeQueue.calls.filter((arguments_) => arguments_.includes("granted")).length, 1);
|
|
160
|
+
|
|
161
|
+
const changedLineage = JSON.parse(JSON.stringify(started).replaceAll(preboundLineage, "different-lineage")) as Record<string, unknown>;
|
|
162
|
+
const lineageQueue = queuedAdapter([capabilities(), changedLineage]);
|
|
163
|
+
await assert.rejects(
|
|
164
|
+
() => client(lineageQueue.adapter).answerConsent!({ cwd: "/repo", consent: decodedConsent, answer: "granted" }),
|
|
165
|
+
/lineage mismatch/,
|
|
166
|
+
);
|
|
167
|
+
assert.equal(lineageQueue.calls.filter((arguments_) => arguments_[0] === "review" && arguments_[1] === "start").length, 1);
|
|
168
|
+
|
|
112
169
|
const changed = structuredClone(decodedConsent);
|
|
113
170
|
changed.targetIdentity = `sha256:${"b".repeat(64)}`;
|
|
114
171
|
await assert.rejects(
|
|
@@ -177,6 +234,68 @@ test("every consent invocation binding guard reports its own reason without laun
|
|
|
177
234
|
}
|
|
178
235
|
});
|
|
179
236
|
|
|
237
|
+
test("fresh consent START accepts the strictly decoded provider-created lineage and rejects a mismatched target", async () => {
|
|
238
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
239
|
+
const freshConsent = structuredClone(decoded);
|
|
240
|
+
for (const choice of freshConsent.choices) {
|
|
241
|
+
choice.invocation = choice.invocation.replace(" --lineage review-consent-fixture", "");
|
|
242
|
+
}
|
|
243
|
+
const freshStart = JSON.parse(JSON.stringify(fixture<Record<string, unknown>>("start.fixture.json"))
|
|
244
|
+
.replaceAll("review-start-fixture", "provider-created-lineage")) as Record<string, unknown>;
|
|
245
|
+
const queue = queuedAdapter([capabilities(), freshStart]);
|
|
246
|
+
const result = await client(queue.adapter).answerConsent!({ cwd: "/repo", consent: freshConsent, answer: "granted" });
|
|
247
|
+
assert.equal(result.kind, "started");
|
|
248
|
+
assert.ok(result.kind === "started");
|
|
249
|
+
assert.equal(result.start.lineageId, "provider-created-lineage");
|
|
250
|
+
assert.equal(queue.calls.filter((arguments_) => arguments_[0] === "review" && arguments_[1] === "start").length, 1);
|
|
251
|
+
assert.equal(queue.calls.at(-1)?.includes("--lineage"), false);
|
|
252
|
+
|
|
253
|
+
const runtimeQueue = queuedAdapter([capabilities(), structuredClone(freshStart)]);
|
|
254
|
+
const runtimeResult = await runtimeClient(runtimeQueue.adapter).answerConsent({ cwd: "/repo", consent: freshConsent, answer: "granted" });
|
|
255
|
+
assert.equal(runtimeResult.kind, "started");
|
|
256
|
+
assert.ok(runtimeResult.kind === "started");
|
|
257
|
+
assert.equal(runtimeResult.start.lineageId, "provider-created-lineage");
|
|
258
|
+
assert.equal(runtimeQueue.calls.filter((arguments_) => arguments_[0] === "review" && arguments_[1] === "start").length, 1);
|
|
259
|
+
assert.equal(runtimeQueue.calls.at(-1)?.includes("--lineage"), false);
|
|
260
|
+
|
|
261
|
+
const mismatchedTarget = structuredClone(freshStart);
|
|
262
|
+
((mismatchedTarget.repository_context as Record<string, unknown>).target_identity as string) = `sha256:${"b".repeat(64)}`;
|
|
263
|
+
const mismatchQueue = queuedAdapter([capabilities(), mismatchedTarget]);
|
|
264
|
+
await assert.rejects(
|
|
265
|
+
() => client(mismatchQueue.adapter).answerConsent!({ cwd: "/repo", consent: freshConsent, answer: "granted" }),
|
|
266
|
+
/target mismatch/,
|
|
267
|
+
);
|
|
268
|
+
assert.equal(mismatchQueue.calls.filter((arguments_) => arguments_[0] === "review" && arguments_[1] === "start").length, 1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("duplicate, empty, missing, and malformed consent lineages fail before provider launch", async () => {
|
|
272
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
273
|
+
const drifted = (replace: (invocation: string) => string): ReviewConsentV2 => {
|
|
274
|
+
const consent = structuredClone(decoded);
|
|
275
|
+
const choice = consent.choices.find((candidate) => candidate.answer === "granted") as { invocation: string };
|
|
276
|
+
choice.invocation = replace(choice.invocation);
|
|
277
|
+
return consent;
|
|
278
|
+
};
|
|
279
|
+
const cases = [
|
|
280
|
+
{ label: "duplicate", consent: drifted((value) => `${value} --lineage duplicate-lineage`) },
|
|
281
|
+
{ label: "empty", consent: drifted((value) => value.replace("--lineage review-consent-fixture", "--lineage=")) },
|
|
282
|
+
{ label: "missing", consent: drifted((value) => value.replace("--lineage review-consent-fixture", "--lineage")) },
|
|
283
|
+
{ label: "malformed", consent: drifted((value) => value.replace("--lineage review-consent-fixture", "--lineage invalid/lineage")) },
|
|
284
|
+
] as const;
|
|
285
|
+
for (const scenario of cases) {
|
|
286
|
+
const queue = queuedAdapter([]);
|
|
287
|
+
await assert.rejects(
|
|
288
|
+
() => client(queue.adapter).answerConsent!({ cwd: "/repo", consent: scenario.consent, answer: "granted" }),
|
|
289
|
+
(error: unknown) => {
|
|
290
|
+
assert.ok(error instanceof NativeReviewConsentBindingError, `${scenario.label} lineage must be a typed binding error`);
|
|
291
|
+
assert.equal(error.reason, "consent-invocation-option-invalid");
|
|
292
|
+
return true;
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
assert.deepEqual(queue.calls, [], `${scenario.label} lineage must not launch the provider`);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
180
299
|
test("declined consent decodes the provider's explicit empty authority fields without creating a lineage", async () => {
|
|
181
300
|
const rawConsent = fixture<Record<string, unknown>>("consent.fixture.json");
|
|
182
301
|
const consent = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(rawConsent);
|
|
@@ -203,3 +322,129 @@ test("declined consent decodes the provider's explicit empty authority fields wi
|
|
|
203
322
|
assert.equal("lineageId" in result, false);
|
|
204
323
|
assert.deepEqual(queue.calls.at(-1), consent.choices[1].invocation.split(" ").slice(1));
|
|
205
324
|
});
|
|
325
|
+
|
|
326
|
+
// --- consent/v3 (dev-binary field-test lane) ---
|
|
327
|
+
//
|
|
328
|
+
// Fixture provenance: consent-v3.captured.json, start-v3-consent-granted
|
|
329
|
+
// .captured.json, and start-v3-consent-declined.captured.json were captured
|
|
330
|
+
// 2026-08-16 from the real binary at /home/gentleman/.cargo/bin/gentle-ai
|
|
331
|
+
// reporting "gentle-ai 2.4.0-main.b1afef46"; full capture commands are
|
|
332
|
+
// documented in tests/review-integration-v2-forward.test.ts.
|
|
333
|
+
|
|
334
|
+
const devbinaryFixtureRoot = join(process.cwd(), "tests", "fixtures", "devbinary");
|
|
335
|
+
const devbinaryFixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(devbinaryFixtureRoot, name), "utf8")) as T;
|
|
336
|
+
|
|
337
|
+
function piConsent(rewriteInvocation: (invocation: string) => string = (invocation) => invocation): Record<string, unknown> {
|
|
338
|
+
const consent = devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json");
|
|
339
|
+
consent.agent = "pi";
|
|
340
|
+
for (const choice of consent.choices as Array<Record<string, unknown>>) {
|
|
341
|
+
choice.invocation = rewriteInvocation(String(choice.invocation));
|
|
342
|
+
}
|
|
343
|
+
return consent;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function piBoundConsent(): Record<string, unknown> {
|
|
347
|
+
return piConsent((invocation) => invocation.replace(" --consent ", " --agent pi --consent "));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
test("Pi START rejects a Pi-bound consent/v3 envelope when any provider choice omits or misbinds the Pi agent", async () => {
|
|
351
|
+
const target = String(devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json").target_identity);
|
|
352
|
+
const tokens = startTransitionTokens(target);
|
|
353
|
+
const invalidUnselectedChoice = (replace: (invocation: string) => string): Record<string, unknown> => {
|
|
354
|
+
const consent = piBoundConsent();
|
|
355
|
+
const choice = (consent.choices as Array<Record<string, unknown>>).find((candidate) => candidate.answer === "declined")!;
|
|
356
|
+
choice.invocation = replace(String(choice.invocation));
|
|
357
|
+
return consent;
|
|
358
|
+
};
|
|
359
|
+
const cases = [
|
|
360
|
+
["missing", piConsent()],
|
|
361
|
+
["embedded", piConsent((invocation) => invocation.replace(" --consent ", " --note=--agent\\ pi --consent "))],
|
|
362
|
+
["duplicate", invalidUnselectedChoice((invocation) => invocation.replace("--agent pi", "--agent pi --agent pi"))],
|
|
363
|
+
["conflicting", invalidUnselectedChoice((invocation) => invocation.replace("--agent pi", "--agent pi --agent claude-code"))],
|
|
364
|
+
] as const;
|
|
365
|
+
for (const [label, consent] of cases) {
|
|
366
|
+
for (const createClient of [client, runtimeClient]) {
|
|
367
|
+
const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
|
|
368
|
+
await assert.rejects(
|
|
369
|
+
() => createClient(queue.adapter).start({ cwd: "/repo" }),
|
|
370
|
+
(error: unknown) => {
|
|
371
|
+
assert.equal((error as { name?: string }).name, "NativeReviewCliError", `${label} binding must fail closed before consent presentation`);
|
|
372
|
+
assert.equal((error as { code?: string }).code, "schema-incompatible");
|
|
373
|
+
return true;
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
assert.deepEqual(queue.calls.at(-1), ["review", "start", ...tokens]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("Pi START rejects a foreign consent/v3 envelope and only relays exact Pi-bound choice invocations", async () => {
|
|
382
|
+
const consent = devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json");
|
|
383
|
+
const target = String(consent.target_identity);
|
|
384
|
+
const tokens = startTransitionTokens(target);
|
|
385
|
+
for (const createClient of [client, runtimeClient]) {
|
|
386
|
+
const foreignQueue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
|
|
387
|
+
await assert.rejects(
|
|
388
|
+
() => createClient(foreignQueue.adapter).start({ cwd: "/repo" }),
|
|
389
|
+
(error: unknown) => {
|
|
390
|
+
assert.equal((error as { name?: string }).name, "NativeReviewCliError");
|
|
391
|
+
assert.equal((error as { code?: string }).code, "schema-incompatible");
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
assert.deepEqual(foreignQueue.calls.at(-1), ["review", "start", ...tokens]);
|
|
396
|
+
for (const piBound of [
|
|
397
|
+
piBoundConsent(),
|
|
398
|
+
piConsent((invocation) => invocation.replace(" --consent ", " --agent=pi --consent ")),
|
|
399
|
+
]) {
|
|
400
|
+
const piQueue = queuedAdapter([capabilities(), executableStartStatus(target), piBound]);
|
|
401
|
+
await assert.rejects(
|
|
402
|
+
() => createClient(piQueue.adapter).start({ cwd: "/repo" }),
|
|
403
|
+
(error: unknown) => {
|
|
404
|
+
assert.equal((error as { name?: string }).name, "NativeReviewConsentRequiredError");
|
|
405
|
+
const required = error as { consent: { raw: Record<string, unknown>; schema: string; targetIdentity: string; agent?: string } };
|
|
406
|
+
assert.deepEqual(required.consent.raw, piBound);
|
|
407
|
+
assert.equal(required.consent.schema, "gentle-ai.review-integration.consent/v3");
|
|
408
|
+
assert.equal(required.consent.targetIdentity, target);
|
|
409
|
+
assert.equal(required.consent.agent, "pi");
|
|
410
|
+
return true;
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
assert.deepEqual(piQueue.calls.at(-1), ["review", "start", ...tokens]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("a granted Pi consent/v3 answer replays the captured exact agent-bound invocation", async () => {
|
|
419
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(piBoundConsent(), "pi");
|
|
420
|
+
const cwd = decoded.choices[0].invocation.split(" --cwd ")[1]!.split(" ")[0]!;
|
|
421
|
+
const started = devbinaryFixture<Record<string, unknown>>("start-v3-consent-granted.captured.json");
|
|
422
|
+
const queue = queuedAdapter([capabilities(), started]);
|
|
423
|
+
const result = await client(queue.adapter).answerConsent!({ cwd, consent: decoded, answer: "granted" });
|
|
424
|
+
assert.equal(result.kind, "started");
|
|
425
|
+
assert.ok(result.kind === "started");
|
|
426
|
+
assert.equal(result.start.lineageId, "review-377c60e10b852cfc");
|
|
427
|
+
assert.equal(result.start.selectedLenses.length, 4);
|
|
428
|
+
// The follow-up runs the provider-owned invocation verbatim, exactly once.
|
|
429
|
+
assert.deepEqual(queue.calls.at(-1), decoded.choices[0].invocation.split(" ").slice(1));
|
|
430
|
+
assert.equal(queue.calls.filter((arguments_) => arguments_.includes("--agent") && arguments_.includes("pi")).length, 1);
|
|
431
|
+
assert.equal(queue.calls.filter((arguments_) => arguments_.includes("granted")).length, 1);
|
|
432
|
+
|
|
433
|
+
const runtimeQueue = queuedAdapter([capabilities(), structuredClone(started)]);
|
|
434
|
+
const runtimeResult = await runtimeClient(runtimeQueue.adapter).answerConsent({ cwd, consent: decoded, answer: "granted" });
|
|
435
|
+
assert.equal(runtimeResult.kind, "started");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("a declined Pi consent/v3 answer replays the captured exact agent-bound invocation and creates no authority", async () => {
|
|
439
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(piBoundConsent(), "pi");
|
|
440
|
+
const cwd = decoded.choices[1].invocation.split(" --cwd ")[1]!.split(" ")[0]!;
|
|
441
|
+
const declined = devbinaryFixture<Record<string, unknown>>("start-v3-consent-declined.captured.json");
|
|
442
|
+
const queue = queuedAdapter([capabilities(), declined]);
|
|
443
|
+
const result = await client(queue.adapter).answerConsent!({ cwd, consent: decoded, answer: "declined" });
|
|
444
|
+
assert.equal(result.kind, "declined");
|
|
445
|
+
assert.ok(result.kind === "declined");
|
|
446
|
+
assert.equal(result.consent, "declined_this_candidate");
|
|
447
|
+
assert.equal(result.targetIdentity, decoded.targetIdentity);
|
|
448
|
+
assert.deepEqual(queue.calls.at(-1), decoded.choices[1].invocation.split(" ").slice(1));
|
|
449
|
+
assert.equal(queue.calls.filter((arguments_) => arguments_.includes("--agent") && arguments_.includes("pi")).length, 1);
|
|
450
|
+
});
|