gentle-pi 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +195 -11
  2. package/assets/agents/gentle-ai-worker.md +9 -0
  3. package/assets/agents/sdd-explore.md +1 -0
  4. package/assets/orchestrator-delegation.md +21 -10
  5. package/assets/orchestrator.md +8 -12
  6. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  7. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  8. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  9. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  10. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  11. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  12. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  13. package/docs/delegated-verification.md +25 -0
  14. package/docs/review-integration.md +1 -1
  15. package/docs/telemetry.md +38 -0
  16. package/extensions/ask-user-choice.ts +26 -20
  17. package/extensions/codegraph-tools.ts +94 -5
  18. package/extensions/gentle-agents.ts +588 -0
  19. package/extensions/gentle-ai.ts +1421 -143
  20. package/extensions/gentle-shell.ts +547 -0
  21. package/extensions/gentle-todo.ts +199 -0
  22. package/extensions/quiet-tools.ts +1 -1
  23. package/lib/agent-home.ts +8 -0
  24. package/lib/agents-config.ts +318 -0
  25. package/lib/agents-history.ts +80 -0
  26. package/lib/agents-protocol.ts +429 -0
  27. package/lib/agents-runner.ts +490 -0
  28. package/lib/agents-transcript.ts +87 -0
  29. package/lib/agents-view.ts +557 -0
  30. package/lib/agents-widget.ts +222 -0
  31. package/lib/gentle-ai-renderer.ts +142 -26
  32. package/lib/native-choice-list.ts +194 -0
  33. package/lib/native-fullscreen-interaction.ts +47 -0
  34. package/lib/native-pointer-region.ts +164 -0
  35. package/lib/native-review-cli.ts +103 -12
  36. package/lib/provider-contract-bundle.ts +88 -6
  37. package/lib/review-candidate-view-owner.ts +177 -0
  38. package/lib/review-candidate-view.ts +127 -35
  39. package/lib/review-consent-ui.ts +65 -0
  40. package/lib/review-host-relay.ts +146 -60
  41. package/lib/review-integration-v2.ts +92 -13
  42. package/lib/review-last-event-controller.ts +1 -0
  43. package/lib/review-relay-contract.ts +11 -0
  44. package/lib/review-repository.ts +2 -2
  45. package/lib/review-risk-assessment.ts +339 -0
  46. package/lib/review-session-standing-permission-ipc.ts +309 -0
  47. package/lib/review-session-standing-permission.ts +219 -0
  48. package/lib/sdd-preflight.ts +2 -2
  49. package/lib/shell-bar.ts +138 -0
  50. package/lib/shell-card.ts +136 -0
  51. package/lib/shell-changes-view.ts +205 -0
  52. package/lib/shell-changes.ts +210 -0
  53. package/lib/shell-gauge.ts +40 -0
  54. package/lib/shell-prompt.ts +119 -0
  55. package/lib/shell-todo.ts +280 -0
  56. package/lib/shell-usage-view.ts +76 -0
  57. package/lib/shell-usage.ts +246 -0
  58. package/lib/telemetry-trigger.ts +151 -0
  59. package/package.json +4 -4
  60. package/runtime/native-review-cli.mjs +102 -11
  61. package/runtime/review-integration-v2.mjs +92 -13
  62. package/runtime/review-relay-contract.mjs +11 -0
  63. package/runtime/review-risk-assessment.mjs +340 -0
  64. package/runtime/telemetry-trigger.mjs +152 -0
  65. package/scripts/build-runtime-modules.mjs +2 -0
  66. package/scripts/gentle-ai-installer.mjs +10 -10
  67. package/scripts/test-packed-runner.mjs +22 -0
  68. package/scripts/verify-package-files.mjs +18 -13
  69. package/skills/_shared/review-ledger-contract.md +9 -1
  70. package/skills/issue-creation/SKILL.md +53 -93
  71. package/tests/agents-config.test.ts +143 -0
  72. package/tests/agents-fake-child.ts +52 -0
  73. package/tests/agents-history.test.ts +54 -0
  74. package/tests/agents-protocol.test.ts +153 -0
  75. package/tests/agents-runner-process.test.ts +111 -0
  76. package/tests/agents-runner.test.ts +402 -0
  77. package/tests/agents-transcript.test.ts +30 -0
  78. package/tests/agents-view.test.ts +274 -0
  79. package/tests/agents-widget.test.ts +111 -0
  80. package/tests/ask-user-choice.test.ts +157 -3
  81. package/tests/codegraph-tools.test.ts +110 -1
  82. package/tests/devbinary/native-review-parity.devtest.ts +108 -0
  83. package/tests/fixtures/agents-process-child.mjs +23 -0
  84. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  85. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  86. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  87. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  89. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  92. package/tests/gentle-agents.test.ts +741 -0
  93. package/tests/gentle-ai-binary.test.ts +1 -1
  94. package/tests/gentle-ai-installer.test.ts +47 -47
  95. package/tests/gentle-ai-renderer.test.ts +65 -0
  96. package/tests/gentle-ai.test.ts +31 -14
  97. package/tests/gentle-card-text.ts +35 -0
  98. package/tests/gentle-shell.test.ts +527 -0
  99. package/tests/gentle-todo.test.ts +182 -0
  100. package/tests/issue-creation-skill.test.ts +103 -0
  101. package/tests/native-choice-list.test.ts +202 -0
  102. package/tests/native-fullscreen-interaction.test.ts +125 -0
  103. package/tests/native-pointer-region.test.ts +245 -0
  104. package/tests/native-review-capability-contract.test.ts +33 -1
  105. package/tests/native-review-cli.test.ts +40 -0
  106. package/tests/native-review-consent.test.ts +91 -0
  107. package/tests/native-review-parity-runtime.test.ts +8 -2
  108. package/tests/native-review-parity.test.ts +29 -22
  109. package/tests/orchestrator-budget.test.ts +71 -2
  110. package/tests/orchestrator-rdd-ownership.test.ts +10 -1
  111. package/tests/package-manifest.test.ts +134 -9
  112. package/tests/provider-contract-bundle.test.ts +76 -0
  113. package/tests/provider-contract-mirror.test.ts +19 -0
  114. package/tests/quiet-tool-rendering.test.ts +96 -37
  115. package/tests/rdd-aware-verification-contract.test.ts +216 -0
  116. package/tests/rdd-status-line.test.ts +286 -0
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-candidate-view.test.ts +452 -6
  119. package/tests/review-contract-prompt.test.ts +142 -0
  120. package/tests/review-controller-native-recovery.test.ts +29 -4
  121. package/tests/review-controller-native-routing.test.ts +321 -4
  122. package/tests/review-controller-workspace-root.test.ts +45 -2
  123. package/tests/review-controller.test.ts +26 -1
  124. package/tests/review-host-relay-routing.test.ts +229 -11
  125. package/tests/review-host-relay.test.ts +195 -7
  126. package/tests/review-integration-v2-forward.test.ts +47 -0
  127. package/tests/review-integration-v2.test.ts +112 -0
  128. package/tests/review-last-event-closure.test.ts +7 -2
  129. package/tests/review-ledger-contract.test.ts +1 -1
  130. package/tests/review-relay-contract.test.ts +26 -0
  131. package/tests/review-repository.test.ts +28 -1
  132. package/tests/review-risk-assessment.test.ts +626 -0
  133. package/tests/review-session-standing-permission-controller.test.ts +608 -0
  134. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  135. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  136. package/tests/review-session-standing-permission.test.ts +126 -0
  137. package/tests/runtime-harness.mjs +1 -0
  138. package/tests/shell-bar.test.ts +176 -0
  139. package/tests/shell-card.test.ts +118 -0
  140. package/tests/shell-changes-view.test.ts +146 -0
  141. package/tests/shell-changes.test.ts +182 -0
  142. package/tests/shell-prompt.test.ts +118 -0
  143. package/tests/shell-todo.test.ts +170 -0
  144. package/tests/shell-usage-view.test.ts +62 -0
  145. package/tests/shell-usage.test.ts +197 -0
  146. package/tests/telemetry-trigger.test.ts +349 -0
  147. package/tests/writer-edit-surface-scope.test.ts +153 -17
  148. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  149. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  150. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  151. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  152. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  153. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -0,0 +1,151 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+
3
+ // gentle-ai#4309 owns anonymous usage telemetry end to end: install and
4
+ // heartbeat events, the notice, every kill switch, and the wire contract.
5
+ // gentle-pi#677 only nudges the local binary once per process, best-effort,
6
+ // and never surfaces the outcome to the user. This module is the pure,
7
+ // testable core of that nudge (`shouldTriggerTelemetry`,
8
+ // `spawnTelemetryTrigger`) plus the decode helper the foreground
9
+ // `/gentle:telemetry` slash command uses to relay `<op> --json` output
10
+ // (`decodeTelemetryTriggerDecision`).
11
+
12
+ export const TELEMETRY_TRIGGER_KILL_TIMEOUT_MS = 3_000;
13
+
14
+ export const TELEMETRY_TRIGGER_CONTRACT = "gentle-ai.telemetry-trigger/v1";
15
+
16
+ export const TELEMETRY_TRIGGER_DECISIONS = [
17
+ "enrolled",
18
+ "sent_install",
19
+ "sent_heartbeat",
20
+ "rate_limited",
21
+ "backoff",
22
+ "disabled",
23
+ ] as const;
24
+ export type TelemetryTriggerDecision = (typeof TELEMETRY_TRIGGER_DECISIONS)[number];
25
+
26
+ export interface TelemetryTriggerDecisionV1 {
27
+ schema: typeof TELEMETRY_TRIGGER_CONTRACT;
28
+ decision: TelemetryTriggerDecision;
29
+ source: string;
30
+ }
31
+
32
+ /**
33
+ * Decodes `gentle-ai telemetry trigger --json` stdout. An older binary
34
+ * without the verb prints `unknown telemetry command` and exits non-zero, so
35
+ * its stdout never parses as JSON here; that -- along with malformed JSON, a
36
+ * mismatched schema, an unrecognized decision, or a missing/non-string
37
+ * source -- means "nothing to do" and returns undefined rather than
38
+ * throwing.
39
+ */
40
+ export function decodeTelemetryTriggerDecision(stdout: string): TelemetryTriggerDecisionV1 | undefined {
41
+ let parsed: unknown;
42
+ try {
43
+ parsed = JSON.parse(stdout);
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
48
+ const record = parsed as Record<string, unknown>;
49
+ if (record.schema !== TELEMETRY_TRIGGER_CONTRACT) return undefined;
50
+ if (typeof record.decision !== "string") return undefined;
51
+ if (!(TELEMETRY_TRIGGER_DECISIONS as readonly string[]).includes(record.decision)) return undefined;
52
+ if (typeof record.source !== "string" || record.source.length === 0) return undefined;
53
+ return {
54
+ schema: TELEMETRY_TRIGGER_CONTRACT,
55
+ decision: record.decision as TelemetryTriggerDecision,
56
+ source: record.source,
57
+ };
58
+ }
59
+
60
+ export type TelemetryKillSwitchReason = "do-not-track" | "telemetry-disabled" | "ci";
61
+
62
+ function telemetryKillSwitchReason(env: Readonly<Record<string, string | undefined>>): TelemetryKillSwitchReason | undefined {
63
+ if (env.DO_NOT_TRACK === "1") return "do-not-track";
64
+ if (env.GENTLE_AI_TELEMETRY === "0") return "telemetry-disabled";
65
+ if (env.CI === "true") return "ci";
66
+ return undefined;
67
+ }
68
+
69
+ /**
70
+ * True unless one of Gentle Pi's own kill switches is set
71
+ * (`DO_NOT_TRACK=1`, `GENTLE_AI_TELEMETRY=0`, or `CI=true`). The binary
72
+ * enforces its own opt-out and rate limiting independently; this predicate
73
+ * only decides whether Gentle Pi spawns the trigger at all.
74
+ */
75
+ export function shouldTriggerTelemetry(env: Readonly<Record<string, string | undefined>>): boolean {
76
+ return telemetryKillSwitchReason(env) === undefined;
77
+ }
78
+
79
+ // A minimal structural subset of node:child_process's ChildProcess, mirroring
80
+ // the ChildLike/Spawn seam already used by lib/agents-runner.ts: narrow
81
+ // enough that tests can stub it without touching a real process.
82
+ export interface TelemetryTriggerChildLike {
83
+ unref(): unknown;
84
+ kill(signal?: NodeJS.Signals | number): boolean;
85
+ on(event: "error", listener: (error: Error) => void): unknown;
86
+ }
87
+
88
+ export interface TelemetryTriggerSpawnOptions {
89
+ cwd: string;
90
+ env: NodeJS.ProcessEnv;
91
+ detached: boolean;
92
+ stdio: "ignore";
93
+ }
94
+
95
+ export type TelemetryTriggerSpawn = (
96
+ command: string,
97
+ args: readonly string[],
98
+ options: TelemetryTriggerSpawnOptions,
99
+ ) => TelemetryTriggerChildLike;
100
+
101
+ export interface SpawnTelemetryTriggerOptions {
102
+ executable: string;
103
+ cwd: string;
104
+ env: NodeJS.ProcessEnv;
105
+ spawn?: TelemetryTriggerSpawn;
106
+ }
107
+
108
+ export type TelemetryTriggerReason = "spawned" | "spawn-error" | TelemetryKillSwitchReason;
109
+
110
+ export interface TelemetryTriggerResult {
111
+ spawned: boolean;
112
+ reason: TelemetryTriggerReason;
113
+ }
114
+
115
+ /**
116
+ * Nudges `<executable> telemetry trigger --json`, detached and
117
+ * fire-and-forget. Never throws, never awaits the child's exit, and never
118
+ * blocks session start: a missing binary, an old binary without the verb, or
119
+ * a spawn failure all just report `{ spawned: false, ... }`. A 3 s timer
120
+ * kills a runaway child; the timer is unref'd so it can never keep the host
121
+ * process alive on its own, matching the detached child's own `unref()`.
122
+ */
123
+ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): TelemetryTriggerResult {
124
+ const killSwitch = telemetryKillSwitchReason(options.env);
125
+ if (killSwitch !== undefined) return { spawned: false, reason: killSwitch };
126
+ const spawnImpl = options.spawn ?? (nodeSpawn as unknown as TelemetryTriggerSpawn);
127
+ try {
128
+ const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
129
+ cwd: options.cwd,
130
+ env: options.env,
131
+ detached: true,
132
+ stdio: "ignore",
133
+ });
134
+ child.on("error", () => {
135
+ // Fire-and-forget: a spawn-time or runtime child error never surfaces
136
+ // to the caller and never affects session start.
137
+ });
138
+ const killTimer = setTimeout(() => {
139
+ try {
140
+ child.kill();
141
+ } catch {
142
+ // Best effort only; the child may already have exited.
143
+ }
144
+ }, TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
145
+ killTimer.unref?.();
146
+ child.unref();
147
+ return { spawned: true, reason: "spawned" };
148
+ } catch {
149
+ return { spawned: false, reason: "spawn-error" };
150
+ }
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,11 +66,11 @@
66
66
  ]
67
67
  },
68
68
  "dependencies": {
69
- "@earendil-works/pi-tui": "*",
69
+ "@earendil-works/pi-tui": "0.85.1",
70
70
  "@heyhuynhgiabuu/pi-pretty": "0.6.14"
71
71
  },
72
72
  "peerDependencies": {
73
- "@earendil-works/pi-coding-agent": "*",
73
+ "@earendil-works/pi-coding-agent": ">=0.85.1",
74
74
  "typebox": "*"
75
75
  },
76
76
  "peerDependenciesMeta": {
@@ -79,7 +79,7 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@earendil-works/pi-coding-agent": "*"
82
+ "@earendil-works/pi-coding-agent": "0.85.1"
83
83
  },
84
84
  "packageManager": "pnpm@11.1.1"
85
85
  }
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, posix, win32 } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
8
8
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
9
+ import { decodeReviewAssessmentV1, } from "./review-risk-assessment.mjs";
9
10
  import {
10
11
  REVIEW_INTEGRATION_CONTRACT,
11
12
  decodeReviewAcknowledgedV1,
@@ -60,6 +61,7 @@ export const NATIVE_REVIEW_OPERATION = {
60
61
  RECONCILE_AUTHORITY: "review/reconcile-authority",
61
62
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
62
63
  MODE: "review/mode",
64
+ ASSESS: "review/assess",
63
65
  REPAIR: "review/repair",
64
66
  CAPTURE_RESULT: "review/capture-result",
65
67
  CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
@@ -112,6 +114,11 @@ export const NATIVE_REVIEW_ERROR_CODE = {
112
114
 
113
115
 
114
116
 
117
+
118
+
119
+
120
+
121
+
115
122
  export const NATIVE_REVIEW_MODE_OPERATION = {
116
123
  STATUS: "status",
117
124
  ENABLE: "enable",
@@ -152,6 +159,17 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
152
159
 
153
160
 
154
161
 
162
+ // Read-only risk assessment request (gentle-pi#662). `baseRef` requires
163
+ // explicit `committedOnly` acknowledgement, exactly like Native START's
164
+ // baseRef/committedOnly pairing, because both select a committed range
165
+ // instead of the ambient working tree.
166
+
167
+
168
+
169
+
170
+
171
+
172
+
155
173
 
156
174
 
157
175
 
@@ -305,7 +323,6 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
305
323
 
306
324
 
307
325
 
308
-
309
326
 
310
327
 
311
328
  /** Raw audited native record; Pi relays it verbatim and never reinterprets it. */
@@ -499,7 +516,12 @@ export const NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION = {
499
516
  } ;
500
517
 
501
518
 
502
- export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = NATIVE_REVIEW_AUTHORITY_STATUS;
519
+ export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = {
520
+ ...NATIVE_REVIEW_AUTHORITY_STATUS,
521
+ INCOMPLETE_STORE_ENTRY: "incomplete-store-entry",
522
+ HISTORICAL_PRE_RECEIPT: "historical-pre-receipt",
523
+ INVALIDATED: "invalidated",
524
+ } ;
503
525
 
504
526
 
505
527
  export const NATIVE_REVIEW_LOCK_STATUS = {
@@ -570,6 +592,11 @@ export const NATIVE_REVIEW_RECOVERY_DISPOSITION = {
570
592
 
571
593
 
572
594
 
595
+
596
+
597
+
598
+
599
+
573
600
 
574
601
 
575
602
 
@@ -790,6 +817,33 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
790
817
  // the columns match the 2.5.0-rc.3 row. riskEvidence and hint stay dark:
791
818
  // still not proven to reach the negotiated START path Pi reads.
792
819
  "2.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
820
+ // Ground-truthed against the published v2.6.0 binary installed from its
821
+ // signed archive: `review capabilities` on the v2 lane advertises
822
+ // capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
823
+ // and the `start/v4` continuation, all of which the decoders already read.
824
+ // `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
825
+ // and `review validate --gate pre-commit` still answers
826
+ // `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
827
+ // against the binary. Declining the consent/v3 prompt during `review start`
828
+ // still returns `risk_evidence` only on that blocking envelope, never on
829
+ // the negotiated `start/v4` continuation, so riskEvidence and hint stay
830
+ // dark for the same reason they have on every row since 2.2.0: still not
831
+ // proven to reach the negotiated START path Pi reads. The closed fields Pi
832
+ // consumes did not change between 2.5.0 and 2.6.0, so the columns match
833
+ // the 2.5.0 row.
834
+ "2.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
835
+ // Ground-truthed against the published v2.7.0 binary installed from its
836
+ // signed archive: `review capabilities` on the v2 lane still advertises
837
+ // capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
838
+ // and the `start/v4` continuation, all of which the decoders already read.
839
+ // `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
840
+ // and `review validate --gate pre-commit` still answers
841
+ // `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
842
+ // against the binary. riskEvidence and hint stay dark for the same reason
843
+ // they have on every row since 2.2.0: still not proven to reach the
844
+ // negotiated START path Pi reads. The closed fields Pi consumes did not
845
+ // change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
846
+ "2.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
793
847
  });
794
848
 
795
849
 
@@ -1032,8 +1086,15 @@ function decodeNativeReviewRecovery(value ) {
1032
1086
  ...(recovery.maintainer_authorization === undefined ? {} : { maintainerAuthorization: requiredString(recovery.maintainer_authorization) }),
1033
1087
  };
1034
1088
  }
1089
+ function decodeNativeReviewDiscardedWorkSummary(value ) {
1090
+ const discardedWork = exactObject(value, ["captured_lens_results", "findings_present"]);
1091
+ return {
1092
+ capturedLensResults: stringArray(discardedWork.captured_lens_results),
1093
+ findingsPresent: booleanValue(discardedWork.findings_present),
1094
+ };
1095
+ }
1035
1096
  function decodeNativeReviewStatusEntry(value ) {
1036
- const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
1097
+ const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery", "discarded_work"]);
1037
1098
  return {
1038
1099
  version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) ,
1039
1100
  ...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
@@ -1044,6 +1105,7 @@ function decodeNativeReviewStatusEntry(value )
1044
1105
  ...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
1045
1106
  ...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
1046
1107
  ...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
1108
+ ...(entry.discarded_work === undefined ? {} : { discardedWork: decodeNativeReviewDiscardedWorkSummary(entry.discarded_work) }),
1047
1109
  problems: stringArray(entry.problems),
1048
1110
  };
1049
1111
  }
@@ -1358,9 +1420,7 @@ class NativeReviewPlainCli {
1358
1420
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
1359
1421
  }
1360
1422
  if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
1361
- for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] ) {
1362
- if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1363
- }
1423
+ if (typeof request.findingsPresent !== "boolean") throw new TypeError("Native ABANDON findingsPresent must be a boolean");
1364
1424
  if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1365
1425
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1366
1426
  "review", "abandon", "--cwd", request.cwd,
@@ -1449,7 +1509,11 @@ class NativeReviewPlainCli {
1449
1509
  }
1450
1510
  }
1451
1511
 
1452
- export function nativeReviewAbandonAuthorization(request ) {
1512
+ export function nativeReviewAbandonAuthorization(request ) {
1513
+ // gentle-ai 0ed9225f removed evidence records from the discarded-work summary,
1514
+ // so the native v2 gate verifies an exact eight-line binding (schema, lineage,
1515
+ // revision, snapshot_identity, reason, captured_lens_results, findings_present,
1516
+ // actor) — there is no evidence_records_present line to derive or relay.
1453
1517
  return [
1454
1518
  "gentle-ai.review-abandon-authorization/v2",
1455
1519
  `lineage=${request.lineage}`,
@@ -1458,7 +1522,6 @@ export function nativeReviewAbandonAuthorization(request
1458
1522
  `reason=${request.reason}`,
1459
1523
  `captured_lens_results=${request.capturedLensResults.join(",")}`,
1460
1524
  `findings_present=${request.findingsPresent}`,
1461
- `evidence_records_present=${request.evidenceRecordsPresent}`,
1462
1525
  `actor=${request.actor.trim()}`,
1463
1526
  ].join("\n");
1464
1527
  }
@@ -1581,14 +1644,20 @@ function splitNativeConsentInvocation(invocation ) {
1581
1644
  let quote ;
1582
1645
  let escaping = false;
1583
1646
  let started = false;
1584
- for (const character of invocation.trim()) {
1647
+ const source = invocation.trim();
1648
+ for (let index = 0; index < source.length; index += 1) {
1649
+ const character = source[index] ;
1585
1650
  if (escaping) {
1586
1651
  current += character;
1587
1652
  escaping = false;
1588
1653
  started = true;
1589
1654
  continue;
1590
1655
  }
1591
- if (character === "\\" && quote !== "'") {
1656
+ // Provider invocations are not shell commands. Keep every path backslash
1657
+ // verbatim, including quoted UNC and drive-root paths. Outside quotes,
1658
+ // only a backslash before whitespace joins a space-containing token.
1659
+ const next = source[index + 1];
1660
+ if (character === "\\" && quote === undefined && next !== undefined && /\s/.test(next)) {
1592
1661
  escaping = true;
1593
1662
  started = true;
1594
1663
  continue;
@@ -1679,7 +1748,9 @@ function consentInvocationArguments(request )
1679
1748
  if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
1680
1749
  const arguments_ = words.slice(1);
1681
1750
  if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
1682
- if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1751
+ const providerCwd = exactConsentOption(arguments_, "--cwd");
1752
+ const requestedCwd = normalizeNativeReviewCwd(request.cwd);
1753
+ if (normalizeNativeReviewCwd(providerCwd) !== requestedCwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1683
1754
  if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
1684
1755
  if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
1685
1756
  const lineageId = optionalConsentLineageOption(arguments_);
@@ -2179,6 +2250,26 @@ export class NativeReviewCliV216 {
2179
2250
  return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
2180
2251
  }
2181
2252
 
2253
+ // Read-only risk assessment (gentle-ai#4295, gentle-pi#662). Never mutates;
2254
+ // an older binary without the verb, or any other process/decode failure,
2255
+ // rejects -- callers (the `gentle_review` tool's `assess` operation) fail
2256
+ // closed to `high`.
2257
+ async assess(request ) {
2258
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
2259
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
2260
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
2261
+ const cwd = await canonicalNativeReviewCwd(request.cwd);
2262
+ const execution = await this.invoke(
2263
+ NATIVE_REVIEW_OPERATION.ASSESS,
2264
+ cwd,
2265
+ ["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
2266
+ false,
2267
+ request.signal,
2268
+ this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
2269
+ );
2270
+ return decode(NATIVE_REVIEW_OPERATION.ASSESS, false, () => decodeReviewAssessmentV1(execution.body));
2271
+ }
2272
+
2182
2273
  // Recovery commands are version-gated plain CLI operations outside the
2183
2274
  // negotiated integration-v1 contract, exactly like reviewStatus.
2184
2275
  reclaim(request ) {
@@ -80,7 +80,10 @@ const RISK_LEVELS = ["low", "medium", "high"] ;
80
80
  const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
81
81
  const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] ;
82
82
  const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] ;
83
- const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop"] ;
83
+ // "collect" and "execute" are the v2 envelope's projection of a live
84
+ // transaction whose next_transition is mandatory: the root action names the
85
+ // transition kind instead of reading as a terminal stop.
86
+ const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop", "collect", "execute"] ;
84
87
  const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] ;
85
88
 
86
89
  export const REVIEW_STATUS_ACTION_DISPOSITION = {
@@ -184,6 +187,19 @@ const CAPABILITIES_SCHEMA_IDENTITIES
184
187
  requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
185
188
  optionalFeatureFloor: 14,
186
189
  }),
190
+ // Ground-truthed against the published v2.6.0 binary: capabilities/v2.5
191
+ // advertises the same required surface as v2.4 (status/v6 is still
192
+ // advertised for compatibility) plus the new status/v7 schema, which is a
193
+ // superset-checked addition, not a requirement -- decodeReviewStatusV3
194
+ // accepts v7 as an additive extension of v6, so the required-schema floor
195
+ // stays unchanged. The v2.6.0 binary advertised 15 optional features
196
+ // (floor stays at 14, its established minimum).
197
+ "gentle-ai.review-integration.capabilities/v2.5": Object.freeze({
198
+ protocolMinor: 5,
199
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.5", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
200
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
201
+ optionalFeatureFloor: 14,
202
+ }),
187
203
  });
188
204
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
189
205
  "base_ref_workspace_overlay",
@@ -391,6 +407,8 @@ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "
391
407
 
392
408
 
393
409
 
410
+
411
+
394
412
 
395
413
 
396
414
 
@@ -533,6 +551,18 @@ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.valu
533
551
 
534
552
 
535
553
 
554
+ // gentle-pi#627: the exact `gentle-ai sync` invocation that resolves a stale
555
+ // managed-asset set (STATUS next_transition stop, and START's preflight
556
+ // failure envelope both carry this same shape).
557
+
558
+
559
+
560
+
561
+
562
+
563
+
564
+
565
+
536
566
 
537
567
 
538
568
 
@@ -597,6 +627,8 @@ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.valu
597
627
 
598
628
 
599
629
 
630
+
631
+
600
632
 
601
633
 
602
634
 
@@ -691,6 +723,8 @@ const FAILURE_CAUSE_CATEGORIES = ["inventory_io_or_layout", "lock_ambiguous", "r
691
723
 
692
724
 
693
725
 
726
+
727
+
694
728
 
695
729
 
696
730
 
@@ -1625,8 +1659,11 @@ function decodeCollectInput(value , label , v5 , v6
1625
1659
  }
1626
1660
 
1627
1661
  if (captureOperation === "review.capture-result") {
1628
- if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined || input.changed_path_manifest === undefined) {
1629
- throw new TypeError(`${label} requires artifact_subject, base_tree, candidate_tree, and changed_path_manifest`);
1662
+ // changed_path_manifest is optional here: the artifact subject's
1663
+ // changed_path_manifest_sha256 already binds the frozen manifest, and
1664
+ // gentle-ai stops inlining one copy per lens (gentle-ai#3922).
1665
+ if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined) {
1666
+ throw new TypeError(`${label} requires artifact_subject, base_tree, and candidate_tree`);
1630
1667
  }
1631
1668
  if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
1632
1669
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
@@ -1659,12 +1696,32 @@ function decodeCollectInput(value , label , v5 , v6
1659
1696
  };
1660
1697
  }
1661
1698
 
1699
+ // gentle-pi#627: gentle-ai's managed-assets continuation (STATUS next_transition
1700
+ // stop reason_code=managed_assets_outdated, and START's preflight failure
1701
+ // envelope) names the exact `gentle-ai sync` invocation that resolves a stale
1702
+ // managed-asset set without abandoning the frozen candidate. Decoded verbatim
1703
+ // from internal/cli/review_operation_contract.go's ReviewManagedAssetsContinuation.
1704
+ export function decodeReviewManagedAssetsContinuationV1(value , label ) {
1705
+ const body = exactRecord(value, label, ["operation", "command"], ["agent", "stale_assets"]);
1706
+ const operation = enumeration(body.operation, ["sync"] , `${label}.operation`);
1707
+ const command = nonempty(body.command, `${label}.command`);
1708
+ const agent = body.agent === undefined ? undefined : nonempty(body.agent, `${label}.agent`);
1709
+ const staleAssets = body.stale_assets === undefined ? undefined : stringArray(body.stale_assets, `${label}.stale_assets`, { minimum: 1 });
1710
+ return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
1711
+ }
1712
+
1662
1713
  export function decodeReviewNextTransitionV3(value , options = {}) {
1663
1714
  const v6 = options.v6 === true;
1664
1715
  const v5 = options.v5 === true || v6;
1665
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1716
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation"]);
1666
1717
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
1667
1718
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1719
+ const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
1720
+ // gentle-pi#627: continuation is optional on every status version an older
1721
+ // gentle-ai may emit a managed_assets_outdated stop with no continuation at
1722
+ // all, and decode must not refuse the whole envelope for that. When present
1723
+ // it is only valid on a stop with this exact reason_code.
1724
+ if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
1668
1725
 
1669
1726
  // status/v5: the bounded correction plan request rides exactly its two
1670
1727
  // reason codes and never any other (vendored status-v5.schema.json).
@@ -1714,7 +1771,7 @@ export function decodeReviewNextTransitionV3(value , options
1714
1771
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1715
1772
  }
1716
1773
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1717
- return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1774
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }) };
1718
1775
  }
1719
1776
 
1720
1777
  // ---------------------------------------------------------------------------
@@ -1762,15 +1819,19 @@ export function decodeReviewStatusV3(value ) {
1762
1819
  // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1763
1820
  // against a live capture and the vendored status-v5.schema.json) is the v3
1764
1821
  // key set plus the optional forecast and the v5-only next_transition
1765
- // surfaces. status/v6 adds the intended-untracked selection; v3 keeps
1766
- // rejecting every v5/v6-only field.
1822
+ // surfaces. status/v6 adds the intended-untracked selection; status/v7
1823
+ // (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
1824
+ // adds only the top-level optional `eligible_untracked_inventory` digest,
1825
+ // so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
1826
+ // field.
1767
1827
  const schema = typeof value === "object" && value !== null ? (value ).schema : undefined;
1768
- const v6 = schema === "gentle-ai.review-integration.status/v6";
1828
+ const v7 = schema === "gentle-ai.review-integration.status/v7";
1829
+ const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
1769
1830
  const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1770
1831
  const body = exactRecord(value, "status", [
1771
1832
  "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1772
- ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : [])]);
1773
- requireIdentity(body, v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1833
+ ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
1834
+ requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1774
1835
 
1775
1836
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1776
1837
  let receipt ;
@@ -1799,11 +1860,12 @@ export function decodeReviewStatusV3(value ) {
1799
1860
 
1800
1861
  let frozen ;
1801
1862
  if (body.frozen !== undefined) {
1802
- const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
1863
+ const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"], ["changed_path_manifest_sha256"]);
1803
1864
  frozen = {
1804
1865
  tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
1805
1866
  originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
1806
1867
  correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
1868
+ ...(source.changed_path_manifest_sha256 === undefined ? {} : { changedPathManifestSha256: sha256(source.changed_path_manifest_sha256, "status.frozen.changed_path_manifest_sha256") }),
1807
1869
  };
1808
1870
  }
1809
1871
  if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
@@ -1852,6 +1914,15 @@ export function decodeReviewStatusV3(value ) {
1852
1914
  };
1853
1915
  }
1854
1916
 
1917
+ // status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
1918
+ // closed loop where `sdd-attempt finish` named a digest status never
1919
+ // published. Absent on the `staged` projection, which never resolves an
1920
+ // inventory, so it stays structurally optional rather than a required v7
1921
+ // field.
1922
+ const eligibleUntrackedInventory = v7 && body.eligible_untracked_inventory !== undefined
1923
+ ? sha256(body.eligible_untracked_inventory, "status.eligible_untracked_inventory")
1924
+ : undefined;
1925
+
1855
1926
  return {
1856
1927
  contract: REVIEW_INTEGRATION_CONTRACT,
1857
1928
  applicability,
@@ -1870,6 +1941,7 @@ export function decodeReviewStatusV3(value ) {
1870
1941
  ...(forecast === undefined ? {} : { forecast }),
1871
1942
  ...(repositoryContext === undefined ? {} : { repositoryContext }),
1872
1943
  ...(validationRequest === undefined ? {} : { validationRequest }),
1944
+ ...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
1873
1945
  raw: body,
1874
1946
  };
1875
1947
  }
@@ -2020,9 +2092,10 @@ function decodeFailureContext(value , label )
2020
2092
  export function decodeReviewFailureV2(value ) {
2021
2093
  const body = exactRecord(value, "failure", [
2022
2094
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
2023
- ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
2095
+ ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context", "continuation"]);
2024
2096
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
2025
2097
  const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
2098
+ const code = text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ });
2026
2099
 
2027
2100
  if (body.progress_identity !== undefined) {
2028
2101
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -2031,13 +2104,18 @@ export function decodeReviewFailureV2(value ) {
2031
2104
  if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
2032
2105
  throw new TypeError("failure.request_digest with review.repair requires progress_identity");
2033
2106
  }
2107
+ // gentle-pi#627: continuation is optional on every version — an older
2108
+ // gentle-ai may emit managed_assets_outdated with no continuation, and
2109
+ // decode must not refuse the whole envelope for that. When present it is
2110
+ // only valid on this exact code.
2111
+ if (body.continuation !== undefined && code !== "managed_assets_outdated") throw new TypeError("failure.continuation is only valid for code managed_assets_outdated");
2034
2112
 
2035
2113
  return {
2036
2114
  schema: "gentle-ai.review-integration.failure/v2",
2037
2115
  contract: REVIEW_INTEGRATION_CONTRACT,
2038
2116
  operation,
2039
2117
  phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] , "failure.phase"),
2040
- code: text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }),
2118
+ code,
2041
2119
  message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
2042
2120
  mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
2043
2121
  authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
@@ -2051,6 +2129,7 @@ export function decodeReviewFailureV2(value ) {
2051
2129
  ...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
2052
2130
  ...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
2053
2131
  ...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
2132
+ ...(body.continuation === undefined ? {} : { continuation: decodeReviewManagedAssetsContinuationV1(body.continuation, "failure.continuation") }),
2054
2133
  raw: body,
2055
2134
  };
2056
2135
  }