gentle-pi 2.1.2 → 2.2.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 +70 -12
- 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 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- 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 +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- 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/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/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 +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -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/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -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/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/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 +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- 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 +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- 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/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/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// gentle-pi.review-relay/v1 — the Pi host relay handshake (gentle-pi#311 P4,
|
|
2
|
+
// provider side gentle-ai#3249/#3254).
|
|
3
|
+
//
|
|
4
|
+
// gentle-ai admits `pi` as a host-mediated runtime identity only when the
|
|
5
|
+
// launcher that relays the invocation declares this exact contract through
|
|
6
|
+
// this exact environment variable. Any other value (or none) keeps Pi
|
|
7
|
+
// fail-closed at admission, before any repository, target, or authority work.
|
|
8
|
+
// That refusal is correct provider behavior and is never worked around here;
|
|
9
|
+
// the single central gentle-ai process runner injects this declaration on
|
|
10
|
+
// every invocation the extension makes.
|
|
11
|
+
//
|
|
12
|
+
// Both values are compiled constants: the contract identity is versioned by
|
|
13
|
+
// release, never by configuration.
|
|
14
|
+
|
|
15
|
+
export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
|
|
16
|
+
export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
|
package/lib/sdd-preflight.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -394,6 +394,57 @@ function copyDirectoryFiles(
|
|
|
394
394
|
return { copied, skipped };
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
// Assets retired by gentle-pi#311 P5: the Pi-owned adversarial review actors.
|
|
398
|
+
// The refuter and validator verdicts now execute through Go-owned pi
|
|
399
|
+
// processes via provider-rendered self-contained vectors, so these agent
|
|
400
|
+
// definitions have no runtime consumer. The migration manifests under
|
|
401
|
+
// assets/migrations are append-only legacy-hash HISTORY (adoption evidence
|
|
402
|
+
// for force-installs) with no removal semantics, so history stays untouched
|
|
403
|
+
// and retirement happens here: an installed copy is deleted only when its
|
|
404
|
+
// content hash proves package ownership (current manifest or legacy
|
|
405
|
+
// history); user-modified copies are left in place and only lose managed
|
|
406
|
+
// ownership.
|
|
407
|
+
const RETIRED_MANAGED_ASSETS = Object.freeze([
|
|
408
|
+
"agents/review-refuter.md",
|
|
409
|
+
"agents/review-validator.md",
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
function removeRetiredManagedAssets(
|
|
413
|
+
agentHome: string,
|
|
414
|
+
manifest: ManagedAssetsManifest,
|
|
415
|
+
): void {
|
|
416
|
+
let legacyHashes: Record<string, readonly string[]> | undefined;
|
|
417
|
+
for (const ownershipKey of RETIRED_MANAGED_ASSETS) {
|
|
418
|
+
const installedPath = join(agentHome, ...ownershipKey.split("/"));
|
|
419
|
+
if (!existsSync(installedPath)) {
|
|
420
|
+
delete manifest.assets[ownershipKey];
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
let installedContent: string | undefined;
|
|
424
|
+
try {
|
|
425
|
+
installedContent = readFileSync(installedPath, "utf8");
|
|
426
|
+
} catch {
|
|
427
|
+
installedContent = undefined;
|
|
428
|
+
}
|
|
429
|
+
if (installedContent === undefined) continue;
|
|
430
|
+
const installedHash = managedAssetHash(installedContent);
|
|
431
|
+
const managed = manifest.assets[ownershipKey] === installedHash;
|
|
432
|
+
const legacy = (legacyHashes ??= readLegacyManagedAssetHashes())[ownershipKey]?.includes(
|
|
433
|
+
managedAssetHash(legacyComparableAssetContent(ownershipKey, installedContent)),
|
|
434
|
+
) === true;
|
|
435
|
+
if (managed || legacy) {
|
|
436
|
+
try {
|
|
437
|
+
rmSync(installedPath);
|
|
438
|
+
} catch {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Managed copies are gone; user-modified copies stay but stop being
|
|
443
|
+
// package-managed either way.
|
|
444
|
+
delete manifest.assets[ownershipKey];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
397
448
|
export function installSddAssets(
|
|
398
449
|
_cwd: string,
|
|
399
450
|
force: boolean,
|
|
@@ -407,6 +458,7 @@ export function installSddAssets(
|
|
|
407
458
|
(cachedLegacyAssetHashes ??= readLegacyManagedAssetHashes());
|
|
408
459
|
}
|
|
409
460
|
const manifest = readManagedAssetsManifest(manifestPath);
|
|
461
|
+
removeRetiredManagedAssets(agentHome, manifest);
|
|
410
462
|
const agents = copyDirectoryFiles(
|
|
411
463
|
join(ASSETS_DIR, "agents"),
|
|
412
464
|
join(agentHome, "agents"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -39,10 +39,13 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build:transaction-runner": "node scripts/build-git-commit-transaction-runner.mjs --write",
|
|
41
41
|
"check:transaction-runner": "node scripts/build-git-commit-transaction-runner.mjs --check",
|
|
42
|
+
"check:provider-contract": "node scripts/check-provider-contract.mjs",
|
|
42
43
|
"postinstall": "node scripts/install-gentle-ai.mjs",
|
|
43
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run test:harness",
|
|
44
|
+
"test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
44
45
|
"test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
|
|
45
46
|
"test:dev-binary": "node --experimental-strip-types --test tests/devbinary/*.devtest.ts",
|
|
47
|
+
"test:cross-lane": "node tests/crosslane/cross-lane.mjs",
|
|
48
|
+
"test:maintainer": "node --experimental-strip-types --test tests/maintainer/*.maintest.ts",
|
|
46
49
|
"test:packed-runner": "node scripts/test-packed-runner.mjs",
|
|
47
50
|
"prepack": "pnpm test && node scripts/verify-package-files.mjs",
|
|
48
51
|
"prepublishOnly": "pnpm test && node scripts/verify-package-files.mjs && pnpm run test:packed-runner"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by scripts/build-git-commit-transaction-runner.mjs. Do not edit.
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
6
|
import {
|
|
6
7
|
GENTLE_AI_INSTALL_METHOD,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
9
10
|
GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
|
|
10
11
|
GENTLE_AI_WINDOWS_SOURCE_TAG,
|
|
12
|
+
INSTALLER_VERSION,
|
|
11
13
|
isGentleAiWindowsGoVersionSupported,
|
|
12
14
|
isWindowsGoSumdbSourceTarget,
|
|
13
15
|
resolveGentleAiReleaseAsset,
|
|
@@ -15,7 +17,10 @@ import {
|
|
|
15
17
|
import { fileURLToPath } from "node:url";
|
|
16
18
|
|
|
17
19
|
export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
|
|
18
|
-
|
|
20
|
+
// Derived from the one authoritative pinned version in
|
|
21
|
+
// scripts/gentle-ai-installer.mjs rather than repeating the literal here, so
|
|
22
|
+
// the two can never independently drift apart the way they once did.
|
|
23
|
+
export const GENTLE_AI_VERSION = INSTALLER_VERSION;
|
|
19
24
|
|
|
20
25
|
export class PackageLocalGentleAiBinaryMissingError extends Error {
|
|
21
26
|
code = GENTLE_AI_BINARY_MISSING_CODE;
|
|
@@ -43,6 +48,165 @@ function sha256(value ) {
|
|
|
43
48
|
return createHash("sha256").update(value).digest("hex");
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Dev-binary override — the maintainer field-test lane.
|
|
53
|
+
//
|
|
54
|
+
// Two explicit activation paths, in precedence order:
|
|
55
|
+
// 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
|
|
56
|
+
// 2. the persistent registration file at
|
|
57
|
+
// <GENTLE_PI_CONFIG_HOME|~/.pi/gentle-ai>/dev-binary.json with the strict
|
|
58
|
+
// shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
|
|
59
|
+
//
|
|
60
|
+
// The registration deliberately pins no digest: it is the unpinned field-test
|
|
61
|
+
// mode, and the binary at that path changes on every rebuild. Every resolution
|
|
62
|
+
// re-validates the file and recomputes the sha256, so a rebuilt binary is
|
|
63
|
+
// followed automatically with a fresh digest and no re-registration.
|
|
64
|
+
//
|
|
65
|
+
// Guardrails per resolution: absolute path, regular non-symlink file, POSIX
|
|
66
|
+
// executable. Any failure — including a malformed registration document or a
|
|
67
|
+
// registered-but-missing binary — is a typed error naming its origin, never a
|
|
68
|
+
// silent fallback to the pinned binary: silently running the pin while the
|
|
69
|
+
// maintainer believes he is field-testing main is the worst possible outcome.
|
|
70
|
+
// With neither activation path present, the pinned supply-chain resolution
|
|
71
|
+
// below stays byte-identical.
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export const GENTLE_AI_DEV_BINARY_ENV = "GENTLE_PI_GENTLE_AI_DEV_BINARY";
|
|
75
|
+
export const GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA = "gentle-pi.dev-binary/v1";
|
|
76
|
+
export const GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE = "dev-binary-override-invalid";
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export class GentleAiDevBinaryOverrideError extends Error {
|
|
92
|
+
code = GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE;
|
|
93
|
+
source ;
|
|
94
|
+
origin ;
|
|
95
|
+
constructor(source , origin , reason ) {
|
|
96
|
+
super(`${GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE}: ${origin} ${reason}. Fix or remove the override; the pinned binary is never used silently while an override is declared.`);
|
|
97
|
+
this.name = "GentleAiDevBinaryOverrideError";
|
|
98
|
+
this.source = source;
|
|
99
|
+
this.origin = origin;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let devBinaryEnvironmentTestingOverlay ;
|
|
104
|
+
|
|
105
|
+
/** Testing-only environment overlay; production code never calls this. */
|
|
106
|
+
export function setGentleAiDevBinaryEnvironmentForTesting(environment ) {
|
|
107
|
+
devBinaryEnvironmentTestingOverlay = environment;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ambientDevBinaryEnvironment() {
|
|
111
|
+
return devBinaryEnvironmentTestingOverlay ?? { env: process.env, home: homedir() };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function gentleAiDevBinaryRegistrationPath(environment = ambientDevBinaryEnvironment()) {
|
|
115
|
+
const configHome = environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
|
|
116
|
+
return join(configHome, "dev-binary.json");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateDevBinary(source , origin , path , platform ) {
|
|
120
|
+
if (typeof path !== "string" || path.length === 0) throw new GentleAiDevBinaryOverrideError(source, origin, "declares an empty dev binary path");
|
|
121
|
+
if (!isAbsolute(path)) throw new GentleAiDevBinaryOverrideError(source, origin, `must name an absolute path, received "${path}"`);
|
|
122
|
+
let details ;
|
|
123
|
+
try {
|
|
124
|
+
details = lstatSync(path);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which does not exist`);
|
|
127
|
+
}
|
|
128
|
+
if (!details.isFile() || details.isSymbolicLink()) throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which is not a regular non-symlink file`);
|
|
129
|
+
if (platform !== "win32" && (details.mode & 0o111) === 0) throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which is not a POSIX executable`);
|
|
130
|
+
let digest ;
|
|
131
|
+
try {
|
|
132
|
+
digest = sha256(readFileSync(path));
|
|
133
|
+
} catch {
|
|
134
|
+
throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which could not be read`);
|
|
135
|
+
}
|
|
136
|
+
return { source, origin, path, sha256: digest };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readDevBinaryRegistration(registrationPath ) {
|
|
140
|
+
let contents ;
|
|
141
|
+
try {
|
|
142
|
+
contents = readFileSync(registrationPath, "utf8");
|
|
143
|
+
} catch {
|
|
144
|
+
throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "could not be read");
|
|
145
|
+
}
|
|
146
|
+
let parsed ;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(contents);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "is not valid JSON");
|
|
151
|
+
}
|
|
152
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "must be a JSON object");
|
|
153
|
+
const record = parsed ;
|
|
154
|
+
const keys = Object.keys(record).sort();
|
|
155
|
+
if (keys.length !== 2 || keys[0] !== "path" || keys[1] !== "schema") throw new GentleAiDevBinaryOverrideError("registration", registrationPath, `must carry exactly the keys "schema" and "path"`);
|
|
156
|
+
if (record.schema !== GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, `must declare schema ${GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA}`);
|
|
157
|
+
if (typeof record.path !== "string" || record.path.length === 0) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "must declare a non-empty string path");
|
|
158
|
+
return record.path;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolves the active dev-binary override, if any. Returns undefined only when
|
|
163
|
+
* neither activation path is present; a present-but-invalid override always
|
|
164
|
+
* throws a typed GentleAiDevBinaryOverrideError naming its origin.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveGentleAiDevBinaryOverride(
|
|
167
|
+
environment = ambientDevBinaryEnvironment(),
|
|
168
|
+
platform = process.platform,
|
|
169
|
+
) {
|
|
170
|
+
const envValue = environment.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
171
|
+
if (envValue !== undefined && envValue.length > 0) return validateDevBinary("env", GENTLE_AI_DEV_BINARY_ENV, envValue, platform);
|
|
172
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
173
|
+
if (!existsSync(registrationPath)) return undefined;
|
|
174
|
+
return validateDevBinary("registration", registrationPath, readDevBinaryRegistration(registrationPath), platform);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Cheap presence probe: is a dev-binary override declared at all? Used by the
|
|
179
|
+
* native CLI to select the unpinned version gate without hashing the binary.
|
|
180
|
+
* Declared-but-invalid still counts as configured — the resolution path will
|
|
181
|
+
* fail loudly with the typed error instead of quietly using the pin.
|
|
182
|
+
*/
|
|
183
|
+
export function gentleAiDevBinaryOverrideConfigured(environment = ambientDevBinaryEnvironment()) {
|
|
184
|
+
const envValue = environment.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
185
|
+
if (envValue !== undefined && envValue.length > 0) return true;
|
|
186
|
+
return existsSync(gentleAiDevBinaryRegistrationPath(environment));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Validates and persistently registers a dev binary; returns the fresh override. */
|
|
190
|
+
export function registerGentleAiDevBinary(
|
|
191
|
+
path ,
|
|
192
|
+
environment = ambientDevBinaryEnvironment(),
|
|
193
|
+
platform = process.platform,
|
|
194
|
+
) {
|
|
195
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
196
|
+
const validated = validateDevBinary("registration", registrationPath, path, platform);
|
|
197
|
+
mkdirSync(dirname(registrationPath), { recursive: true });
|
|
198
|
+
writeFileSync(registrationPath, `${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path })}\n`);
|
|
199
|
+
return { registrationPath, override: validated };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Deletes the persistent registration; returns whether one existed. */
|
|
203
|
+
export function unregisterGentleAiDevBinary(environment = ambientDevBinaryEnvironment()) {
|
|
204
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
205
|
+
if (!existsSync(registrationPath)) return false;
|
|
206
|
+
rmSync(registrationPath);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
46
210
|
function isConfined(path , directory ) {
|
|
47
211
|
const relativePath = relative(directory, path);
|
|
48
212
|
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
@@ -107,7 +271,14 @@ export function resolveGentleAiBinary(
|
|
|
107
271
|
packageRoot = dirname(dirname(fileURLToPath(import.meta.url))),
|
|
108
272
|
platform = process.platform,
|
|
109
273
|
readBinary = readFileSync,
|
|
274
|
+
environment = ambientDevBinaryEnvironment(),
|
|
110
275
|
) {
|
|
276
|
+
// The explicit dev-binary override wins over the pinned supply-chain path.
|
|
277
|
+
// Its typed errors propagate: a declared override never falls back to the
|
|
278
|
+
// pin. Without a declared override this call returns undefined and the
|
|
279
|
+
// pinned resolution below is byte-identical to the pre-override behavior.
|
|
280
|
+
const override = resolveGentleAiDevBinaryOverride(environment, platform);
|
|
281
|
+
if (override !== undefined) return override.path;
|
|
111
282
|
const binaryPath = gentleAiBinaryPath(packageRoot, platform);
|
|
112
283
|
const versionDirectory = dirname(binaryPath);
|
|
113
284
|
const manifestPath = join(versionDirectory, "integrity.json");
|
|
@@ -181,13 +181,68 @@ function absoluteGitPath(cwd , name ) {
|
|
|
181
181
|
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Runs a probe that may exit nonzero as an expected signal (absent ref, unborn
|
|
185
|
+
// HEAD). Returns the exit status and trimmed stdout. Timeout and I/O failures
|
|
186
|
+
// propagate instead of being masked as a status, so callers fail closed.
|
|
187
|
+
function probeGit(cwd , args ) {
|
|
188
|
+
try {
|
|
189
|
+
const stdout = execFileSync("git", args, {
|
|
190
|
+
cwd,
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
193
|
+
timeout: GIT_TIMEOUT_MS,
|
|
194
|
+
windowsHide: true,
|
|
195
|
+
});
|
|
196
|
+
return { status: 0, stdout: stdout.trim() };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const detail = error ;
|
|
199
|
+
if (detail.code === "ETIMEDOUT" || detail.killed === true) throw error;
|
|
200
|
+
if (typeof detail.status === "number") return { status: detail.status, stdout: typeof detail.stdout === "string" ? detail.stdout.trim() : "" };
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Resolves HEAD to a commit SHA, or undefined only for a valid unborn symbolic
|
|
206
|
+
// HEAD (symbolic HEAD pointing at a branch with no commits). Timeout, I/O,
|
|
207
|
+
// corruption, and all other failures propagate (fail closed on uncertain HEAD
|
|
208
|
+
// state). Classification uses status-based probes, not localized stderr text.
|
|
209
|
+
function resolveHead(cwd ) {
|
|
210
|
+
try {
|
|
211
|
+
return git(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const detail = error ;
|
|
214
|
+
if (detail.code === "ETIMEDOUT" || detail.killed === true) throw error;
|
|
215
|
+
if (typeof detail.status !== "number") throw error;
|
|
216
|
+
const symbolic = probeGit(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
217
|
+
if (symbolic.status !== 0) throw error;
|
|
218
|
+
// show-ref --verify --quiet distinguishes: status 1 = ref absent (valid
|
|
219
|
+
// unborn), status 0 = ref exists and valid (rethrow original HEAD error),
|
|
220
|
+
// any other status (128, etc.) = corruption/missing object (fail closed).
|
|
221
|
+
const refProbe = probeGit(cwd, ["show-ref", "--verify", "--quiet", symbolic.stdout]);
|
|
222
|
+
if (refProbe.status === 1) return undefined;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
184
227
|
function repositoryBinding(cwd ) {
|
|
185
228
|
const root = realpathSync(git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
186
229
|
const commonDirValue = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
187
230
|
const gitDirValue = git(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
188
231
|
const commonDir = realpathSync(commonDirValue);
|
|
189
232
|
const gitDir = realpathSync(gitDirValue);
|
|
190
|
-
|
|
233
|
+
// Preserve the durable repository identity across the unborn-handling
|
|
234
|
+
// upgrade: a born repository keeps the byte-for-byte previous formula
|
|
235
|
+
// `sha256(canonicalJson({ common_directory: commonDir, roots }))` with
|
|
236
|
+
// `roots` the sorted root commits reachable from HEAD. An unborn
|
|
237
|
+
// repository has no HEAD, so `rev-list HEAD` cannot run; resolveHead
|
|
238
|
+
// already classifies HEAD state and propagates timeout/corruption/I/O
|
|
239
|
+
// failures rather than masking them, so the unborn branch gets a
|
|
240
|
+
// deterministic safe roots representation (the empty set) without
|
|
241
|
+
// hiding real errors.
|
|
242
|
+
const head = resolveHead(root);
|
|
243
|
+
const roots = head === undefined
|
|
244
|
+
? []
|
|
245
|
+
: git(root, ["rev-list", "--max-parents=0", "HEAD"]).split(/\r?\n/).filter(Boolean).sort();
|
|
191
246
|
const repositoryId = sha256(canonicalJson({ common_directory: commonDir, roots }));
|
|
192
247
|
const worktreeKey = sha256(gitDir).slice("sha256:".length, "sha256:".length + 24);
|
|
193
248
|
const stateDir = join(commonDir, "gentle-pi", "commit-transactions", worktreeKey);
|
|
@@ -242,11 +297,11 @@ function decodeRecord(value ) {
|
|
|
242
297
|
if (record.schema !== TRANSACTION_SCHEMA) throw new Error("commit transaction record schema is incompatible");
|
|
243
298
|
for (const field of [
|
|
244
299
|
"transaction_id", "repository_id", "repository_root", "common_directory", "git_directory",
|
|
245
|
-
"command", "command_hash", "
|
|
300
|
+
"command", "command_hash", "original_index_tree",
|
|
246
301
|
"original_index_hash", "authorized_pre_hook_tree", "state", "created_at", "updated_at", "record_hash",
|
|
247
302
|
]) if (typeof record[field] !== "string" || (record[field] ).length === 0) throw new Error(`commit transaction record ${field} is invalid`);
|
|
248
303
|
if (!isStringArray(record.arguments) || !isStringArray(record.invocation_ids) || !isStringArray(record.lineage_history)) throw new Error("commit transaction record arrays are invalid");
|
|
249
|
-
for (const field of ["post_hook_tree", "post_hook_index_hash", "authorized_tree", "authority_revision", "gate_context_hash", "committed_head", "committed_tree", "git_created_head", "git_created_tree", "error"] ) {
|
|
304
|
+
for (const field of ["original_head", "original_head_tree", "post_hook_tree", "post_hook_index_hash", "authorized_tree", "authority_revision", "gate_context_hash", "committed_head", "committed_tree", "git_created_head", "git_created_tree", "error"] ) {
|
|
250
305
|
if (record[field] !== undefined && typeof record[field] !== "string") throw new Error(`commit transaction record ${field} is invalid`);
|
|
251
306
|
}
|
|
252
307
|
if (!Number.isSafeInteger(record.hook_runs) || (record.hook_runs ) < 0) throw new Error("commit transaction hook count is invalid");
|
|
@@ -438,7 +493,7 @@ export function prepareCommitTransactionInvocation(input
|
|
|
438
493
|
) {
|
|
439
494
|
assertSafeCommitArguments(input.arguments);
|
|
440
495
|
const binding = repositoryBinding(input.cwd);
|
|
441
|
-
const head =
|
|
496
|
+
const head = resolveHead(binding.root);
|
|
442
497
|
const currentTree = git(binding.root, ["write-tree"]);
|
|
443
498
|
if (currentTree !== input.authorization.intendedTree) throw new Error("commit transaction pre-hook index no longer matches its controller authorization");
|
|
444
499
|
let transactionId = randomUUID();
|
|
@@ -541,7 +596,7 @@ function validateNativeTree(result , lineageId , tre
|
|
|
541
596
|
}
|
|
542
597
|
|
|
543
598
|
function createRecord(binding , invocation , now ) {
|
|
544
|
-
const head =
|
|
599
|
+
const head = resolveHead(binding.root);
|
|
545
600
|
const tree = git(binding.root, ["write-tree"]);
|
|
546
601
|
if (tree !== invocation.authorization.intendedTree) throw new Error("commit transaction index changed after controller authorization");
|
|
547
602
|
const timestamp = now().toISOString();
|
|
@@ -556,7 +611,7 @@ function createRecord(binding , invocation
|
|
|
556
611
|
command_hash: invocation.commandHash,
|
|
557
612
|
arguments: [...invocation.arguments],
|
|
558
613
|
original_head: head,
|
|
559
|
-
original_head_tree: git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]),
|
|
614
|
+
original_head_tree: head === undefined ? undefined : git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]),
|
|
560
615
|
original_index_tree: tree,
|
|
561
616
|
original_index_hash: indexFingerprint(binding.root),
|
|
562
617
|
authorized_pre_hook_tree: invocation.authorization.intendedTree,
|
|
@@ -572,13 +627,17 @@ function createRecord(binding , invocation
|
|
|
572
627
|
function assertInvocationMatches(binding , record , invocation ) {
|
|
573
628
|
if (record.repository_id !== binding.repositoryId || record.repository_root !== binding.root || record.common_directory !== binding.commonDir || record.git_directory !== binding.gitDir) throw new Error("commit transaction repository identity changed");
|
|
574
629
|
if (record.transaction_id !== invocation.transactionId || record.command_hash !== invocation.commandHash || record.command !== invocation.command || canonicalJson(record.arguments) !== canonicalJson(invocation.arguments)) throw new Error("commit transaction exact retry does not match the durable command intent");
|
|
575
|
-
if (
|
|
630
|
+
if (resolveHead(binding.root) !== record.original_head) throw new Error("commit transaction HEAD changed before reconciliation");
|
|
576
631
|
}
|
|
577
632
|
|
|
578
633
|
function recoverCompletedCommit(binding , record , now ) {
|
|
579
634
|
if (record.state !== COMMIT_TRANSACTION_STATE.COMMIT_RUNNING && record.state !== COMMIT_TRANSACTION_STATE.COMMITTED) return undefined;
|
|
580
|
-
const head =
|
|
635
|
+
const head = resolveHead(binding.root);
|
|
581
636
|
if (head === record.original_head) return undefined;
|
|
637
|
+
if (head === undefined) {
|
|
638
|
+
transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { error: "HEAD disappeared during commit transaction" }, now);
|
|
639
|
+
throw new Error("commit transaction incident: HEAD disappeared during commit; publication remains blocked");
|
|
640
|
+
}
|
|
582
641
|
const tree = git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
|
|
583
642
|
if (record.git_created_head === undefined || record.git_created_tree === undefined || head !== record.git_created_head || tree !== record.git_created_tree || tree !== record.authorized_tree) {
|
|
584
643
|
transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { committed_head: head, committed_tree: tree, error: "HEAD identity differs from the exact Git-created authorized commit" }, now);
|
|
@@ -685,14 +744,15 @@ export async function runGitCommitTransaction(
|
|
|
685
744
|
const commit = await runProcess("git", ["-c", `core.hooksPath=${proxy}`, "commit", ...invocation.arguments], binding.root);
|
|
686
745
|
if (dependencies.failpoint === "after-commit-before-proof") throw new Error("commit transaction test interruption after Git returned");
|
|
687
746
|
record = readRecord(binding.activePath) ?? record;
|
|
688
|
-
const head =
|
|
689
|
-
const headTree = git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
|
|
690
|
-
|
|
747
|
+
const head = resolveHead(binding.root);
|
|
748
|
+
const headTree = head === undefined ? undefined : git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
|
|
749
|
+
const headChanged = head !== record.original_head;
|
|
750
|
+
if (headChanged && head === record.git_created_head && headTree === record.git_created_tree && headTree === record.authorized_tree) {
|
|
691
751
|
const committed = transition(binding, record, COMMIT_TRANSACTION_STATE.COMMITTED, { committed_head: head, committed_tree: headTree, ...(commit.code === 0 && commit.signal === null ? {} : { error: `Git returned ${commit.signal ?? `exit ${commit.code}`} after creating the authorized commit` }) }, now);
|
|
692
752
|
archive(binding, committed);
|
|
693
|
-
return { transactionId: record.transaction_id, status: "committed", head, tree: headTree
|
|
753
|
+
return { transactionId: record.transaction_id, status: "committed", head: head , tree: headTree };
|
|
694
754
|
}
|
|
695
|
-
if (
|
|
755
|
+
if (headChanged) {
|
|
696
756
|
transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { committed_head: head, committed_tree: headTree, error: "HEAD identity differs from the exact Git-created authorized commit" }, now);
|
|
697
757
|
throw new Error("commit transaction incident: HEAD identity changed after Git created the authorized commit; publication remains blocked");
|
|
698
758
|
}
|
|
@@ -702,7 +762,7 @@ export async function runGitCommitTransaction(
|
|
|
702
762
|
if (record !== undefined && dependencies.signal?.aborted === true && existsSync(binding.activePath)) {
|
|
703
763
|
try {
|
|
704
764
|
const active = readRecord(binding.activePath) ?? record;
|
|
705
|
-
if (
|
|
765
|
+
if (resolveHead(binding.root) === active.original_head) transition(binding, active, COMMIT_TRANSACTION_STATE.INTERRUPTED, { error: "commit transaction was cancelled" }, now);
|
|
706
766
|
} catch { /* retain the earlier durable state */ }
|
|
707
767
|
}
|
|
708
768
|
throw error;
|
|
@@ -783,7 +843,7 @@ export function abandonCommitTransaction(cwd ) {
|
|
|
783
843
|
try {
|
|
784
844
|
const record = readRecord(binding.activePath);
|
|
785
845
|
if (record === undefined) throw new Error("active commit transaction disappeared during recovery");
|
|
786
|
-
if (
|
|
846
|
+
if (resolveHead(binding.root) !== record.original_head) throw new Error("cannot abandon a commit transaction after HEAD changed; reconcile the committed tree instead");
|
|
787
847
|
const abandoned = transition(binding, record, COMMIT_TRANSACTION_STATE.ABANDONED, { error: "explicitly abandoned without changing HEAD or index" }, now);
|
|
788
848
|
return archive(binding, abandoned);
|
|
789
849
|
} finally { releaseLock(); }
|