gentle-pi 2.1.2 → 2.3.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 +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
+
// gentle-pi.review-relay/v1 — the Pi host relay handshake (gentle-pi#311 P4,
|
|
3
|
+
// provider side gentle-ai#3249/#3254).
|
|
4
|
+
//
|
|
5
|
+
// gentle-ai admits `pi` as a host-mediated runtime identity only when the
|
|
6
|
+
// launcher that relays the invocation declares this exact contract through
|
|
7
|
+
// this exact environment variable. Any other value (or none) keeps Pi
|
|
8
|
+
// fail-closed at admission, before any repository, target, or authority work.
|
|
9
|
+
// That refusal is correct provider behavior and is never worked around here;
|
|
10
|
+
// the single central gentle-ai process runner injects this declaration on
|
|
11
|
+
// every invocation the extension makes.
|
|
12
|
+
//
|
|
13
|
+
// Both values are compiled constants: the contract identity is versioned by
|
|
14
|
+
// release, never by configuration.
|
|
15
|
+
|
|
16
|
+
export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
|
|
17
|
+
export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
|
|
@@ -8,11 +8,11 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
9
9
|
const sources = [
|
|
10
10
|
"gentle-ai-binary",
|
|
11
|
+
"review-relay-contract",
|
|
11
12
|
"review-integration-v2",
|
|
12
13
|
"native-review-cli",
|
|
13
|
-
"git-commit-transaction",
|
|
14
14
|
];
|
|
15
|
-
const header = "// Generated by scripts/build-
|
|
15
|
+
const header = "// Generated by scripts/build-runtime-modules.mjs. Do not edit.\n";
|
|
16
16
|
|
|
17
17
|
function assertNoTrailingWhitespace(content, label) {
|
|
18
18
|
const line = content.split("\n").findIndex((value) => /[ \t]+$/.test(value));
|
|
@@ -34,7 +34,7 @@ async function generatedBytes(name) {
|
|
|
34
34
|
async function main() {
|
|
35
35
|
const mode = process.argv[2];
|
|
36
36
|
if ((mode !== "--write" && mode !== "--check") || process.argv.length !== 3) {
|
|
37
|
-
throw new Error("usage: build-
|
|
37
|
+
throw new Error("usage: build-runtime-modules.mjs --write|--check");
|
|
38
38
|
}
|
|
39
39
|
const runtime = join(root, "runtime");
|
|
40
40
|
if (mode === "--write") await mkdir(runtime, { recursive: true });
|
|
@@ -55,9 +55,9 @@ async function main() {
|
|
|
55
55
|
if (actual !== expected) drift.push(destination);
|
|
56
56
|
}
|
|
57
57
|
if (drift.length > 0) {
|
|
58
|
-
throw new Error(`generated
|
|
58
|
+
throw new Error(`generated runtime is stale: ${drift.join(", ")}`);
|
|
59
59
|
}
|
|
60
|
-
process.stdout.write(`
|
|
60
|
+
process.stdout.write(`runtime ${mode === "--write" ? "generated" : "matches TypeScript sources"} (${sources.length} modules)\n`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
main().catch((error) => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Offline drift check for the mirrored gentle-ai provider contract bundle
|
|
3
|
+
// (gentle-pi#311 P2).
|
|
4
|
+
//
|
|
5
|
+
// Fails when the mirror, the lock record, the schemas, the vectors, or the
|
|
6
|
+
// generated consumer baselines disagree in any direction. Runs completely
|
|
7
|
+
// offline against `contracts/review-provider-contract-mirror/` and never
|
|
8
|
+
// touches the network or the Gentle AI release pin.
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
generateProviderContractBaselines,
|
|
16
|
+
piRuntimeRegistration,
|
|
17
|
+
verifyProviderContractBundleTree,
|
|
18
|
+
} from "../lib/provider-contract-bundle.ts";
|
|
19
|
+
import {
|
|
20
|
+
PROVIDER_CONTRACT_ACQUISITION,
|
|
21
|
+
PROVIDER_CONTRACT_LOCK_FILE,
|
|
22
|
+
PROVIDER_CONTRACT_LOCK_SCHEMA,
|
|
23
|
+
PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
24
|
+
} from "./mirror-provider-contract.mjs";
|
|
25
|
+
|
|
26
|
+
function sha256Hex(bytes) {
|
|
27
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function recordEquals(problems, label, expected, actual) {
|
|
31
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
32
|
+
const actualKeys = Object.keys(actual).sort();
|
|
33
|
+
for (const key of expectedKeys) {
|
|
34
|
+
if (!(key in actual)) problems.push(`${label}: ${key} is recorded in the lock but absent from the mirror`);
|
|
35
|
+
else if (actual[key] !== expected[key]) problems.push(`${label}: ${key} drifted (lock ${expected[key]}, mirror ${actual[key]})`);
|
|
36
|
+
}
|
|
37
|
+
for (const key of actualKeys) {
|
|
38
|
+
if (!(key in expected)) problems.push(`${label}: ${key} exists in the mirror but is not recorded in the lock`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Returns a list of drift problems; an empty list means the mirror, lock,
|
|
43
|
+
// schemas, vectors, and generated baselines all agree.
|
|
44
|
+
export function checkProviderContractMirror(packageRoot) {
|
|
45
|
+
const problems = [];
|
|
46
|
+
const mirrorRoot = join(packageRoot, ...PROVIDER_CONTRACT_MIRROR_ROOT.split("/"));
|
|
47
|
+
const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
|
|
48
|
+
if (!existsSync(lockPath)) {
|
|
49
|
+
return [`${PROVIDER_CONTRACT_MIRROR_ROOT}/${PROVIDER_CONTRACT_LOCK_FILE} is missing; run scripts/mirror-provider-contract.mjs against a verified local bundle`];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let lock;
|
|
53
|
+
try {
|
|
54
|
+
lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return [`lock record is not valid JSON: ${error instanceof Error ? error.message : String(error)}`];
|
|
57
|
+
}
|
|
58
|
+
if (lock === null || typeof lock !== "object" || Array.isArray(lock)) return ["lock record is not a JSON object"];
|
|
59
|
+
if (lock.schema !== PROVIDER_CONTRACT_LOCK_SCHEMA) problems.push(`lock schema is ${JSON.stringify(lock.schema)}; expected ${JSON.stringify(PROVIDER_CONTRACT_LOCK_SCHEMA)}`);
|
|
60
|
+
if (lock.acquisition !== PROVIDER_CONTRACT_ACQUISITION) problems.push(`lock acquisition is ${JSON.stringify(lock.acquisition)}; expected ${JSON.stringify(PROVIDER_CONTRACT_ACQUISITION)}`);
|
|
61
|
+
if (typeof lock.contract_semver !== "string") return [...problems, "lock contract_semver is not a string"];
|
|
62
|
+
if (lock.pi_registered !== true) problems.push("lock does not record pi as a registered runtime identity");
|
|
63
|
+
|
|
64
|
+
// The mirror directory must contain exactly the lock and the locked version.
|
|
65
|
+
const expectedVersionDirectory = `v${lock.contract_semver}`;
|
|
66
|
+
for (const entry of readdirSync(mirrorRoot)) {
|
|
67
|
+
if (entry !== PROVIDER_CONTRACT_LOCK_FILE && entry !== expectedVersionDirectory) {
|
|
68
|
+
problems.push(`mirror has unexpected entry ${JSON.stringify(entry)}; only ${PROVIDER_CONTRACT_LOCK_FILE} and ${expectedVersionDirectory} belong there`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const versionRoot = join(mirrorRoot, expectedVersionDirectory);
|
|
73
|
+
let bundle;
|
|
74
|
+
try {
|
|
75
|
+
bundle = verifyProviderContractBundleTree(join(versionRoot, "bundle"));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return [...problems, `mirrored bundle failed verification: ${error instanceof Error ? error.message : String(error)}`];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (bundle.contractSemver !== lock.contract_semver) {
|
|
81
|
+
problems.push(`mirrored bundle declares contract ${bundle.contractSemver}; lock records ${lock.contract_semver}`);
|
|
82
|
+
}
|
|
83
|
+
if (bundle.treeSha256 !== lock.tree_sha256) {
|
|
84
|
+
problems.push(`mirrored bundle tree digest ${bundle.treeSha256} does not match lock tree_sha256 ${lock.tree_sha256}`);
|
|
85
|
+
}
|
|
86
|
+
recordEquals(problems, "bundle entries", lock.entries ?? {}, Object.fromEntries(bundle.entrySha256));
|
|
87
|
+
const lockRuntimes = Array.isArray(lock.runtimes) ? lock.runtimes : [];
|
|
88
|
+
const bundleRuntimes = [...(bundle.runtimes ?? [])];
|
|
89
|
+
if (JSON.stringify(lockRuntimes) !== JSON.stringify(bundleRuntimes)) {
|
|
90
|
+
problems.push(`lock runtimes ${JSON.stringify(lockRuntimes)} do not match mirrored manifest runtimes ${JSON.stringify(bundleRuntimes)}`);
|
|
91
|
+
}
|
|
92
|
+
if (!piRuntimeRegistration(bundle).registered) {
|
|
93
|
+
problems.push("mirrored bundle does not register the pi runtime identity");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Regenerate the consumer contract surface from the verified mirror and
|
|
97
|
+
// require byte-identical generated files plus matching lock digests.
|
|
98
|
+
const baselines = generateProviderContractBaselines(bundle);
|
|
99
|
+
const generatedRoot = join(versionRoot, "generated");
|
|
100
|
+
const onDisk = existsSync(generatedRoot) ? readdirSync(generatedRoot).sort() : [];
|
|
101
|
+
const expectedGenerated = [...baselines.keys()].sort();
|
|
102
|
+
for (const name of onDisk) {
|
|
103
|
+
if (!baselines.has(name)) problems.push(`generated/${name} exists on disk but is not a generated provider baseline`);
|
|
104
|
+
}
|
|
105
|
+
const actualGeneratedSha256 = {};
|
|
106
|
+
for (const name of expectedGenerated) {
|
|
107
|
+
const path = join(generatedRoot, name);
|
|
108
|
+
if (!existsSync(path)) {
|
|
109
|
+
problems.push(`generated/${name} is missing from the mirror`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const actual = readFileSync(path);
|
|
113
|
+
const expected = Buffer.from(baselines.get(name), "utf8");
|
|
114
|
+
if (!actual.equals(expected)) problems.push(`generated/${name} is not byte-identical to the baseline derived from the verified mirror`);
|
|
115
|
+
actualGeneratedSha256[`generated/${name}`] = sha256Hex(actual);
|
|
116
|
+
}
|
|
117
|
+
recordEquals(problems, "generated baselines", lock.generated ?? {}, actualGeneratedSha256);
|
|
118
|
+
|
|
119
|
+
return problems;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const packageRoot = join(fileURLToPath(new URL("..", import.meta.url)));
|
|
124
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
125
|
+
if (problems.length > 0) {
|
|
126
|
+
console.error("gentle-pi provider contract mirror has drifted:");
|
|
127
|
+
for (const problem of problems) console.error(`- ${problem}`);
|
|
128
|
+
console.error("\nRe-run scripts/mirror-provider-contract.mjs against a verified local bundle, or restore the mirrored bytes.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const lock = JSON.parse(readFileSync(join(packageRoot, ...PROVIDER_CONTRACT_MIRROR_ROOT.split("/"), PROVIDER_CONTRACT_LOCK_FILE), "utf8"));
|
|
132
|
+
console.log(`gentle-pi provider contract mirror check passed (contract ${lock.contract_semver}, ${Object.keys(lock.entries).length} bundle entries, ${Object.keys(lock.generated).length} generated baselines, acquisition ${lock.acquisition}).`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
136
|
+
if (isMainModule) {
|
|
137
|
+
await main();
|
|
138
|
+
}
|
|
@@ -19,24 +19,36 @@ import { execFile } from "node:child_process";
|
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
20
|
|
|
21
21
|
const execFileAsync = promisify(execFile);
|
|
22
|
-
const RELEASE_BASE_URL = "https://github.com/Gentleman-Programming/gentle-ai/releases/download/v2.2.2/";
|
|
23
22
|
const MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
24
23
|
const MAX_REDIRECTS = 3;
|
|
25
24
|
const DOWNLOAD_TIMEOUTS = { headers: 10_000, body: 30_000, attempts: 2, retryDelay: 100 };
|
|
26
25
|
const GO_COMMAND_TIMEOUT_MS = 120_000;
|
|
26
|
+
// `go install` compiles the pinned module from source on Windows. A clean
|
|
27
|
+
// build on a cold module cache measured about 232 s (#400), so the 120 s
|
|
28
|
+
// probe bound killed a healthy build; fifteen minutes leaves close to a 4x
|
|
29
|
+
// margin for slower disks and real-time scanners while still bounding a hung
|
|
30
|
+
// toolchain. The `go version` and `where.exe` probes keep the 120 s bound.
|
|
31
|
+
const GO_INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
27
32
|
const GO_COMMAND_MAX_BUFFER = 1024 * 1024;
|
|
28
33
|
const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
29
|
-
|
|
34
|
+
// The one authoritative pinned Gentle AI version. Every other location in this
|
|
35
|
+
// module (the release download URL, the Windows source tag, and the reported
|
|
36
|
+
// version check below) derives from this constant instead of repeating the
|
|
37
|
+
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
|
+
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
+
export const INSTALLER_VERSION = "2.5.0";
|
|
40
|
+
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
30
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
31
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
32
43
|
GO_SUMDB_SOURCE_BUILD: "go-sumdb-source-build",
|
|
33
44
|
});
|
|
34
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai";
|
|
35
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v2";
|
|
36
|
-
export const GENTLE_AI_WINDOWS_SOURCE_TAG =
|
|
37
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.
|
|
38
|
-
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum
|
|
39
|
-
|
|
47
|
+
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.5.0`
|
|
49
|
+
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
+
// tag resolves to commit f5dd1a6c, the published v2.5.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0nvTLJFAf9ruBDwTvi+aDrWt/dt/GKvUP2cFVB6c2DI=";
|
|
40
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
41
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
42
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -55,28 +67,73 @@ export class GentleAiInstallerError extends Error {
|
|
|
55
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
56
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
57
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
58
|
-
// still holds it. The v2.
|
|
70
|
+
// still holds it. The v2.5.0 digests are pinned from the published release:
|
|
59
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
60
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
61
73
|
// executables.
|
|
62
74
|
export const GENTLE_AI_PENDING_DIGEST = "PENDING-GENTLE-AI-RELEASE-DIGEST";
|
|
63
75
|
|
|
76
|
+
// A raw prerelease asset is less durable than a signed stable archive, and the
|
|
77
|
+
// install path has no other Darwin/Linux source, so the gentle-pi repository
|
|
78
|
+
// hosts a byte-identical mirror of the pinned prerelease assets under its own
|
|
79
|
+
// release tag. The mirror is only a second download source: every byte from
|
|
80
|
+
// either source must still match the same pinned SHA-256 digests, and a
|
|
81
|
+
// checksum mismatch never falls through to the other source.
|
|
82
|
+
export const GENTLE_AI_MIRROR_RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-pi/releases/download/gentle-ai-mirror-v${INSTALLER_VERSION}/`;
|
|
83
|
+
export const GENTLE_AI_ASSET_UNAVAILABLE_CODE = "GENTLE_AI_ASSET_UNAVAILABLE";
|
|
84
|
+
|
|
64
85
|
function asset(name, sha256, binarySha256, executable) {
|
|
65
|
-
|
|
86
|
+
const mirrorUrl = INSTALLER_VERSION.includes("-") ? `${GENTLE_AI_MIRROR_RELEASE_BASE_URL}${name}` : undefined;
|
|
87
|
+
return Object.freeze({ name, sha256, binarySha256, executable, url: `${RELEASE_BASE_URL}${name}`, ...(mirrorUrl === undefined ? {} : { mirrorUrl }) });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Download failure (not integrity failure) is the only condition that falls
|
|
91
|
+
// through to the mirror; when both sources are unavailable the error names the
|
|
92
|
+
// documented recovery instead of leaving a dead end until the next repin.
|
|
93
|
+
async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
94
|
+
const download = options.download ?? downloadGentleAiAsset;
|
|
95
|
+
try {
|
|
96
|
+
await download(asset.url, destination);
|
|
97
|
+
} catch (primaryError) {
|
|
98
|
+
if (asset.mirrorUrl === undefined) throw primaryError;
|
|
99
|
+
try {
|
|
100
|
+
await download(asset.mirrorUrl, destination);
|
|
101
|
+
} catch (mirrorError) {
|
|
102
|
+
throw new GentleAiInstallerError(
|
|
103
|
+
GENTLE_AI_ASSET_UNAVAILABLE_CODE,
|
|
104
|
+
`Gentle AI ${asset.name} is unavailable from the pinned release and its gentle-pi mirror. Retry later, or set GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1 and register a locally built v${INSTALLER_VERSION} binary through ~/.pi/gentle-ai/dev-binary.json.`,
|
|
105
|
+
mirrorError,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
66
109
|
}
|
|
67
110
|
|
|
68
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
69
|
-
// distributing unsigned Windows builds in c4b764d0, so v2.
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v2.5.0 publishes signed
|
|
70
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
71
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
72
115
|
// upstream ships signed Windows assets.
|
|
73
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
74
|
-
"darwin/amd64": asset("gentle-ai_2.
|
|
75
|
-
"darwin/arm64": asset("gentle-ai_2.
|
|
76
|
-
"linux/amd64": asset("gentle-ai_2.
|
|
77
|
-
"linux/arm64": asset("gentle-ai_2.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_2.5.0_darwin_amd64.tar.gz", "8204ed692d7ff16fae2eb8bd7b3132b5e4e4dd7b9ea40e93bfced57394da27c0", "929817d320e94ad62be7ed546ad8746e62bd63ab97e3f38c94456f221f163eca", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_2.5.0_darwin_arm64.tar.gz", "ce06831cea9fd2ac7b2b36d9e98e63988246e0fbafbbd2cc7c003ec2f1463a23", "c859cd88138eb41e83ef9b75309697aaff2acb9f720c9a029b432095f66aea02", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_2.5.0_linux_amd64.tar.gz", "2ba84a3a7ba2b1193019bde2acb05b02cbf222b667568c919686352b3caab113", "5acedd14b7927a7665636bf728c14a614597ce85dd91313bc50cbc96881c6cab", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_2.5.0_linux_arm64.tar.gz", "16a6243d17c146e3fc0024f6f005c5bd141fbb2dda4b56e269318e499ddc170b", "ce75ba1174955813ef462f4e7a50dd59a8e8167575033f6e17aa8d8488630cb8", "gentle-ai"),
|
|
78
121
|
});
|
|
79
122
|
|
|
123
|
+
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
124
|
+
// the raw executable itself: prereleases are hand-built and ship raw binaries,
|
|
125
|
+
// while every stable release publishes signed archives, so a raw asset under a
|
|
126
|
+
// stable pin means the pin itself is wrong. Anything else fails closed before
|
|
127
|
+
// a byte is downloaded.
|
|
128
|
+
export function gentleAiAssetForm(name, installerVersion = INSTALLER_VERSION) {
|
|
129
|
+
if (name.endsWith(".tar.gz") || name.endsWith(".zip")) return "archive";
|
|
130
|
+
if (/^gentle-ai_[0-9A-Za-z.+~-]+_(darwin|linux|windows)_(amd64|arm64)(\.exe)?$/.test(name)) {
|
|
131
|
+
if (!installerVersion.includes("-")) throw new Error(`raw Gentle AI release asset ${name} is only admitted for a prerelease pin; stable pin ${installerVersion} requires a signed archive`);
|
|
132
|
+
return "raw-binary";
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`unsupported Gentle AI release asset form: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
80
137
|
function upstreamArchitecture(architecture) {
|
|
81
138
|
return architecture === "x64" ? "amd64" : architecture;
|
|
82
139
|
}
|
|
@@ -96,7 +153,7 @@ export function resolveGentleAiReleaseAsset(platform = process.platform, archite
|
|
|
96
153
|
const windowsSource = isWindowsGoSumdbSourceTarget(platform, architecture)
|
|
97
154
|
? "; Windows x64/arm64 use Go SumDB source installation because no signed Windows archive is published"
|
|
98
155
|
: "";
|
|
99
|
-
throw new Error(`unsupported Gentle AI platform/architecture: ${platform}/${architecture};
|
|
156
|
+
throw new Error(`unsupported Gentle AI platform/architecture: ${platform}/${architecture}; pinned release asset pairs are darwin/x64, darwin/arm64, linux/x64, and linux/arm64${windowsSource}`);
|
|
100
157
|
}
|
|
101
158
|
return resolved;
|
|
102
159
|
}
|
|
@@ -241,8 +298,8 @@ function isCanonicalManifest(contents, parsed, expected) {
|
|
|
241
298
|
|
|
242
299
|
function commandOutput(result) { return typeof result?.stdout === "string" ? result.stdout : ""; }
|
|
243
300
|
|
|
244
|
-
function commandOptions(env, cwd) {
|
|
245
|
-
return { cwd, env, shell: false, windowsHide: true, timeout
|
|
301
|
+
function commandOptions(env, cwd, timeout = GO_COMMAND_TIMEOUT_MS) {
|
|
302
|
+
return { cwd, env, shell: false, windowsHide: true, timeout, maxBuffer: GO_COMMAND_MAX_BUFFER };
|
|
246
303
|
}
|
|
247
304
|
|
|
248
305
|
function outputVersion(output) {
|
|
@@ -330,11 +387,13 @@ async function verifyGoBuildMetadata(execute, goPath, binaryPath, environment, c
|
|
|
330
387
|
catch (error) { if (error instanceof GentleAiInstallerError) throw error; throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, "Gentle AI source build metadata could not be verified.", error); }
|
|
331
388
|
}
|
|
332
389
|
|
|
390
|
+
function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
391
|
+
|
|
333
392
|
async function assertExactGentleAiVersion(execute, binaryPath, environment, cwd) {
|
|
334
393
|
let result;
|
|
335
394
|
try { result = await runCommand(execute, binaryPath, ["version"], commandOptions(environment, cwd)); }
|
|
336
395
|
catch (error) { throw new GentleAiInstallerError(GENTLE_AI_VERSION_MISMATCH_CODE, `Gentle AI source build at ${binaryPath} could not report its version.`, error); }
|
|
337
|
-
if (
|
|
396
|
+
if (!new RegExp(`^gentle-ai ${escapeRegExp(INSTALLER_VERSION)}\\r?\\n?$`).test(commandOutput(result))) throw new GentleAiInstallerError(GENTLE_AI_VERSION_MISMATCH_CODE, `Gentle AI source build reported ${JSON.stringify(commandOutput(result).trim())}; expected gentle-ai ${INSTALLER_VERSION}.`);
|
|
338
397
|
}
|
|
339
398
|
|
|
340
399
|
function sameFile(before, after) { return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs; }
|
|
@@ -574,7 +633,7 @@ async function installWindowsGentleAiFromGoSumdb(options, packageRoot, architect
|
|
|
574
633
|
const existing = versionBundlePath(runtimeRoot);
|
|
575
634
|
if (await existingWindowsSourceBundleMatches(existing, execute, goPath, environment, architecture)) return { installed: false, binaryPath: join(existing, "gentle-ai.exe"), method: GENTLE_AI_INSTALL_METHOD.GO_SUMDB_SOURCE_BUILD };
|
|
576
635
|
await assertGoToolchain(execute, goPath, environment, stagingDirectory);
|
|
577
|
-
try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment, stagingDirectory)); }
|
|
636
|
+
try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment, stagingDirectory, GO_INSTALL_TIMEOUT_MS)); }
|
|
578
637
|
catch (error) { throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, `Gentle AI Go SumDB source installation failed for ${GENTLE_AI_WINDOWS_SOURCE_PACKAGE}.`, error); }
|
|
579
638
|
const builtBinary = join(environment.GOBIN, "gentle-ai.exe"), binaryPath = join(stagingDirectory, "gentle-ai.exe");
|
|
580
639
|
const details = await lstat(builtBinary);
|
|
@@ -599,18 +658,22 @@ async function installSignedRelease(options, packageRoot, platform, arch, asset)
|
|
|
599
658
|
if (await existingSignedBundleMatches(existing, asset, platform)) return { installed: false, binaryPath: join(existing, asset.executable), asset };
|
|
600
659
|
const stagingDirectory = await mkdtemp(join(runtimeRoot, `.v${INSTALLER_VERSION}.staging-`));
|
|
601
660
|
try {
|
|
661
|
+
const form = gentleAiAssetForm(asset.name);
|
|
602
662
|
const archive = join(stagingDirectory, asset.name);
|
|
603
|
-
await (
|
|
663
|
+
await downloadPinnedGentleAiAsset(asset, archive, options);
|
|
604
664
|
if ((await sha256File(archive)) !== asset.sha256) throw new Error(`Gentle AI archive checksum mismatch for ${asset.name}`);
|
|
665
|
+
let source = archive;
|
|
605
666
|
const extracted = join(stagingDirectory, "extracted");
|
|
606
|
-
|
|
607
|
-
|
|
667
|
+
if (form === "archive") {
|
|
668
|
+
await (options.extractArchive ?? extractGentleAiArchive)(archive, extracted);
|
|
669
|
+
source = await expectedRegularFile(extracted, asset.executable);
|
|
670
|
+
}
|
|
608
671
|
if (asset.binarySha256 && (await sha256File(source)) !== asset.binarySha256) throw new Error(`Gentle AI binary checksum mismatch for ${asset.name}`);
|
|
609
672
|
const binaryPath = join(stagingDirectory, asset.executable);
|
|
610
673
|
await copyFile(source, binaryPath);
|
|
611
674
|
if (platform !== "win32") await chmod(binaryPath, 0o700);
|
|
612
675
|
await writeFile(join(stagingDirectory, "integrity.json"), canonicalManifest(signedReleaseManifest(asset, await sha256File(binaryPath))), { mode: 0o600 });
|
|
613
|
-
await safeRemoveDirectory(extracted);
|
|
676
|
+
if (form === "archive") await safeRemoveDirectory(extracted);
|
|
614
677
|
await rm(archive, { force: true });
|
|
615
678
|
const published = await publishBundle(runtimeRoot, stagingDirectory, options);
|
|
616
679
|
return { installed: true, binaryPath: join(published, asset.executable), asset };
|