jorgex-stack 1.2.1 → 1.2.3

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.
Files changed (3) hide show
  1. package/README.md +22 -2
  2. package/dist/cli.js +1008 -51
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JorgeX Stack
2
2
 
3
- Portable multi-agent harness: one configuration source — 15 agents, 17 skills, hooks, persistent memory ([Engram](https://github.com/Gentleman-Programming/engram)), MCPs, and system prompt — installable with one command in **Claude Code**, **Codex CLI**, and **OpenCode**.
3
+ Portable multi-agent harness: one configuration source — 15 agents, 17 skills, hooks, persistent memory ([Engram](https://github.com/Gentleman-Programming/engram)), MCPs, and system prompt — installable with one command in **Claude Code**, **Codex CLI**, **OpenCode**, and **Pi**.
4
4
 
5
5
  > Inspired by [gentle-ai](https://github.com/Gentleman-Programming/gentle-ai), rebuilt for the JorgeX stack.
6
6
 
@@ -75,7 +75,7 @@ Flags:
75
75
  pnpm dlx jorgex-stack install --mode programmatic --subagent-concurrency serial --yes
76
76
  ```
77
77
 
78
- This installs into all detected runtimes. To be explicit, add `--agents opencode,claude-code,codex` or a comma-separated subset. Always pass `--mode programmatic`; without `--mode`, `--yes` and non-TTY installs default to `human`.
78
+ This installs into all detected runtimes. To be explicit, add `--agents opencode,claude-code,codex,pi` or a comma-separated subset. Always pass `--mode programmatic`; without `--mode`, `--yes` and non-TTY installs default to `human`.
79
79
 
80
80
  OpenCode also requires an existing selection in `~/.jorgex-stack/model-map.json`; run `pnpm dlx jorgex-stack models --agents opencode` interactively once before a headless install.
81
81
 
@@ -95,6 +95,26 @@ Programmatic mode does **not** provide:
95
95
  - Any special stdout streaming guarantee — the runtime's normal output rules apply.
96
96
  - Telemetry, JSONL streams, or runtime token-budget enforcement.
97
97
 
98
+ ### Pi runtime
99
+
100
+ Pi is package-managed rather than file-managed. Stack supports the exact published package **`jorgex-pi@0.2.2`** and keeps Pi out of the adapter/component manifest and model map.
101
+
102
+ ```bash
103
+ pnpm dlx jorgex-stack install --agents pi
104
+ pnpm dlx jorgex-stack doctor --agents pi
105
+ pnpm dlx jorgex-stack models --agents pi
106
+ pnpm dlx jorgex-stack sync --agents pi
107
+ pnpm dlx jorgex-stack uninstall --agents pi
108
+ ```
109
+
110
+ Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. The managed Pi package entry is the exact source object `{ "source": "npm:jorgex-pi@0.2.2", "skills": [] }`: Pi discovers the canonical shared skills from `~/.agents/skills`, so the package copy is disabled and does not create duplicate skill loading. A scope-bound receipt under `~/.jorgex-stack/pi-receipt.json` records ownership only after the package runner reports a healthy install and stores the verified Engram executable as `engram.binary`, using the schema v1 consumed by `jorgex-pi@0.2.2`. Receipts created before the Engram binding existed require deliberate removal with the previous Stack release followed by reinstall; they are never adopted automatically. Manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state fails closed and is never adopted or removed silently.
111
+
112
+ Engram remains mandatory and user-owned. An existing binary is preserved. Interactive install may offer the existing native `brew`/`go`/release channel with a default-No confirmation; `--yes` and non-TTY installs fail with a remedy when Engram is absent. No Pi lifecycle operation updates or deletes the Engram database or memories. Under `--target-dir`, Stack accepts only `<target>/bin/engram`, isolates Pi/Home/XDG/AppData/temp/npm-cache paths inside the target, and never consults the host Engram or Pi configuration.
113
+
114
+ The 24-hour npm maturity rule applies to the managed adoption boundary: development and PR validation may start against the exact published artifact, but merging the adoption PR, publishing that Stack adoption, and running the real managed installation wait until the package has been public for at least 24 hours unless Jorge explicitly documents an exception in the PR.
115
+
116
+ `update --agents pi` only runs the Pi package lifecycle; it does not enter the global Stack updater. `update --check --agents pi` is a read-only Pi doctor. Uninstall runs package cleanup, backs up Pi's settings before removal, removes only the exact receipt-owned package after verifying absence, and preserves all companion/user state. Full behavior, failure states and troubleshooting are in [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
117
+
98
118
  ### Browser automation
99
119
 
100
120
  Browser automation is opt-in and explicit. The legacy `agent-browser` integration has been removed; rely on the two surfaces below.
package/dist/cli.js CHANGED
@@ -3713,6 +3713,883 @@ function readPackageMetadata() {
3713
3713
  return { name, version };
3714
3714
  }
3715
3715
 
3716
+ // src/lib/pi-runtime.ts
3717
+ import os5 from "os";
3718
+ import path30 from "path";
3719
+ import fs22 from "fs";
3720
+ import { spawnSync } from "child_process";
3721
+ import { createHash } from "crypto";
3722
+
3723
+ // src/lib/pi-package-lifecycle.ts
3724
+ import path29 from "path";
3725
+ var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
3726
+ "foundation-contract-v1",
3727
+ "runner-json-v1"
3728
+ ]);
3729
+ function sameRecord(left, right) {
3730
+ return JSON.stringify(left) === JSON.stringify(right);
3731
+ }
3732
+ function ownership(receipt) {
3733
+ return { receipt, adapters: false, manifest: false, modelMap: false };
3734
+ }
3735
+ function blocked(input, reason) {
3736
+ return {
3737
+ kind: "blocked",
3738
+ reason,
3739
+ receiptPath: input.scope.receiptPath,
3740
+ ownership: ownership(input.receiptJson !== null)
3741
+ };
3742
+ }
3743
+ function packageSource(entry) {
3744
+ if (typeof entry === "string") return entry;
3745
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
3746
+ const source = Reflect.get(entry, "source");
3747
+ return typeof source === "string" ? source : null;
3748
+ }
3749
+ function isJorgeXPiSource(source) {
3750
+ return source.includes("jorgex-pi");
3751
+ }
3752
+ function parsePackageSources(settingsJson) {
3753
+ try {
3754
+ const parsed = JSON.parse(settingsJson);
3755
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3756
+ const packages = Reflect.get(parsed, "packages");
3757
+ if (!Array.isArray(packages)) return null;
3758
+ const sources = packages.map((entry) => ({ entry, source: packageSource(entry) }));
3759
+ return sources.every((value) => value.source !== null) ? sources : null;
3760
+ } catch {
3761
+ return null;
3762
+ }
3763
+ }
3764
+ function isExactManagedPackage(entry, source) {
3765
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
3766
+ const keys = Object.keys(entry);
3767
+ const skills = Reflect.get(entry, "skills");
3768
+ return keys.length === 2 && keys.includes("source") && keys.includes("skills") && Reflect.get(entry, "source") === source && Array.isArray(skills) && skills.length === 0;
3769
+ }
3770
+ function expectedReceipt(candidate, state, scope, engramBin) {
3771
+ return {
3772
+ schemaVersion: 1,
3773
+ state,
3774
+ candidate: {
3775
+ package: candidate.package,
3776
+ tarball: candidate.tarball,
3777
+ provenance: candidate.provenance
3778
+ },
3779
+ scope,
3780
+ engram: { binary: engramBin }
3781
+ };
3782
+ }
3783
+ function parseReceiptShape(receiptJson) {
3784
+ try {
3785
+ const parsed = JSON.parse(receiptJson);
3786
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3787
+ const schemaVersion = Reflect.get(parsed, "schemaVersion");
3788
+ if (schemaVersion !== 1) return null;
3789
+ const state = Reflect.get(parsed, "state");
3790
+ const candidate = Reflect.get(parsed, "candidate");
3791
+ if (state !== "installing" && state !== "installed" || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
3792
+ return null;
3793
+ }
3794
+ const packageValue = Reflect.get(candidate, "package");
3795
+ const tarball = Reflect.get(candidate, "tarball");
3796
+ const provenance = Reflect.get(candidate, "provenance");
3797
+ const scope = Reflect.get(parsed, "scope");
3798
+ const engram = Reflect.get(parsed, "engram");
3799
+ if (packageValue === null || typeof packageValue !== "object" || tarball === null || typeof tarball !== "object" || provenance === null || typeof provenance !== "object" || scope === null || typeof scope !== "object" || Array.isArray(scope)) {
3800
+ return null;
3801
+ }
3802
+ const source = Reflect.get(packageValue, "source");
3803
+ const name = Reflect.get(packageValue, "name");
3804
+ const version = Reflect.get(packageValue, "version");
3805
+ const scopeKind = Reflect.get(scope, "kind");
3806
+ const codingAgentDir = Reflect.get(scope, "codingAgentDir");
3807
+ if (name !== "jorgex-pi" || typeof version !== "string" || typeof source !== "string" || source !== `npm:jorgex-pi@${version}` || scopeKind !== "real" && scopeKind !== "target-dir" || typeof codingAgentDir !== "string") {
3808
+ return null;
3809
+ }
3810
+ if (engram === void 0) return "upgrade-required";
3811
+ if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path29.isAbsolute(Reflect.get(engram, "binary"))) {
3812
+ return null;
3813
+ }
3814
+ return parsed;
3815
+ } catch {
3816
+ return null;
3817
+ }
3818
+ }
3819
+ function parseReceipt(receiptJson, candidate, scope, engramBin) {
3820
+ const parsed = parseReceiptShape(receiptJson);
3821
+ if (parsed === null || parsed === "upgrade-required") return parsed;
3822
+ const expected = expectedReceipt(candidate, parsed.state, scope, engramBin);
3823
+ return sameRecord(parsed, expected) ? expected : null;
3824
+ }
3825
+ function candidateIsValid(candidate, observed) {
3826
+ return candidate.package.name === "jorgex-pi" && candidate.package.source === `npm:${candidate.package.name}@${candidate.package.version}` && candidate.contract.schemaVersion === 1 && candidate.contract.runner.schemaVersion === 1 && candidate.contract.runner.bin === "jorgex-pi" && candidate.contract.runner.maxStdoutBytes === 65536 && candidate.contract.managedExternalWrites.length === 0 && [...REQUIRED_CAPABILITIES].every((capability) => candidate.contract.capabilities.includes(capability)) && sameRecord(candidate.tarball, observed);
3827
+ }
3828
+ function planPiPackageLifecycle(input) {
3829
+ if (!candidateIsValid(input.candidate, input.observedTarball)) {
3830
+ return blocked(input, "tarball-integrity");
3831
+ }
3832
+ if (!input.candidate.pi.testedVersions.includes(input.pi.version)) {
3833
+ return blocked(input, "unsupported-pi-version");
3834
+ }
3835
+ if (input.engramBin === null) return blocked(input, "engram-missing");
3836
+ const sources = parsePackageSources(input.pi.settingsJson);
3837
+ if (sources === null) return blocked(input, "settings-corrupt");
3838
+ const matchingSources = sources.filter(({ source }) => isJorgeXPiSource(source));
3839
+ const exactSources = matchingSources.filter(({ source }) => source === input.candidate.package.source);
3840
+ if (exactSources.length > 1) return blocked(input, "duplicate-package");
3841
+ if (matchingSources.some(({ source }) => source !== input.candidate.package.source)) {
3842
+ return blocked(input, "source-divergent");
3843
+ }
3844
+ let receipt = null;
3845
+ if (input.receiptJson !== null) {
3846
+ const parsedReceipt = parseReceipt(input.receiptJson, input.candidate, {
3847
+ kind: input.scope.kind,
3848
+ codingAgentDir: path29.resolve(input.scope.codingAgentDir)
3849
+ }, input.engramBin);
3850
+ if (parsedReceipt === "upgrade-required") return blocked(input, "receipt-upgrade-required");
3851
+ if (parsedReceipt === null) return blocked(input, "receipt-corrupt");
3852
+ receipt = parsedReceipt;
3853
+ if (receipt.state === "installing") return blocked(input, "partial-state");
3854
+ const exactSource = exactSources[0];
3855
+ if (exactSources.length !== 1 || exactSource === void 0 || !isExactManagedPackage(exactSource.entry, input.candidate.package.source)) {
3856
+ return blocked(input, "source-divergent");
3857
+ }
3858
+ }
3859
+ if (exactSources.length === 1 && receipt === null) {
3860
+ return {
3861
+ kind: "manual-existing",
3862
+ receiptPath: input.scope.receiptPath,
3863
+ ownership: ownership(false)
3864
+ };
3865
+ }
3866
+ if (exactSources.length === 1 && receipt !== null) {
3867
+ return {
3868
+ kind: "ready",
3869
+ receiptPath: input.scope.receiptPath,
3870
+ ownership: ownership(true)
3871
+ };
3872
+ }
3873
+ return {
3874
+ kind: "install",
3875
+ receiptPath: input.scope.receiptPath,
3876
+ invocation: {
3877
+ executable: input.pi.executable,
3878
+ args: ["install", input.candidate.package.source, "--no-approve"],
3879
+ environment: input.scope.environment
3880
+ },
3881
+ receipt: expectedReceipt(input.candidate, "installing", {
3882
+ kind: input.scope.kind,
3883
+ codingAgentDir: path29.resolve(input.scope.codingAgentDir)
3884
+ }, input.engramBin),
3885
+ ownership: ownership(true)
3886
+ };
3887
+ }
3888
+ function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
3889
+ if (stderr !== "" || !stdout.endsWith("\n") || Buffer.byteLength(stdout) > candidate.contract.runner.maxStdoutBytes) {
3890
+ return null;
3891
+ }
3892
+ const body = stdout.slice(0, -1);
3893
+ if (body === "" || body.includes("\n") || body.includes("\r")) return null;
3894
+ try {
3895
+ const parsed = JSON.parse(body);
3896
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3897
+ const record = parsed;
3898
+ if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path29.isAbsolute(record.package.root) || path29.resolve(packageRunner) !== path29.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
3899
+ return null;
3900
+ }
3901
+ return record;
3902
+ } catch {
3903
+ return null;
3904
+ }
3905
+ }
3906
+ function runPackageCommand(input, deps, command) {
3907
+ const result = deps.run({
3908
+ executable: input.packageRunner,
3909
+ args: [command, "--json"],
3910
+ environment: input.environment
3911
+ });
3912
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
3913
+ return parseRunnerRecord(result.stdout, result.stderr, command, input.candidate, input.packageRunner) ?? { kind: "blocked", reason: "runner-output" };
3914
+ }
3915
+ function isBlockedResult(value) {
3916
+ return "kind" in value;
3917
+ }
3918
+ function executePiPackageLifecycle(input, deps) {
3919
+ if (input.plan.kind === "manual-existing") return { kind: "manual-existing" };
3920
+ if (input.operation === "install") {
3921
+ if (input.plan.kind !== "install" || input.plan.receipt === void 0 || input.plan.invocation === void 0) {
3922
+ return { kind: "blocked", reason: "runner-unhealthy" };
3923
+ }
3924
+ deps.writeReceipt(input.plan.receipt);
3925
+ const installed = deps.run(input.plan.invocation);
3926
+ if (installed.exitCode !== 0 || installed.stderr !== "") {
3927
+ return { kind: "blocked", reason: "pi-install-failed" };
3928
+ }
3929
+ const doctor = runPackageCommand(input, deps, "doctor");
3930
+ if (isBlockedResult(doctor)) return doctor;
3931
+ const doctorResult = doctor.result;
3932
+ if (doctorResult === null || typeof doctorResult !== "object" || Reflect.get(doctorResult, "healthy") !== true) {
3933
+ return { kind: "blocked", reason: "runner-unhealthy" };
3934
+ }
3935
+ const receipt = { ...input.plan.receipt, state: "installed" };
3936
+ deps.writeReceipt(receipt);
3937
+ return { kind: "installed", receipt };
3938
+ }
3939
+ if (input.plan.kind !== "ready") return { kind: "blocked", reason: "runner-unhealthy" };
3940
+ const command = runPackageCommand(input, deps, input.operation);
3941
+ if (isBlockedResult(command)) return command;
3942
+ if (input.operation === "sync") {
3943
+ const result = command.result;
3944
+ if (result === null || typeof result !== "object" || Reflect.get(result, "changed") !== false) {
3945
+ return { kind: "blocked", reason: "runner-unhealthy" };
3946
+ }
3947
+ return { kind: "synced", actions: [] };
3948
+ }
3949
+ const models = command.result;
3950
+ if (models === null || typeof models !== "object" || Reflect.get(models, "mode") !== "inherit-session" || !sameRecord(Reflect.get(models, "tiers"), ["strong", "standard", "cheap"])) {
3951
+ return { kind: "blocked", reason: "runner-unhealthy" };
3952
+ }
3953
+ return { kind: "models", models: { mode: "inherit-session", tiers: ["strong", "standard", "cheap"] } };
3954
+ }
3955
+ function receiptUpgradeRequired() {
3956
+ return {
3957
+ kind: "blocked",
3958
+ reason: "receipt-upgrade-required",
3959
+ remedy: "El receipt no enlaza Engram; usa la versi\xF3n anterior de Stack para desinstalarlo y luego reinstala."
3960
+ };
3961
+ }
3962
+ function validateOwnedOperationState(input) {
3963
+ const sources = parsePackageSources(input.detected.settingsJson);
3964
+ if (sources === null) return { kind: "blocked", reason: "settings-corrupt" };
3965
+ const matchingSources = sources.filter(({ source: source2 }) => isJorgeXPiSource(source2));
3966
+ if (matchingSources.length > 1) return { kind: "blocked", reason: "duplicate-package" };
3967
+ if (input.receiptJson === null) {
3968
+ return { kind: "blocked", reason: matchingSources.length === 1 ? "manual-existing" : "source-divergent" };
3969
+ }
3970
+ const parsedReceipt = parseReceiptShape(input.receiptJson);
3971
+ if (parsedReceipt === "upgrade-required") return receiptUpgradeRequired();
3972
+ if (parsedReceipt === null) return { kind: "blocked", reason: "receipt-corrupt" };
3973
+ const receipt = parsedReceipt;
3974
+ if (receipt.state !== "installed") return { kind: "blocked", reason: "partial-state" };
3975
+ const accepted = input.registry.acceptedCandidates ?? [input.registry.candidate];
3976
+ if (!accepted.some((candidate) => sameRecord(receipt.candidate, {
3977
+ package: candidate.package,
3978
+ tarball: candidate.tarball,
3979
+ provenance: candidate.provenance
3980
+ }))) {
3981
+ return { kind: "blocked", reason: "receipt-untrusted" };
3982
+ }
3983
+ if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path29.resolve(receipt.scope.codingAgentDir) !== path29.resolve(input.paths.codingAgentDir)) {
3984
+ return { kind: "blocked", reason: "source-divergent" };
3985
+ }
3986
+ if (input.engramBin !== null && path29.resolve(receipt.engram.binary) !== path29.resolve(input.engramBin)) {
3987
+ return { kind: "blocked", reason: "receipt-corrupt" };
3988
+ }
3989
+ const source = receipt.candidate.package.source;
3990
+ const matchingSource = matchingSources[0];
3991
+ if (matchingSources.length !== 1 || matchingSource === void 0 || matchingSource.source !== source || !isExactManagedPackage(matchingSource.entry, source)) {
3992
+ return { kind: "blocked", reason: "source-divergent" };
3993
+ }
3994
+ return { receipt, source };
3995
+ }
3996
+ function operationWasBlocked(value) {
3997
+ return "kind" in value;
3998
+ }
3999
+ function runManagedRunner(input, deps, command) {
4000
+ const result = deps.run({
4001
+ executable: input.detected.packageRunner,
4002
+ args: [command, "--json"],
4003
+ environment: input.paths.environment
4004
+ });
4005
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
4006
+ const parsed = parseRunnerRecord(
4007
+ result.stdout,
4008
+ result.stderr,
4009
+ command,
4010
+ input.registry.candidate,
4011
+ input.detected.packageRunner
4012
+ );
4013
+ return parsed ?? { kind: "blocked", reason: "runner-output" };
4014
+ }
4015
+ function managedRunnerWasBlocked(value) {
4016
+ return "kind" in value;
4017
+ }
4018
+ function runPiPackageManagedOperation(input, deps) {
4019
+ if (input.engramBin === null && input.operation !== "uninstall") {
4020
+ return {
4021
+ kind: "blocked",
4022
+ reason: "engram-missing",
4023
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4024
+ };
4025
+ }
4026
+ const owned = validateOwnedOperationState(input);
4027
+ if (operationWasBlocked(owned)) return owned;
4028
+ if (input.operation === "doctor") {
4029
+ if (!sameRecord(owned.receipt.candidate, {
4030
+ package: input.registry.candidate.package,
4031
+ tarball: input.registry.candidate.tarball,
4032
+ provenance: input.registry.candidate.provenance
4033
+ })) {
4034
+ return { kind: "blocked", reason: "source-divergent" };
4035
+ }
4036
+ const doctor = runManagedRunner(input, deps, "doctor");
4037
+ if (managedRunnerWasBlocked(doctor)) return doctor;
4038
+ const result = doctor.result;
4039
+ return result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true ? { kind: "healthy" } : { kind: "blocked", reason: "runner-unhealthy" };
4040
+ }
4041
+ if (input.operation === "uninstall") {
4042
+ if (!sameRecord(owned.receipt.candidate, {
4043
+ package: input.registry.candidate.package,
4044
+ tarball: input.registry.candidate.tarball,
4045
+ provenance: input.registry.candidate.provenance
4046
+ })) {
4047
+ return { kind: "blocked", reason: "source-divergent" };
4048
+ }
4049
+ const cleanup = runManagedRunner(input, deps, "cleanup");
4050
+ if (managedRunnerWasBlocked(cleanup)) return cleanup;
4051
+ deps.backupSettings();
4052
+ const removed = deps.run({
4053
+ executable: input.detected.executable,
4054
+ args: ["remove", owned.source, "--no-approve"],
4055
+ environment: input.paths.environment
4056
+ });
4057
+ if (removed.exitCode !== 0 || removed.stderr !== "") return { kind: "blocked", reason: "remove-failed" };
4058
+ if (!deps.isPackageAbsent()) return { kind: "blocked", reason: "absence-unverified" };
4059
+ deps.deleteReceipt();
4060
+ return { kind: "uninstalled" };
4061
+ }
4062
+ const nextSource = input.registry.candidate.package.source;
4063
+ if (nextSource === owned.source) return { kind: "healthy" };
4064
+ return {
4065
+ kind: "blocked",
4066
+ reason: "verified-update-required",
4067
+ remedy: "A cross-version Pi update requires verified replacement and rollback tgz artifacts."
4068
+ };
4069
+ }
4070
+
4071
+ // src/lib/pi-runtime.ts
4072
+ var PI_RUNTIME_CANDIDATE = {
4073
+ package: {
4074
+ name: "jorgex-pi",
4075
+ version: "0.2.2",
4076
+ source: "npm:jorgex-pi@0.2.2"
4077
+ },
4078
+ provenance: {
4079
+ commit: "99631aa3712f51a625d196e949e48e27f55031a2"
4080
+ },
4081
+ tarball: {
4082
+ bytes: 89101513,
4083
+ sha256: "e1c6b63719995cf7ba2c96c3b753f19d8f2f0be74f2af9bc319576b7383913f4",
4084
+ sha512: "7b81dc1eb6030d562c70857dcf739798df94c88bddd240b2752c558fc1d21403faa411aa88e182a01664a17e06e2caeef35f1507eff45c97f4acc521469c45a1"
4085
+ },
4086
+ pi: {
4087
+ testedVersions: ["0.84.2"]
4088
+ },
4089
+ contract: {
4090
+ schemaVersion: 1,
4091
+ capabilities: [
4092
+ "foundation-contract-v1",
4093
+ "stack-snapshot-v1",
4094
+ "runtime-agents-v1",
4095
+ "permission-gated-tools-v1",
4096
+ "structured-questions-v1",
4097
+ "web-access-v1",
4098
+ "goal-continuation-v1",
4099
+ "mcp-adapter-v1",
4100
+ "engram-runtime-tools-v1",
4101
+ "runner-json-v1",
4102
+ "tui-branding-v1"
4103
+ ],
4104
+ runner: {
4105
+ bin: "jorgex-pi",
4106
+ commands: ["status", "doctor", "models", "sync", "cleanup"],
4107
+ schemaVersion: 1,
4108
+ maxStdoutBytes: 65536
4109
+ },
4110
+ managedExternalWrites: []
4111
+ }
4112
+ };
4113
+ var PI_RUNTIME_REGISTRY = {
4114
+ pi: {
4115
+ id: "pi",
4116
+ kind: "package-managed",
4117
+ source: PI_RUNTIME_CANDIDATE.package.source,
4118
+ tarball: PI_RUNTIME_CANDIDATE.tarball,
4119
+ pi: PI_RUNTIME_CANDIDATE.pi,
4120
+ candidate: PI_RUNTIME_CANDIDATE,
4121
+ acceptedCandidates: [PI_RUNTIME_CANDIDATE]
4122
+ }
4123
+ };
4124
+ async function resolvePiEngramRequirement(input, deps) {
4125
+ if (input.targetDir !== void 0) {
4126
+ const targetBin = deps.detectTarget(input.targetDir);
4127
+ return targetBin === null ? {
4128
+ kind: "blocked",
4129
+ reason: "engram-missing-target",
4130
+ remedy: "A\xF1ade el binario Engram dentro del target-dir antes de reintentar."
4131
+ } : { kind: "existing", bin: targetBin, scope: "target-dir" };
4132
+ }
4133
+ const existing = deps.detectHost();
4134
+ if (existing !== null) return { kind: "existing", bin: existing, scope: "host" };
4135
+ if (input.yes || !input.interactive) {
4136
+ return {
4137
+ kind: "blocked",
4138
+ reason: "engram-required",
4139
+ remedy: "Instala Engram de forma interactiva o configura ENGRAM_BIN antes de reintentar."
4140
+ };
4141
+ }
4142
+ const accepted = await deps.confirm({
4143
+ message: "Engram es obligatorio para JorgeX Pi. \xBFInstalar ahora el binario mediante el canal nativo?",
4144
+ initialValue: false
4145
+ });
4146
+ if (!accepted) return { kind: "offer", accepted: false };
4147
+ const installed = await deps.installNative({ version: "1.20.0", channels: ["brew", "go", "url"] });
4148
+ if (!installed) {
4149
+ return {
4150
+ kind: "blocked",
4151
+ reason: "engram-install-failed",
4152
+ remedy: "Instala Engram manualmente o configura ENGRAM_BIN antes de reintentar."
4153
+ };
4154
+ }
4155
+ const detected = deps.detectHost();
4156
+ return detected === null ? {
4157
+ kind: "blocked",
4158
+ reason: "engram-install-unverified",
4159
+ remedy: "La instalaci\xF3n termin\xF3, pero Engram no qued\xF3 detectable; configura ENGRAM_BIN."
4160
+ } : { kind: "existing", bin: detected, scope: "host" };
4161
+ }
4162
+ function flatCandidateReceipt(candidate, scope, state, engramBin) {
4163
+ const match = /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source);
4164
+ const packageValue = candidate.package ?? {
4165
+ name: "jorgex-pi",
4166
+ version: match?.[1] ?? PI_RUNTIME_CANDIDATE.package.version,
4167
+ source: candidate.source
4168
+ };
4169
+ return {
4170
+ schemaVersion: 1,
4171
+ state,
4172
+ candidate: {
4173
+ package: packageValue,
4174
+ tarball: { bytes: candidate.bytes, sha256: candidate.sha256, sha512: candidate.sha512 },
4175
+ provenance: candidate.provenance ?? { commit: PI_RUNTIME_CANDIDATE.provenance.commit }
4176
+ },
4177
+ scope,
4178
+ engram: { binary: engramBin }
4179
+ };
4180
+ }
4181
+ function normalizeInstalledSource(settingsJson, alias, canonical) {
4182
+ try {
4183
+ const parsed = JSON.parse(settingsJson);
4184
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4185
+ const packages = Reflect.get(parsed, "packages");
4186
+ if (!Array.isArray(packages)) return null;
4187
+ const hasCanonical = packages.some((entry) => entry === canonical || entry !== null && typeof entry === "object" && !Array.isArray(entry) && Reflect.get(entry, "source") === canonical);
4188
+ if (packages.filter((entry) => entry === alias).length !== 1 || hasCanonical) return null;
4189
+ Reflect.set(parsed, "packages", packages.map((entry) => entry === alias ? { source: canonical, skills: [] } : entry));
4190
+ return JSON.stringify(parsed);
4191
+ } catch {
4192
+ return null;
4193
+ }
4194
+ }
4195
+ function healthyDoctor(stdout, stderr, packageRunner, candidate) {
4196
+ if (stderr !== "" || !stdout.endsWith("\n") || stdout.slice(0, -1).includes("\n")) return false;
4197
+ try {
4198
+ const record = JSON.parse(stdout.slice(0, -1));
4199
+ if (record === null || typeof record !== "object" || Array.isArray(record)) return false;
4200
+ const packageValue = Reflect.get(record, "package");
4201
+ const result = Reflect.get(record, "result");
4202
+ return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path30.resolve(packageRunner) === path30.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
4203
+ } catch {
4204
+ return false;
4205
+ }
4206
+ }
4207
+ function installPiFromVerifiedTarball(input, deps) {
4208
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.piExecutable) : targetPaths(input.targetDir, input.engramBin, input.piExecutable);
4209
+ const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path30.join(path30.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
4210
+ const artifact = deps.download(destination);
4211
+ if (artifact.bytes !== input.candidate.bytes || artifact.sha256 !== input.candidate.sha256 || artifact.sha512 !== input.candidate.sha512) {
4212
+ return { kind: "blocked", reason: "tarball-integrity" };
4213
+ }
4214
+ deps.backupSettings();
4215
+ const scope = {
4216
+ kind: input.targetDir === void 0 ? "real" : "target-dir",
4217
+ codingAgentDir: path30.resolve(paths.codingAgentDir)
4218
+ };
4219
+ const installing = flatCandidateReceipt(input.candidate, scope, "installing", input.engramBin);
4220
+ deps.writeReceiptAtomic(`${JSON.stringify(installing)}
4221
+ `);
4222
+ const alias = `npm:jorgex-pi@file:${artifact.path}`;
4223
+ const installed = deps.run({
4224
+ executable: input.piExecutable,
4225
+ args: ["install", alias, "--no-approve"],
4226
+ environment: paths.environment
4227
+ });
4228
+ if (installed.exitCode !== 0 || installed.stderr !== "") return { kind: "blocked", reason: "pi-install-failed" };
4229
+ const normalized = normalizeInstalledSource(deps.readSettings(), alias, input.candidate.source);
4230
+ if (normalized === null) return { kind: "blocked", reason: "settings-corrupt" };
4231
+ deps.rewriteSettings(normalized);
4232
+ const doctor = deps.run({
4233
+ executable: process.execPath,
4234
+ args: [paths.packageRunner, "doctor", "--json"],
4235
+ environment: paths.environment
4236
+ });
4237
+ if (doctor.exitCode !== 0 || !healthyDoctor(doctor.stdout, doctor.stderr, paths.packageRunner, input.candidate)) {
4238
+ return { kind: "blocked", reason: "runner-unhealthy" };
4239
+ }
4240
+ const receipt = flatCandidateReceipt(input.candidate, scope, "installed", input.engramBin);
4241
+ deps.writeReceiptAtomic(`${JSON.stringify(receipt)}
4242
+ `);
4243
+ return { kind: "installed", receipt };
4244
+ }
4245
+ function runtimePath(piExecutable) {
4246
+ const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), process.env.SystemRoot ? path30.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
4247
+ return [...new Set(entries.filter((entry) => entry !== null))].join(path30.delimiter);
4248
+ }
4249
+ function targetPaths(targetDir, engramBin, piExecutable) {
4250
+ const root = path30.resolve(targetDir);
4251
+ const codingAgentDir = path30.join(root, "pi-agent");
4252
+ const home = path30.join(root, "home");
4253
+ const temporary = path30.join(root, "tmp");
4254
+ return {
4255
+ codingAgentDir,
4256
+ receiptPath: path30.join(root, "state", "pi-receipt.json"),
4257
+ packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4258
+ environment: {
4259
+ HOME: home,
4260
+ USERPROFILE: home,
4261
+ APPDATA: path30.join(root, "appdata"),
4262
+ LOCALAPPDATA: path30.join(root, "localappdata"),
4263
+ XDG_CONFIG_HOME: path30.join(root, "xdg-config"),
4264
+ XDG_DATA_HOME: path30.join(root, "xdg-data"),
4265
+ XDG_CACHE_HOME: path30.join(root, "xdg-cache"),
4266
+ TEMP: temporary,
4267
+ TMP: temporary,
4268
+ TMPDIR: temporary,
4269
+ npm_config_cache: path30.join(root, "npm-cache"),
4270
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
4271
+ NPM_CONFIG_UPDATE_NOTIFIER: "false",
4272
+ PI_CODING_AGENT_DIR: codingAgentDir,
4273
+ ...engramBin === null ? {} : { ENGRAM_BIN: engramBin },
4274
+ PATH: runtimePath(piExecutable)
4275
+ }
4276
+ };
4277
+ }
4278
+ function userPaths(engramBin, piExecutable) {
4279
+ const home = os5.homedir();
4280
+ const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent");
4281
+ return {
4282
+ codingAgentDir,
4283
+ receiptPath: path30.join(dataDir(), "pi-receipt.json"),
4284
+ packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4285
+ environment: {
4286
+ HOME: home,
4287
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path30.join(home, ".config"),
4288
+ XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path30.join(home, ".cache"),
4289
+ TMPDIR: os5.tmpdir(),
4290
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
4291
+ NPM_CONFIG_UPDATE_NOTIFIER: "false",
4292
+ PI_CODING_AGENT_DIR: codingAgentDir,
4293
+ ...engramBin === null ? {} : { ENGRAM_BIN: engramBin },
4294
+ PATH: runtimePath(piExecutable)
4295
+ }
4296
+ };
4297
+ }
4298
+ function persistReturnedReceipt(result, paths, deps) {
4299
+ if (result.receipt !== void 0) {
4300
+ deps.writeReceiptAtomic(paths.receiptPath, `${JSON.stringify(result.receipt)}
4301
+ `);
4302
+ }
4303
+ }
4304
+ function runPiRuntime(input, deps) {
4305
+ if (input.engramBin === null && input.operation !== "uninstall") {
4306
+ return {
4307
+ kind: "blocked",
4308
+ reason: "engram-missing",
4309
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4310
+ };
4311
+ }
4312
+ if (input.operation === "install" && input.verifiedArtifact === void 0) {
4313
+ return { kind: "blocked", reason: "tarball-integrity" };
4314
+ }
4315
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
4316
+ const settingsJson = deps.readSettings(path30.join(paths.codingAgentDir, "settings.json"));
4317
+ const receiptJson = deps.readReceipt(paths.receiptPath);
4318
+ const lifecycleInput = {
4319
+ candidate: PI_RUNTIME_CANDIDATE,
4320
+ observedTarball: input.verifiedArtifact ?? PI_RUNTIME_CANDIDATE.tarball,
4321
+ pi: {
4322
+ executable: input.detected.executable,
4323
+ version: input.detected.version,
4324
+ packageRunner: paths.packageRunner,
4325
+ settingsJson
4326
+ },
4327
+ engramBin: input.engramBin,
4328
+ receiptJson,
4329
+ scope: {
4330
+ kind: input.targetDir === void 0 ? "real" : "target-dir",
4331
+ codingAgentDir: paths.codingAgentDir,
4332
+ receiptPath: paths.receiptPath,
4333
+ environment: paths.environment
4334
+ }
4335
+ };
4336
+ if (input.operation === "install" || input.operation === "sync" || input.operation === "models") {
4337
+ const plan = deps.prepare(lifecycleInput);
4338
+ const result2 = deps.execute({
4339
+ operation: input.operation,
4340
+ plan,
4341
+ candidate: PI_RUNTIME_CANDIDATE,
4342
+ packageRunner: paths.packageRunner,
4343
+ environment: paths.environment
4344
+ });
4345
+ persistReturnedReceipt(result2, paths, deps);
4346
+ return result2;
4347
+ }
4348
+ const result = deps.operate({
4349
+ operation: input.operation,
4350
+ interactive: false,
4351
+ registry: PI_RUNTIME_REGISTRY.pi,
4352
+ detected: {
4353
+ executable: input.detected.executable,
4354
+ packageRunner: paths.packageRunner,
4355
+ settingsJson
4356
+ },
4357
+ engramBin: input.engramBin,
4358
+ receiptJson,
4359
+ paths: {
4360
+ targetDir: input.targetDir !== void 0,
4361
+ codingAgentDir: paths.codingAgentDir,
4362
+ receiptPath: paths.receiptPath,
4363
+ environment: paths.environment
4364
+ },
4365
+ removeArgs: ["remove", PI_RUNTIME_CANDIDATE.package.source, "--no-approve"]
4366
+ });
4367
+ persistReturnedReceipt(result, paths, deps);
4368
+ return result;
4369
+ }
4370
+ function readJsonFile(file) {
4371
+ return JSON.parse(fs22.readFileSync(file, "utf8"));
4372
+ }
4373
+ function packageVersionFromExecutable(executable) {
4374
+ let current;
4375
+ try {
4376
+ current = path30.dirname(fs22.realpathSync(executable));
4377
+ } catch {
4378
+ return null;
4379
+ }
4380
+ for (let depth = 0; depth < 8; depth++) {
4381
+ const manifests = [
4382
+ path30.join(current, "package.json"),
4383
+ path30.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
4384
+ ];
4385
+ for (const manifest of manifests) {
4386
+ try {
4387
+ const parsed = readJsonFile(manifest);
4388
+ if (parsed !== null && typeof parsed === "object" && Reflect.get(parsed, "name") === "@earendil-works/pi-coding-agent" && typeof Reflect.get(parsed, "version") === "string") {
4389
+ return Reflect.get(parsed, "version");
4390
+ }
4391
+ } catch {
4392
+ }
4393
+ }
4394
+ const parent = path30.dirname(current);
4395
+ if (parent === current) break;
4396
+ current = parent;
4397
+ }
4398
+ return null;
4399
+ }
4400
+ function detectPiRuntime() {
4401
+ const executable = lookPath("pi");
4402
+ const home = os5.homedir();
4403
+ return {
4404
+ id: "pi",
4405
+ name: "Pi",
4406
+ installed: executable !== null,
4407
+ executable,
4408
+ version: executable === null ? null : packageVersionFromExecutable(executable),
4409
+ codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent")
4410
+ };
4411
+ }
4412
+ function hasManagedPiRuntime(targetDir) {
4413
+ const receipt = targetDir === void 0 ? path30.join(dataDir(), "pi-receipt.json") : path30.join(path30.resolve(targetDir), "state", "pi-receipt.json");
4414
+ return fs22.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
4415
+ }
4416
+ function resolvePiEngramBin(targetDir) {
4417
+ if (targetDir === void 0) return detectEngram();
4418
+ const candidate = path30.join(path30.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
4419
+ return fs22.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
4420
+ }
4421
+ function readOptional(file, fallback) {
4422
+ try {
4423
+ return fs22.readFileSync(file, "utf8");
4424
+ } catch (error) {
4425
+ if (error.code === "ENOENT") return fallback;
4426
+ throw error;
4427
+ }
4428
+ }
4429
+ function hashPiTarball(file) {
4430
+ const descriptor = fs22.openSync(file, "r");
4431
+ const sha256 = createHash("sha256");
4432
+ const sha512 = createHash("sha512");
4433
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
4434
+ let bytes = 0;
4435
+ try {
4436
+ while (true) {
4437
+ const read = fs22.readSync(descriptor, buffer, 0, buffer.length, null);
4438
+ if (read === 0) break;
4439
+ bytes += read;
4440
+ const chunk = buffer.subarray(0, read);
4441
+ sha256.update(chunk);
4442
+ sha512.update(chunk);
4443
+ }
4444
+ } finally {
4445
+ fs22.closeSync(descriptor);
4446
+ }
4447
+ return { path: file, bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
4448
+ }
4449
+ async function acquirePiTarball(destination) {
4450
+ const existing = fs22.statSync(destination, { throwIfNoEntry: false });
4451
+ if (existing?.isFile()) {
4452
+ const observed = hashPiTarball(destination);
4453
+ if (observed.bytes === PI_RUNTIME_CANDIDATE.tarball.bytes && observed.sha256 === PI_RUNTIME_CANDIDATE.tarball.sha256 && observed.sha512 === PI_RUNTIME_CANDIDATE.tarball.sha512) {
4454
+ return observed;
4455
+ }
4456
+ fs22.rmSync(destination, { force: true });
4457
+ }
4458
+ fs22.mkdirSync(path30.dirname(destination), { recursive: true });
4459
+ const partial = `${destination}.partial-${process.pid}`;
4460
+ const response = await fetch(`https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`, {
4461
+ redirect: "error",
4462
+ headers: { accept: "application/octet-stream" }
4463
+ });
4464
+ if (!response.ok || response.body === null) throw new Error(`No se pudo descargar jorgex-pi@${PI_RUNTIME_CANDIDATE.package.version} (${response.status}).`);
4465
+ const descriptor = fs22.openSync(partial, "wx", 384);
4466
+ let bytes = 0;
4467
+ try {
4468
+ for await (const chunk of response.body) {
4469
+ const buffer = Buffer.from(chunk);
4470
+ bytes += buffer.length;
4471
+ if (bytes > PI_RUNTIME_CANDIDATE.tarball.bytes) throw new Error("El tarball de jorgex-pi excede el tama\xF1o fijado.");
4472
+ let offset = 0;
4473
+ while (offset < buffer.length) offset += fs22.writeSync(descriptor, buffer, offset);
4474
+ }
4475
+ } catch (error) {
4476
+ fs22.closeSync(descriptor);
4477
+ fs22.rmSync(partial, { force: true });
4478
+ throw error;
4479
+ }
4480
+ fs22.closeSync(descriptor);
4481
+ fs22.renameSync(partial, destination);
4482
+ return hashPiTarball(destination);
4483
+ }
4484
+ function runProcess(invocation) {
4485
+ const planned = /\.mjs$/i.test(invocation.executable) ? { command: process.execPath, args: [invocation.executable, ...invocation.args] } : planDetectedBinCommand(invocation.executable, invocation.args);
4486
+ if (planned === null) return { exitCode: 1, stdout: "", stderr: "unsafe executable" };
4487
+ const result = spawnSync(planned.command, planned.args, {
4488
+ encoding: "utf8",
4489
+ env: invocation.environment,
4490
+ shell: false,
4491
+ timeout: 12e4,
4492
+ maxBuffer: PI_RUNTIME_CANDIDATE.contract.runner.maxStdoutBytes + 1,
4493
+ stdio: ["ignore", "pipe", "pipe"]
4494
+ });
4495
+ return {
4496
+ exitCode: result.status ?? 1,
4497
+ stdout: result.stdout ?? "",
4498
+ stderr: result.stderr ?? (result.error?.message ?? "")
4499
+ };
4500
+ }
4501
+ async function runPiRuntimeSystem(input) {
4502
+ if (input.engramBin === null && input.operation !== "uninstall") return runPiRuntime(input, {
4503
+ readSettings: () => {
4504
+ throw new Error("unreachable");
4505
+ },
4506
+ readReceipt: () => {
4507
+ throw new Error("unreachable");
4508
+ },
4509
+ writeReceiptAtomic: () => {
4510
+ throw new Error("unreachable");
4511
+ },
4512
+ prepare: () => {
4513
+ throw new Error("unreachable");
4514
+ },
4515
+ execute: () => {
4516
+ throw new Error("unreachable");
4517
+ },
4518
+ operate: () => {
4519
+ throw new Error("unreachable");
4520
+ }
4521
+ });
4522
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
4523
+ if (input.operation === "install") {
4524
+ if (input.engramBin === null) {
4525
+ return {
4526
+ kind: "blocked",
4527
+ reason: "engram-required",
4528
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4529
+ };
4530
+ }
4531
+ const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path30.join(path30.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
4532
+ let artifact;
4533
+ try {
4534
+ artifact = await acquirePiTarball(destination);
4535
+ } catch (error) {
4536
+ return { kind: "blocked", reason: "tarball-download", remedy: error instanceof Error ? error.message : String(error) };
4537
+ }
4538
+ return installPiFromVerifiedTarball({
4539
+ targetDir: input.targetDir,
4540
+ piExecutable: input.detected.executable,
4541
+ engramBin: input.engramBin,
4542
+ candidate: {
4543
+ source: PI_RUNTIME_CANDIDATE.package.source,
4544
+ ...PI_RUNTIME_CANDIDATE.tarball,
4545
+ package: PI_RUNTIME_CANDIDATE.package,
4546
+ provenance: PI_RUNTIME_CANDIDATE.provenance
4547
+ }
4548
+ }, {
4549
+ download: () => artifact,
4550
+ backupSettings: () => createBackup(
4551
+ [path30.join(paths.codingAgentDir, "settings.json")],
4552
+ "pi-package-install",
4553
+ input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
4554
+ ),
4555
+ run: runProcess,
4556
+ readSettings: () => readOptional(path30.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
4557
+ rewriteSettings: (content) => writeText(path30.join(paths.codingAgentDir, "settings.json"), `${content}
4558
+ `),
4559
+ writeReceiptAtomic: (content) => writeText(paths.receiptPath, content)
4560
+ });
4561
+ }
4562
+ const packageRoot = path30.dirname(path30.dirname(paths.packageRunner));
4563
+ const writeReceipt = (receipt) => {
4564
+ writeText(paths.receiptPath, `${JSON.stringify(receipt, null, 2)}
4565
+ `);
4566
+ };
4567
+ const deps = {
4568
+ readSettings: (file) => readOptional(file, '{"packages":[]}'),
4569
+ readReceipt: (file) => readOptional(file, null),
4570
+ writeReceiptAtomic: (file, content) => writeText(file, content),
4571
+ prepare: (value) => planPiPackageLifecycle(value),
4572
+ execute: (value) => executePiPackageLifecycle(
4573
+ value,
4574
+ { writeReceipt, run: runProcess }
4575
+ ),
4576
+ operate: (value) => runPiPackageManagedOperation(
4577
+ value,
4578
+ {
4579
+ backupSettings: () => createBackup(
4580
+ [path30.join(paths.codingAgentDir, "settings.json")],
4581
+ "pi-package-uninstall",
4582
+ input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
4583
+ ),
4584
+ run: runProcess,
4585
+ isPackageAbsent: () => !fs22.existsSync(packageRoot),
4586
+ deleteReceipt: () => fs22.rmSync(paths.receiptPath, { force: true })
4587
+ }
4588
+ )
4589
+ };
4590
+ return runPiRuntime(input, deps);
4591
+ }
4592
+
3716
4593
  // src/cli.ts
3717
4594
  var VERSION = readPackageVersion();
3718
4595
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
@@ -3911,19 +4788,71 @@ function parseCliArgs(argv) {
3911
4788
  if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
3912
4789
  return { action: "run", command, flags };
3913
4790
  }
3914
- async function resolveRuntimes(flags) {
4791
+ function isFileManagedRuntime(runtime) {
4792
+ return runtime !== "pi";
4793
+ }
4794
+ async function resolveRuntimes(flags, includeAvailablePi = false) {
3915
4795
  if (flags.agents.length > 0) return flags.agents;
3916
- const detected = Object.values(ADAPTERS).filter((a) => a.detect().installed);
4796
+ const detected = Object.values(ADAPTERS).filter((adapter) => adapter.detect().installed).map((adapter) => ({ id: adapter.id, name: adapter.name }));
4797
+ const pi = detectPiRuntime();
4798
+ if (pi.installed && (includeAvailablePi || hasManagedPiRuntime(flags.targetDir))) {
4799
+ detected.push({ id: "pi", name: "Pi" });
4800
+ }
3917
4801
  if (detected.length === 0) return [];
3918
- if (flags.yes || !process.stdout.isTTY || flags.targetDir !== void 0) return detected.map((a) => a.id);
4802
+ if (flags.yes || !process.stdout.isTTY || flags.targetDir !== void 0) return detected.map((runtime) => runtime.id);
3919
4803
  const choice = await p6.multiselect({
3920
4804
  message: "\xBFEn qu\xE9 runtimes? (detectados en esta m\xE1quina)",
3921
- options: detected.map((a) => ({ value: a.id, label: a.name })),
3922
- initialValues: detected.map((a) => a.id)
4805
+ options: detected.map((runtime) => ({ value: runtime.id, label: runtime.name })),
4806
+ initialValues: detected.map((runtime) => runtime.id)
3923
4807
  });
3924
4808
  if (p6.isCancel(choice)) return null;
3925
4809
  return choice;
3926
4810
  }
4811
+ async function runSelectedPi(operation, targetDir, yes = false) {
4812
+ const detected = detectPiRuntime();
4813
+ if (!detected.installed || detected.executable === null) {
4814
+ console.error("Pi no detectado. Instala el runtime Pi antes de gestionar jorgex-pi.");
4815
+ return 1;
4816
+ }
4817
+ if (detected.version === null) {
4818
+ console.error("No se pudo verificar la versi\xF3n instalada de Pi sin ejecutarlo; revisa la instalaci\xF3n de Pi.");
4819
+ return 1;
4820
+ }
4821
+ let engramBin = resolvePiEngramBin(targetDir);
4822
+ if (operation === "install" && engramBin === null) {
4823
+ const requirement = await resolvePiEngramRequirement({
4824
+ targetDir,
4825
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
4826
+ yes
4827
+ }, {
4828
+ detectHost: () => resolvePiEngramBin(),
4829
+ detectTarget: (root) => resolvePiEngramBin(root),
4830
+ confirm: async ({ message, initialValue }) => {
4831
+ const answer = await p6.confirm({ message, initialValue });
4832
+ return !p6.isCancel(answer) && answer;
4833
+ },
4834
+ installNative: async ({ version }) => updateEngram("Gentleman-Programming/engram", version)
4835
+ });
4836
+ if (requirement.kind !== "existing") {
4837
+ console.error(requirement.kind === "offer" ? "Pi: instalaci\xF3n cancelada; Engram sigue siendo obligatorio." : `Pi: ${requirement.reason}. ${requirement.remedy}`);
4838
+ return 1;
4839
+ }
4840
+ engramBin = requirement.bin;
4841
+ }
4842
+ const result = await runPiRuntimeSystem({
4843
+ operation,
4844
+ targetDir,
4845
+ detected: { executable: detected.executable, version: detected.version },
4846
+ engramBin
4847
+ });
4848
+ if (result.kind === "blocked") {
4849
+ console.error(`Pi: ${result.reason ?? "operaci\xF3n bloqueada"}${result.remedy ? `. ${result.remedy}` : ""}`);
4850
+ return 1;
4851
+ }
4852
+ if (result.kind === "models" && result.models !== void 0) console.log(JSON.stringify(result.models));
4853
+ else p6.log.success(`Pi: ${result.kind}.`);
4854
+ return 0;
4855
+ }
3927
4856
  function printHelp() {
3928
4857
  console.log(`jorgex-stack v${VERSION}
3929
4858
 
@@ -3941,7 +4870,7 @@ Comandos:
3941
4870
  desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
3942
4871
 
3943
4872
  Opciones:
3944
- --agents, -a opencode,claude-code,codex Runtimes destino (default: detectados)
4873
+ --agents, -a opencode,claude-code,codex,pi Runtimes destino (default: detectados)
3945
4874
  --mode human|programmatic Modo de instalaci\xF3n (default: preferencia guardada o human)
3946
4875
  --subagent-concurrency serial|parallel Concurrencia de subagentes en modo programmatic
3947
4876
  --target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
@@ -3988,34 +4917,42 @@ Flags disponibles: jorgex-stack --help`
3988
4917
  switch (command) {
3989
4918
  case "install":
3990
4919
  case "sync": {
3991
- const mode = await resolveInstallMode(flags);
3992
- if (mode === null) return;
3993
- const runtimes = await resolveRuntimes(flags);
4920
+ const runtimes = await resolveRuntimes(flags, command === "install");
3994
4921
  if (runtimes === null) return;
3995
4922
  if (runtimes.length === 0) {
3996
- console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
4923
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex, pi).");
3997
4924
  process.exitCode = 1;
3998
4925
  return;
3999
4926
  }
4000
- const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, runtimes);
4001
- if (devtoolsMcpSelection === null) return;
4002
- const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
4003
- if (playwrightToolConsent === null) return;
4004
- const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, runtimes);
4005
- if (!hasOpenCodeModels) {
4006
- process.exitCode = 1;
4007
- return;
4927
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
4928
+ let exitCode = 0;
4929
+ if (fileRuntimes.length > 0) {
4930
+ const mode = await resolveInstallMode(flags);
4931
+ if (mode === null) return;
4932
+ const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, fileRuntimes);
4933
+ if (devtoolsMcpSelection === null) return;
4934
+ const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
4935
+ if (playwrightToolConsent === null) return;
4936
+ const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes);
4937
+ if (!hasOpenCodeModels) {
4938
+ process.exitCode = 1;
4939
+ return;
4940
+ }
4941
+ exitCode = await runInstall({
4942
+ runtimes: fileRuntimes,
4943
+ targetDir: flags.targetDir,
4944
+ dryRun: flags.dryRun,
4945
+ yes: flags.yes,
4946
+ mode,
4947
+ playwrightToolConsent,
4948
+ devtoolsMcpSelection
4949
+ });
4008
4950
  }
4009
- const installExitCode = await runInstall({
4010
- runtimes,
4011
- targetDir: flags.targetDir,
4012
- dryRun: flags.dryRun,
4013
- yes: flags.yes,
4014
- mode,
4015
- playwrightToolConsent,
4016
- devtoolsMcpSelection
4017
- });
4018
- process.exitCode = installExitCode;
4951
+ if (runtimes.includes("pi")) {
4952
+ if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
4953
+ else exitCode = Math.max(exitCode, await runSelectedPi(command, flags.targetDir, flags.yes));
4954
+ }
4955
+ process.exitCode = exitCode;
4019
4956
  return;
4020
4957
  }
4021
4958
  case "uninstall": {
@@ -4026,41 +4963,56 @@ Flags disponibles: jorgex-stack --help`
4026
4963
  process.exitCode = 1;
4027
4964
  return;
4028
4965
  }
4029
- process.exitCode = await runUninstall({
4030
- runtimes,
4966
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
4967
+ let exitCode = fileRuntimes.length > 0 || flags.removePlaywright ? await runUninstall({
4968
+ runtimes: fileRuntimes,
4031
4969
  targetDir: flags.targetDir,
4032
4970
  dryRun: flags.dryRun,
4033
4971
  yes: flags.yes,
4034
4972
  removeEngram: flags.removeEngram,
4035
4973
  removePlaywright: flags.removePlaywright
4036
- });
4974
+ }) : 0;
4975
+ if (runtimes.includes("pi")) {
4976
+ if (flags.dryRun) p6.log.info("Pi: uninstall previsto; dry-run conserva paquete y receipt.");
4977
+ else exitCode = Math.max(exitCode, await runSelectedPi("uninstall", flags.targetDir));
4978
+ }
4979
+ process.exitCode = exitCode;
4037
4980
  return;
4038
4981
  }
4039
4982
  case "doctor": {
4040
- process.exitCode = await runDoctor();
4983
+ const fileDoctorSelected = flags.agents.length === 0 || flags.agents.some(isFileManagedRuntime);
4984
+ let exitCode = fileDoctorSelected ? await runDoctor() : 0;
4985
+ const piSelected = flags.agents.includes("pi") || flags.agents.length === 0 && detectPiRuntime().installed && hasManagedPiRuntime(flags.targetDir);
4986
+ if (piSelected) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir));
4987
+ process.exitCode = exitCode;
4041
4988
  return;
4042
4989
  }
4043
4990
  case "update": {
4044
- if (flags.check) {
4045
- process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
4046
- return;
4047
- }
4048
- if (flags.dryRun) {
4049
- process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
4991
+ if (flags.check || flags.dryRun) {
4992
+ const piExplicit = flags.agents.includes("pi");
4993
+ const fileRuntimeExplicit = flags.agents.some(isFileManagedRuntime);
4994
+ let exitCode = flags.agents.length === 0 || fileRuntimeExplicit ? await runUpdateCheck(VERSION, flags.targetDir === void 0) : 0;
4995
+ if (piExplicit) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir));
4996
+ process.exitCode = exitCode;
4050
4997
  return;
4051
4998
  }
4052
4999
  const runtimes = await resolveRuntimes(flags);
4053
5000
  if (runtimes === null) return;
5001
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
5002
+ if (fileRuntimes.length === 0 && runtimes.includes("pi")) {
5003
+ process.exitCode = await runSelectedPi("update", flags.targetDir);
5004
+ return;
5005
+ }
4054
5006
  const preferenceFile = installModePreferenceFile();
4055
5007
  const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
4056
5008
  const hasSavedMode = hasInstallModePreference(preferenceFile);
4057
5009
  const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
4058
- const mode = runtimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
5010
+ const mode = fileRuntimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
4059
5011
  if (mode === null) return;
4060
- const canSync = runtimes.length === 0 || canResolveMode;
4061
- if (runtimes.length > 0 && canSync) {
5012
+ const canSync = fileRuntimes.length === 0 || canResolveMode;
5013
+ if (fileRuntimes.length > 0 && canSync) {
4062
5014
  const code = await runInstall({
4063
- runtimes,
5015
+ runtimes: fileRuntimes,
4064
5016
  targetDir: flags.targetDir,
4065
5017
  dryRun: flags.dryRun,
4066
5018
  yes: true,
@@ -4070,7 +5022,7 @@ Flags disponibles: jorgex-stack --help`
4070
5022
  process.exitCode = code;
4071
5023
  return;
4072
5024
  }
4073
- } else if (runtimes.length > 0) {
5025
+ } else if (fileRuntimes.length > 0) {
4074
5026
  console.error("No hay modo guardado; se omite el sync previo y se contin\xFAa con update. Usa --mode expl\xEDcito si quieres sincronizar.");
4075
5027
  }
4076
5028
  const result = await runInteractiveUpdate(
@@ -4080,13 +5032,16 @@ Flags disponibles: jorgex-stack --help`
4080
5032
  flags.targetDir === void 0
4081
5033
  );
4082
5034
  process.exitCode = result.exitCode;
4083
- if (result.syncRequired && runtimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
5035
+ if (result.exitCode === 0 && runtimes.includes("pi")) {
5036
+ process.exitCode = Math.max(process.exitCode, await runSelectedPi("update", flags.targetDir));
5037
+ }
5038
+ if (result.syncRequired && fileRuntimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
4084
5039
  p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
4085
- } else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
5040
+ } else if (result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
4086
5041
  const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
4087
5042
  if (!p6.isCancel(apply) && apply) {
4088
5043
  process.exitCode = await runInstall({
4089
- runtimes,
5044
+ runtimes: fileRuntimes,
4090
5045
  targetDir: flags.targetDir,
4091
5046
  dryRun: false,
4092
5047
  yes: false,
@@ -4095,7 +5050,7 @@ Flags disponibles: jorgex-stack --help`
4095
5050
  } else {
4096
5051
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
4097
5052
  }
4098
- } else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
5053
+ } else if (result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
4099
5054
  console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
4100
5055
  }
4101
5056
  return;
@@ -4104,13 +5059,15 @@ Flags disponibles: jorgex-stack --help`
4104
5059
  const runtimes = await resolveRuntimes(flags);
4105
5060
  if (runtimes === null) return;
4106
5061
  if (runtimes.length === 0) {
4107
- console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
5062
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex, pi).");
4108
5063
  process.exitCode = 1;
4109
5064
  return;
4110
5065
  }
4111
- const code = await runModelsPicker({ yes: flags.yes, runtimes });
5066
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
5067
+ let code = fileRuntimes.length > 0 ? await runModelsPicker({ yes: flags.yes, runtimes: fileRuntimes }) : 0;
5068
+ if (runtimes.includes("pi")) code = Math.max(code, await runSelectedPi("models", flags.targetDir));
4112
5069
  process.exitCode = code;
4113
- if (code === 0 && !flags.yes && process.stdout.isTTY) {
5070
+ if (code === 0 && fileRuntimes.length > 0 && !flags.yes && process.stdout.isTTY) {
4114
5071
  const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
4115
5072
  if (!p6.isCancel(apply) && apply) {
4116
5073
  const preferenceFile = installModePreferenceFile();
@@ -4123,7 +5080,7 @@ Flags disponibles: jorgex-stack --help`
4123
5080
  }
4124
5081
  const mode = await resolveInstallMode(flags, false);
4125
5082
  if (mode === null) return;
4126
- process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
5083
+ process.exitCode = await runInstall({ runtimes: fileRuntimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
4127
5084
  } else {
4128
5085
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
4129
5086
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI y OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",