gentle-pi 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- 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 +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -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 +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -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 +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- 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 +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -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 +608 -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 +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
package/lib/review-host-relay.ts
CHANGED
|
@@ -284,7 +284,19 @@ export interface ReviewHostRelayResult {
|
|
|
284
284
|
readonly submission: string;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/** Opaque materialize-and-review result, not yet submitted to the provider. */
|
|
288
|
+
export interface ReviewHostRelayPreparedResult {
|
|
289
|
+
/** Copy-safe request snapshot captured before materialization starts. */
|
|
290
|
+
readonly request: ReviewHostRelayRequest;
|
|
291
|
+
readonly promptByteLength: number;
|
|
292
|
+
readonly resultByteLength: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const preparedResultBytes = new WeakMap<ReviewHostRelayPreparedResult, Buffer>();
|
|
296
|
+
|
|
287
297
|
export type ReviewHostRelayRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayResult>;
|
|
298
|
+
export type ReviewHostRelayPreparationRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayPreparedResult>;
|
|
299
|
+
export type ReviewHostRelaySubmissionRunner = (prepared: ReviewHostRelayPreparedResult) => Promise<ReviewHostRelayResult>;
|
|
288
300
|
|
|
289
301
|
const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
290
302
|
|
|
@@ -442,38 +454,54 @@ function assertTokens(name: string, tokens: readonly string[]): void {
|
|
|
442
454
|
}
|
|
443
455
|
}
|
|
444
456
|
|
|
457
|
+
function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): ReviewHostRelayRequest {
|
|
458
|
+
assertTokens("capture", request.captureArgumentTokens);
|
|
459
|
+
// The completing form is validated before any process launches: a materialize
|
|
460
|
+
// slot without a provider-owned submission is a typed contract mismatch,
|
|
461
|
+
// never a synthesized invocation.
|
|
462
|
+
resolveReviewHostRelaySubmission(request.submission);
|
|
463
|
+
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
464
|
+
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
465
|
+
const environment = Object.freeze({ ...(request.environment ?? process.env) }) as NodeJS.ProcessEnv;
|
|
466
|
+
const submission = request.submission === undefined ? undefined : Object.freeze({
|
|
467
|
+
operationToken: request.submission.operationToken,
|
|
468
|
+
argumentTokens: Object.freeze([...request.submission.argumentTokens]),
|
|
469
|
+
values: Object.freeze(request.submission.values.map((value) => Object.freeze({ ...value }))),
|
|
470
|
+
});
|
|
471
|
+
return Object.freeze({
|
|
472
|
+
...request,
|
|
473
|
+
captureArgumentTokens: Object.freeze([...request.captureArgumentTokens]),
|
|
474
|
+
...(submission === undefined ? {} : { submission }),
|
|
475
|
+
gentleAiExecutable,
|
|
476
|
+
environment,
|
|
477
|
+
gentleAiTimeoutMs: request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS,
|
|
478
|
+
targetCwd: request.targetCwd ?? process.cwd(),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
445
482
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* a failure; the caller re-queries negotiated STATUS instead of retrying.
|
|
483
|
+
* Materializes one provider-bound reviewer prompt and runs its opaque Pi
|
|
484
|
+
* subprocess. It does not submit anything, so independent reviewer work can
|
|
485
|
+
* finish before the caller performs provider-ordered admission.
|
|
450
486
|
*/
|
|
451
|
-
export async function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
const gentleAiEnvironment = { ...baseEnvironment, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
|
|
463
|
-
const gentleAiTimeoutMs = request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS;
|
|
464
|
-
const targetCwd = request.targetCwd ?? process.cwd();
|
|
465
|
-
|
|
466
|
-
// (a) Materialize the Go-issued opaque prompt. This invocation is also the
|
|
467
|
-
// capability detection: an old binary's unknown-flag refusal proves the
|
|
468
|
-
// relay surface is absent, and the provider's handshake refusal surfaces
|
|
469
|
-
// verbatim. No version sniffing.
|
|
487
|
+
export async function prepareReviewHostRelaySlot(
|
|
488
|
+
request: ReviewHostRelayRequest,
|
|
489
|
+
reviewer: typeof runOpaquePiReviewer = runOpaquePiReviewer,
|
|
490
|
+
): Promise<ReviewHostRelayPreparedResult> {
|
|
491
|
+
// Copy mutable transport configuration before the first async boundary. The
|
|
492
|
+
// supplied AbortSignal intentionally stays live across materialize, reviewer,
|
|
493
|
+
// and submit, preserving the established cancellation behavior.
|
|
494
|
+
const preparedRequest = snapshotReviewHostRelayRequest(request);
|
|
495
|
+
|
|
496
|
+
// The provider materializes the opaque prompt and detects whether this relay
|
|
497
|
+
// surface is available. No version sniffing or prompt reconstruction occurs.
|
|
470
498
|
let materialized: ProcessCapture;
|
|
471
499
|
try {
|
|
472
|
-
materialized = await collectGentleAiProcess(
|
|
473
|
-
cwd: targetCwd
|
|
474
|
-
env:
|
|
475
|
-
timeoutMs: gentleAiTimeoutMs
|
|
476
|
-
...(
|
|
500
|
+
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review", "capture-result", ...preparedRequest.captureArgumentTokens], {
|
|
501
|
+
cwd: preparedRequest.targetCwd!,
|
|
502
|
+
env: { ...preparedRequest.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
503
|
+
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
504
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
477
505
|
});
|
|
478
506
|
} catch (error) {
|
|
479
507
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", `gentle-ai prompt materialization could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -481,45 +509,91 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
|
|
|
481
509
|
if (materialized.exitCode !== 0 || materialized.timedOut) {
|
|
482
510
|
const stderr = materialized.stderr.toString("utf8");
|
|
483
511
|
const refusal = classifyReviewHostRelayRefusal(stderr);
|
|
484
|
-
const timing = { elapsedMs: materialized.elapsedMs, timeoutMs: gentleAiTimeoutMs };
|
|
512
|
+
const timing = { elapsedMs: materialized.elapsedMs, timeoutMs: preparedRequest.gentleAiTimeoutMs! };
|
|
485
513
|
if (refusal === "unknown-flag") {
|
|
486
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, {
|
|
514
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, {
|
|
515
|
+
exitCode: materialized.exitCode,
|
|
516
|
+
stderr,
|
|
517
|
+
timedOut: materialized.timedOut,
|
|
518
|
+
...timing,
|
|
519
|
+
});
|
|
487
520
|
}
|
|
488
521
|
if (refusal === "handshake") {
|
|
489
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, {
|
|
522
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, {
|
|
523
|
+
exitCode: materialized.exitCode,
|
|
524
|
+
stderr,
|
|
525
|
+
timedOut: materialized.timedOut,
|
|
526
|
+
...timing,
|
|
527
|
+
});
|
|
490
528
|
}
|
|
491
529
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", materialized.timedOut
|
|
492
|
-
? `gentle-ai prompt materialization exceeded its ${gentleAiTimeoutMs}ms bound after ${materialized.elapsedMs}ms`
|
|
530
|
+
? `gentle-ai prompt materialization exceeded its ${preparedRequest.gentleAiTimeoutMs!}ms bound after ${materialized.elapsedMs}ms`
|
|
493
531
|
: "gentle-ai prompt materialization failed", { exitCode: materialized.exitCode, stderr, timedOut: materialized.timedOut, ...timing });
|
|
494
532
|
}
|
|
495
533
|
const promptBytes = materialized.stdout;
|
|
496
534
|
if (promptBytes.length === 0) {
|
|
497
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", {
|
|
535
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", {
|
|
536
|
+
exitCode: 0,
|
|
537
|
+
stderr: materialized.stderr.toString("utf8"),
|
|
538
|
+
elapsedMs: materialized.elapsedMs,
|
|
539
|
+
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
540
|
+
});
|
|
498
541
|
}
|
|
499
542
|
// The reviewer bound is derived from the prompt the provider actually
|
|
500
|
-
// materialized
|
|
501
|
-
//
|
|
502
|
-
const piTimeoutMs =
|
|
543
|
+
// materialized. The explicit request timeout is a test seam that wins over
|
|
544
|
+
// both the user-owned environment override and the scale-derived bound.
|
|
545
|
+
const piTimeoutMs = preparedRequest.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, preparedRequest.environment);
|
|
503
546
|
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// the established relay boundary.
|
|
547
|
+
// The pure adapter owns the fresh isolated Pi process. Its input and output
|
|
548
|
+
// are opaque bytes; this coordinator only maps transport failures.
|
|
507
549
|
let piResult: OpaquePiReviewerResult;
|
|
508
550
|
try {
|
|
509
|
-
piResult = await
|
|
510
|
-
...(
|
|
511
|
-
environment:
|
|
551
|
+
piResult = await reviewer(promptBytes, {
|
|
552
|
+
...(preparedRequest.piExecutable === undefined ? {} : { piExecutable: preparedRequest.piExecutable }),
|
|
553
|
+
environment: preparedRequest.environment,
|
|
512
554
|
timeoutMs: piTimeoutMs,
|
|
513
|
-
...(
|
|
555
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
514
556
|
});
|
|
515
557
|
} catch (error) {
|
|
516
558
|
throw relayPiTransportError(error, promptBytes.length, piTimeoutMs);
|
|
517
559
|
}
|
|
518
|
-
const
|
|
560
|
+
const prepared = Object.freeze({
|
|
561
|
+
request: preparedRequest,
|
|
562
|
+
promptByteLength: promptBytes.length,
|
|
563
|
+
resultByteLength: piResult.stdoutByteLength,
|
|
564
|
+
});
|
|
565
|
+
preparedResultBytes.set(prepared, Buffer.from(piResult.stdout));
|
|
566
|
+
return prepared;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Starts every reviewer before awaiting any result. If one or more reviewers
|
|
571
|
+
* fail, it rejects only after every started transport has settled and reports
|
|
572
|
+
* the earliest failed request in provider order.
|
|
573
|
+
*/
|
|
574
|
+
export async function runReviewHostRelayReviewerGroup(
|
|
575
|
+
requests: readonly ReviewHostRelayRequest[],
|
|
576
|
+
prepare: ReviewHostRelayPreparationRunner = prepareReviewHostRelaySlot,
|
|
577
|
+
): Promise<readonly ReviewHostRelayPreparedResult[]> {
|
|
578
|
+
if (requests.length === 0) {
|
|
579
|
+
throw new TypeError("Pi host relay reviewer group requires at least one provider-bound request");
|
|
580
|
+
}
|
|
581
|
+
const settled = await Promise.allSettled(requests.map(async (request) => await prepare(request)));
|
|
582
|
+
const failed = settled.find((result) => result.status === "rejected");
|
|
583
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
584
|
+
return settled.map((result) => (result as PromiseFulfilledResult<ReviewHostRelayPreparedResult>).value);
|
|
585
|
+
}
|
|
519
586
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Submits one already-reviewed opaque result through the exact provider-owned
|
|
589
|
+
* completing form. Only the provider-declared artifact slot is substituted.
|
|
590
|
+
*/
|
|
591
|
+
export async function submitReviewHostRelayPreparedResult(prepared: ReviewHostRelayPreparedResult): Promise<ReviewHostRelayResult> {
|
|
592
|
+
const resultBytes = preparedResultBytes.get(prepared);
|
|
593
|
+
if (resultBytes === undefined) throw new TypeError("Pi host relay requires a recognized prepared result");
|
|
594
|
+
const { request } = prepared;
|
|
595
|
+
assertTokens("capture", request.captureArgumentTokens);
|
|
596
|
+
const submissionBinding = resolveReviewHostRelaySubmission(request.submission);
|
|
523
597
|
const stagingDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-host-relay-result-"));
|
|
524
598
|
let primaryFailure = false;
|
|
525
599
|
try {
|
|
@@ -528,14 +602,16 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
|
|
|
528
602
|
await writeFile(resultFile, resultBytes, { mode: 0o600 });
|
|
529
603
|
await chmod(resultFile, 0o600);
|
|
530
604
|
const submitTokens = submissionBinding.argumentTokens.map((token, index) =>
|
|
531
|
-
index === submissionBinding.substitutionLocation
|
|
605
|
+
index === submissionBinding.substitutionLocation
|
|
606
|
+
? token.split(REVIEW_HOST_RELAY_SUBMISSION_VALUE_SLOT).join(resultFile)
|
|
607
|
+
: token,
|
|
532
608
|
);
|
|
533
609
|
let submission: ProcessCapture;
|
|
534
610
|
try {
|
|
535
|
-
submission = await collectGentleAiProcess(
|
|
536
|
-
cwd: targetCwd
|
|
537
|
-
env:
|
|
538
|
-
timeoutMs: gentleAiTimeoutMs
|
|
611
|
+
submission = await collectGentleAiProcess(request.gentleAiExecutable!, ["review", submissionBinding.operationToken, ...submitTokens], {
|
|
612
|
+
cwd: request.targetCwd!,
|
|
613
|
+
env: { ...request.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
614
|
+
timeoutMs: request.gentleAiTimeoutMs!,
|
|
539
615
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
540
616
|
});
|
|
541
617
|
} catch (error) {
|
|
@@ -543,20 +619,22 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
|
|
|
543
619
|
}
|
|
544
620
|
if (submission.exitCode !== 0 || submission.timedOut || submission.stdout.length === 0) {
|
|
545
621
|
const stderr = submission.stderr.toString("utf8");
|
|
546
|
-
const details = { exitCode: submission.exitCode, stderr, timedOut: submission.timedOut, elapsedMs: submission.elapsedMs, timeoutMs: gentleAiTimeoutMs };
|
|
547
|
-
// A typed admission refusal
|
|
548
|
-
//
|
|
549
|
-
// signal, untyped exit) stays unknown pending STATUS.
|
|
622
|
+
const details = { exitCode: submission.exitCode, stderr, timedOut: submission.timedOut, elapsedMs: submission.elapsedMs, timeoutMs: request.gentleAiTimeoutMs! };
|
|
623
|
+
// A typed admission refusal proves the provider consumed no slot.
|
|
624
|
+
// Every other launched submission stays unknown pending fresh STATUS.
|
|
550
625
|
if (isReviewHostRelayAdmissionRefusal(submission, stderr)) {
|
|
551
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", stderr.trim(), {
|
|
626
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", stderr.trim(), {
|
|
627
|
+
...details,
|
|
628
|
+
mutationOutcome: "none",
|
|
629
|
+
});
|
|
552
630
|
}
|
|
553
631
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", submission.timedOut
|
|
554
|
-
? `gentle-ai capture submission exceeded its ${gentleAiTimeoutMs}ms bound after ${submission.elapsedMs}ms`
|
|
632
|
+
? `gentle-ai capture submission exceeded its ${request.gentleAiTimeoutMs!}ms bound after ${submission.elapsedMs}ms`
|
|
555
633
|
: "gentle-ai refused the relayed capture submission", details);
|
|
556
634
|
}
|
|
557
635
|
return {
|
|
558
|
-
promptByteLength:
|
|
559
|
-
resultByteLength:
|
|
636
|
+
promptByteLength: prepared.promptByteLength,
|
|
637
|
+
resultByteLength: prepared.resultByteLength,
|
|
560
638
|
submission: submission.stdout.toString("utf8"),
|
|
561
639
|
};
|
|
562
640
|
} catch (error) {
|
|
@@ -576,3 +654,11 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
|
|
|
576
654
|
}
|
|
577
655
|
}
|
|
578
656
|
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Compatibility one-binding path: materialize → opaque Pi adapter → submit.
|
|
660
|
+
* It preserves the established API and its typed failure behavior exactly.
|
|
661
|
+
*/
|
|
662
|
+
export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): Promise<ReviewHostRelayResult> {
|
|
663
|
+
return await submitReviewHostRelayPreparedResult(await prepareReviewHostRelaySlot(request));
|
|
664
|
+
}
|
|
@@ -79,7 +79,10 @@ const RISK_LEVELS = ["low", "medium", "high"] as const;
|
|
|
79
79
|
const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
80
80
|
const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
|
|
81
81
|
const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
|
|
82
|
-
|
|
82
|
+
// "collect" and "execute" are the v2 envelope's projection of a live
|
|
83
|
+
// transaction whose next_transition is mandatory: the root action names the
|
|
84
|
+
// transition kind instead of reading as a terminal stop.
|
|
85
|
+
const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop", "collect", "execute"] as const;
|
|
83
86
|
const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
|
|
84
87
|
type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
|
|
85
88
|
export const REVIEW_STATUS_ACTION_DISPOSITION = {
|
|
@@ -183,6 +186,19 @@ const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: n
|
|
|
183
186
|
requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
|
|
184
187
|
optionalFeatureFloor: 14,
|
|
185
188
|
}),
|
|
189
|
+
// Ground-truthed against the published v2.6.0 binary: capabilities/v2.5
|
|
190
|
+
// advertises the same required surface as v2.4 (status/v6 is still
|
|
191
|
+
// advertised for compatibility) plus the new status/v7 schema, which is a
|
|
192
|
+
// superset-checked addition, not a requirement -- decodeReviewStatusV3
|
|
193
|
+
// accepts v7 as an additive extension of v6, so the required-schema floor
|
|
194
|
+
// stays unchanged. The v2.6.0 binary advertised 15 optional features
|
|
195
|
+
// (floor stays at 14, its established minimum).
|
|
196
|
+
"gentle-ai.review-integration.capabilities/v2.5": Object.freeze({
|
|
197
|
+
protocolMinor: 5,
|
|
198
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.5", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
|
|
199
|
+
requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
|
|
200
|
+
optionalFeatureFloor: 14,
|
|
201
|
+
}),
|
|
186
202
|
});
|
|
187
203
|
const OPTIONAL_FEATURE_NAMES = Object.freeze([
|
|
188
204
|
"base_ref_workspace_overlay",
|
|
@@ -372,6 +388,8 @@ export interface ReviewStatusFrozenV1 {
|
|
|
372
388
|
tier: RiskLevel;
|
|
373
389
|
originalChangedLines: number;
|
|
374
390
|
correctionBudget: number;
|
|
391
|
+
/** Digest of the frozen changed-path manifest; binds manifest-less capture inputs (gentle-ai#3922). */
|
|
392
|
+
changedPathManifestSha256?: string;
|
|
375
393
|
}
|
|
376
394
|
|
|
377
395
|
export interface ReviewStatusReconciliationV1 {
|
|
@@ -532,6 +550,16 @@ export interface ReviewCollectInputV3 {
|
|
|
532
550
|
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
533
551
|
}
|
|
534
552
|
|
|
553
|
+
// gentle-pi#627: the exact `gentle-ai sync` invocation that resolves a stale
|
|
554
|
+
// managed-asset set (STATUS next_transition stop, and START's preflight
|
|
555
|
+
// failure envelope both carry this same shape).
|
|
556
|
+
export interface ReviewManagedAssetsContinuationV1 {
|
|
557
|
+
operation: "sync";
|
|
558
|
+
command: string;
|
|
559
|
+
agent?: string;
|
|
560
|
+
staleAssets?: readonly string[];
|
|
561
|
+
}
|
|
562
|
+
|
|
535
563
|
export interface ReviewNextTransitionV3 {
|
|
536
564
|
kind: "execute" | "collect" | "stop";
|
|
537
565
|
reasonCode: string;
|
|
@@ -539,6 +567,8 @@ export interface ReviewNextTransitionV3 {
|
|
|
539
567
|
collect?: { inputs: readonly ReviewCollectInputV3[] };
|
|
540
568
|
/** status/v5 only: the bounded correction plan request. */
|
|
541
569
|
correctionRequest?: ReviewCorrectionPlanRequestV1;
|
|
570
|
+
/** stop only, reason_code managed_assets_outdated: the sync continuation. */
|
|
571
|
+
continuation?: ReviewManagedAssetsContinuationV1;
|
|
542
572
|
}
|
|
543
573
|
|
|
544
574
|
export interface ReviewStatusV3 {
|
|
@@ -569,6 +599,8 @@ export interface ReviewStatusV3 {
|
|
|
569
599
|
repositoryContext?: ReviewRepositoryContextV2;
|
|
570
600
|
/** status/v5 only: the provider-owned request mirrored by the targeted-validator collect input. */
|
|
571
601
|
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
602
|
+
/** status/v7 only: the eligible untracked-path inventory digest, absent on the `staged` projection. */
|
|
603
|
+
eligibleUntrackedInventory?: string;
|
|
572
604
|
raw: Readonly<Record<string, unknown>>;
|
|
573
605
|
}
|
|
574
606
|
|
|
@@ -690,6 +722,8 @@ export interface ReviewFailureV2 {
|
|
|
690
722
|
causeCategory?: ReviewFailureCauseCategoryV2;
|
|
691
723
|
cause?: string;
|
|
692
724
|
context?: ReviewFailureContextV2;
|
|
725
|
+
/** code=managed_assets_outdated only: the sync continuation (gentle-pi#627). */
|
|
726
|
+
continuation?: ReviewManagedAssetsContinuationV1;
|
|
693
727
|
raw: Readonly<Record<string, unknown>>;
|
|
694
728
|
}
|
|
695
729
|
|
|
@@ -1624,8 +1658,11 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
|
|
|
1624
1658
|
}
|
|
1625
1659
|
|
|
1626
1660
|
if (captureOperation === "review.capture-result") {
|
|
1627
|
-
|
|
1628
|
-
|
|
1661
|
+
// changed_path_manifest is optional here: the artifact subject's
|
|
1662
|
+
// changed_path_manifest_sha256 already binds the frozen manifest, and
|
|
1663
|
+
// gentle-ai stops inlining one copy per lens (gentle-ai#3922).
|
|
1664
|
+
if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined) {
|
|
1665
|
+
throw new TypeError(`${label} requires artifact_subject, base_tree, and candidate_tree`);
|
|
1629
1666
|
}
|
|
1630
1667
|
if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
|
|
1631
1668
|
} else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
|
|
@@ -1658,12 +1695,32 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
|
|
|
1658
1695
|
};
|
|
1659
1696
|
}
|
|
1660
1697
|
|
|
1698
|
+
// gentle-pi#627: gentle-ai's managed-assets continuation (STATUS next_transition
|
|
1699
|
+
// stop reason_code=managed_assets_outdated, and START's preflight failure
|
|
1700
|
+
// envelope) names the exact `gentle-ai sync` invocation that resolves a stale
|
|
1701
|
+
// managed-asset set without abandoning the frozen candidate. Decoded verbatim
|
|
1702
|
+
// from internal/cli/review_operation_contract.go's ReviewManagedAssetsContinuation.
|
|
1703
|
+
export function decodeReviewManagedAssetsContinuationV1(value: unknown, label: string): ReviewManagedAssetsContinuationV1 {
|
|
1704
|
+
const body = exactRecord(value, label, ["operation", "command"], ["agent", "stale_assets"]);
|
|
1705
|
+
const operation = enumeration(body.operation, ["sync"] as const, `${label}.operation`);
|
|
1706
|
+
const command = nonempty(body.command, `${label}.command`);
|
|
1707
|
+
const agent = body.agent === undefined ? undefined : nonempty(body.agent, `${label}.agent`);
|
|
1708
|
+
const staleAssets = body.stale_assets === undefined ? undefined : stringArray(body.stale_assets, `${label}.stale_assets`, { minimum: 1 });
|
|
1709
|
+
return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1661
1712
|
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1662
1713
|
const v6 = options.v6 === true;
|
|
1663
1714
|
const v5 = options.v5 === true || v6;
|
|
1664
|
-
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
|
|
1715
|
+
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation"]);
|
|
1665
1716
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
|
|
1666
1717
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1718
|
+
const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
|
|
1719
|
+
// gentle-pi#627: continuation is optional on every status version an older
|
|
1720
|
+
// gentle-ai may emit a managed_assets_outdated stop with no continuation at
|
|
1721
|
+
// all, and decode must not refuse the whole envelope for that. When present
|
|
1722
|
+
// it is only valid on a stop with this exact reason_code.
|
|
1723
|
+
if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
|
|
1667
1724
|
|
|
1668
1725
|
// status/v5: the bounded correction plan request rides exactly its two
|
|
1669
1726
|
// reason codes and never any other (vendored status-v5.schema.json).
|
|
@@ -1713,7 +1770,7 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1713
1770
|
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1714
1771
|
}
|
|
1715
1772
|
if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
|
|
1716
|
-
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1773
|
+
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }) };
|
|
1717
1774
|
}
|
|
1718
1775
|
|
|
1719
1776
|
// ---------------------------------------------------------------------------
|
|
@@ -1761,15 +1818,19 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1761
1818
|
// Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
|
|
1762
1819
|
// against a live capture and the vendored status-v5.schema.json) is the v3
|
|
1763
1820
|
// key set plus the optional forecast and the v5-only next_transition
|
|
1764
|
-
// surfaces. status/v6 adds the intended-untracked selection;
|
|
1765
|
-
//
|
|
1821
|
+
// surfaces. status/v6 adds the intended-untracked selection; status/v7
|
|
1822
|
+
// (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
|
|
1823
|
+
// adds only the top-level optional `eligible_untracked_inventory` digest,
|
|
1824
|
+
// so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
|
|
1825
|
+
// field.
|
|
1766
1826
|
const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
|
|
1767
|
-
const
|
|
1827
|
+
const v7 = schema === "gentle-ai.review-integration.status/v7";
|
|
1828
|
+
const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
|
|
1768
1829
|
const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
|
|
1769
1830
|
const body = exactRecord(value, "status", [
|
|
1770
1831
|
"schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1771
|
-
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : [])]);
|
|
1772
|
-
requireIdentity(body, v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1832
|
+
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
|
|
1833
|
+
requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1773
1834
|
|
|
1774
1835
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
|
|
1775
1836
|
let receipt: ReviewStatusReceiptV1 | undefined;
|
|
@@ -1798,11 +1859,12 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1798
1859
|
|
|
1799
1860
|
let frozen: ReviewStatusFrozenV1 | undefined;
|
|
1800
1861
|
if (body.frozen !== undefined) {
|
|
1801
|
-
const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
|
|
1862
|
+
const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"], ["changed_path_manifest_sha256"]);
|
|
1802
1863
|
frozen = {
|
|
1803
1864
|
tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
|
|
1804
1865
|
originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
|
|
1805
1866
|
correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
|
|
1867
|
+
...(source.changed_path_manifest_sha256 === undefined ? {} : { changedPathManifestSha256: sha256(source.changed_path_manifest_sha256, "status.frozen.changed_path_manifest_sha256") }),
|
|
1806
1868
|
};
|
|
1807
1869
|
}
|
|
1808
1870
|
if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
|
|
@@ -1851,6 +1913,15 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1851
1913
|
};
|
|
1852
1914
|
}
|
|
1853
1915
|
|
|
1916
|
+
// status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
|
|
1917
|
+
// closed loop where `sdd-attempt finish` named a digest status never
|
|
1918
|
+
// published. Absent on the `staged` projection, which never resolves an
|
|
1919
|
+
// inventory, so it stays structurally optional rather than a required v7
|
|
1920
|
+
// field.
|
|
1921
|
+
const eligibleUntrackedInventory = v7 && body.eligible_untracked_inventory !== undefined
|
|
1922
|
+
? sha256(body.eligible_untracked_inventory, "status.eligible_untracked_inventory")
|
|
1923
|
+
: undefined;
|
|
1924
|
+
|
|
1854
1925
|
return {
|
|
1855
1926
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1856
1927
|
applicability,
|
|
@@ -1869,6 +1940,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1869
1940
|
...(forecast === undefined ? {} : { forecast }),
|
|
1870
1941
|
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
1871
1942
|
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1943
|
+
...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
|
|
1872
1944
|
raw: body,
|
|
1873
1945
|
};
|
|
1874
1946
|
}
|
|
@@ -2019,9 +2091,10 @@ function decodeFailureContext(value: unknown, label: string): ReviewFailureConte
|
|
|
2019
2091
|
export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
2020
2092
|
const body = exactRecord(value, "failure", [
|
|
2021
2093
|
"schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
|
|
2022
|
-
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
|
|
2094
|
+
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context", "continuation"]);
|
|
2023
2095
|
requireIdentity(body, "gentle-ai.review-integration.failure/v2");
|
|
2024
2096
|
const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
|
|
2097
|
+
const code = text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ });
|
|
2025
2098
|
|
|
2026
2099
|
if (body.progress_identity !== undefined) {
|
|
2027
2100
|
if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
|
|
@@ -2030,13 +2103,18 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
2030
2103
|
if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
|
|
2031
2104
|
throw new TypeError("failure.request_digest with review.repair requires progress_identity");
|
|
2032
2105
|
}
|
|
2106
|
+
// gentle-pi#627: continuation is optional on every version — an older
|
|
2107
|
+
// gentle-ai may emit managed_assets_outdated with no continuation, and
|
|
2108
|
+
// decode must not refuse the whole envelope for that. When present it is
|
|
2109
|
+
// only valid on this exact code.
|
|
2110
|
+
if (body.continuation !== undefined && code !== "managed_assets_outdated") throw new TypeError("failure.continuation is only valid for code managed_assets_outdated");
|
|
2033
2111
|
|
|
2034
2112
|
return {
|
|
2035
2113
|
schema: "gentle-ai.review-integration.failure/v2",
|
|
2036
2114
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
2037
2115
|
operation,
|
|
2038
2116
|
phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] as const, "failure.phase"),
|
|
2039
|
-
code
|
|
2117
|
+
code,
|
|
2040
2118
|
message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
|
|
2041
2119
|
mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
|
|
2042
2120
|
authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
|
|
@@ -2050,6 +2128,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
2050
2128
|
...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
|
|
2051
2129
|
...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
|
|
2052
2130
|
...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
|
|
2131
|
+
...(body.continuation === undefined ? {} : { continuation: decodeReviewManagedAssetsContinuationV1(body.continuation, "failure.continuation") }),
|
|
2053
2132
|
raw: body,
|
|
2054
2133
|
};
|
|
2055
2134
|
}
|
|
@@ -14,3 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
|
|
16
16
|
export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
|
|
17
|
+
|
|
18
|
+
// The host is this extension: every process the session starts (pi's shell
|
|
19
|
+
// tools, Gentle Agents children) runs under it, so the handshake belongs in
|
|
20
|
+
// the session environment too, not only in the extension's own invocations.
|
|
21
|
+
// Without it the first `gentle-ai review status --agent pi` typed in the
|
|
22
|
+
// session shell fails closed (gentle-pi#550). Returns whether anything changed.
|
|
23
|
+
export function declareReviewRelayHandshake(env: NodeJS.ProcessEnv): boolean {
|
|
24
|
+
if (env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] === GENTLE_PI_REVIEW_RELAY_CONTRACT) return false;
|
|
25
|
+
env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = GENTLE_PI_REVIEW_RELAY_CONTRACT;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
package/lib/review-repository.ts
CHANGED
|
@@ -55,8 +55,8 @@ export function reviewGitEnvironment(): NodeJS.ProcessEnv {
|
|
|
55
55
|
const environment: NodeJS.ProcessEnv = {};
|
|
56
56
|
for (const [key, value] of Object.entries(process.env)) if (!key.startsWith("GIT_")) environment[key] = value;
|
|
57
57
|
environment.GIT_CONFIG_NOSYSTEM = "1";
|
|
58
|
-
environment.GIT_CONFIG_GLOBAL =
|
|
59
|
-
environment.GIT_CONFIG_SYSTEM =
|
|
58
|
+
environment.GIT_CONFIG_GLOBAL = "/dev/null";
|
|
59
|
+
environment.GIT_CONFIG_SYSTEM = "/dev/null";
|
|
60
60
|
environment.GIT_OPTIONAL_LOCKS = "0";
|
|
61
61
|
environment.LC_ALL = "C";
|
|
62
62
|
environment.LANG = "C";
|