gentle-pi 2.4.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 +170 -12
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/orchestrator-delegation.md +19 -9
- package/assets/orchestrator.md +5 -5
- package/docs/delegated-verification.md +25 -0
- 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 +898 -79
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- 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 +88 -12
- 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-integration-v2.ts +58 -8
- 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/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 +87 -11
- package/runtime/review-integration-v2.mjs +58 -8
- 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 +6 -2
- package/skills/_shared/review-ledger-contract.md +3 -1
- 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/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 +28 -12
- 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/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 +16 -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 +69 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +17 -6
- 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-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +3 -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 +25 -0
- package/tests/review-host-relay-routing.test.ts +20 -4
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- 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/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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// gentle-ai#4309 owns anonymous usage telemetry end to end: install and
|
|
4
|
+
// heartbeat events, the notice, every kill switch, and the wire contract.
|
|
5
|
+
// gentle-pi#677 only nudges the local binary once per process, best-effort,
|
|
6
|
+
// and never surfaces the outcome to the user. This module is the pure,
|
|
7
|
+
// testable core of that nudge (`shouldTriggerTelemetry`,
|
|
8
|
+
// `spawnTelemetryTrigger`) plus the decode helper the foreground
|
|
9
|
+
// `/gentle:telemetry` slash command uses to relay `<op> --json` output
|
|
10
|
+
// (`decodeTelemetryTriggerDecision`).
|
|
11
|
+
|
|
12
|
+
export const TELEMETRY_TRIGGER_KILL_TIMEOUT_MS = 3_000;
|
|
13
|
+
|
|
14
|
+
export const TELEMETRY_TRIGGER_CONTRACT = "gentle-ai.telemetry-trigger/v1";
|
|
15
|
+
|
|
16
|
+
export const TELEMETRY_TRIGGER_DECISIONS = [
|
|
17
|
+
"enrolled",
|
|
18
|
+
"sent_install",
|
|
19
|
+
"sent_heartbeat",
|
|
20
|
+
"rate_limited",
|
|
21
|
+
"backoff",
|
|
22
|
+
"disabled",
|
|
23
|
+
] as const;
|
|
24
|
+
export type TelemetryTriggerDecision = (typeof TELEMETRY_TRIGGER_DECISIONS)[number];
|
|
25
|
+
|
|
26
|
+
export interface TelemetryTriggerDecisionV1 {
|
|
27
|
+
schema: typeof TELEMETRY_TRIGGER_CONTRACT;
|
|
28
|
+
decision: TelemetryTriggerDecision;
|
|
29
|
+
source: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decodes `gentle-ai telemetry trigger --json` stdout. An older binary
|
|
34
|
+
* without the verb prints `unknown telemetry command` and exits non-zero, so
|
|
35
|
+
* its stdout never parses as JSON here; that -- along with malformed JSON, a
|
|
36
|
+
* mismatched schema, an unrecognized decision, or a missing/non-string
|
|
37
|
+
* source -- means "nothing to do" and returns undefined rather than
|
|
38
|
+
* throwing.
|
|
39
|
+
*/
|
|
40
|
+
export function decodeTelemetryTriggerDecision(stdout: string): TelemetryTriggerDecisionV1 | undefined {
|
|
41
|
+
let parsed: unknown;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(stdout);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
48
|
+
const record = parsed as Record<string, unknown>;
|
|
49
|
+
if (record.schema !== TELEMETRY_TRIGGER_CONTRACT) return undefined;
|
|
50
|
+
if (typeof record.decision !== "string") return undefined;
|
|
51
|
+
if (!(TELEMETRY_TRIGGER_DECISIONS as readonly string[]).includes(record.decision)) return undefined;
|
|
52
|
+
if (typeof record.source !== "string" || record.source.length === 0) return undefined;
|
|
53
|
+
return {
|
|
54
|
+
schema: TELEMETRY_TRIGGER_CONTRACT,
|
|
55
|
+
decision: record.decision as TelemetryTriggerDecision,
|
|
56
|
+
source: record.source,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type TelemetryKillSwitchReason = "do-not-track" | "telemetry-disabled" | "ci";
|
|
61
|
+
|
|
62
|
+
function telemetryKillSwitchReason(env: Readonly<Record<string, string | undefined>>): TelemetryKillSwitchReason | undefined {
|
|
63
|
+
if (env.DO_NOT_TRACK === "1") return "do-not-track";
|
|
64
|
+
if (env.GENTLE_AI_TELEMETRY === "0") return "telemetry-disabled";
|
|
65
|
+
if (env.CI === "true") return "ci";
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True unless one of Gentle Pi's own kill switches is set
|
|
71
|
+
* (`DO_NOT_TRACK=1`, `GENTLE_AI_TELEMETRY=0`, or `CI=true`). The binary
|
|
72
|
+
* enforces its own opt-out and rate limiting independently; this predicate
|
|
73
|
+
* only decides whether Gentle Pi spawns the trigger at all.
|
|
74
|
+
*/
|
|
75
|
+
export function shouldTriggerTelemetry(env: Readonly<Record<string, string | undefined>>): boolean {
|
|
76
|
+
return telemetryKillSwitchReason(env) === undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A minimal structural subset of node:child_process's ChildProcess, mirroring
|
|
80
|
+
// the ChildLike/Spawn seam already used by lib/agents-runner.ts: narrow
|
|
81
|
+
// enough that tests can stub it without touching a real process.
|
|
82
|
+
export interface TelemetryTriggerChildLike {
|
|
83
|
+
unref(): unknown;
|
|
84
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
85
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TelemetryTriggerSpawnOptions {
|
|
89
|
+
cwd: string;
|
|
90
|
+
env: NodeJS.ProcessEnv;
|
|
91
|
+
detached: boolean;
|
|
92
|
+
stdio: "ignore";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type TelemetryTriggerSpawn = (
|
|
96
|
+
command: string,
|
|
97
|
+
args: readonly string[],
|
|
98
|
+
options: TelemetryTriggerSpawnOptions,
|
|
99
|
+
) => TelemetryTriggerChildLike;
|
|
100
|
+
|
|
101
|
+
export interface SpawnTelemetryTriggerOptions {
|
|
102
|
+
executable: string;
|
|
103
|
+
cwd: string;
|
|
104
|
+
env: NodeJS.ProcessEnv;
|
|
105
|
+
spawn?: TelemetryTriggerSpawn;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type TelemetryTriggerReason = "spawned" | "spawn-error" | TelemetryKillSwitchReason;
|
|
109
|
+
|
|
110
|
+
export interface TelemetryTriggerResult {
|
|
111
|
+
spawned: boolean;
|
|
112
|
+
reason: TelemetryTriggerReason;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Nudges `<executable> telemetry trigger --json`, detached and
|
|
117
|
+
* fire-and-forget. Never throws, never awaits the child's exit, and never
|
|
118
|
+
* blocks session start: a missing binary, an old binary without the verb, or
|
|
119
|
+
* a spawn failure all just report `{ spawned: false, ... }`. A 3 s timer
|
|
120
|
+
* kills a runaway child; the timer is unref'd so it can never keep the host
|
|
121
|
+
* process alive on its own, matching the detached child's own `unref()`.
|
|
122
|
+
*/
|
|
123
|
+
export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): TelemetryTriggerResult {
|
|
124
|
+
const killSwitch = telemetryKillSwitchReason(options.env);
|
|
125
|
+
if (killSwitch !== undefined) return { spawned: false, reason: killSwitch };
|
|
126
|
+
const spawnImpl = options.spawn ?? (nodeSpawn as unknown as TelemetryTriggerSpawn);
|
|
127
|
+
try {
|
|
128
|
+
const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
|
|
129
|
+
cwd: options.cwd,
|
|
130
|
+
env: options.env,
|
|
131
|
+
detached: true,
|
|
132
|
+
stdio: "ignore",
|
|
133
|
+
});
|
|
134
|
+
child.on("error", () => {
|
|
135
|
+
// Fire-and-forget: a spawn-time or runtime child error never surfaces
|
|
136
|
+
// to the caller and never affects session start.
|
|
137
|
+
});
|
|
138
|
+
const killTimer = setTimeout(() => {
|
|
139
|
+
try {
|
|
140
|
+
child.kill();
|
|
141
|
+
} catch {
|
|
142
|
+
// Best effort only; the child may already have exited.
|
|
143
|
+
}
|
|
144
|
+
}, TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
|
|
145
|
+
killTimer.unref?.();
|
|
146
|
+
child.unref();
|
|
147
|
+
return { spawned: true, reason: "spawned" };
|
|
148
|
+
} catch {
|
|
149
|
+
return { spawned: false, reason: "spawn-error" };
|
|
150
|
+
}
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@earendil-works/pi-tui": "
|
|
69
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
70
70
|
"@heyhuynhgiabuu/pi-pretty": "0.6.14"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@earendil-works/pi-coding-agent": "
|
|
73
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
74
74
|
"typebox": "*"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@earendil-works/pi-coding-agent": "
|
|
82
|
+
"@earendil-works/pi-coding-agent": "0.85.1"
|
|
83
83
|
},
|
|
84
84
|
"packageManager": "pnpm@11.1.1"
|
|
85
85
|
}
|
|
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
|
|
8
8
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
|
|
9
|
+
import { decodeReviewAssessmentV1, } from "./review-risk-assessment.mjs";
|
|
9
10
|
import {
|
|
10
11
|
REVIEW_INTEGRATION_CONTRACT,
|
|
11
12
|
decodeReviewAcknowledgedV1,
|
|
@@ -60,6 +61,7 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
60
61
|
RECONCILE_AUTHORITY: "review/reconcile-authority",
|
|
61
62
|
REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
|
|
62
63
|
MODE: "review/mode",
|
|
64
|
+
ASSESS: "review/assess",
|
|
63
65
|
REPAIR: "review/repair",
|
|
64
66
|
CAPTURE_RESULT: "review/capture-result",
|
|
65
67
|
CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
|
|
@@ -112,6 +114,11 @@ export const NATIVE_REVIEW_ERROR_CODE = {
|
|
|
112
114
|
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
115
122
|
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
116
123
|
STATUS: "status",
|
|
117
124
|
ENABLE: "enable",
|
|
@@ -152,6 +159,17 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
|
152
159
|
|
|
153
160
|
|
|
154
161
|
|
|
162
|
+
// Read-only risk assessment request (gentle-pi#662). `baseRef` requires
|
|
163
|
+
// explicit `committedOnly` acknowledgement, exactly like Native START's
|
|
164
|
+
// baseRef/committedOnly pairing, because both select a committed range
|
|
165
|
+
// instead of the ambient working tree.
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
155
173
|
|
|
156
174
|
|
|
157
175
|
|
|
@@ -305,7 +323,6 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
|
|
|
305
323
|
|
|
306
324
|
|
|
307
325
|
|
|
308
|
-
|
|
309
326
|
|
|
310
327
|
|
|
311
328
|
/** Raw audited native record; Pi relays it verbatim and never reinterprets it. */
|
|
@@ -499,7 +516,12 @@ export const NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION = {
|
|
|
499
516
|
} ;
|
|
500
517
|
|
|
501
518
|
|
|
502
|
-
export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS =
|
|
519
|
+
export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = {
|
|
520
|
+
...NATIVE_REVIEW_AUTHORITY_STATUS,
|
|
521
|
+
INCOMPLETE_STORE_ENTRY: "incomplete-store-entry",
|
|
522
|
+
HISTORICAL_PRE_RECEIPT: "historical-pre-receipt",
|
|
523
|
+
INVALIDATED: "invalidated",
|
|
524
|
+
} ;
|
|
503
525
|
|
|
504
526
|
|
|
505
527
|
export const NATIVE_REVIEW_LOCK_STATUS = {
|
|
@@ -570,6 +592,11 @@ export const NATIVE_REVIEW_RECOVERY_DISPOSITION = {
|
|
|
570
592
|
|
|
571
593
|
|
|
572
594
|
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
573
600
|
|
|
574
601
|
|
|
575
602
|
|
|
@@ -805,6 +832,18 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
805
832
|
// consumes did not change between 2.5.0 and 2.6.0, so the columns match
|
|
806
833
|
// the 2.5.0 row.
|
|
807
834
|
"2.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
835
|
+
// Ground-truthed against the published v2.7.0 binary installed from its
|
|
836
|
+
// signed archive: `review capabilities` on the v2 lane still advertises
|
|
837
|
+
// capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
|
|
838
|
+
// and the `start/v4` continuation, all of which the decoders already read.
|
|
839
|
+
// `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
|
|
840
|
+
// and `review validate --gate pre-commit` still answers
|
|
841
|
+
// `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
|
|
842
|
+
// against the binary. riskEvidence and hint stay dark for the same reason
|
|
843
|
+
// they have on every row since 2.2.0: still not proven to reach the
|
|
844
|
+
// negotiated START path Pi reads. The closed fields Pi consumes did not
|
|
845
|
+
// change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
|
|
846
|
+
"2.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
808
847
|
});
|
|
809
848
|
|
|
810
849
|
|
|
@@ -1047,8 +1086,15 @@ function decodeNativeReviewRecovery(value ) {
|
|
|
1047
1086
|
...(recovery.maintainer_authorization === undefined ? {} : { maintainerAuthorization: requiredString(recovery.maintainer_authorization) }),
|
|
1048
1087
|
};
|
|
1049
1088
|
}
|
|
1089
|
+
function decodeNativeReviewDiscardedWorkSummary(value ) {
|
|
1090
|
+
const discardedWork = exactObject(value, ["captured_lens_results", "findings_present"]);
|
|
1091
|
+
return {
|
|
1092
|
+
capturedLensResults: stringArray(discardedWork.captured_lens_results),
|
|
1093
|
+
findingsPresent: booleanValue(discardedWork.findings_present),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1050
1096
|
function decodeNativeReviewStatusEntry(value ) {
|
|
1051
|
-
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
|
|
1097
|
+
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery", "discarded_work"]);
|
|
1052
1098
|
return {
|
|
1053
1099
|
version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) ,
|
|
1054
1100
|
...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
|
|
@@ -1059,6 +1105,7 @@ function decodeNativeReviewStatusEntry(value )
|
|
|
1059
1105
|
...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
|
|
1060
1106
|
...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
|
|
1061
1107
|
...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
|
|
1108
|
+
...(entry.discarded_work === undefined ? {} : { discardedWork: decodeNativeReviewDiscardedWorkSummary(entry.discarded_work) }),
|
|
1062
1109
|
problems: stringArray(entry.problems),
|
|
1063
1110
|
};
|
|
1064
1111
|
}
|
|
@@ -1373,9 +1420,7 @@ class NativeReviewPlainCli {
|
|
|
1373
1420
|
if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
|
|
1374
1421
|
}
|
|
1375
1422
|
if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
|
|
1376
|
-
|
|
1377
|
-
if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
|
|
1378
|
-
}
|
|
1423
|
+
if (typeof request.findingsPresent !== "boolean") throw new TypeError("Native ABANDON findingsPresent must be a boolean");
|
|
1379
1424
|
if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
|
|
1380
1425
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
|
|
1381
1426
|
"review", "abandon", "--cwd", request.cwd,
|
|
@@ -1464,7 +1509,11 @@ class NativeReviewPlainCli {
|
|
|
1464
1509
|
}
|
|
1465
1510
|
}
|
|
1466
1511
|
|
|
1467
|
-
export function nativeReviewAbandonAuthorization(request
|
|
1512
|
+
export function nativeReviewAbandonAuthorization(request ) {
|
|
1513
|
+
// gentle-ai 0ed9225f removed evidence records from the discarded-work summary,
|
|
1514
|
+
// so the native v2 gate verifies an exact eight-line binding (schema, lineage,
|
|
1515
|
+
// revision, snapshot_identity, reason, captured_lens_results, findings_present,
|
|
1516
|
+
// actor) — there is no evidence_records_present line to derive or relay.
|
|
1468
1517
|
return [
|
|
1469
1518
|
"gentle-ai.review-abandon-authorization/v2",
|
|
1470
1519
|
`lineage=${request.lineage}`,
|
|
@@ -1473,7 +1522,6 @@ export function nativeReviewAbandonAuthorization(request
|
|
|
1473
1522
|
`reason=${request.reason}`,
|
|
1474
1523
|
`captured_lens_results=${request.capturedLensResults.join(",")}`,
|
|
1475
1524
|
`findings_present=${request.findingsPresent}`,
|
|
1476
|
-
`evidence_records_present=${request.evidenceRecordsPresent}`,
|
|
1477
1525
|
`actor=${request.actor.trim()}`,
|
|
1478
1526
|
].join("\n");
|
|
1479
1527
|
}
|
|
@@ -1596,14 +1644,20 @@ function splitNativeConsentInvocation(invocation ) {
|
|
|
1596
1644
|
let quote ;
|
|
1597
1645
|
let escaping = false;
|
|
1598
1646
|
let started = false;
|
|
1599
|
-
|
|
1647
|
+
const source = invocation.trim();
|
|
1648
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1649
|
+
const character = source[index] ;
|
|
1600
1650
|
if (escaping) {
|
|
1601
1651
|
current += character;
|
|
1602
1652
|
escaping = false;
|
|
1603
1653
|
started = true;
|
|
1604
1654
|
continue;
|
|
1605
1655
|
}
|
|
1606
|
-
|
|
1656
|
+
// Provider invocations are not shell commands. Keep every path backslash
|
|
1657
|
+
// verbatim, including quoted UNC and drive-root paths. Outside quotes,
|
|
1658
|
+
// only a backslash before whitespace joins a space-containing token.
|
|
1659
|
+
const next = source[index + 1];
|
|
1660
|
+
if (character === "\\" && quote === undefined && next !== undefined && /\s/.test(next)) {
|
|
1607
1661
|
escaping = true;
|
|
1608
1662
|
started = true;
|
|
1609
1663
|
continue;
|
|
@@ -1694,7 +1748,9 @@ function consentInvocationArguments(request )
|
|
|
1694
1748
|
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
|
|
1695
1749
|
const arguments_ = words.slice(1);
|
|
1696
1750
|
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
|
|
1697
|
-
|
|
1751
|
+
const providerCwd = exactConsentOption(arguments_, "--cwd");
|
|
1752
|
+
const requestedCwd = normalizeNativeReviewCwd(request.cwd);
|
|
1753
|
+
if (normalizeNativeReviewCwd(providerCwd) !== requestedCwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1698
1754
|
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1699
1755
|
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
1700
1756
|
const lineageId = optionalConsentLineageOption(arguments_);
|
|
@@ -2194,6 +2250,26 @@ export class NativeReviewCliV216 {
|
|
|
2194
2250
|
return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
|
|
2195
2251
|
}
|
|
2196
2252
|
|
|
2253
|
+
// Read-only risk assessment (gentle-ai#4295, gentle-pi#662). Never mutates;
|
|
2254
|
+
// an older binary without the verb, or any other process/decode failure,
|
|
2255
|
+
// rejects -- callers (the `gentle_review` tool's `assess` operation) fail
|
|
2256
|
+
// closed to `high`.
|
|
2257
|
+
async assess(request ) {
|
|
2258
|
+
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
|
|
2259
|
+
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
|
|
2260
|
+
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
|
|
2261
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
2262
|
+
const execution = await this.invoke(
|
|
2263
|
+
NATIVE_REVIEW_OPERATION.ASSESS,
|
|
2264
|
+
cwd,
|
|
2265
|
+
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
|
|
2266
|
+
false,
|
|
2267
|
+
request.signal,
|
|
2268
|
+
this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
|
|
2269
|
+
);
|
|
2270
|
+
return decode(NATIVE_REVIEW_OPERATION.ASSESS, false, () => decodeReviewAssessmentV1(execution.body));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2197
2273
|
// Recovery commands are version-gated plain CLI operations outside the
|
|
2198
2274
|
// negotiated integration-v1 contract, exactly like reviewStatus.
|
|
2199
2275
|
reclaim(request ) {
|
|
@@ -80,7 +80,10 @@ const RISK_LEVELS = ["low", "medium", "high"] ;
|
|
|
80
80
|
const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
|
|
81
81
|
const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] ;
|
|
82
82
|
const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] ;
|
|
83
|
-
|
|
83
|
+
// "collect" and "execute" are the v2 envelope's projection of a live
|
|
84
|
+
// transaction whose next_transition is mandatory: the root action names the
|
|
85
|
+
// transition kind instead of reading as a terminal stop.
|
|
86
|
+
const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop", "collect", "execute"] ;
|
|
84
87
|
const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] ;
|
|
85
88
|
|
|
86
89
|
export const REVIEW_STATUS_ACTION_DISPOSITION = {
|
|
@@ -404,6 +407,8 @@ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "
|
|
|
404
407
|
|
|
405
408
|
|
|
406
409
|
|
|
410
|
+
|
|
411
|
+
|
|
407
412
|
|
|
408
413
|
|
|
409
414
|
|
|
@@ -546,6 +551,18 @@ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.valu
|
|
|
546
551
|
|
|
547
552
|
|
|
548
553
|
|
|
554
|
+
// gentle-pi#627: the exact `gentle-ai sync` invocation that resolves a stale
|
|
555
|
+
// managed-asset set (STATUS next_transition stop, and START's preflight
|
|
556
|
+
// failure envelope both carry this same shape).
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
549
566
|
|
|
550
567
|
|
|
551
568
|
|
|
@@ -706,6 +723,8 @@ const FAILURE_CAUSE_CATEGORIES = ["inventory_io_or_layout", "lock_ambiguous", "r
|
|
|
706
723
|
|
|
707
724
|
|
|
708
725
|
|
|
726
|
+
|
|
727
|
+
|
|
709
728
|
|
|
710
729
|
|
|
711
730
|
|
|
@@ -1640,8 +1659,11 @@ function decodeCollectInput(value , label , v5 , v6
|
|
|
1640
1659
|
}
|
|
1641
1660
|
|
|
1642
1661
|
if (captureOperation === "review.capture-result") {
|
|
1643
|
-
|
|
1644
|
-
|
|
1662
|
+
// changed_path_manifest is optional here: the artifact subject's
|
|
1663
|
+
// changed_path_manifest_sha256 already binds the frozen manifest, and
|
|
1664
|
+
// gentle-ai stops inlining one copy per lens (gentle-ai#3922).
|
|
1665
|
+
if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined) {
|
|
1666
|
+
throw new TypeError(`${label} requires artifact_subject, base_tree, and candidate_tree`);
|
|
1645
1667
|
}
|
|
1646
1668
|
if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
|
|
1647
1669
|
} else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
|
|
@@ -1674,12 +1696,32 @@ function decodeCollectInput(value , label , v5 , v6
|
|
|
1674
1696
|
};
|
|
1675
1697
|
}
|
|
1676
1698
|
|
|
1699
|
+
// gentle-pi#627: gentle-ai's managed-assets continuation (STATUS next_transition
|
|
1700
|
+
// stop reason_code=managed_assets_outdated, and START's preflight failure
|
|
1701
|
+
// envelope) names the exact `gentle-ai sync` invocation that resolves a stale
|
|
1702
|
+
// managed-asset set without abandoning the frozen candidate. Decoded verbatim
|
|
1703
|
+
// from internal/cli/review_operation_contract.go's ReviewManagedAssetsContinuation.
|
|
1704
|
+
export function decodeReviewManagedAssetsContinuationV1(value , label ) {
|
|
1705
|
+
const body = exactRecord(value, label, ["operation", "command"], ["agent", "stale_assets"]);
|
|
1706
|
+
const operation = enumeration(body.operation, ["sync"] , `${label}.operation`);
|
|
1707
|
+
const command = nonempty(body.command, `${label}.command`);
|
|
1708
|
+
const agent = body.agent === undefined ? undefined : nonempty(body.agent, `${label}.agent`);
|
|
1709
|
+
const staleAssets = body.stale_assets === undefined ? undefined : stringArray(body.stale_assets, `${label}.stale_assets`, { minimum: 1 });
|
|
1710
|
+
return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1677
1713
|
export function decodeReviewNextTransitionV3(value , options = {}) {
|
|
1678
1714
|
const v6 = options.v6 === true;
|
|
1679
1715
|
const v5 = options.v5 === true || v6;
|
|
1680
|
-
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
|
|
1716
|
+
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation"]);
|
|
1681
1717
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
|
|
1682
1718
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1719
|
+
const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
|
|
1720
|
+
// gentle-pi#627: continuation is optional on every status version an older
|
|
1721
|
+
// gentle-ai may emit a managed_assets_outdated stop with no continuation at
|
|
1722
|
+
// all, and decode must not refuse the whole envelope for that. When present
|
|
1723
|
+
// it is only valid on a stop with this exact reason_code.
|
|
1724
|
+
if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
|
|
1683
1725
|
|
|
1684
1726
|
// status/v5: the bounded correction plan request rides exactly its two
|
|
1685
1727
|
// reason codes and never any other (vendored status-v5.schema.json).
|
|
@@ -1729,7 +1771,7 @@ export function decodeReviewNextTransitionV3(value , options
|
|
|
1729
1771
|
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1730
1772
|
}
|
|
1731
1773
|
if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
|
|
1732
|
-
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1774
|
+
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }) };
|
|
1733
1775
|
}
|
|
1734
1776
|
|
|
1735
1777
|
// ---------------------------------------------------------------------------
|
|
@@ -1818,11 +1860,12 @@ export function decodeReviewStatusV3(value ) {
|
|
|
1818
1860
|
|
|
1819
1861
|
let frozen ;
|
|
1820
1862
|
if (body.frozen !== undefined) {
|
|
1821
|
-
const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
|
|
1863
|
+
const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"], ["changed_path_manifest_sha256"]);
|
|
1822
1864
|
frozen = {
|
|
1823
1865
|
tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
|
|
1824
1866
|
originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
|
|
1825
1867
|
correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
|
|
1868
|
+
...(source.changed_path_manifest_sha256 === undefined ? {} : { changedPathManifestSha256: sha256(source.changed_path_manifest_sha256, "status.frozen.changed_path_manifest_sha256") }),
|
|
1826
1869
|
};
|
|
1827
1870
|
}
|
|
1828
1871
|
if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
|
|
@@ -2049,9 +2092,10 @@ function decodeFailureContext(value , label )
|
|
|
2049
2092
|
export function decodeReviewFailureV2(value ) {
|
|
2050
2093
|
const body = exactRecord(value, "failure", [
|
|
2051
2094
|
"schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
|
|
2052
|
-
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
|
|
2095
|
+
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context", "continuation"]);
|
|
2053
2096
|
requireIdentity(body, "gentle-ai.review-integration.failure/v2");
|
|
2054
2097
|
const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
|
|
2098
|
+
const code = text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ });
|
|
2055
2099
|
|
|
2056
2100
|
if (body.progress_identity !== undefined) {
|
|
2057
2101
|
if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
|
|
@@ -2060,13 +2104,18 @@ export function decodeReviewFailureV2(value ) {
|
|
|
2060
2104
|
if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
|
|
2061
2105
|
throw new TypeError("failure.request_digest with review.repair requires progress_identity");
|
|
2062
2106
|
}
|
|
2107
|
+
// gentle-pi#627: continuation is optional on every version — an older
|
|
2108
|
+
// gentle-ai may emit managed_assets_outdated with no continuation, and
|
|
2109
|
+
// decode must not refuse the whole envelope for that. When present it is
|
|
2110
|
+
// only valid on this exact code.
|
|
2111
|
+
if (body.continuation !== undefined && code !== "managed_assets_outdated") throw new TypeError("failure.continuation is only valid for code managed_assets_outdated");
|
|
2063
2112
|
|
|
2064
2113
|
return {
|
|
2065
2114
|
schema: "gentle-ai.review-integration.failure/v2",
|
|
2066
2115
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
2067
2116
|
operation,
|
|
2068
2117
|
phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] , "failure.phase"),
|
|
2069
|
-
code
|
|
2118
|
+
code,
|
|
2070
2119
|
message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
|
|
2071
2120
|
mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
|
|
2072
2121
|
authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
|
|
@@ -2080,6 +2129,7 @@ export function decodeReviewFailureV2(value ) {
|
|
|
2080
2129
|
...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
|
|
2081
2130
|
...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
|
|
2082
2131
|
...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
|
|
2132
|
+
...(body.continuation === undefined ? {} : { continuation: decodeReviewManagedAssetsContinuationV1(body.continuation, "failure.continuation") }),
|
|
2083
2133
|
raw: body,
|
|
2084
2134
|
};
|
|
2085
2135
|
}
|
|
@@ -15,3 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
|
|
17
17
|
export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
|
|
18
|
+
|
|
19
|
+
// The host is this extension: every process the session starts (pi's shell
|
|
20
|
+
// tools, Gentle Agents children) runs under it, so the handshake belongs in
|
|
21
|
+
// the session environment too, not only in the extension's own invocations.
|
|
22
|
+
// Without it the first `gentle-ai review status --agent pi` typed in the
|
|
23
|
+
// session shell fails closed (gentle-pi#550). Returns whether anything changed.
|
|
24
|
+
export function declareReviewRelayHandshake(env ) {
|
|
25
|
+
if (env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] === GENTLE_PI_REVIEW_RELAY_CONTRACT) return false;
|
|
26
|
+
env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = GENTLE_PI_REVIEW_RELAY_CONTRACT;
|
|
27
|
+
return true;
|
|
28
|
+
}
|