gentle-pi 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +799 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +130 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +283 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +199 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +166 -0
  68. package/lib/runtime-metrics-pi-identity.ts +113 -0
  69. package/lib/runtime-metrics-policy.ts +51 -0
  70. package/lib/runtime-metrics.ts +255 -0
  71. package/lib/sdd-preflight.ts +362 -81
  72. package/lib/sdd-research-capabilities.ts +228 -0
  73. package/lib/sdd-status.ts +29 -7
  74. package/lib/session-worktree-registry.ts +118 -0
  75. package/lib/shell-bar.ts +47 -1
  76. package/lib/shell-card.ts +1 -4
  77. package/lib/shell-changes-view.ts +362 -37
  78. package/lib/shell-changes.ts +81 -1
  79. package/lib/shell-prompt.ts +11 -15
  80. package/lib/shell-sidebar-banner.ts +11 -0
  81. package/lib/shell-sidebar-layout.ts +213 -0
  82. package/lib/shell-sidebar.ts +41 -0
  83. package/lib/shell-todo.ts +28 -11
  84. package/lib/telemetry-trigger.ts +2 -0
  85. package/package.json +6 -3
  86. package/runtime/gentle-ai-binary.mjs +3 -1
  87. package/runtime/native-review-cli.mjs +283 -1
  88. package/runtime/review-integration-v2.mjs +187 -5
  89. package/runtime/telemetry-trigger.mjs +2 -0
  90. package/scripts/build-runtime-modules.mjs +9 -1
  91. package/scripts/check-types.mjs +125 -0
  92. package/scripts/gentle-ai-installer.mjs +10 -10
  93. package/scripts/install-gentle-ai.mjs +12 -0
  94. package/scripts/install-tui-mode-setting.mjs +114 -0
  95. package/scripts/test-packed-runner.mjs +16 -2
  96. package/scripts/types-baseline.json +99 -0
  97. package/scripts/verify-package-files.mjs +4 -2
  98. package/skills/_shared/review-ledger-contract.md +17 -1
  99. package/skills/issue-creation/SKILL.md +3 -3
  100. package/skills/judgment-day/SKILL.md +17 -3
  101. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  102. package/tests/agent-profiles.test.ts +722 -0
  103. package/tests/agents-completion-delivery.test.ts +94 -0
  104. package/tests/agents-config.test.ts +62 -0
  105. package/tests/agents-fake-child.ts +15 -1
  106. package/tests/agents-grouping.test.ts +179 -0
  107. package/tests/agents-integration.test.ts +100 -0
  108. package/tests/agents-messaging.test.ts +94 -0
  109. package/tests/agents-protocol.test.ts +45 -0
  110. package/tests/agents-queries.test.ts +190 -0
  111. package/tests/agents-responsive.test.ts +43 -0
  112. package/tests/agents-runner.test.ts +571 -14
  113. package/tests/agents-thread-view.test.ts +45 -0
  114. package/tests/agents-view.test.ts +476 -65
  115. package/tests/agents-widget.test.ts +31 -1
  116. package/tests/artifact-language.test.ts +25 -2
  117. package/tests/ask-user-choice.test.ts +169 -3
  118. package/tests/asset-installation-runtime.test.ts +108 -0
  119. package/tests/autonomous-guard.test.ts +116 -1
  120. package/tests/codegraph-tools.test.ts +2 -1
  121. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  122. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  123. package/tests/feature-request-form.test.ts +67 -0
  124. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  125. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  126. package/tests/gentle-agents.test.ts +1460 -33
  127. package/tests/gentle-ai-binary.test.ts +7 -2
  128. package/tests/gentle-ai-installer.test.ts +47 -47
  129. package/tests/gentle-ai-renderer.test.ts +38 -0
  130. package/tests/gentle-ai.test.ts +944 -4
  131. package/tests/gentle-shell.test.ts +303 -12
  132. package/tests/gentle-todo.test.ts +54 -10
  133. package/tests/install-tui-mode-setting.test.ts +324 -0
  134. package/tests/issue-creation-skill.test.ts +22 -0
  135. package/tests/model-routing-authority.test.ts +12 -0
  136. package/tests/native-review-capability-contract.test.ts +12 -1
  137. package/tests/native-review-cli.test.ts +277 -3
  138. package/tests/native-review-parity.test.ts +14 -7
  139. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  140. package/tests/orchestrator-presence.test.ts +389 -0
  141. package/tests/package-manifest.test.ts +232 -7
  142. package/tests/profiles-orchestrator.test.ts +208 -0
  143. package/tests/quiet-tool-rendering.test.ts +1 -0
  144. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  145. package/tests/review-agent-end-preflight.test.ts +332 -24
  146. package/tests/review-candidate-view.test.ts +304 -6
  147. package/tests/review-consent-ui.test.ts +352 -0
  148. package/tests/review-contract-prompt.test.ts +14 -0
  149. package/tests/review-controller-native-routing.test.ts +563 -3
  150. package/tests/review-controller.test.ts +1 -1
  151. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  152. package/tests/review-host-relay-routing.test.ts +364 -4
  153. package/tests/review-host-relay.test.ts +29 -0
  154. package/tests/review-integration-v2-forward.test.ts +44 -0
  155. package/tests/review-integration-v2.test.ts +164 -0
  156. package/tests/review-last-event-closure.test.ts +105 -1
  157. package/tests/review-ledger-contract.test.ts +61 -6
  158. package/tests/review-reminder-receipt.test.ts +62 -0
  159. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  160. package/tests/review-session-standing-permission.test.ts +30 -0
  161. package/tests/runtime-harness.mjs +447 -39
  162. package/tests/runtime-metrics-children.test.ts +206 -0
  163. package/tests/runtime-metrics-delivery.test.ts +85 -0
  164. package/tests/runtime-metrics-extension.test.ts +187 -0
  165. package/tests/runtime-metrics-native.test.ts +209 -0
  166. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  167. package/tests/runtime-metrics-policy.test.ts +62 -0
  168. package/tests/runtime-metrics.test.ts +184 -0
  169. package/tests/sdd-agent-tools.test.ts +10 -1
  170. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  171. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  172. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  173. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  174. package/tests/sdd-preflight.test.ts +252 -8
  175. package/tests/sdd-research-capabilities.test.ts +256 -0
  176. package/tests/sdd-research-live.test.ts +241 -0
  177. package/tests/sdd-selection-transport.test.ts +504 -0
  178. package/tests/sdd-status.test.ts +51 -0
  179. package/tests/session-worktree-registry.test.ts +135 -0
  180. package/tests/shell-card.test.ts +24 -3
  181. package/tests/shell-changes-view.test.ts +471 -8
  182. package/tests/shell-changes.test.ts +168 -0
  183. package/tests/shell-prompt.test.ts +28 -6
  184. package/tests/shell-sidebar-banner.test.ts +23 -0
  185. package/tests/shell-sidebar-layout.test.ts +387 -0
  186. package/tests/shell-sidebar.test.ts +50 -0
  187. package/tests/shell-todo.test.ts +100 -11
  188. package/tests/startup-banner.test.ts +126 -0
  189. package/tests/telemetry-trigger.test.ts +3 -1
@@ -910,6 +910,78 @@ test("ordinary START binds the native workspace candidate and returns the native
910
910
  assert.equal(startCalls, 1);
911
911
  });
912
912
 
913
+ test("ordinary START preserves sanitized foreign diagnostics through one ambiguous reconciliation", async (t) => {
914
+ const cwd = repository(t);
915
+ const target = startStatus(cwd);
916
+ let targetCalls = 0;
917
+ let startCalls = 0;
918
+ const native = {
919
+ targetStatus: async () => {
920
+ targetCalls += 1;
921
+ return target;
922
+ },
923
+ start: async () => {
924
+ startCalls += 1;
925
+ throw {
926
+ name: "NativeReviewCliError",
927
+ code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
928
+ mutationOutcome: "unknown",
929
+ nextAction: "review.status",
930
+ diagnostics: {
931
+ operation: "review/start",
932
+ error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
933
+ exit_code: 1,
934
+ timed_out: false,
935
+ output_limit_exceeded: false,
936
+ stderr: "token=secret",
937
+ },
938
+ };
939
+ },
940
+ } as unknown as NativeReviewCli;
941
+
942
+ const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
943
+ assert.equal(result.outcome, "native-mutation-status-reconciled");
944
+ assert.equal(result.mutation_outcome, "unknown");
945
+ assert.deepEqual(result.diagnostics, {
946
+ operation: "review/start",
947
+ error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
948
+ exit_code: 1,
949
+ timed_out: false,
950
+ output_limit_exceeded: false,
951
+ stderr: "token=[REDACTED]",
952
+ });
953
+ assert.match(String(result.required_status_action), /Run target-scoped review\.status/);
954
+ assert.equal(result.next_action, "start");
955
+ assert.equal(targetCalls, 2, "START failure reconciles STATUS exactly once");
956
+ assert.equal(startCalls, 1, "reconciliation never replays START");
957
+ });
958
+
959
+ test("ambiguous START reconciliation preserves the provider-selected recovery directive", async (t) => {
960
+ const cwd = repository(t), recoveryStatus = status("recovery-lineage");
961
+ recoveryStatus.action = "recover";
962
+ recoveryStatus.actionDisposition = "escalated";
963
+ recoveryStatus.nextTransition = { kind: "stop", reasonCode: "manual_intervention_required" };
964
+ let targetCalls = 0, startCalls = 0;
965
+ const native = {
966
+ targetStatus: async () => {
967
+ targetCalls += 1;
968
+ return targetCalls === 1 ? startStatus(cwd) : recoveryStatus;
969
+ },
970
+ start: async () => {
971
+ startCalls += 1;
972
+ throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, "review/start", true, true, "unknown mutation");
973
+ },
974
+ } as unknown as NativeReviewCli;
975
+
976
+ const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
977
+ assert.equal(result.outcome, "native-mutation-status-reconciled");
978
+ assert.equal(result.mutation_outcome, "unknown");
979
+ assert.equal(result.next_action, "recover-with-provider-disposition");
980
+ assert.equal(result.required_status_action, "Use only the provider-selected recovery disposition; do not substitute scope_changed, invalidated, or escalated.");
981
+ assert.equal(targetCalls, 2, "START failure reconciles STATUS exactly once");
982
+ assert.equal(startCalls, 1, "reconciliation never replays START");
983
+ });
984
+
913
985
  for (const statusSchema of ["gentle-ai.review-integration.status/v6", "gentle-ai.review-integration.status/v7"]) test(`pre-lineage intended-untracked selection revalidates and starts at an explicit workspace root (${statusSchema})`, async (t) => {
914
986
  const cwd = realpathSync(repository(t)), sessionCwd = repository(t), eligible = "selected.md";
915
987
  writeFileSync(join(cwd, eligible), "selected\n");
@@ -953,6 +1025,470 @@ for (const statusSchema of ["gentle-ai.review-integration.status/v6", "gentle-ai
953
1025
  assert.equal(requests.every((request) => !("lineageId" in request)), true);
954
1026
  });
955
1027
 
1028
+ // gentle-pi#706: inspect names the exact continuation for the intended-untracked
1029
+ // stop and can resolve it in one call through top-level untrackedScope.
1030
+ function untrackedStopFixture(
1031
+ t: test.TestContext,
1032
+ statusSchema = "gentle-ai.review-integration.status/v7",
1033
+ ): {
1034
+ cwd: string;
1035
+ eligible: string;
1036
+ initial: ReviewStatusV3;
1037
+ target: ReviewStatusV3;
1038
+ selection: ReviewCollectInputV3;
1039
+ } {
1040
+ const cwd = repository(t),
1041
+ eligible = "selected.md";
1042
+ writeFileSync(join(cwd, eligible), "selected\n");
1043
+ const initialTarget = startStatus(cwd),
1044
+ target = startStatus(cwd, undefined, [eligible]);
1045
+ const selection: ReviewCollectInputV3 = {
1046
+ name: "intended_untracked_selection",
1047
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
1048
+ captureOperation: "external.select_intended_untracked",
1049
+ arguments: [
1050
+ { name: "target_identity", value: SHA },
1051
+ { name: "projection", value: "workspace" },
1052
+ { name: "base_tree", value: initialTarget.projection.baseTree },
1053
+ {
1054
+ name: "candidate_tree",
1055
+ value: initialTarget.projection.currentCandidateTree,
1056
+ },
1057
+ { name: "eligible_paths_json", value: JSON.stringify([eligible]) },
1058
+ { name: "expected_untracked_inventory", value: SHA },
1059
+ ],
1060
+ submission: {
1061
+ operationToken: "status",
1062
+ argumentTokens: [
1063
+ "--contract=gentle-ai.review-integration/v2",
1064
+ "--next-transition=true",
1065
+ "--agent=pi",
1066
+ "--projection=workspace",
1067
+ "--intended-untracked-selection={{value}}",
1068
+ ],
1069
+ values: [
1070
+ {
1071
+ slot: "intended_untracked_selection",
1072
+ domain: "schema_bound_json",
1073
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
1074
+ substitutionLocation: 4,
1075
+ },
1076
+ ],
1077
+ },
1078
+ };
1079
+ const initial = {
1080
+ ...initialTarget,
1081
+ nextTransition: {
1082
+ kind: "collect",
1083
+ reasonCode: "intended_untracked_selection_required",
1084
+ collect: { inputs: [selection] },
1085
+ },
1086
+ raw: { schema: statusSchema },
1087
+ } as ReviewStatusV3;
1088
+ return { cwd, eligible, initial, target, selection };
1089
+ }
1090
+
1091
+ for (const statusSchema of [
1092
+ "gentle-ai.review-integration.status/v6",
1093
+ "gentle-ai.review-integration.status/v7",
1094
+ ])
1095
+ test(`inspect on the intended-untracked stop returns nextStep and the selection binding (${statusSchema})`, async (t) => {
1096
+ const { cwd, initial } = untrackedStopFixture(t, statusSchema);
1097
+ let targetCalls = 0;
1098
+ const native = {
1099
+ targetStatus: async () => {
1100
+ targetCalls += 1;
1101
+ return initial;
1102
+ },
1103
+ } as unknown as NativeReviewCli;
1104
+ const result = await __testing.executeReviewControllerOperation(
1105
+ { operation: "inspect" },
1106
+ cwd,
1107
+ native,
1108
+ );
1109
+ assert.equal(result.status, "blocked");
1110
+ assert.equal(typeof result.selectionBinding, "string");
1111
+ assert.equal(typeof result.nextStep, "string");
1112
+ for (const fragment of [
1113
+ "select-intended-untracked",
1114
+ "untrackedScope",
1115
+ ".gitignore",
1116
+ "path names",
1117
+ ]) {
1118
+ assert.equal((result.nextStep as string).includes(fragment), true, fragment);
1119
+ }
1120
+ assert.equal(targetCalls, 1);
1121
+ });
1122
+
1123
+ test("inspect with untrackedScope exclude resolves the intended-untracked stop in one round trip", async (t) => {
1124
+ const { cwd, initial, target, selection } = untrackedStopFixture(t);
1125
+ const requests: Array<Record<string, unknown>> = [],
1126
+ retained = new Map();
1127
+ const native = {
1128
+ targetStatus: async (request: Record<string, unknown>) => {
1129
+ requests.push(request);
1130
+ return "intendedUntrackedSelection" in request ? target : initial;
1131
+ },
1132
+ } as unknown as NativeReviewCli;
1133
+ const result = await __testing.executeReviewControllerOperation(
1134
+ { operation: "inspect", untrackedScope: "exclude" },
1135
+ cwd,
1136
+ native,
1137
+ undefined,
1138
+ undefined,
1139
+ undefined,
1140
+ retained,
1141
+ );
1142
+ assert.equal(result.status, "ready");
1143
+ assert.equal("selectionBinding" in result, false);
1144
+ assert.equal(requests.length, 2);
1145
+ assert.equal(requests[1]!.untrackedScope, "exclude");
1146
+ assert.equal(requests[1]!.expectedUntrackedInventory, SHA);
1147
+ assert.deepEqual(requests[1]!.intendedUntracked, []);
1148
+ assert.deepEqual(requests[1]!.intendedUntrackedSelection, {
1149
+ argumentTokens: selection.submission!.argumentTokens,
1150
+ value: JSON.stringify({
1151
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
1152
+ untracked_scope: "exclude",
1153
+ expected_untracked_inventory: SHA,
1154
+ intended_untracked: [],
1155
+ }),
1156
+ });
1157
+ const retainedEntry = retained.get(`${cwd}\u0000`) as
1158
+ | { untrackedScope: string; submission?: unknown }
1159
+ | undefined;
1160
+ assert.notEqual(retainedEntry, undefined);
1161
+ assert.equal(retainedEntry!.untrackedScope, "exclude");
1162
+ assert.notEqual(retainedEntry!.submission, undefined);
1163
+ });
1164
+
1165
+ test("inspect with untrackedScope select resolves the stop with exactly the selected paths", async (t) => {
1166
+ const { cwd, eligible, initial, target } = untrackedStopFixture(t);
1167
+ const requests: Array<Record<string, unknown>> = [];
1168
+ const native = {
1169
+ targetStatus: async (request: Record<string, unknown>) => {
1170
+ requests.push(request);
1171
+ return "intendedUntrackedSelection" in request ? target : initial;
1172
+ },
1173
+ } as unknown as NativeReviewCli;
1174
+ const result = await __testing.executeReviewControllerOperation(
1175
+ {
1176
+ operation: "inspect",
1177
+ untrackedScope: "select",
1178
+ intendedUntracked: [eligible],
1179
+ },
1180
+ cwd,
1181
+ native,
1182
+ );
1183
+ assert.equal(result.status, "ready");
1184
+ assert.equal(requests.length, 2);
1185
+ assert.equal(requests[1]!.untrackedScope, "select");
1186
+ assert.deepEqual(requests[1]!.intendedUntracked, [eligible]);
1187
+ const submission = requests[1]!.intendedUntrackedSelection as {
1188
+ value: string;
1189
+ };
1190
+ assert.deepEqual(JSON.parse(submission.value), {
1191
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
1192
+ untracked_scope: "select",
1193
+ expected_untracked_inventory: SHA,
1194
+ intended_untracked: [eligible],
1195
+ });
1196
+ });
1197
+
1198
+ test("inspect with untrackedScope select rejects a path outside the eligible inventory", async (t) => {
1199
+ const { cwd, initial } = untrackedStopFixture(t);
1200
+ let targetCalls = 0;
1201
+ const native = {
1202
+ targetStatus: async () => {
1203
+ targetCalls += 1;
1204
+ return initial;
1205
+ },
1206
+ } as unknown as NativeReviewCli;
1207
+ const result = await __testing.executeReviewControllerOperation(
1208
+ {
1209
+ operation: "inspect",
1210
+ untrackedScope: "select",
1211
+ intendedUntracked: ["docs/unknown.md"],
1212
+ },
1213
+ cwd,
1214
+ native,
1215
+ );
1216
+ assert.equal(result.status, "blocked");
1217
+ assert.equal(result.outcome, "inspect-untracked-scope-invalid");
1218
+ assert.equal(result.mutation_performed, false);
1219
+ assert.equal(result.mutation_outcome, "none");
1220
+ assert.equal(targetCalls, 1);
1221
+ });
1222
+
1223
+ for (const invalid of [
1224
+ { untrackedScope: "select" },
1225
+ { untrackedScope: "exclude", intendedUntracked: ["selected.md"] },
1226
+ ] as const)
1227
+ test(`inspect rejects an inconsistent untracked scope combination (${JSON.stringify(invalid)})`, async (t) => {
1228
+ const { cwd, initial } = untrackedStopFixture(t);
1229
+ let targetCalls = 0;
1230
+ const native = {
1231
+ targetStatus: async () => {
1232
+ targetCalls += 1;
1233
+ return initial;
1234
+ },
1235
+ } as unknown as NativeReviewCli;
1236
+ if (invalid.untrackedScope === "exclude") {
1237
+ await assert.rejects(
1238
+ () => __testing.executeReviewControllerOperation({ operation: "inspect", ...invalid }, cwd, native),
1239
+ /requires untrackedScope select/,
1240
+ );
1241
+ assert.equal(targetCalls, 0);
1242
+ return;
1243
+ }
1244
+ const result = await __testing.executeReviewControllerOperation(
1245
+ { operation: "inspect", ...invalid },
1246
+ cwd,
1247
+ native,
1248
+ );
1249
+ assert.equal(result.status, "blocked");
1250
+ assert.equal(result.outcome, "inspect-untracked-scope-invalid");
1251
+ assert.equal(result.mutation_performed, false);
1252
+ assert.equal(result.mutation_outcome, "none");
1253
+ assert.equal(targetCalls, 1);
1254
+ });
1255
+
1256
+ test("inspect with untrackedScope and no intended-untracked stop reports not-required", async (t) => {
1257
+ const { cwd, target } = untrackedStopFixture(t);
1258
+ let targetCalls = 0;
1259
+ const native = {
1260
+ targetStatus: async () => {
1261
+ targetCalls += 1;
1262
+ return target;
1263
+ },
1264
+ } as unknown as NativeReviewCli;
1265
+ const result = await __testing.executeReviewControllerOperation(
1266
+ { operation: "inspect", untrackedScope: "exclude" },
1267
+ cwd,
1268
+ native,
1269
+ );
1270
+ assert.equal(result.status, "ready");
1271
+ assert.equal(result.untracked_selection, "not-required");
1272
+ assert.equal(targetCalls, 1);
1273
+ });
1274
+
1275
+ test("plain START adopts the pre-lineage selection retained by inspect and deletes it after success", async (t) => {
1276
+ const { cwd, eligible, initial, target, selection } = untrackedStopFixture(t);
1277
+ const requests: Array<Record<string, unknown>> = [],
1278
+ starts: Array<Record<string, unknown>> = [],
1279
+ retained = new Map();
1280
+ const native = {
1281
+ targetStatus: async (request: Record<string, unknown>) => {
1282
+ requests.push(request);
1283
+ return "intendedUntrackedSelection" in request ? target : initial;
1284
+ },
1285
+ start: async (request: Record<string, unknown>) => {
1286
+ starts.push(request);
1287
+ return {
1288
+ lineageId: "review-started",
1289
+ state: "reviewing",
1290
+ riskLevel: "low",
1291
+ selectedLenses: [],
1292
+ changedFiles: 1,
1293
+ changedLines: 1,
1294
+ correctionBudget: 1,
1295
+ action: "created",
1296
+ lensesRequired: false,
1297
+ riskReasons: [],
1298
+ raw: {},
1299
+ };
1300
+ },
1301
+ } as unknown as NativeReviewCli;
1302
+ const resolved = await __testing.executeReviewControllerOperation(
1303
+ {
1304
+ operation: "inspect",
1305
+ untrackedScope: "select",
1306
+ intendedUntracked: [eligible],
1307
+ },
1308
+ cwd,
1309
+ native,
1310
+ undefined,
1311
+ undefined,
1312
+ undefined,
1313
+ retained,
1314
+ );
1315
+ assert.equal(resolved.status, "ready");
1316
+ const started = await __testing.executeReviewControllerOperation(
1317
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
1318
+ cwd,
1319
+ native,
1320
+ undefined,
1321
+ undefined,
1322
+ undefined,
1323
+ retained,
1324
+ );
1325
+ assert.equal(started.operation, "start");
1326
+ assert.equal(started.result.lineage_id, "review-started");
1327
+ assert.equal(starts.length, 1);
1328
+ assert.equal(requests.length, 3);
1329
+ const startStatusRequest = requests[2]!;
1330
+ assert.equal(startStatusRequest.untrackedScope, "select");
1331
+ assert.equal(startStatusRequest.expectedUntrackedInventory, SHA);
1332
+ assert.deepEqual(startStatusRequest.intendedUntracked, [eligible]);
1333
+ assert.deepEqual(startStatusRequest.intendedUntrackedSelection, {
1334
+ argumentTokens: selection.submission!.argumentTokens,
1335
+ value: JSON.stringify({
1336
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
1337
+ untracked_scope: "select",
1338
+ expected_untracked_inventory: SHA,
1339
+ intended_untracked: [eligible],
1340
+ }),
1341
+ });
1342
+ assert.equal(starts[0]!.untrackedScope, "select");
1343
+ assert.equal(retained.has(`${cwd}\u0000`), false);
1344
+ const lineageEntry = retained.get(`${cwd}\u0000review-started`) as
1345
+ | { untrackedScope: string; submission?: unknown }
1346
+ | undefined;
1347
+ assert.notEqual(lineageEntry, undefined);
1348
+ assert.equal(lineageEntry!.untrackedScope, "select");
1349
+ assert.notEqual(lineageEntry!.submission, undefined);
1350
+ });
1351
+
1352
+ test("untrackedScope is accepted only by the inspect operation", async (t) => {
1353
+ const { cwd, initial } = untrackedStopFixture(t);
1354
+ const native = {
1355
+ targetStatus: async () => initial,
1356
+ } as unknown as NativeReviewCli;
1357
+ await assert.rejects(
1358
+ () =>
1359
+ __testing.executeReviewControllerOperation(
1360
+ { operation: "status", untrackedScope: "exclude" } as never,
1361
+ cwd,
1362
+ native,
1363
+ ),
1364
+ /does not accept untrackedScope/,
1365
+ );
1366
+ await assert.rejects(
1367
+ () =>
1368
+ __testing.executeReviewControllerOperation(
1369
+ {
1370
+ operation: "start",
1371
+ input: JSON.stringify({ mode: "ordinary" }),
1372
+ untrackedScope: "exclude",
1373
+ } as never,
1374
+ cwd,
1375
+ native,
1376
+ ),
1377
+ /does not accept untrackedScope/,
1378
+ );
1379
+ });
1380
+
1381
+ test("top-level intendedUntracked is accepted only by inspect select", async (t) => {
1382
+ const { cwd, initial } = untrackedStopFixture(t);
1383
+ const native = { targetStatus: async () => initial } as unknown as NativeReviewCli;
1384
+ for (const parameters of [
1385
+ { operation: "inspect", intendedUntracked: [] },
1386
+ { operation: "inspect", untrackedScope: "exclude", intendedUntracked: [] },
1387
+ { operation: "status", intendedUntracked: [] },
1388
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }), intendedUntracked: [] },
1389
+ ] as const) {
1390
+ await assert.rejects(
1391
+ () => __testing.executeReviewControllerOperation(parameters as never, cwd, native),
1392
+ /intendedUntracked/,
1393
+ JSON.stringify(parameters),
1394
+ );
1395
+ }
1396
+ });
1397
+
1398
+ test("a fresh unsuccessful inspect invalidates its prior pre-lineage selection", async (t) => {
1399
+ const { cwd, eligible, initial, target } = untrackedStopFixture(t);
1400
+ const retained = new Map();
1401
+ let failInspect = false;
1402
+ const native = {
1403
+ targetStatus: async (request: Record<string, unknown>) => {
1404
+ if (failInspect) throw new Error("inspect unavailable");
1405
+ return "intendedUntrackedSelection" in request ? target : initial;
1406
+ },
1407
+ } as unknown as NativeReviewCli;
1408
+ await __testing.executeReviewControllerOperation(
1409
+ { operation: "inspect", untrackedScope: "select", intendedUntracked: [eligible] },
1410
+ cwd, native, undefined, undefined, undefined, retained,
1411
+ );
1412
+ assert.equal(retained.has(`${cwd}\u0000`), true);
1413
+ failInspect = true;
1414
+ const failed = await __testing.executeReviewControllerOperation(
1415
+ { operation: "inspect" }, cwd, native, undefined, undefined, undefined, retained,
1416
+ );
1417
+ assert.equal(failed.outcome, "native-status-unavailable");
1418
+ assert.equal(retained.has(`${cwd}\u0000`), false);
1419
+ });
1420
+
1421
+ test("START rejects a retained pre-lineage selection when fresh STATUS identifies a changed candidate", async (t) => {
1422
+ const { cwd, eligible, initial, target } = untrackedStopFixture(t);
1423
+ const retained = new Map();
1424
+ const candidateA = {
1425
+ ...target,
1426
+ targetIdentity: "target-a",
1427
+ projection: { ...target.projection, currentCandidateTree: "candidate-a" },
1428
+ } as ReviewStatusV3;
1429
+ const candidateB = {
1430
+ ...target,
1431
+ targetIdentity: "target-b",
1432
+ projection: { ...target.projection, currentCandidateTree: "candidate-b" },
1433
+ } as ReviewStatusV3;
1434
+ let resolved = false;
1435
+ let starts = 0;
1436
+ const native = {
1437
+ targetStatus: async (request: Record<string, unknown>) => {
1438
+ if (!("intendedUntrackedSelection" in request)) return initial;
1439
+ if (!resolved) { resolved = true; return candidateA; }
1440
+ return candidateB;
1441
+ },
1442
+ start: async () => {
1443
+ starts += 1;
1444
+ return { lineageId: "unexpected", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} };
1445
+ },
1446
+ } as unknown as NativeReviewCli;
1447
+ await __testing.executeReviewControllerOperation(
1448
+ { operation: "inspect", untrackedScope: "select", intendedUntracked: [eligible] },
1449
+ cwd, native, undefined, null, undefined, retained,
1450
+ );
1451
+ const rejected = await __testing.executeReviewControllerOperation(
1452
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
1453
+ cwd, native, undefined, null, undefined, retained,
1454
+ );
1455
+ assert.equal(rejected.outcome, "native-start-retained-selection-candidate-mismatch");
1456
+ assert.equal(starts, 0);
1457
+ assert.equal(retained.has(`${cwd}\u0000`), false);
1458
+ });
1459
+
1460
+ test("a failed START retains a pre-lineage selection for a same-candidate retry", async (t) => {
1461
+ const { cwd, eligible, initial, target } = untrackedStopFixture(t);
1462
+ const retained = new Map();
1463
+ let starts = 0;
1464
+ const native = {
1465
+ targetStatus: async (request: Record<string, unknown>) =>
1466
+ "intendedUntrackedSelection" in request ? target : initial,
1467
+ start: async () => {
1468
+ starts += 1;
1469
+ if (starts === 1) throw Object.assign(new Error("retryable failure"), { mutationOutcome: "none" });
1470
+ return { lineageId: "retried", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} };
1471
+ },
1472
+ } as unknown as NativeReviewCli;
1473
+ await __testing.executeReviewControllerOperation(
1474
+ { operation: "inspect", untrackedScope: "select", intendedUntracked: [eligible] },
1475
+ cwd, native, undefined, null, undefined, retained,
1476
+ );
1477
+ const failed = await __testing.executeReviewControllerOperation(
1478
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
1479
+ cwd, native, undefined, null, undefined, retained,
1480
+ );
1481
+ assert.equal(failed.mutation_outcome, "none");
1482
+ assert.equal(retained.has(`${cwd}\u0000`), true);
1483
+ const retried = await __testing.executeReviewControllerOperation(
1484
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
1485
+ cwd, native, undefined, null, undefined, retained,
1486
+ );
1487
+ assert.equal(retried.result.lineage_id, "retried");
1488
+ assert.equal(starts, 2);
1489
+ assert.equal(retained.has(`${cwd}\u0000`), false);
1490
+ });
1491
+
956
1492
  test("ordinary START relays native consent without authoring or advancing it", async (t) => {
957
1493
  const cwd = repository(t);
958
1494
  const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
@@ -1003,6 +1539,24 @@ test("STATUS preserves a native process failure without inventing authority reco
1003
1539
  assert.equal(result.mutation_outcome, "none");
1004
1540
  });
1005
1541
 
1542
+ test("foreign native errors with malformed diagnostics remain untrusted", async () => {
1543
+ const native = {
1544
+ targetStatus: async () => { throw {
1545
+ name: "NativeReviewCliError",
1546
+ code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
1547
+ diagnostics: {
1548
+ operation: "review/status",
1549
+ error_code: "forged-code",
1550
+ timed_out: false,
1551
+ output_limit_exceeded: false,
1552
+ },
1553
+ }; },
1554
+ } as unknown as NativeReviewCli;
1555
+ const result = await __testing.executeReviewControllerOperation({ operation: "status" }, process.cwd(), native);
1556
+ assert.equal(result.outcome, "native-operation-failed");
1557
+ assert.equal("diagnostics" in result, false);
1558
+ });
1559
+
1006
1560
  test("STATUS preserves ambiguous native status as read-only provider-owned state", async () => {
1007
1561
  const lineageId = "ambiguous-lineage";
1008
1562
  const native = {
@@ -1382,13 +1936,17 @@ test("START and consent ambiguity reconciliation register their returned committ
1382
1936
  const lineageId = "reconciled-collect", input = correctionPlanInput(lineageId), unknown = () => { throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, "review/start", true, true, "unknown mutation"); };
1383
1937
  const selectors = (requests: readonly Record<string, unknown>[]) => requests.map(({ baseRef: base, committedOnly }) => ({ baseRef: base, committedOnly }));
1384
1938
  const directRequests: Array<Record<string, unknown>> = [], directRoutes = new Map();
1939
+ let directStartCalls = 0;
1385
1940
  const directNative = {
1386
1941
  targetStatus: async (request: Record<string, unknown>) => { directRequests.push(request); return directRequests.length === 1 ? startStatus(cwd, baseRef) : status(lineageId, [input]); },
1387
- start: unknown,
1942
+ start: () => { directStartCalls += 1; return unknown(); },
1388
1943
  captureCorrectionPlan: async () => ({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-correction-plan", lineageId, state: "correction_required", storeRevision: SHA }),
1389
1944
  } as unknown as NativeReviewCli;
1390
- await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef, committedOnly: true }) }, cwd, directNative, undefined, undefined, undefined, directRoutes);
1391
- const directCapture = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), correctionLines: 1 }, cwd, directNative, undefined, undefined, directRoutes, true);
1945
+ const directStart = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef, committedOnly: true }) }, cwd, directNative, undefined, undefined, undefined, directRoutes);
1946
+ assert.deepEqual({ outcome: directStart.outcome, status: directStart.status, mutationOutcome: directStart.mutation_outcome, startCalls: directStartCalls, statusCalls: directRequests.length }, { outcome: "native-mutation-status-reconciled", status: "blocked", mutationOutcome: "unknown", startCalls: 1, statusCalls: 2 });
1947
+ assert.equal((directStart.diagnostics as { error_code?: string }).error_code, NATIVE_REVIEW_ERROR_CODE.NON_ZERO);
1948
+ assert.equal("next_action" in directStart, false);
1949
+ const directCapture = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: bindingOf(directStart), correctionLines: 1 }, cwd, directNative, undefined, undefined, directRoutes, true);
1392
1950
  const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
1393
1951
  const consentRequests: Array<Record<string, unknown>> = [], consentRoutes = new Map(), registry = new PendingReviewConsentRegistry(), session = Symbol("consent-session");
1394
1952
  const consentNative = {
@@ -1403,9 +1961,11 @@ test("START and consent ambiguity reconciliation register their returned committ
1403
1961
  const consentCapture = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), correctionLines: 1 }, cwd, consentNative, undefined, undefined, consentRoutes, true);
1404
1962
  assert.deepEqual({
1405
1963
  outcomes: [directCapture.outcome, consentCapture.outcome],
1964
+ directStartCalls,
1406
1965
  selectors: [selectors(directRequests), selectors(consentRequests)],
1407
1966
  }, {
1408
1967
  outcomes: ["native-last-event-closure", "native-last-event-closure"],
1968
+ directStartCalls: 1,
1409
1969
  selectors: [Array.from({ length: 3 }, () => ({ baseRef, committedOnly: true })), Array.from({ length: 3 }, () => ({ baseRef, committedOnly: true }))],
1410
1970
  });
1411
1971
  });
@@ -208,7 +208,7 @@ test("controller SDD status treats removed OpenSpec recovery authority and delet
208
208
  const status = await __testing.resolveControllerSddStatus(fixture.repository, changeName, false, "openspec");
209
209
 
210
210
  assert.equal(status.dependencies.archive, "blocked");
211
- assert.equal(status.nextRecommended, "Active change not found: recover-legacy-review-authority.");
211
+ assert.equal(status.nextRecommended, "blocked");
212
212
  assert.match(status.blockedReasons.join("\n"), /active change not found/i);
213
213
  });
214
214