gentle-pi 2.4.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.
- package/README.md +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -184,6 +184,44 @@ test("failure enforces exact keys, enums, and identifiers", () => {
|
|
|
184
184
|
assert.throws(() => decodeReviewFailureV2(badCode), /code/);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
test("failure with code managed_assets_outdated decodes its continuation, degrades without one, and forbids it elsewhere", () => {
|
|
188
|
+
const source: JsonObject = {
|
|
189
|
+
schema: "gentle-ai.review-integration.failure/v2",
|
|
190
|
+
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
191
|
+
operation: "review.start",
|
|
192
|
+
phase: "preflight",
|
|
193
|
+
code: "managed_assets_outdated",
|
|
194
|
+
message: "the managed asset set is stale",
|
|
195
|
+
mutation_outcome: "not_started",
|
|
196
|
+
authority_applicability: "current_target",
|
|
197
|
+
retry_safe: true,
|
|
198
|
+
replayability: "manual_action_required",
|
|
199
|
+
required_inputs: [],
|
|
200
|
+
next_action: "explicit-maintainer-action",
|
|
201
|
+
continuation: { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"] },
|
|
202
|
+
};
|
|
203
|
+
assert.deepEqual(decodeReviewFailureV2(source).continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
|
|
204
|
+
|
|
205
|
+
const missingCommand = clone(source);
|
|
206
|
+
delete (missingCommand.continuation as JsonObject).command;
|
|
207
|
+
assert.throws(() => decodeReviewFailureV2(missingCommand), /command.*required|required.*command/);
|
|
208
|
+
|
|
209
|
+
// an older gentle-ai may emit managed_assets_outdated with no continuation
|
|
210
|
+
// at all; decode must not refuse the whole envelope for that
|
|
211
|
+
const missingContinuation = clone(source);
|
|
212
|
+
delete missingContinuation.continuation;
|
|
213
|
+
assert.equal(decodeReviewFailureV2(missingContinuation).continuation, undefined);
|
|
214
|
+
|
|
215
|
+
const unexpectedContinuation: JsonObject = { ...clone(source), code: "gate_scope_changed" };
|
|
216
|
+
assert.throws(() => decodeReviewFailureV2(unexpectedContinuation), /continuation is only valid/);
|
|
217
|
+
|
|
218
|
+
// an envelope without continuation, on a code that does not require one, is unchanged
|
|
219
|
+
const withoutContinuation = clone(source);
|
|
220
|
+
withoutContinuation.code = "gate_scope_changed";
|
|
221
|
+
delete withoutContinuation.continuation;
|
|
222
|
+
assert.equal(decodeReviewFailureV2(withoutContinuation).continuation, undefined);
|
|
223
|
+
});
|
|
224
|
+
|
|
187
225
|
test("net-new decoders: consent, next-transition, and artifact-subject reject malformed payloads", () => {
|
|
188
226
|
const consentSource = fixture<JsonObject>("consent.fixture.json");
|
|
189
227
|
assertRequired(decodeReviewConsentV2, consentSource, ["schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path"]);
|
|
@@ -405,6 +443,205 @@ test("next_transition decodes an execute variant and rejects a stop that carries
|
|
|
405
443
|
assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
|
|
406
444
|
});
|
|
407
445
|
|
|
446
|
+
// gentle-pi#627: gentle-ai reports a stale managed-asset set as a typed stop
|
|
447
|
+
// carrying the exact `gentle-ai sync` invocation that resolves it.
|
|
448
|
+
function managedAssetsContinuation(overrides: Partial<JsonObject> = {}): JsonObject {
|
|
449
|
+
return { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"], ...overrides };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
test("next_transition stop decodes a managed_assets_outdated continuation and rejects a malformed one", () => {
|
|
453
|
+
const stop: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: managedAssetsContinuation() };
|
|
454
|
+
const decoded = decodeReviewNextTransitionV3(stop);
|
|
455
|
+
assert.deepEqual(decoded.continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
|
|
456
|
+
|
|
457
|
+
const bare: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: { operation: "sync", command: "gentle-ai sync" } };
|
|
458
|
+
assert.deepEqual(decodeReviewNextTransitionV3(bare).continuation, { operation: "sync", command: "gentle-ai sync" });
|
|
459
|
+
|
|
460
|
+
const missingCommand = clone(stop);
|
|
461
|
+
delete (missingCommand.continuation as JsonObject).command;
|
|
462
|
+
assert.throws(() => decodeReviewNextTransitionV3(missingCommand), /command.*required|required.*command/);
|
|
463
|
+
|
|
464
|
+
const emptyCommand = clone(stop);
|
|
465
|
+
(emptyCommand.continuation as JsonObject).command = "";
|
|
466
|
+
assert.throws(() => decodeReviewNextTransitionV3(emptyCommand), /command/);
|
|
467
|
+
|
|
468
|
+
// an older gentle-ai may emit this stop with no continuation at all; decode
|
|
469
|
+
// must not refuse the whole envelope for that
|
|
470
|
+
const missingForReasonCode = { kind: "stop", reason_code: "managed_assets_outdated" };
|
|
471
|
+
assert.equal(decodeReviewNextTransitionV3(missingForReasonCode).continuation, undefined);
|
|
472
|
+
|
|
473
|
+
const unexpectedOnOtherReasonCode = { kind: "stop", reason_code: "rdd_disabled", continuation: managedAssetsContinuation() };
|
|
474
|
+
assert.throws(() => decodeReviewNextTransitionV3(unexpectedOnOtherReasonCode), /continuation is only valid/);
|
|
475
|
+
|
|
476
|
+
// envelopes without continuation stay unchanged
|
|
477
|
+
const plainStop = decodeReviewNextTransitionV3({ kind: "stop", reason_code: "rdd_disabled" });
|
|
478
|
+
assert.equal(plainStop.continuation, undefined);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// gentle-pi#638: after the host relay declares a selected lens slot
|
|
482
|
+
// unachievable through the native capture-unachievable verb, gentle-ai's
|
|
483
|
+
// STATUS stops with reason_code unachievable_lens_slot and carries one entry
|
|
484
|
+
// per declared slot, each naming the complete withdraw command
|
|
485
|
+
// (contracts/review-integration/v2/schemas/status-v7.schema.json, the stop
|
|
486
|
+
// variant). A restart that never saw the collect offer must still be able to
|
|
487
|
+
// retract the declaration from the stop alone, so the withdraw binding is
|
|
488
|
+
// decoded, never dropped.
|
|
489
|
+
function unachievableSlot(overrides: Partial<JsonObject> = {}): JsonObject {
|
|
490
|
+
return {
|
|
491
|
+
lens: "review-risk",
|
|
492
|
+
selected_order: 0,
|
|
493
|
+
subject_hash: digest,
|
|
494
|
+
reason: "relay_transport_bound_exceeded",
|
|
495
|
+
withdraw: {
|
|
496
|
+
operation: "review.capture-unachievable",
|
|
497
|
+
command: `gentle-ai review capture-unachievable --lineage=review-fixture --expected-revision=${digest} --target=${digest} --repository-context=rctx1_${"e".repeat(64)} --request-hash=${digest} --withdraw=true`,
|
|
498
|
+
arguments: [
|
|
499
|
+
{ name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" },
|
|
500
|
+
{ name: "expected-revision", value: digest, token: `--expected-revision=${digest}` },
|
|
501
|
+
{ name: "target", value: digest, token: `--target=${digest}` },
|
|
502
|
+
{ name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
|
|
503
|
+
{ name: "request-hash", value: digest, token: `--request-hash=${digest}` },
|
|
504
|
+
{ name: "withdraw", value: "true", token: "--withdraw=true" },
|
|
505
|
+
],
|
|
506
|
+
binding: { lineage_id: "review-fixture", revision: digest, target_identity: digest, repository_context: `rctx1_${"e".repeat(64)}` },
|
|
507
|
+
},
|
|
508
|
+
...overrides,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
test("next_transition stop decodes unachievable_lens_slots and their withdraw bindings only on that reason code", () => {
|
|
513
|
+
const stop: JsonObject = { kind: "stop", reason_code: "unachievable_lens_slot", unachievable_lens_slots: [unachievableSlot()] };
|
|
514
|
+
const decoded = decodeReviewNextTransitionV3(stop);
|
|
515
|
+
assert.equal(decoded.kind, "stop");
|
|
516
|
+
assert.equal(decoded.reasonCode, "unachievable_lens_slot");
|
|
517
|
+
const slot = decoded.unachievableLensSlots?.[0];
|
|
518
|
+
assert.equal(slot?.lens, "review-risk");
|
|
519
|
+
assert.equal(slot?.selectedOrder, 0);
|
|
520
|
+
assert.equal(slot?.subjectHash, digest);
|
|
521
|
+
assert.equal(slot?.reason, "relay_transport_bound_exceeded");
|
|
522
|
+
assert.equal(slot?.detail, undefined);
|
|
523
|
+
assert.equal(slot?.withdraw.operation, "review.capture-unachievable");
|
|
524
|
+
assert.deepEqual(slot?.withdraw.arguments.map((argument) => argument.token), ["--lineage=review-fixture", `--expected-revision=${digest}`, `--target=${digest}`, `--repository-context=rctx1_${"e".repeat(64)}`, `--request-hash=${digest}`, "--withdraw=true"]);
|
|
525
|
+
assert.equal(slot?.withdraw.binding.targetIdentity, digest);
|
|
526
|
+
assert.equal(slot?.withdraw.command, `gentle-ai review capture-unachievable --lineage=review-fixture --expected-revision=${digest} --target=${digest} --repository-context=rctx1_${"e".repeat(64)} --request-hash=${digest} --withdraw=true`);
|
|
527
|
+
|
|
528
|
+
// detail is the one optional field the schema declares
|
|
529
|
+
const detailed = decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ detail: "killed after 2256004ms against a 2256000ms relay bound" })] });
|
|
530
|
+
assert.equal(detailed.unachievableLensSlots?.[0]?.detail, "killed after 2256004ms against a 2256000ms relay bound");
|
|
531
|
+
|
|
532
|
+
// malformed entries are refused: a negative selected_order, a slot missing
|
|
533
|
+
// its withdraw form, a foreign withdraw operation, or an empty array
|
|
534
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ selected_order: -1 })] }), /selected_order/);
|
|
535
|
+
const withoutWithdraw = unachievableSlot();
|
|
536
|
+
delete withoutWithdraw.withdraw;
|
|
537
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [withoutWithdraw] }), /withdraw.*required|required.*withdraw/);
|
|
538
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ withdraw: { ...(unachievableSlot().withdraw as JsonObject), operation: "review.capture-result" } })] }), /operation/);
|
|
539
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [] }), /length/);
|
|
540
|
+
|
|
541
|
+
// the slots are only valid on their own exact stop reason code
|
|
542
|
+
assert.throws(() => decodeReviewNextTransitionV3({ kind: "stop", reason_code: "rdd_disabled", unachievable_lens_slots: [unachievableSlot()] }), /unachievable_lens_slots is only valid/);
|
|
543
|
+
// an execute or collect transition never carries them
|
|
544
|
+
assert.throws(() => decodeReviewNextTransitionV3({ kind: "execute", reason_code: "fresh_target_ready", execute: { operation: "review.start", arguments: [{ name: "lineage", value: "review-fixture" }], preconditions: [], binding: { target_identity: digest } }, unachievable_lens_slots: [unachievableSlot()] }), /unachievable_lens_slots is only valid/);
|
|
545
|
+
// plain stops stay unchanged
|
|
546
|
+
assert.equal(decodeReviewNextTransitionV3({ kind: "stop", reason_code: "rdd_disabled" }).unachievableLensSlots, undefined);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// gentle-pi#822: the withdraw form names the slot identity twice — as named arguments and as the binding object — and a slot whose two renderings disagree never decodes, so restart cannot withdraw a different slot.
|
|
550
|
+
test("next_transition stop refuses an unachievable slot whose withdraw arguments disagree with its identity", () => {
|
|
551
|
+
const stop: JsonObject = { kind: "stop", reason_code: "unachievable_lens_slot", unachievable_lens_slots: [unachievableSlot()] };
|
|
552
|
+
const drift = (name: string, value: string): JsonObject => {
|
|
553
|
+
const drifted = unachievableSlot();
|
|
554
|
+
const withdraw = drifted.withdraw as JsonObject;
|
|
555
|
+
withdraw.arguments = (withdraw.arguments as unknown[]).map((argument) => (argument as JsonObject).name === name ? { ...(argument as JsonObject), value } : argument);
|
|
556
|
+
return drifted;
|
|
557
|
+
};
|
|
558
|
+
for (const [name, value] of [["request-hash", `sha256:${"b".repeat(64)}`], ["target", `sha256:${"c".repeat(64)}`], ["lineage", "review-fixture-drifted"], ["expected-revision", `sha256:${"d".repeat(64)}`]] as const) {
|
|
559
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [drift(name, value)] }), new RegExp(`${name} does not match`), name);
|
|
560
|
+
}
|
|
561
|
+
// the pristine fixture still decodes with both renderings agreeing
|
|
562
|
+
assert.doesNotThrow(() => decodeReviewNextTransitionV3(stop));
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// gentle-pi#822: identity arguments appear exactly once, and the rendered
|
|
566
|
+
// command is an exact, ordered rendering of every validated provider token.
|
|
567
|
+
// Substring checks would accept a missing non-identity token, repeated command,
|
|
568
|
+
// suffix, or shell payload despite the command no longer naming this slot alone.
|
|
569
|
+
test("next_transition stop refuses duplicate identities and non-exact withdraw commands", () => {
|
|
570
|
+
const stop: JsonObject = { kind: "stop", reason_code: "unachievable_lens_slot", unachievable_lens_slots: [unachievableSlot()] };
|
|
571
|
+
const duplicated = unachievableSlot();
|
|
572
|
+
const duplicatedWithdraw = duplicated.withdraw as JsonObject;
|
|
573
|
+
duplicatedWithdraw.arguments = [...(duplicatedWithdraw.arguments as unknown[]), { name: "lineage", value: "review-fixture-smuggled", token: "--lineage=review-fixture-smuggled" }];
|
|
574
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [duplicated] }), /withdraw\.arguments lineage must appear exactly once/);
|
|
575
|
+
|
|
576
|
+
const canonical = `gentle-ai review capture-unachievable --lineage=review-fixture --expected-revision=${digest} --target=${digest} --repository-context=rctx1_${"e".repeat(64)} --request-hash=${digest} --withdraw=true`;
|
|
577
|
+
for (const [name, command] of [
|
|
578
|
+
["omitted token", canonical.replace(" --withdraw=true", "")],
|
|
579
|
+
["duplicate command", `gentle-ai review capture-unachievable ${canonical}`],
|
|
580
|
+
["extra suffix", `${canonical} --surplus=true`],
|
|
581
|
+
["shell payload", `${canonical} && printf injected`],
|
|
582
|
+
] as const) {
|
|
583
|
+
const malformed = unachievableSlot();
|
|
584
|
+
(malformed.withdraw as JsonObject).command = command;
|
|
585
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [malformed] }), /withdraw\.command must exactly render/, name);
|
|
586
|
+
}
|
|
587
|
+
assert.doesNotThrow(() => decodeReviewNextTransitionV3(stop));
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// gentle-pi#822 (CodeRabbit finding): withdrawal is a narrowly scoped, runnable
|
|
591
|
+
// retraction. Its one affirmative flag is part of the exact native vector, not
|
|
592
|
+
// just another provider-defined argument that a malformed STATUS can omit or
|
|
593
|
+
// negate while still rendering a plausible command.
|
|
594
|
+
test("next_transition stop requires exactly one canonical --withdraw=true argument", () => {
|
|
595
|
+
const stop: JsonObject = { kind: "stop", reason_code: "unachievable_lens_slot", unachievable_lens_slots: [unachievableSlot()] };
|
|
596
|
+
const canonicalCommand = (slot: JsonObject): string => {
|
|
597
|
+
const withdraw = slot.withdraw as JsonObject;
|
|
598
|
+
return `gentle-ai review capture-unachievable ${(withdraw.arguments as JsonObject[]).map((argument) => String(argument.token)).join(" ")}`;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const omitted = unachievableSlot();
|
|
602
|
+
const omittedWithdraw = omitted.withdraw as JsonObject;
|
|
603
|
+
omittedWithdraw.arguments = (omittedWithdraw.arguments as JsonObject[]).filter((argument) => argument.name !== "withdraw");
|
|
604
|
+
omittedWithdraw.command = canonicalCommand(omitted);
|
|
605
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [omitted] }), /withdraw must appear exactly once/);
|
|
606
|
+
|
|
607
|
+
const falseValue = unachievableSlot();
|
|
608
|
+
const falseWithdraw = falseValue.withdraw as JsonObject;
|
|
609
|
+
falseWithdraw.arguments = (falseWithdraw.arguments as JsonObject[]).map((argument) => argument.name === "withdraw"
|
|
610
|
+
? { ...argument, value: "false", token: "--withdraw=false" }
|
|
611
|
+
: argument);
|
|
612
|
+
falseWithdraw.command = canonicalCommand(falseValue);
|
|
613
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [falseValue] }), /withdraw must be true/);
|
|
614
|
+
|
|
615
|
+
const noncanonicalToken = unachievableSlot();
|
|
616
|
+
const tokenWithdraw = noncanonicalToken.withdraw as JsonObject;
|
|
617
|
+
tokenWithdraw.arguments = (tokenWithdraw.arguments as JsonObject[]).map((argument) => argument.name === "withdraw"
|
|
618
|
+
? { ...argument, token: "--withdraw true" }
|
|
619
|
+
: argument);
|
|
620
|
+
tokenWithdraw.command = canonicalCommand(noncanonicalToken);
|
|
621
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [noncanonicalToken] }), /token must exactly render/);
|
|
622
|
+
|
|
623
|
+
const duplicate = unachievableSlot();
|
|
624
|
+
const duplicateWithdraw = duplicate.withdraw as JsonObject;
|
|
625
|
+
duplicateWithdraw.arguments = [...duplicateWithdraw.arguments as JsonObject[], { name: "withdraw", value: "true", token: "--withdraw=true" }];
|
|
626
|
+
duplicateWithdraw.command = canonicalCommand(duplicate);
|
|
627
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [duplicate] }), /withdraw must appear exactly once/);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// gentle-pi#822 (CodeRabbit finding): Go bounds the CAPTURE_UNACHIEVABLE detail at 512 UTF-8 bytes (native-review-cli.ts enforces the same limit when declaring), so the transition decoder mirrors that bound — measured in bytes, not UTF-16 code units — and a STATUS stop can never carry a detail this client would have refused to declare.
|
|
631
|
+
test("next_transition stop bounds an unachievable slot detail at 512 UTF-8 bytes", () => {
|
|
632
|
+
const stop: JsonObject = { kind: "stop", reason_code: "unachievable_lens_slot", unachievable_lens_slots: [unachievableSlot()] };
|
|
633
|
+
// exactly 512 one-byte characters still decodes
|
|
634
|
+
assert.equal(decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ detail: "a".repeat(512) })] }).unachievableLensSlots?.[0]?.detail, "a".repeat(512));
|
|
635
|
+
// exactly 512 bytes of two-byte characters still decodes: 256 × 2
|
|
636
|
+
assert.equal(decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ detail: "é".repeat(256) })] }).unachievableLensSlots?.[0]?.detail, "é".repeat(256));
|
|
637
|
+
// 257 two-byte characters are 514 bytes
|
|
638
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ detail: "é".repeat(257) })] }), /detail exceeds 512 bytes/);
|
|
639
|
+
// 171 three-byte characters are 513 bytes
|
|
640
|
+
assert.throws(() => decodeReviewNextTransitionV3({ ...stop, unachievable_lens_slots: [unachievableSlot({ detail: "世".repeat(171) })] }), /detail exceeds 512 bytes/);
|
|
641
|
+
// an absent detail still decodes as undefined
|
|
642
|
+
assert.equal(decodeReviewNextTransitionV3(stop).unachievableLensSlots?.[0]?.detail, undefined);
|
|
643
|
+
});
|
|
644
|
+
|
|
408
645
|
function approvedAcknowledgementTransition(): JsonObject {
|
|
409
646
|
return {
|
|
410
647
|
kind: "execute",
|
|
@@ -700,6 +937,40 @@ test("v5 targeted-validator collect inputs carry the exact provider-owned valida
|
|
|
700
937
|
assert.throws(() => decodeReviewNextTransitionV3(weakenedRequest, { v5: true }), /policy_content/);
|
|
701
938
|
});
|
|
702
939
|
|
|
940
|
+
test("status.frozen carries the optional frozen manifest digest", () => {
|
|
941
|
+
const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
|
|
942
|
+
delete current.receipt;
|
|
943
|
+
if (current.action === "finalize") {
|
|
944
|
+
current.action = "stop";
|
|
945
|
+
delete current.next_transition;
|
|
946
|
+
delete current.forecast;
|
|
947
|
+
}
|
|
948
|
+
assert.equal(typeof (current.frozen as JsonObject | undefined)?.tier, "string", "the captured status carries frozen");
|
|
949
|
+
const digest = `sha256:${"ab".repeat(32)}`;
|
|
950
|
+
(current.frozen as JsonObject).changed_path_manifest_sha256 = digest;
|
|
951
|
+
assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, digest);
|
|
952
|
+
(current.frozen as JsonObject).changed_path_manifest_sha256 = "not-a-digest";
|
|
953
|
+
assert.throws(() => decodeReviewStatusV3(current), /changed_path_manifest_sha256/);
|
|
954
|
+
delete (current.frozen as JsonObject).changed_path_manifest_sha256;
|
|
955
|
+
assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, undefined);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("a capture-result collect input decodes without changed_path_manifest, which the artifact subject digest already binds", () => {
|
|
959
|
+
const status = fixture<JsonObject>("status.fixture.json");
|
|
960
|
+
const transition = clone((status.next_transition ?? {}) as JsonObject);
|
|
961
|
+
const inputs = ((transition.collect as JsonObject | undefined)?.inputs ?? []) as JsonObject[];
|
|
962
|
+
const captureInput = inputs.find((input) => input.capture_operation === "review.capture-result");
|
|
963
|
+
assert.ok(captureInput, "the status fixture carries a capture-result collect input");
|
|
964
|
+
assert.doesNotThrow(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), "with the manifest");
|
|
965
|
+
delete captureInput.changed_path_manifest;
|
|
966
|
+
const decoded = decodeReviewNextTransitionV3(transition, { v5: true, v6: true });
|
|
967
|
+
const decodedInput = decoded.collect?.inputs.find((input) => input.captureOperation === "review.capture-result");
|
|
968
|
+
assert.ok(decodedInput?.artifactSubject?.changedPathManifestSha256, "the digest on the artifact subject is the binding");
|
|
969
|
+
assert.equal(decodedInput?.changedPathManifest, undefined);
|
|
970
|
+
delete captureInput.artifact_subject;
|
|
971
|
+
assert.throws(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), /requires artifact_subject, base_tree, and candidate_tree/);
|
|
972
|
+
});
|
|
973
|
+
|
|
703
974
|
test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
|
|
704
975
|
const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
|
|
705
976
|
delete current.receipt;
|
|
@@ -711,6 +982,11 @@ test("status enforces authority/frozen/receipt conditionals and decodes the requ
|
|
|
711
982
|
const decoded = decodeReviewStatusV3(current);
|
|
712
983
|
assert.equal(decoded.repair.status, "unsupported");
|
|
713
984
|
assert.equal(decoded.authority?.state, "reviewing");
|
|
985
|
+
for (const action of ["collect", "execute"]) {
|
|
986
|
+
const projected = clone(current);
|
|
987
|
+
projected.action = action;
|
|
988
|
+
assert.equal(decodeReviewStatusV3(projected).action, action, "a live transaction's root action names its mandated transition kind");
|
|
989
|
+
}
|
|
714
990
|
|
|
715
991
|
const missingAuthority = clone(current);
|
|
716
992
|
delete missingAuthority.authority;
|
|
@@ -156,6 +156,45 @@ test("strictly decodes the real zero-lens closed START and every last-event clos
|
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
test("approved closure preserves complete admitted reviewer results before acknowledgement", () => {
|
|
160
|
+
const approved = closure("review/capture-result", "review-results");
|
|
161
|
+
approved.reviewer_results = [{
|
|
162
|
+
lens: "review-reliability",
|
|
163
|
+
findings: [{
|
|
164
|
+
id: "R3-W01",
|
|
165
|
+
lens: "reliability",
|
|
166
|
+
location: "lib/session.ts:4",
|
|
167
|
+
severity: "SUGGESTION",
|
|
168
|
+
claim: "the complete explanatory narrative remains available",
|
|
169
|
+
proof_refs: ["the changed line was inspected"],
|
|
170
|
+
evidence_class: "insufficient",
|
|
171
|
+
causal_disposition: "pre-existing",
|
|
172
|
+
}],
|
|
173
|
+
evidence: ["inspected the complete frozen candidate"],
|
|
174
|
+
result_hash: SHA,
|
|
175
|
+
}];
|
|
176
|
+
const decoded = decodeReviewLastEventClosureV1(approved);
|
|
177
|
+
assert.deepEqual(decoded.reviewerResults, [{
|
|
178
|
+
lens: "review-reliability",
|
|
179
|
+
findings: [{
|
|
180
|
+
id: "R3-W01",
|
|
181
|
+
lens: "reliability",
|
|
182
|
+
location: "lib/session.ts:4",
|
|
183
|
+
severity: "SUGGESTION",
|
|
184
|
+
claim: "the complete explanatory narrative remains available",
|
|
185
|
+
proofRefs: ["the changed line was inspected"],
|
|
186
|
+
evidenceClass: "insufficient",
|
|
187
|
+
causalDisposition: "pre-existing",
|
|
188
|
+
}],
|
|
189
|
+
evidence: ["inspected the complete frozen candidate"],
|
|
190
|
+
resultHash: SHA,
|
|
191
|
+
}]);
|
|
192
|
+
|
|
193
|
+
const correctionPlan = closure("review.capture-correction-plan", "review-results");
|
|
194
|
+
correctionPlan.reviewer_results = approved.reviewer_results;
|
|
195
|
+
assert.throws(() => decodeReviewLastEventClosureV1(correctionPlan), /reviewer_results requires approved state/);
|
|
196
|
+
});
|
|
197
|
+
|
|
159
198
|
test("retired legacy client no longer exposes a FINALIZE route", () => {
|
|
160
199
|
assert.equal("NativeReviewCliV214" in nativeReviewCliModule, false);
|
|
161
200
|
assert.equal("NativeReviewCliV213" in nativeReviewCliModule, false);
|
|
@@ -331,18 +370,23 @@ test("terminal capture closes directly, nonterminal capture does not auto-follow
|
|
|
331
370
|
assert.equal(statusCalls, 2, "only initial STATUS plus one ambiguity reconciliation");
|
|
332
371
|
});
|
|
333
372
|
|
|
334
|
-
test("unknown-capture reconciliation
|
|
373
|
+
test("unknown-capture reconciliation preserves agentless legacy callers and forwards an explicit Pi agent", async () => {
|
|
335
374
|
const calls: Array<Record<string, unknown>> = [];
|
|
336
375
|
const native = {
|
|
337
376
|
targetStatus: async (request: Record<string, unknown>) => {
|
|
338
377
|
calls.push(request);
|
|
339
378
|
assert.equal(request.lineageId, "reconcile-lineage");
|
|
340
|
-
return { targetIdentity: SHA, authority: { lineageId: "reconcile-lineage" } } as ReviewStatusV3;
|
|
379
|
+
return { applicability: "current_target", targetIdentity: SHA, authority: { lineageId: "reconcile-lineage" } } as ReviewStatusV3;
|
|
341
380
|
},
|
|
342
381
|
} as unknown as NativeReviewCli;
|
|
343
382
|
const result = await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA });
|
|
344
383
|
await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { baseRef: "refs/heads/main", committedOnly: true });
|
|
345
|
-
|
|
384
|
+
await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { agent: "pi" });
|
|
385
|
+
assert.deepEqual(calls, [
|
|
386
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage" },
|
|
387
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage", baseRef: "refs/heads/main", committedOnly: true },
|
|
388
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage", agent: "pi" },
|
|
389
|
+
]);
|
|
346
390
|
assert.equal(result.targetIdentity, SHA);
|
|
347
391
|
let launches = 0;
|
|
348
392
|
const strictNative = new nativeReviewCliModule.NativeReviewCliV216(async () => { launches += 1; throw new Error("must not launch"); }, "/package/.gentle-ai/gentle-ai");
|
|
@@ -350,6 +394,71 @@ test("unknown-capture reconciliation validates the target-scoped lineage without
|
|
|
350
394
|
assert.equal(launches, 0);
|
|
351
395
|
});
|
|
352
396
|
|
|
397
|
+
test("unknown targeted-validator capture reconciliation requires current exact authority and target", async () => {
|
|
398
|
+
const lineageId = "targeted-validator-reconciliation";
|
|
399
|
+
const input = roleInput(lineageId, "review.capture-validation");
|
|
400
|
+
const expected = status(lineageId, [input]);
|
|
401
|
+
const cases = [
|
|
402
|
+
["current matching STATUS", expected, true],
|
|
403
|
+
["unrelated STATUS", { ...expected, applicability: "unrelated" }, false],
|
|
404
|
+
["STATUS without authority", { ...expected, authority: undefined }, false],
|
|
405
|
+
["STATUS for another lineage", { ...expected, authority: { ...expected.authority!, lineageId: "other-lineage" } }, false],
|
|
406
|
+
["STATUS for another target", { ...expected, targetIdentity: `sha256:${"d".repeat(64)}` }, false],
|
|
407
|
+
] as const;
|
|
408
|
+
|
|
409
|
+
for (const [_name, reconciliationStatus, trusted] of cases) {
|
|
410
|
+
let statusCalls = 0;
|
|
411
|
+
let captureCalls = 0;
|
|
412
|
+
let startCalls = 0;
|
|
413
|
+
const native = {
|
|
414
|
+
targetStatus: async () => {
|
|
415
|
+
statusCalls += 1;
|
|
416
|
+
return statusCalls === 1 ? expected : reconciliationStatus;
|
|
417
|
+
},
|
|
418
|
+
captureProviderRole: async () => {
|
|
419
|
+
captureCalls += 1;
|
|
420
|
+
throw Object.assign(new Error("targeted-validator response lost"), {
|
|
421
|
+
mutationOutcome: "unknown",
|
|
422
|
+
nextAction: "review.status",
|
|
423
|
+
failureEnvelope: { raw: { code: "targeted-validator-response-lost" }, mutationOutcome: "unknown" },
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
start: async () => { startCalls += 1; },
|
|
427
|
+
} as unknown as NativeReviewCli;
|
|
428
|
+
|
|
429
|
+
const result = await capture(lineageId, input, native);
|
|
430
|
+
assert.equal(result.outcome, trusted ? "native-capture-outcome-unknown" : "native-capture-status-reconciliation-failed");
|
|
431
|
+
assert.equal(captureCalls, 1, "an ambiguous capture is never replayed");
|
|
432
|
+
assert.equal(statusCalls, 2, "an ambiguous capture gets one reconciliation STATUS");
|
|
433
|
+
assert.equal(startCalls, 0, "reconciliation never invokes START");
|
|
434
|
+
if (trusted) {
|
|
435
|
+
assert.equal(result.status, "reconciled");
|
|
436
|
+
assert.deepEqual((result.native_failure as { native_failure: unknown }).native_failure, { code: "targeted-validator-response-lost" });
|
|
437
|
+
} else {
|
|
438
|
+
assert.deepEqual(result.native_failure, { code: "targeted-validator-response-lost" }, "the original capture failure is preserved");
|
|
439
|
+
assert.ok(result.reconciliation_failure, "the rejected STATUS diagnostic remains nested");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("unknown-capture reconciliation rejects a binding without a target identity", async () => {
|
|
445
|
+
const lineageId = "missing-reconciliation-target";
|
|
446
|
+
const input = roleInput(lineageId, "review.capture-validation");
|
|
447
|
+
let statusCalls = 0;
|
|
448
|
+
const native = {
|
|
449
|
+
targetStatus: async () => {
|
|
450
|
+
statusCalls += 1;
|
|
451
|
+
return status(lineageId, [input]);
|
|
452
|
+
},
|
|
453
|
+
} as unknown as NativeReviewCli;
|
|
454
|
+
|
|
455
|
+
await assert.rejects(
|
|
456
|
+
() => reconcileUnknownReviewLastEventCapture(native, process.cwd(), { lineageId }),
|
|
457
|
+
/capture reconciliation returned missing or different target/,
|
|
458
|
+
);
|
|
459
|
+
assert.equal(statusCalls, 1, "the missing binding is rejected after one reconciliation STATUS");
|
|
460
|
+
});
|
|
461
|
+
|
|
353
462
|
// One approved terminal closure carrying the exact continuation the provider
|
|
354
463
|
// renders: closed positional arguments, the single approved precondition, and a
|
|
355
464
|
// binding that names the candidate being burned.
|
|
@@ -66,13 +66,28 @@ function assertNativeJsonHasNoMetadata(path: string, value: unknown): void {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
/Judgment Day starts only when explicitly requested and replaces ordinary review for that lineage\./,
|
|
69
|
+
const JUDGMENT_DAY_DISCOVERY_PATTERNS = [
|
|
71
70
|
/Judgment Day starts with exactly two blind judges and zero refuters\./,
|
|
72
71
|
/Judgment Day alone may iterate discovery and scoped re-judgment, for at most two rounds\./,
|
|
73
72
|
/Findings surviving round two escalate; no third-round transition exists\./,
|
|
74
73
|
] as const;
|
|
75
74
|
|
|
75
|
+
const JUDGMENT_DAY_STANDALONE_SEMANTICS =
|
|
76
|
+
"Judgment Day is independent: it neither enables nor replaces ordinary review; a separately requested ordinary review remains independent.";
|
|
77
|
+
|
|
78
|
+
const OBSOLETE_JUDGMENT_DAY_REPLACEMENT =
|
|
79
|
+
/Judgment Day starts only when explicitly requested and replaces ordinary review for that lineage\./;
|
|
80
|
+
|
|
81
|
+
const JUDGMENT_DAY_SEMANTIC_SURFACES = [
|
|
82
|
+
CANONICAL,
|
|
83
|
+
JD_SKILL,
|
|
84
|
+
JD_PROMPTS,
|
|
85
|
+
...JUDGES,
|
|
86
|
+
FIX_AGENT,
|
|
87
|
+
"assets/orchestrator-delegation.md",
|
|
88
|
+
SDD_WORKFLOW,
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
76
91
|
const JUDGMENT_DAY_REJUDGMENT_PATTERNS = [
|
|
77
92
|
/Initial discovery and scoped re-judgment are separate modes\./,
|
|
78
93
|
/On controller-requested scoped re-judgment, receive only requested frozen IDs, their exact hash-bound rows, and the fix diff\./,
|
|
@@ -115,7 +130,7 @@ test("canonical contract defines compact risk, causal admission, correction, CAS
|
|
|
115
130
|
/reviewer and validator outputs remain semantically untrusted/i,
|
|
116
131
|
/do not report.*trusted local orchestrator.*security finding/i,
|
|
117
132
|
/untrusted repository content.*malformed inputs.*stale authority.*path drift.*external callers/i,
|
|
118
|
-
...
|
|
133
|
+
...JUDGMENT_DAY_DISCOVERY_PATTERNS,
|
|
119
134
|
]);
|
|
120
135
|
assert.match(read(README), /Review outcomes and receipt state are informational; commit, push, pull-request, and release delivery follow ordinary repository policy\./);
|
|
121
136
|
assert.doesNotMatch(read(README), /one one-shot authorization for the exact command/i);
|
|
@@ -267,7 +282,7 @@ test("the Pi-owned adversarial role agents are retired: roles execute through Go
|
|
|
267
282
|
for (const path of JUDGES) {
|
|
268
283
|
test(`${path} preserves graph-v1 Judgment Day discovery and scoped re-judgment`, () => {
|
|
269
284
|
const content = read(path);
|
|
270
|
-
assertMatches(path, content,
|
|
285
|
+
assertMatches(path, content, JUDGMENT_DAY_DISCOVERY_PATTERNS);
|
|
271
286
|
assertMatches(path, content, JUDGMENT_DAY_REJUDGMENT_PATTERNS);
|
|
272
287
|
});
|
|
273
288
|
}
|
|
@@ -303,13 +318,53 @@ test("Judgment Day judge prompts contain distinct graph-v1 discovery and re-judg
|
|
|
303
318
|
assert.doesNotMatch(judgePrompt, /End with `Skill Resolution:/);
|
|
304
319
|
});
|
|
305
320
|
|
|
321
|
+
test("Judgment Day canonical and packaged surfaces preserve the independent lifecycle", () => {
|
|
322
|
+
for (const path of JUDGMENT_DAY_SEMANTIC_SURFACES) {
|
|
323
|
+
const content = read(path);
|
|
324
|
+
assert.ok(content.includes(JUDGMENT_DAY_STANDALONE_SEMANTICS), `${path} must use the current standalone Judgment Day sentence`);
|
|
325
|
+
assert.doesNotMatch(content, OBSOLETE_JUDGMENT_DAY_REPLACEMENT, `${path} must reject the obsolete replacement semantics`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
306
329
|
test("Judgment Day skill and prompts preserve bounded fix and re-judgment authority", () => {
|
|
307
|
-
|
|
308
|
-
|
|
330
|
+
const skill = read(JD_SKILL);
|
|
331
|
+
const judgePrompt = fencedBlock(JD_PROMPTS, "## Judge Prompt");
|
|
332
|
+
assertMatches(JD_SKILL, skill, [...JUDGMENT_DAY_DISCOVERY_PATTERNS, ...JUDGMENT_DAY_REJUDGMENT_PATTERNS, ...FIX_PATTERNS]);
|
|
333
|
+
assertMatches(JD_PROMPTS, judgePrompt, JUDGMENT_DAY_DISCOVERY_PATTERNS);
|
|
309
334
|
assertMatches(JD_PROMPTS, fencedBlock(JD_PROMPTS, "## Fix Agent Prompt"), FIX_PATTERNS);
|
|
310
335
|
assertMatches(FIX_AGENT, read(FIX_AGENT), FIX_PATTERNS);
|
|
311
336
|
});
|
|
312
337
|
|
|
338
|
+
test("Judgment Day fix routing has one canonical shape and never falls back to generic roles", () => {
|
|
339
|
+
const canonicalShape = [
|
|
340
|
+
"## Judgment Day activation",
|
|
341
|
+
"User explicitly requested Judgment Day.",
|
|
342
|
+
"## Exact authorized severe IDs",
|
|
343
|
+
"- `JD-A-001`",
|
|
344
|
+
"## Judgment Day correction batch",
|
|
345
|
+
"Round: 1 of 2.",
|
|
346
|
+
"Frozen ledger SHA-256: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`",
|
|
347
|
+
"## Exact frozen finding rows",
|
|
348
|
+
'{"id":"JD-A-001","lens":"judgment-day","location":"path/to/authorized-file.ts:1","severity":"CRITICAL","status_at_freeze":"open","evidence_class":"deterministic","evidence_claim":"Concrete user-impact claim supported by the frozen location."}',
|
|
349
|
+
"## Allowed edit surfaces",
|
|
350
|
+
].join("\n");
|
|
351
|
+
for (const [path, content] of [
|
|
352
|
+
[CANONICAL, read(CANONICAL)],
|
|
353
|
+
[FIX_AGENT, read(FIX_AGENT)],
|
|
354
|
+
[JD_SKILL, read(JD_SKILL)],
|
|
355
|
+
[JD_PROMPTS, fencedBlock(JD_PROMPTS, "## Fix Agent Prompt")],
|
|
356
|
+
["assets/orchestrator-delegation.md", read("assets/orchestrator-delegation.md")],
|
|
357
|
+
[SDD_WORKFLOW, read(SDD_WORKFLOW)],
|
|
358
|
+
] as const) {
|
|
359
|
+
assert.ok(content.includes(canonicalShape), `${path} must carry the canonical Judgment Day fix shape`);
|
|
360
|
+
assert.match(content, /requires no graph-v1 or native review lineage/i);
|
|
361
|
+
}
|
|
362
|
+
const routing = `${read("assets/orchestrator-delegation.md")}\n${read(SDD_WORKFLOW)}`;
|
|
363
|
+
assert.match(routing, /Judgment Day phase roles are never generic fallbacks\./);
|
|
364
|
+
assert.match(routing, /If the generic writer chain is unavailable, use the documented native generic fallback or stop\./);
|
|
365
|
+
assert.match(read(SDD_WORKFLOW), /\| default\s+\| balanced\s+\| SDD phase fallback; never a Judgment Day role\s+\|/);
|
|
366
|
+
});
|
|
367
|
+
|
|
313
368
|
test("orchestrator, injected skill, and README defer RDD lifecycle ownership to Gentle AI", () => {
|
|
314
369
|
const boundary = "This package injects the mirrored provider-bundle review execution contract into this session's system prompt at start; Gentle AI writes nothing into the Pi system prompt, and this package owns everything else here. Absent that mirrored contract, this package invents no lifecycle instructions.";
|
|
315
370
|
const orchestrator = union(ORCHESTRATOR);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
|
+
import { declareReviewRelayHandshake, GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../lib/review-relay-contract.ts";
|
|
6
|
+
|
|
7
|
+
// gentle-pi#550: the session shell must inherit the relay handshake, so a
|
|
8
|
+
// `gentle-ai review ... --agent pi` typed in a shell tool is eligible without
|
|
9
|
+
// the operator reading package source for the value.
|
|
10
|
+
|
|
11
|
+
test("declareReviewRelayHandshake exports the exact contract and reports whether it changed anything", () => {
|
|
12
|
+
const env: NodeJS.ProcessEnv = {};
|
|
13
|
+
assert.equal(declareReviewRelayHandshake(env), true);
|
|
14
|
+
assert.equal(env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
15
|
+
assert.equal(declareReviewRelayHandshake(env), false, "already declared");
|
|
16
|
+
const stale: NodeJS.ProcessEnv = { [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: "gentle-pi.review-relay/v0" };
|
|
17
|
+
assert.equal(declareReviewRelayHandshake(stale), true, "a wrong value is replaced: this extension is the host");
|
|
18
|
+
assert.equal(stale[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("loading the extension declares the handshake in the session environment", () => {
|
|
22
|
+
const processEnv: NodeJS.ProcessEnv = { PATH: "/bin" };
|
|
23
|
+
createGentleAiExtension({ nativeReviewCli: null, processEnv })({ on() {}, registerTool() {}, registerCommand() {} } as unknown as ExtensionAPI);
|
|
24
|
+
assert.equal(processEnv[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
25
|
+
assert.equal(processEnv.PATH, "/bin");
|
|
26
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { consumeReviewMutation, pendingReviewMutation, recordReviewMutation, REVIEW_REMINDER_RECEIPT } from "../lib/review-reminder-receipt.ts";
|
|
4
|
+
|
|
5
|
+
function fixture() {
|
|
6
|
+
let sessionId = "parent";
|
|
7
|
+
const entries: Array<{ type: string; customType: string; data: unknown }> = [];
|
|
8
|
+
const session = { getSessionId: () => sessionId, getBranch: () => entries };
|
|
9
|
+
const host = { appendEntry: (customType: string, data: unknown) => { entries.push({ type: "custom", customType, data }); } };
|
|
10
|
+
const write = (toolCallId: string, root = "/repo") => recordReviewMutation(host, session, root, { source: "direct", toolName: "write", toolCallId });
|
|
11
|
+
return { host, session, entries, write, switchSession: (id: string) => { sessionId = id; } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("receipts survive reload but are isolated by session, root, and active branch", () => {
|
|
15
|
+
const f = fixture();
|
|
16
|
+
f.write("a");
|
|
17
|
+
const captured = pendingReviewMutation(f.session, "/repo");
|
|
18
|
+
assert.ok(captured);
|
|
19
|
+
assert.equal(pendingReviewMutation({ ...f.session }, "/repo"), captured);
|
|
20
|
+
assert.equal(pendingReviewMutation(f.session, "/sibling"), undefined);
|
|
21
|
+
f.switchSession("fork");
|
|
22
|
+
assert.equal(pendingReviewMutation(f.session, "/repo"), undefined);
|
|
23
|
+
f.switchSession("new");
|
|
24
|
+
assert.equal(pendingReviewMutation(f.session, "/repo"), undefined);
|
|
25
|
+
f.switchSession("parent");
|
|
26
|
+
assert.equal(pendingReviewMutation({ ...f.session, getBranch: () => [] }, "/repo"), undefined);
|
|
27
|
+
f.write("a");
|
|
28
|
+
assert.equal(f.entries.length, 1, "duplicate tool completion cannot re-enable a generation");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
for (const kind of ["nudged", "acknowledged"] as const) {
|
|
32
|
+
test(`${kind} consumes only its captured prefix, retaining a concurrent own mutation`, () => {
|
|
33
|
+
const f = fixture();
|
|
34
|
+
f.write("a");
|
|
35
|
+
const captured = pendingReviewMutation(f.session, "/repo");
|
|
36
|
+
f.write("b");
|
|
37
|
+
consumeReviewMutation(f.host, f.session, "/repo", captured, kind, "whole-target");
|
|
38
|
+
assert.equal(pendingReviewMutation(f.session, "/repo", captured), undefined);
|
|
39
|
+
const next = pendingReviewMutation(f.session, "/repo");
|
|
40
|
+
assert.ok(next && next !== captured);
|
|
41
|
+
consumeReviewMutation(f.host, f.session, "/repo", next, kind, "whole-target");
|
|
42
|
+
assert.equal(pendingReviewMutation(f.session, "/repo"), undefined);
|
|
43
|
+
f.write("c");
|
|
44
|
+
assert.ok(pendingReviewMutation(f.session, "/repo"));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("malformed, unknown-version, and unmatched consumption entries do not establish ownership", () => {
|
|
49
|
+
const f = fixture();
|
|
50
|
+
f.write("valid");
|
|
51
|
+
const good = f.entries.pop()!;
|
|
52
|
+
const data = good.data as Record<string, unknown>;
|
|
53
|
+
for (const patch of [{ kind: "spawn" }, { source: "prose" }, { toolName: "bash" }, { sessionId: "" }, { root: "relative" }, { taskId: "foreign" }, { extra: true }, { toolCallId: "" }, { id: null }]) {
|
|
54
|
+
f.entries.push({ ...good, data: { ...data, ...patch } });
|
|
55
|
+
}
|
|
56
|
+
f.entries.push({ ...good, customType: "gentle-pi.review-reminder-receipt/v99" });
|
|
57
|
+
assert.equal(pendingReviewMutation(f.session, "/repo"), undefined);
|
|
58
|
+
f.entries.push(good);
|
|
59
|
+
const captured = pendingReviewMutation(f.session, "/repo");
|
|
60
|
+
consumeReviewMutation(f.host, f.session, "/repo", "unknown", "acknowledged", "target");
|
|
61
|
+
assert.equal(pendingReviewMutation(f.session, "/repo"), captured);
|
|
62
|
+
});
|