gentle-pi 1.0.2 → 1.0.4

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 (76) hide show
  1. package/README.md +65 -12
  2. package/assets/agents/jd-judge-a.md +1 -1
  3. package/assets/agents/jd-judge-b.md +1 -1
  4. package/assets/agents/review-readability.md +1 -1
  5. package/assets/agents/review-refuter.md +17 -9
  6. package/assets/agents/review-reliability.md +1 -1
  7. package/assets/agents/review-resilience.md +1 -1
  8. package/assets/agents/review-risk.md +1 -1
  9. package/assets/agents/review-validator.md +3 -3
  10. package/assets/agents/sdd-apply.md +7 -1
  11. package/assets/agents/sdd-status.md +7 -4
  12. package/assets/agents/sdd-tasks.md +11 -0
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/migrations/managed-assets-v0.10.7.json +7 -0
  15. package/assets/orchestrator-delegation.md +4 -4
  16. package/assets/orchestrator.md +1 -1
  17. package/assets/support/sdd-status-contract.md +14 -4
  18. package/extensions/gentle-ai.ts +1772 -148
  19. package/lib/gentle-ai-binary.ts +89 -0
  20. package/lib/native-review-cli.ts +552 -0
  21. package/lib/review-authority-supersession.ts +1055 -0
  22. package/lib/review-candidate-view.ts +546 -0
  23. package/lib/review-compact-contract.ts +94 -7
  24. package/lib/review-compact-gate.ts +88 -4
  25. package/lib/review-compact-store.ts +24 -3
  26. package/lib/review-facade.ts +175 -24
  27. package/lib/review-refuter-adapter.ts +129 -0
  28. package/lib/review-snapshot.ts +42 -0
  29. package/lib/sdd-preflight.ts +9 -5
  30. package/lib/sdd-status.ts +197 -39
  31. package/package.json +2 -1
  32. package/scripts/gentle-ai-installer.mjs +225 -0
  33. package/scripts/install-gentle-ai.mjs +14 -0
  34. package/scripts/verify-package-files.mjs +6 -0
  35. package/skills/_shared/review-ledger-contract.md +9 -7
  36. package/skills/gentle-ai/SKILL.md +8 -6
  37. package/skills/judgment-day/SKILL.md +1 -1
  38. package/skills/judgment-day/references/prompts-and-formats.md +1 -1
  39. package/tests/artifact-language.test.ts +20 -0
  40. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +25 -0
  41. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +8 -0
  42. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +139 -0
  43. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +200 -0
  44. package/tests/fixtures/native-review-cli/v2.1.2/start.json +12 -0
  45. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +24 -0
  46. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +20 -0
  47. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +28 -0
  48. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +25 -0
  49. package/tests/fixtures/native-review-cli/v2.1.3/finalize.json +8 -0
  50. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +139 -0
  51. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +200 -0
  52. package/tests/fixtures/native-review-cli/v2.1.3/start.json +14 -0
  53. package/tests/fixtures/native-review-cli/v2.1.3/validate-allow.json +24 -0
  54. package/tests/fixtures/native-review-cli/v2.1.3/validate-deny-empty-context.json +20 -0
  55. package/tests/fixtures/native-review-cli/v2.1.3/validate-deny.json +28 -0
  56. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +132 -0
  57. package/tests/gentle-ai-binary.test.ts +148 -0
  58. package/tests/gentle-ai-installer.test.ts +219 -0
  59. package/tests/native-review-cli.test.ts +675 -0
  60. package/tests/native-review-parity-runtime.test.ts +170 -0
  61. package/tests/package-manifest.test.ts +27 -2
  62. package/tests/review-authority-recovery-docs.test.ts +25 -0
  63. package/tests/review-authority-supersession.test.ts +581 -0
  64. package/tests/review-candidate-view.test.ts +288 -0
  65. package/tests/review-compact-contract.test.ts +36 -4
  66. package/tests/review-compact-gate.test.ts +236 -7
  67. package/tests/review-compact-store.test.ts +11 -0
  68. package/tests/review-controller-native-routing.test.ts +2280 -0
  69. package/tests/review-controller.test.ts +342 -28
  70. package/tests/review-facade.test.ts +129 -19
  71. package/tests/review-ledger-contract.test.ts +58 -5
  72. package/tests/review-refuter-adapter.test.ts +89 -0
  73. package/tests/review-snapshot.test.ts +25 -0
  74. package/tests/runtime-harness.mjs +23 -0
  75. package/tests/sdd-preflight.test.ts +78 -0
  76. package/tests/sdd-status.test.ts +127 -6
@@ -0,0 +1,552 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { isAbsolute, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ export const NATIVE_REVIEW_OPERATION = {
11
+ VERSION: "version",
12
+ START: "review/start",
13
+ FINALIZE: "review/finalize",
14
+ VALIDATE: "review/validate",
15
+ BIND_SDD: "review/bind-sdd",
16
+ SDD_STATUS: "sdd-status",
17
+ } as const;
18
+ export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
19
+
20
+ export const NATIVE_REVIEW_ERROR_CODE = {
21
+ UNAVAILABLE: "unavailable",
22
+ TIMEOUT: "timeout",
23
+ NON_ZERO: "non-zero",
24
+ SIGNAL: "signal",
25
+ UNEXPECTED_STDERR: "unexpected-stderr",
26
+ OUTPUT_LIMIT: "output-limit",
27
+ EMPTY_OUTPUT: "empty-output",
28
+ MALFORMED_JSON: "malformed-json",
29
+ SCHEMA_INCOMPATIBLE: "schema-incompatible",
30
+ IDENTITY_MISMATCH: "identity-mismatch",
31
+ VERSION_INCOMPATIBLE: "version-incompatible",
32
+ CANCELLED: "cancelled",
33
+ PACKAGE_BINARY_MISSING: "package-local-binary-missing",
34
+ } as const;
35
+ export type NativeReviewErrorCode = (typeof NATIVE_REVIEW_ERROR_CODE)[keyof typeof NATIVE_REVIEW_ERROR_CODE];
36
+
37
+ export interface ExecFileRequest { file: string; arguments: readonly string[]; cwd: string; timeoutMs: number; maxBufferBytes: number; signal?: AbortSignal; }
38
+ export interface ExecFileResult { stdout: string; stderr: string; exitCode: number; signal: NodeJS.Signals | null; timedOut: boolean; outputLimitExceeded: boolean; }
39
+ export type ExecFileAdapter = (request: ExecFileRequest) => Promise<ExecFileResult>;
40
+
41
+ export const NATIVE_SDD_ARTIFACT_STORE = {
42
+ OPENSPEC: "openspec",
43
+ ENGRAM: "engram",
44
+ NONE: "none",
45
+ } as const;
46
+ export type NativeSddArtifactStore = (typeof NATIVE_SDD_ARTIFACT_STORE)[keyof typeof NATIVE_SDD_ARTIFACT_STORE];
47
+
48
+ export const NATIVE_SDD_ARTIFACT_STATE = {
49
+ MISSING: "missing",
50
+ DONE: "done",
51
+ PARTIAL: "partial",
52
+ } as const;
53
+ export type NativeSddArtifactState = (typeof NATIVE_SDD_ARTIFACT_STATE)[keyof typeof NATIVE_SDD_ARTIFACT_STATE];
54
+
55
+ export interface NativeSddArtifactStates {
56
+ proposal: NativeSddArtifactState;
57
+ specs: NativeSddArtifactState;
58
+ design: NativeSddArtifactState;
59
+ tasks: NativeSddArtifactState;
60
+ applyProgress: NativeSddArtifactState;
61
+ verifyReport: NativeSddArtifactState;
62
+ reviewPolicy?: NativeSddArtifactState;
63
+ reviewLedger: NativeSddArtifactState;
64
+ reviewReceipt: NativeSddArtifactState;
65
+ reviewBundle: NativeSddArtifactState;
66
+ reviewContext: NativeSddArtifactState;
67
+ reviewState: NativeSddArtifactState;
68
+ }
69
+
70
+ export interface NativeReviewCli {
71
+ start(request: NativeStartRequest): Promise<NativeStartResult>;
72
+ finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult>;
73
+ validate(request: NativeValidateRequest): Promise<NativeValidateResult>;
74
+ bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult>;
75
+ sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult>;
76
+ }
77
+
78
+ export interface NativeStartRequest { cwd: string; baseRef?: string; lineageId?: string; policyPath?: string; focus?: string; signal?: AbortSignal; }
79
+ export interface NativeFinalizeLensResult { lens: string; document: unknown; }
80
+ export interface NativeFinalizeRequest {
81
+ cwd: string;
82
+ lineageId?: string;
83
+ resultFiles?: readonly string[];
84
+ lensResults?: readonly NativeFinalizeLensResult[];
85
+ refuterFile?: string;
86
+ refuterDocument?: unknown;
87
+ correctionLines?: number;
88
+ validationFile?: string;
89
+ validationDocument?: unknown;
90
+ evidenceFile?: string;
91
+ evidenceDocument?: string;
92
+ failed?: boolean;
93
+ signal?: AbortSignal;
94
+ }
95
+ export interface NativeValidateRequest { cwd: string; gate: string; lineageId?: string; flags?: readonly string[]; signal?: AbortSignal; }
96
+ export interface NativeBindSddRequest { cwd: string; change: string; lineage: string; expectedBindingRevision: string; signal?: AbortSignal; }
97
+ export interface NativeSddStatusRequest { cwd: string; change: string; signal?: AbortSignal; }
98
+ export interface NativeGateContext { lineageId: string; storeRevision: string; raw: Record<string, unknown>; }
99
+ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } as const;
100
+ export type NativeStartAction = (typeof NATIVE_START_ACTION)[keyof typeof NATIVE_START_ACTION];
101
+ export interface NativeStartResult { lineageId: string; state: "reviewing" | "correction_required" | "validating" | "approved" | "escalated"; riskLevel: string; selectedLenses: readonly string[]; changedFiles: number; changedLines: number; correctionBudget: number; action: NativeStartAction; lensesRequired: boolean; }
102
+ export interface NativeValidateResult { allowed: boolean; result: "allow" | "scope-changed" | "invalidated" | "escalated"; action: string; reason: string; gateContext: NativeGateContext; }
103
+ export interface NativeFinalizeResult { lineageId: string; state: string; action: string; storeRevision: string; receiptPath?: string; }
104
+ export interface NativeBindSddResult {
105
+ revision: string;
106
+ change: string;
107
+ lineage: string;
108
+ authorityRevision: string;
109
+ receiptHash: string;
110
+ gateContext: NativeGateContext;
111
+ }
112
+ export interface NativeSddStatusResult {
113
+ ready: boolean;
114
+ artifactStore: NativeSddArtifactStore;
115
+ artifacts: NativeSddArtifactStates;
116
+ nextRecommended: string;
117
+ [key: string]: unknown;
118
+ }
119
+
120
+ export function isCanonicalProcessString(value: unknown): value is string {
121
+ return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
122
+ }
123
+
124
+ const NATIVE_RISK_LEVEL = ["low", "medium", "high"] as const;
125
+ const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
126
+ const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] as const;
127
+ const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
128
+ const NATIVE_GATE_RESULT = ["allow", "scope-changed", "invalidated", "escalated"] as const;
129
+ const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] as const;
130
+ const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
131
+ const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] as const;
132
+
133
+ export const NATIVE_CLI_CONTRACTS = Object.freeze({
134
+ "2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: false, inventory: false }),
135
+ });
136
+
137
+ export class NativeReviewCliError extends Error {
138
+ readonly code: NativeReviewErrorCode;
139
+ readonly operation: NativeReviewOperation;
140
+ readonly launchAttempted: boolean;
141
+ readonly mutating: boolean;
142
+ readonly mutationOutcome: "none" | "unknown";
143
+ readonly nextAction?: "replay-exact-native-operation";
144
+ constructor(code: NativeReviewErrorCode, operation: NativeReviewOperation, launchAttempted: boolean, mutating: boolean, message: string) {
145
+ super(message);
146
+ this.name = "NativeReviewCliError";
147
+ this.code = code;
148
+ this.operation = operation;
149
+ this.launchAttempted = launchAttempted;
150
+ this.mutating = mutating;
151
+ this.mutationOutcome = launchAttempted && mutating ? "unknown" : "none";
152
+ this.nextAction = this.mutationOutcome === "unknown" ? "replay-exact-native-operation" : undefined;
153
+ }
154
+ }
155
+
156
+ export function createNodeExecFileAdapter(): ExecFileAdapter {
157
+ return async (request) => {
158
+ try {
159
+ const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal });
160
+ return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
161
+ } catch (error) {
162
+ const detail = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number; signal?: NodeJS.Signals; killed?: boolean };
163
+ if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
164
+ return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: detail.killed === true, outputLimitExceeded: detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" };
165
+ }
166
+ };
167
+ }
168
+
169
+ function object(value: unknown): Record<string, unknown> {
170
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("expected object");
171
+ return value as Record<string, unknown>;
172
+ }
173
+ function exactObject(value: unknown, required: readonly string[], optional: readonly string[] = []): Record<string, unknown> {
174
+ const parsed = object(value);
175
+ const allowed = [...required, ...optional];
176
+ if (required.some((key) => !(key in parsed)) || Object.keys(parsed).some((key) => !allowed.includes(key))) throw new Error("unexpected object shape");
177
+ return parsed;
178
+ }
179
+ function requiredString(value: unknown): string { if (typeof value !== "string" || value.length === 0) throw new Error("expected string"); return value; }
180
+ function stringValue(value: unknown): string { if (typeof value !== "string") throw new Error("expected string"); return value; }
181
+ function booleanValue(value: unknown): boolean { if (typeof value !== "boolean") throw new Error("expected boolean"); return value; }
182
+ function nonNegativeInteger(value: unknown): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("expected safe non-negative integer"); return value; }
183
+ function stringArray(value: unknown): readonly string[] { if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) throw new Error("expected string array"); return value; }
184
+ function decodeSelectedLenses(value: unknown, riskLevel: string, lensesRequired: boolean): readonly string[] {
185
+ if (value === null && riskLevel === "low" && !lensesRequired) return [];
186
+ return stringArray(value);
187
+ }
188
+ function enumString(value: unknown, allowed: readonly string[]): string { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
189
+ function parseJson(stdout: string, operation: NativeReviewOperation, mutating: boolean): Record<string, unknown> {
190
+ if (stdout.length === 0) throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT, operation, true, mutating, "native command returned empty output");
191
+ try { return object(JSON.parse(stdout)); } catch { throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON, operation, true, mutating, "native command returned malformed JSON"); }
192
+ }
193
+ function decode<T>(operation: NativeReviewOperation, mutating: boolean, callback: () => T): T {
194
+ try { return callback(); } catch (error) { if (error instanceof NativeReviewCliError) throw error; throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, operation, true, mutating, "native response is schema incompatible"); }
195
+ }
196
+ function decodeReleaseEvidence(value: unknown): void {
197
+ const release = exactObject(value, ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "publication_state", "evidence_freshness_hash", "evidence_freshness_state"]);
198
+ for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
199
+ if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
200
+ }
201
+ function decodeGateContext(value: unknown): NativeGateContext {
202
+ const context = exactObject(
203
+ value,
204
+ ["gate", "lineage_id", "generation", "base_tree", "candidate_tree", "paths_digest", "fix_delta_hash", "policy_hash", "ledger_hash", "evidence_hash", "base_relationship_valid"],
205
+ ["store_revision", "genesis_revision", "chain_identity", "bundle_digest", "external_evidence", "base_advanced_compatible", "release", "pre_pr_boundary", "denial"],
206
+ );
207
+ const gate = stringValue(context.gate);
208
+ if (gate !== "" && !(NATIVE_GATE as readonly string[]).includes(gate)) throw new Error("invalid gate context gate");
209
+ for (const field of ["lineage_id", "base_tree", "candidate_tree", "paths_digest", "fix_delta_hash", "policy_hash", "ledger_hash", "evidence_hash"]) stringValue(context[field]);
210
+ for (const field of ["store_revision", "genesis_revision", "chain_identity", "bundle_digest"]) if (context[field] !== undefined) stringValue(context[field]);
211
+ nonNegativeInteger(context.generation);
212
+ booleanValue(context.base_relationship_valid);
213
+ if (context.external_evidence !== undefined) enumString(context.external_evidence, ["invalidating", "escalating"]);
214
+ if (context.denial !== undefined) {
215
+ const denial = exactObject(context.denial, ["stage", "code"]);
216
+ requiredString(denial.stage); requiredString(denial.code);
217
+ }
218
+ if (context.pre_pr_boundary !== undefined) {
219
+ const boundary = exactObject(context.pre_pr_boundary, ["source", "selector", "commit"], ["remote", "remote_ref", "remote_identity"]);
220
+ enumString(boundary.source, ["explicit", "publication-default"]); requiredString(boundary.selector); stringValue(boundary.commit);
221
+ for (const field of ["remote", "remote_ref", "remote_identity"]) if (boundary[field] !== undefined) requiredString(boundary[field]);
222
+ }
223
+ if (context.base_advanced_compatible !== undefined) {
224
+ const proof = exactObject(context.base_advanced_compatible, ["status", "compatible", "old_base_tree", "new_base_tree", "original_patch_identity", "delivered_patch_identity", "delivered_paths_digest", "base_advance_paths_digest", "paths_disjoint", "merged_result_tree", "ci_attestation_artifact_hash", "ci_attestation_issuer", "ci_status"]);
225
+ for (const field of ["status", "old_base_tree", "new_base_tree", "original_patch_identity", "delivered_patch_identity", "delivered_paths_digest", "base_advance_paths_digest", "merged_result_tree", "ci_attestation_artifact_hash", "ci_attestation_issuer", "ci_status"]) requiredString(proof[field]);
226
+ booleanValue(proof.compatible); booleanValue(proof.paths_disjoint);
227
+ }
228
+ if (context.release !== undefined) decodeReleaseEvidence(context.release);
229
+ return {
230
+ lineageId: stringValue(context.lineage_id),
231
+ storeRevision: context.store_revision === undefined ? "" : stringValue(context.store_revision),
232
+ raw: context,
233
+ };
234
+ }
235
+ function decodeSnapshot(value: unknown): void {
236
+ const snapshot = exactObject(value, ["kind", "base_tree", "candidate_tree", "paths_digest", "intended_untracked", "intended_untracked_proof", "paths", "identity"], ["ledger_ids"]);
237
+ enumString(snapshot.kind, ["current-changes", "base-diff", "commit-range", "fix-diff"]);
238
+ for (const field of ["base_tree", "candidate_tree", "paths_digest", "intended_untracked_proof", "identity"]) requiredString(snapshot[field]);
239
+ stringArray(snapshot.intended_untracked); stringArray(snapshot.paths);
240
+ if (snapshot.ledger_ids !== undefined) stringArray(snapshot.ledger_ids);
241
+ }
242
+ function decodeFinding(value: unknown): void {
243
+ const finding = exactObject(value, ["id"], ["lens", "location", "severity", "claim", "proof_refs"]);
244
+ requiredString(finding.id);
245
+ if (finding.lens !== undefined) enumString(finding.lens, ["risk", "resilience", "readability", "reliability"]);
246
+ if (finding.location !== undefined) stringValue(finding.location);
247
+ if (finding.severity !== undefined) enumString(finding.severity, ["BLOCKER", "CRITICAL", "WARNING", "SUGGESTION"]);
248
+ if (finding.claim !== undefined) stringValue(finding.claim);
249
+ if (finding.proof_refs !== undefined) stringArray(finding.proof_refs);
250
+ }
251
+ function decodeLensResult(value: unknown): void {
252
+ const result = exactObject(value, ["lens", "findings", "evidence", "result_hash"]);
253
+ enumString(result.lens, NATIVE_REVIEW_LENS);
254
+ if (!Array.isArray(result.findings)) throw new Error("invalid lens findings");
255
+ for (const finding of result.findings) decodeFinding(finding);
256
+ stringArray(result.evidence); requiredString(result.result_hash);
257
+ }
258
+ function decodeFindingEvidence(value: unknown): void {
259
+ const evidence = exactObject(value, ["finding_id", "class", "proof"], ["causal_disposition"]);
260
+ requiredString(evidence.finding_id); enumString(evidence.class, ["deterministic", "inferential", "insufficient"]); requiredString(evidence.proof);
261
+ if (evidence.causal_disposition !== undefined) enumString(evidence.causal_disposition, ["introduced", "behavior-activated", "worsened", "pre-existing", "base-only", "unknown"]);
262
+ }
263
+ function decodeValidationCheck(value: unknown): void {
264
+ const check = exactObject(value, ["evidence_hash", "fix_delta_hash", "passed"]);
265
+ requiredString(check.evidence_hash); requiredString(check.fix_delta_hash); booleanValue(check.passed);
266
+ }
267
+ function decodeReviewTransaction(value: unknown): void {
268
+ const transaction = exactObject(
269
+ value,
270
+ ["schema", "lineage_id", "mode", "generation", "state", "snapshot", "base_tree", "paths_digest", "initial_review_tree", "final_candidate_tree", "fix_delta_hash", "policy_hash", "ledger_hash", "ledger_findings_hash", "evidence_hash", "judge_proofs", "counters", "findings", "classifications", "outcomes", "fix_finding_ids", "pending_refuter_ids", "fix_caused_findings", "follow_ups"],
271
+ ["genesis_paths", "invalidation_reason", "judge_proof_hash", "judge_agreement_hash", "release", "failed_evidence_revision", "original_criteria", "correction_regression", "risk_level", "selected_lenses", "lens_results", "original_changed_lines", "correction_budget", "proposed_correction_lines", "actual_correction_lines"],
272
+ );
273
+ if (transaction.schema !== "gentle-ai.review-transaction/v1") throw new Error("invalid review transaction schema");
274
+ requiredString(transaction.lineage_id); enumString(transaction.mode, ["ordinary_4r", "ordinary_bounded", "judgment_day"]); nonNegativeInteger(transaction.generation);
275
+ enumString(transaction.state, ["unreviewed", "reviewing", "judges_confirmed", "findings_frozen", "evidence_classified", "fix_required", "fixing", "fix_validating", "ready_final_verification", "final_verifying", "approved", "escalated", "invalidated"]);
276
+ decodeSnapshot(transaction.snapshot);
277
+ for (const field of ["base_tree", "paths_digest", "initial_review_tree", "final_candidate_tree", "fix_delta_hash", "policy_hash", "ledger_hash", "ledger_findings_hash", "evidence_hash"]) stringValue(transaction[field]);
278
+ for (const field of ["genesis_paths", "fix_finding_ids", "pending_refuter_ids"]) if (transaction[field] !== undefined) stringArray(transaction[field]);
279
+ for (const field of ["invalidation_reason", "judge_proof_hash", "judge_agreement_hash", "failed_evidence_revision"]) if (transaction[field] !== undefined) requiredString(transaction[field]);
280
+ if (!Array.isArray(transaction.judge_proofs)) throw new Error("invalid judge proofs");
281
+ for (const proof of transaction.judge_proofs) {
282
+ const row = exactObject(proof, ["judge_id", "execution_hash", "result_hash", "blind", "confirmed"]);
283
+ requiredString(row.judge_id); requiredString(row.execution_hash); requiredString(row.result_hash); booleanValue(row.blind); booleanValue(row.confirmed);
284
+ }
285
+ const counters = exactObject(transaction.counters, ["full_reviews", "refuter_batches", "fix_batches", "scoped_fix_validations", "final_verifications", "fix_rounds", "scoped_rejudgments", "judge_executions"], ["risk_executions", "resilience_executions", "readability_executions", "reliability_executions"]);
286
+ for (const value of Object.values(counters)) nonNegativeInteger(value);
287
+ for (const field of ["findings", "fix_caused_findings"]) {
288
+ if (!Array.isArray(transaction[field])) throw new Error("invalid transaction findings");
289
+ for (const finding of transaction[field]) decodeFinding(finding);
290
+ }
291
+ const classifications = object(transaction.classifications);
292
+ for (const evidence of Object.values(classifications)) decodeFindingEvidence(evidence);
293
+ const outcomes = object(transaction.outcomes);
294
+ for (const outcome of Object.values(outcomes)) enumString(outcome, ["corroborated", "refuted", "inconclusive", "info"]);
295
+ if (!Array.isArray(transaction.follow_ups)) throw new Error("invalid follow-ups");
296
+ for (const followUp of transaction.follow_ups) {
297
+ const row = exactObject(followUp, ["observation", "proof_refs"]);
298
+ requiredString(row.observation); stringArray(row.proof_refs);
299
+ }
300
+ for (const field of ["original_criteria", "correction_regression"]) if (transaction[field] !== undefined) decodeValidationCheck(transaction[field]);
301
+ if (transaction.release !== undefined) decodeReleaseEvidence(transaction.release);
302
+ if (transaction.risk_level !== undefined) enumString(transaction.risk_level, NATIVE_RISK_LEVEL);
303
+ if (transaction.selected_lenses !== undefined) for (const lens of stringArray(transaction.selected_lenses)) enumString(lens, NATIVE_REVIEW_LENS);
304
+ if (transaction.lens_results !== undefined) {
305
+ if (!Array.isArray(transaction.lens_results)) throw new Error("invalid lens results");
306
+ for (const result of transaction.lens_results) decodeLensResult(result);
307
+ }
308
+ for (const field of ["original_changed_lines", "correction_budget", "proposed_correction_lines", "actual_correction_lines"]) if (transaction[field] !== undefined) nonNegativeInteger(transaction[field]);
309
+ }
310
+ function hasCanonicalSelectedLenses(riskLevel: string, selectedLenses: readonly string[]): boolean {
311
+ if (new Set(selectedLenses).size !== selectedLenses.length) return false;
312
+ if (riskLevel === "low") return selectedLenses.length === 0;
313
+ if (riskLevel === "medium") return selectedLenses.length === 1;
314
+ return selectedLenses.length === NATIVE_REVIEW_LENS.length
315
+ && NATIVE_REVIEW_LENS.every((lens) => selectedLenses.includes(lens));
316
+ }
317
+
318
+ function hasValidLensesRequired(action: NativeStartAction, state: string, riskLevel: string, lensesRequired: boolean): boolean {
319
+ if (riskLevel === "low") return !lensesRequired;
320
+ if (action === NATIVE_START_ACTION.CREATED) return state === "reviewing" && lensesRequired;
321
+ if (action === NATIVE_START_ACTION.RESUMED) return !lensesRequired;
322
+ if (action === NATIVE_START_ACTION.REUSE_RECEIPT) return state === "approved" && !lensesRequired;
323
+ return !lensesRequired;
324
+ }
325
+
326
+ function nativeError(code: NativeReviewErrorCode, operation: NativeReviewOperation, mutating: boolean, message: string): NativeReviewCliError {
327
+ return new NativeReviewCliError(code, operation, true, mutating, message);
328
+ }
329
+
330
+ interface NativeJsonExecution {
331
+ body: Record<string, unknown>;
332
+ exitCode: number;
333
+ }
334
+
335
+ export class NativeReviewCliV214 {
336
+ private readonly adapter: ExecFileAdapter;
337
+ private readonly executable: string | (() => string);
338
+ private readonly timeoutMs: number;
339
+ private readonly maxBufferBytes: number;
340
+ private readonly cleanupDirectory: (directory: string) => Promise<void>;
341
+ constructor(adapter: ExecFileAdapter, executable: string | (() => string) = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = 1024 * 1024, cleanupDirectory = (directory: string) => rm(directory, { recursive: true, force: true })) {
342
+ if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
343
+ this.adapter = adapter;
344
+ this.executable = executable;
345
+ this.timeoutMs = timeoutMs;
346
+ this.maxBufferBytes = maxBufferBytes;
347
+ this.cleanupDirectory = cleanupDirectory;
348
+ }
349
+
350
+ private executablePath(operation: NativeReviewOperation, mutating: boolean): string {
351
+ try {
352
+ const executable = typeof this.executable === "string" ? this.executable : this.executable();
353
+ if (!isAbsolute(executable) || executable === "gentle-ai") throw new TypeError("Native review requires an absolute package-local executable");
354
+ return executable;
355
+ }
356
+ catch (error) {
357
+ if (error instanceof PackageLocalGentleAiBinaryMissingError) {
358
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING, operation, mutating, error.message);
359
+ }
360
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "package-local native process could not start");
361
+ }
362
+ }
363
+
364
+ private async execute(operation: NativeReviewOperation, cwd: string, arguments_: readonly string[], mutating: boolean, signal?: AbortSignal): Promise<NativeJsonExecution> {
365
+ let result: ExecFileResult;
366
+ try { result = await this.adapter({ file: this.executablePath(operation, mutating), arguments: arguments_, cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
367
+ catch (error) {
368
+ if (error instanceof NativeReviewCliError) throw nativeError(error.code, operation, mutating, error.message);
369
+ if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
370
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
371
+ }
372
+ if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out");
373
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit");
374
+ if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled");
375
+ const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
376
+ if (result.exitCode !== 0 && !structuredValidateDenial) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed");
377
+ if (result.stderr.trim().length > 0 && !structuredValidateDenial) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr");
378
+ return { body: parseJson(result.stdout, operation, mutating), exitCode: result.exitCode };
379
+ }
380
+
381
+ private async verifyVersion(cwd: string, signal?: AbortSignal): Promise<void> {
382
+ let result: ExecFileResult;
383
+ try { result = await this.adapter({ file: this.executablePath(NATIVE_REVIEW_OPERATION.VERSION, false), arguments: ["version"], cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
384
+ catch (error) {
385
+ if (error instanceof NativeReviewCliError) throw error;
386
+ if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
387
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
388
+ }
389
+ if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out");
390
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit");
391
+ if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled");
392
+ if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed");
393
+ if (result.stderr.trim().length > 0 || result.stdout.replace(/\r\n$/, "\n") !== "gentle-ai 2.1.4\n") throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai 2.1.4 is required");
394
+ }
395
+
396
+ async start(request: NativeStartRequest): Promise<NativeStartResult> {
397
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
398
+ await this.verifyVersion(request.cwd, request.signal);
399
+ const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", "--cwd", request.cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]), ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.policyPath ? ["--policy", request.policyPath] : []), ...(request.focus ? ["--focus", request.focus] : [])], true, request.signal);
400
+ return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
401
+ const body = exactObject(result, ["operation", "lineage_id", "state", "risk_level", "selected_lenses", "changed_files", "changed_lines", "correction_budget", "action", "lenses_required", "projection"]);
402
+ if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE as readonly string[]).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
403
+ const lineageId = requiredString(body.lineage_id);
404
+ if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
405
+ const riskLevel = requiredString(body.risk_level);
406
+ const action = enumString(body.action, NATIVE_START_ACTION_VALUES) as NativeStartAction;
407
+ const lensesRequired = booleanValue(body.lenses_required);
408
+ const selectedLenses = decodeSelectedLenses(body.selected_lenses, riskLevel, lensesRequired);
409
+ if (
410
+ !(NATIVE_RISK_LEVEL as readonly string[]).includes(riskLevel) ||
411
+ selectedLenses.some((lens) => !(NATIVE_REVIEW_LENS as readonly string[]).includes(lens)) ||
412
+ !hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
413
+ !hasValidLensesRequired(action, body.state as string, riskLevel, lensesRequired)
414
+ ) throw new Error("unknown or contradictory start enum");
415
+ return { lineageId, state: body.state as NativeStartResult["state"], riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired };
416
+ });
417
+ }
418
+
419
+ private async stageDocument(directory: string, name: string, document: unknown): Promise<string> {
420
+ const path = join(directory, `${name}.json`);
421
+ await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
422
+ await chmod(path, 0o600);
423
+ return path;
424
+ }
425
+ private async stageEvidence(directory: string, evidence: string): Promise<string> {
426
+ const path = join(directory, "evidence.txt");
427
+ await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
428
+ await chmod(path, 0o600);
429
+ return path;
430
+ }
431
+
432
+ async finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult> {
433
+ if (request.evidenceDocument !== undefined && (typeof request.evidenceDocument !== "string" || request.evidenceDocument.length === 0)) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
434
+ await this.verifyVersion(request.cwd, request.signal);
435
+ const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
436
+ const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
437
+ try {
438
+ if (directory) await chmod(directory, 0o700);
439
+ const resultFiles = directory && request.lensResults ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
440
+ const refuterFile = directory && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
441
+ const validationFile = directory && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
442
+ const evidenceFile = directory && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
443
+ const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, ["review", "finalize", "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...resultFiles.flatMap((path) => ["--result", path]), ...(refuterFile ? ["--refuter", refuterFile] : []), ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]), ...(validationFile ? ["--validation", validationFile] : []), ...(evidenceFile ? ["--evidence", evidenceFile] : []), ...(request.failed ? ["--failed"] : [])], true, request.signal);
444
+ return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
445
+ const body = exactObject(result, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path"]);
446
+ if (body.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
447
+ const lineageId = requiredString(body.lineage_id);
448
+ if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
449
+ const state = requiredString(body.state);
450
+ if (!(NATIVE_FINALIZE_STATE as readonly string[]).includes(state)) throw new Error("unknown finalize state");
451
+ return { lineageId, state, action: requiredString(body.action), storeRevision: requiredString(body.store_revision), ...(body.receipt_path === undefined ? {} : { receiptPath: requiredString(body.receipt_path) }) };
452
+ });
453
+ } finally { if (directory) await this.cleanupDirectory(directory).catch(() => undefined); }
454
+ }
455
+
456
+ async validate(request: NativeValidateRequest): Promise<NativeValidateResult> {
457
+ await this.verifyVersion(request.cwd, request.signal);
458
+ const execution = await this.execute(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, ["review", "validate", "--gate", request.gate, "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.flags ?? [])], false, request.signal);
459
+ return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
460
+ const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"]);
461
+ const gateResult = enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"];
462
+ const action = requiredString(body.action);
463
+ const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
464
+ const expectedExitCode = gateResult === "allow" ? 0 : 1;
465
+ if (body.schema !== "gentle-ai.review-gate-result/v1" || typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
466
+ const gateContext = decodeGateContext(body.context);
467
+ const returnedGate = gateContext.raw.gate;
468
+ if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
469
+ if (request.lineageId && returnedGate !== "" && gateContext.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.VALIDATE, false, "native gate lineage mismatch");
470
+ return { allowed: body.allowed, result: gateResult, action, reason: requiredString(body.reason), gateContext };
471
+ });
472
+ }
473
+
474
+ async bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult> {
475
+ await this.verifyVersion(request.cwd, request.signal);
476
+ const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, ["review", "bind-sdd", "--cwd", request.cwd, "--change", request.change, "--lineage", request.lineage, `--expected-binding-revision=${request.expectedBindingRevision}`], true, request.signal);
477
+ return decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => {
478
+ const body = exactObject(result, ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
479
+ if (body.schema !== "gentle-ai.sdd-review-binding/v1" || body.change !== request.change || body.lineage !== request.lineage) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding identity mismatch");
480
+ const receiptHash = requiredString(body.receipt_hash);
481
+ const gateContext = decodeGateContext(body.gate_context);
482
+ const authorityRevision = requiredString(body.authority_revision);
483
+ if (gateContext.lineageId !== request.lineage || gateContext.storeRevision !== authorityRevision || gateContext.raw.gate !== "post-apply") throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding gate mismatch");
484
+ return {
485
+ revision: requiredString(body.revision),
486
+ change: requiredString(body.change),
487
+ lineage: requiredString(body.lineage),
488
+ authorityRevision,
489
+ receiptHash,
490
+ gateContext,
491
+ };
492
+ });
493
+ }
494
+
495
+ async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult> {
496
+ await this.verifyVersion(request.cwd, request.signal);
497
+ const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.SDD_STATUS, request.cwd, ["sdd-status", request.change, "--cwd", request.cwd, "--json", "--instructions"], false, request.signal);
498
+ return decode(NATIVE_REVIEW_OPERATION.SDD_STATUS, false, () => {
499
+ const body = exactObject(result, ["schemaName", "schemaVersion", "changeName", "artifactStore", "planningHome", "changeRoot", "artifactPaths", "contextFiles", "artifacts", "taskProgress", "dependencies", "applyState", "actionContext", "relationships", "remediationState", "nextRecommended", "blockedReasons"], ["reviewGate", "reviewTransaction", "phaseInstructions"]);
500
+ if (body.schemaName !== "gentle-ai.sdd-status" || body.schemaVersion !== 1 || body.changeName !== request.change || !["openspec", "engram", "none"].includes(body.artifactStore as string) || !["blocked", "all_done", "ready"].includes(body.applyState as string)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.SDD_STATUS, false, "native status identity mismatch");
501
+ const paths = ["proposal", "specs", "design", "tasks", "applyProgress", "verifyReport", "reviewPolicy", "reviewLedger", "reviewReceipt", "reviewBundle", "reviewContext", "reviewState"];
502
+ const pathMap = (value: unknown) => { const parsed = exactObject(value, paths); for (const path of paths) stringArray(parsed[path]); };
503
+ const planningHome = exactObject(body.planningHome, ["mode", "path"]);
504
+ if (planningHome.mode !== "repo-local") throw new Error("invalid planning home");
505
+ requiredString(planningHome.path); requiredString(body.changeRoot); pathMap(body.artifactPaths); pathMap(body.contextFiles);
506
+ const artifactStates = paths.filter((path) => path !== "reviewPolicy" || body.artifactStore === NATIVE_SDD_ARTIFACT_STORE.ENGRAM);
507
+ const artifacts = exactObject(body.artifacts, artifactStates);
508
+ for (const path of artifactStates) if (!Object.values(NATIVE_SDD_ARTIFACT_STATE).includes(artifacts[path] as NativeSddArtifactState)) throw new Error("invalid artifact state");
509
+ const taskProgress = exactObject(body.taskProgress, ["total", "completed", "pending", "allComplete"]);
510
+ const total = nonNegativeInteger(taskProgress.total), completed = nonNegativeInteger(taskProgress.completed), pending = nonNegativeInteger(taskProgress.pending);
511
+ if (typeof taskProgress.allComplete !== "boolean" || completed + pending !== total || taskProgress.allComplete !== (pending === 0)) throw new Error("invalid task progress");
512
+ const dependencies = exactObject(body.dependencies, ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]);
513
+ for (const phase of ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]) if (!["blocked", "ready", "all_done"].includes(dependencies[phase] as string)) throw new Error("invalid dependency state");
514
+ const actionContext = exactObject(body.actionContext, ["mode", "workspaceRoot", "allowedEditRoots"]);
515
+ if (actionContext.mode !== "repo-local" || requiredString(actionContext.workspaceRoot).length === 0 || stringArray(actionContext.allowedEditRoots).length === 0) throw new Error("invalid action context");
516
+ const relationships = exactObject(body.relationships, ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]);
517
+ for (const field of ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]) stringArray(relationships[field]);
518
+ const remediation = exactObject(body.remediationState, ["required", "complete", "failedEvidenceRevision", "lineageId", "generation", "fixBatch", "reason"]);
519
+ if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || ["failedEvidenceRevision", "lineageId", "reason"].some((field) => typeof remediation[field] !== "string")) throw new Error("invalid remediation state");
520
+ nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
521
+ let reviewGateResult: string | undefined;
522
+ if (body.reviewGate !== undefined) {
523
+ const gate = exactObject(body.reviewGate, ["result", "reason"]);
524
+ reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
525
+ }
526
+ if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
527
+ if (body.phaseInstructions !== undefined) {
528
+ const instructions = exactObject(body.phaseInstructions, ["apply", "verify", "remediate", "archive"]);
529
+ for (const phase of ["apply", "verify", "remediate", "archive"]) stringArray(instructions[phase]);
530
+ }
531
+ const nextRecommended = requiredString(body.nextRecommended);
532
+ if (!(NATIVE_SDD_NEXT_ACTION as readonly string[]).includes(nextRecommended)) throw new Error("unknown SDD next action");
533
+ const blockedReasons = stringArray(body.blockedReasons);
534
+ return {
535
+ ...body,
536
+ artifactStore: body.artifactStore as NativeSddArtifactStore,
537
+ artifacts: artifacts as unknown as NativeSddArtifactStates,
538
+ nextRecommended,
539
+ ready:
540
+ (NATIVE_SDD_POST_REVIEW_ACTION as readonly string[]).includes(nextRecommended) &&
541
+ blockedReasons.length === 0 &&
542
+ reviewGateResult === "allow",
543
+ };
544
+ });
545
+ }
546
+ }
547
+
548
+ export function createNativeReviewCli(adapter = createNodeExecFileAdapter(), executable: string | (() => string) = resolveGentleAiBinary): NativeReviewCliV214 { return new NativeReviewCliV214(adapter, executable); }
549
+
550
+ // Response-schema fixtures remain v2.1.3 historical contracts; the production
551
+ // client above accepts only the v2.1.4 runtime release.
552
+ export { NativeReviewCliV214 as NativeReviewCliV213 };