jorgex-stack 1.9.0 → 1.9.2

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 CHANGED
@@ -106,7 +106,7 @@ Programmatic mode does **not** provide:
106
106
 
107
107
  ### Pi runtime
108
108
 
109
- Pi combines the frozen **snapshot v2** package with a Stack-owned shared projection. This adoption targets the exact published package **`jorgex-pi@0.7.0`** and keeps Pi out of the adapter/component manifest and model map. The Stack release that consumes it must pin `npm:jorgex-pi@0.7.0` in `src/lib/pi-runtime.ts`; this section does not claim that a user installation or `main` has already consumed it.
109
+ Pi combines the frozen **snapshot v2** package with a Stack-owned shared projection. This PR03 candidate targets the exact published package **`jorgex-pi@0.8.0`** and keeps Pi out of the adapter/component manifest and model map. PR03 pins that candidate in `src/lib/pi-runtime.ts` without changing `package.json`; the published Stack 1.9.0 release still recognizes `npm:jorgex-pi@0.7.0`, so this section does not claim that `main` or an end-user installation has consumed Pi 0.8.0.
110
110
 
111
111
  ```bash
112
112
  pnpm dlx jorgex-stack install --agents pi
@@ -116,9 +116,11 @@ pnpm dlx jorgex-stack sync --agents pi
116
116
  pnpm dlx jorgex-stack uninstall --agents pi
117
117
  ```
118
118
 
119
- 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. For `0.7.0`, the frozen tarball is `89125185` bytes; the URL is derived from the version and the authoritative size/SHA-256/SHA-512 pin remains in `src/lib/pi-runtime.ts` rather than being duplicated here. 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. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. When the managed Playwright preference is active, the projection also adds or removes the marked `jorgex:browser` section dynamically. The Pi-only `install --agents pi --playwright` flow installs and persists that Playwright capability just like the other harnesses. Chrome DevTools MCP and Context7 remain outside the Pi scope. The managed Pi package entry is the exact object `{ "source": "npm:jorgex-pi@0.7.0", "skills": [], "prompts": [] }`; filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Both receipts are scope-bound and fail closed for manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state.
119
+ 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. For `0.8.0`, the frozen tarball is `89128340` bytes; the URL is derived from the version and the authoritative size/SHA-256/SHA-512 pin remains in `src/lib/pi-runtime.ts` rather than being duplicated here. 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. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. When the managed Playwright preference is active, the projection also adds or removes the marked `jorgex:browser` section dynamically. The Pi-only `install --agents pi --playwright` flow installs and persists that Playwright capability just like the other harnesses. Chrome DevTools MCP and Context7 remain outside the Pi scope. The managed Pi package entry is the exact object `{ "source": "npm:jorgex-pi@0.8.0", "skills": [], "prompts": [] }`; filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Both receipts are scope-bound and fail closed for manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state.
120
120
 
121
- The published artifact has three separate provenance anchors: the release checkout and tarball producer is `41b41b7a49617b59c7eb72bdedaa75455b362496`; the SLSA workflow run is `33481871250` on ref `main`, with resolved dependencies at the same commit `41b41b7...` (manual `0.7.0`, with no bump commit); and the Stack parity source is `1327d8dbe68e4118272a74a06c44a731bb346efa`. Registry metadata has no `gitHead`. The npm attestation binds the exact tarball SHA-512 to workflow run `33481871250`; README does not treat registry metadata as a separate source identity.
121
+ The published Pi 0.8.0 direct-package snapshot adds `work-audit`: the snapshot grows from **17 to 18 skill trees** (96 to 97 files), and the active runtime allowlist grows from **16 to 17 skills**. `playwright-cli` remains in the snapshot but inactive because browser automation is a separate opt-in integration.
122
+
123
+ The published artifact has two separate provenance anchors: the release checkout and tarball producer is `9f999747df3e335947a61d38e581555367973b09` (`main`, release `0.8.0`); and the Stack parity source is `11e7666ea4e40bde1de8bc434610747eb797ab9c`. Registry metadata has no `gitHead`; README does not invent a separate source identity, attestation or signature.
122
124
 
123
125
  Install, sync and uninstall back up every managed file before changing it and are idempotent. `doctor` reports package and projection drift without repairing it. Uninstall removes only receipt-owned package/projection state, retains shared files also owned by another runtime, and preserves user content outside marked sections. Engram remains user-owned and is never removed; the receipts only carry the verified executable hand-off required by the package.
124
126
 
@@ -126,7 +128,7 @@ The package owns Pi's native primary-model projection: `openai-codex/gpt-5.6-sol
126
128
 
127
129
  Engram remains mandatory and user-owned. An existing binary is preserved. Interactive install may offer the native `brew`/`go`/release channel with explicit confirmation; `--yes` and non-TTY installs fail with a remedy when Engram is absent. The database and memories are never updated or deleted, and uninstall never deletes the Engram binary. 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.
128
130
 
129
- The transition from `jorgex-pi@0.6.1` to `jorgex-pi@0.7.0` is not in-place. Each Stack release recognizes only the Pi receipt for its exact pin. The published Stack **`jorgex-stack@1.7.5`** is the corroborated release that recognizes `npm:jorgex-pi@0.6.1`; use it explicitly, never `latest`, to clean an old `0.6.1` receipt before installing `jorgex-stack@1.8.1`, which recognizes `0.7.0`. Rollback is symmetric: first use `jorgex-stack@1.8.1` to clean the present `0.7.0` receipt, then `jorgex-stack@1.7.5` to restore `0.6.1`. The earlier `0.4.0`/`jorgex-stack@1.7.1` pairing is historical context only, not part of this transition. Never edit receipts or hashes and never delete `HOME`, Engram, or another runtime's projection to force trust. The exact examples and failure behavior are in [docs/references/pi-runtime.md](docs/references/pi-runtime.md); the commands there are documentation, not commands executed by this adoption.
131
+ The transition from `jorgex-pi@0.7.0` to `jorgex-pi@0.8.0` is not in-place. Each Stack release recognizes only the Pi receipt for its exact pin. The published Stack **`jorgex-stack@1.9.0`** is the corroborated release that still recognizes `npm:jorgex-pi@0.7.0`. PR03 does not change `package.json`; after the merge, the workflow will publish the first free patch in `1.9.x`, expected to be `1.9.1`. T15 must verify the final published version and its recognition of `npm:jorgex-pi@0.8.0` before it is used for a real installation. Use exact versions, never `latest`, and never edit receipts or hashes or delete `HOME`, Engram, or another runtime's projection to force trust. The examples are in `docs/references/pi-runtime.md`; they are documentation, not commands executed by this adoption.
130
132
 
131
133
  The 24-hour npm maturity rule applies only to real managed installation or consumption of the new Pi package. Development, PR validation, merge and Stack publication may proceed immediately against the exact verified artifact; installing it on a real user scope before 24 hours requires Jorge's explicit exception.
132
134
 
package/dist/cli.js CHANGED
@@ -5142,16 +5142,16 @@ import { createHash } from "crypto";
5142
5142
  var PI_RUNTIME_CANDIDATE = {
5143
5143
  package: {
5144
5144
  name: "jorgex-pi",
5145
- version: "0.7.0",
5146
- source: "npm:jorgex-pi@0.7.0"
5145
+ version: "0.8.0",
5146
+ source: "npm:jorgex-pi@0.8.0"
5147
5147
  },
5148
5148
  provenance: {
5149
- commit: "41b41b7a49617b59c7eb72bdedaa75455b362496"
5149
+ commit: "9f999747df3e335947a61d38e581555367973b09"
5150
5150
  },
5151
5151
  tarball: {
5152
- bytes: 89125185,
5153
- sha256: "4db392a68187a05a530e6d5ac4c45f834aab5972201edc9ef86da547981109e5",
5154
- sha512: "fb5fdcf15463f3dab36be9b2e6cb55b7397831fe8a172972666a0d6d146ba5e26e70b74e2962dc2702b4ae6ee025094291597c910f967dd924c5a51bd0cc655b"
5152
+ bytes: 89128340,
5153
+ sha256: "b001f9dea23669b7211af228df6ea6442bcd90a9a928aa1a47c3e7132966f989",
5154
+ sha512: "a591bf223e2d48ddecc89341253dc0ac41a6f843796c04ca62f47dc7587b3ec9afafdffba63e52fe99f833aec5ac0b34926eaf3c26f2755003bbf8e6891f6bd9"
5155
5155
  },
5156
5156
  pi: {
5157
5157
  testedVersions: ["0.84.2"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,32 @@ interface ScriptResult {
16
16
  scriptPath: string;
17
17
  }
18
18
 
19
+ const logToOpenCode = async (
20
+ client: unknown,
21
+ level: "warn" | "error",
22
+ message: string,
23
+ extra?: unknown,
24
+ ) => {
25
+ if (typeof client !== "object" || client === null) return;
26
+ const app = (client as { app?: unknown }).app;
27
+ if (typeof app !== "object" || app === null) return;
28
+ const log = (app as { log?: unknown }).log;
29
+ if (typeof log !== "function") return;
30
+
31
+ const safeExtra =
32
+ extra instanceof Error
33
+ ? { message: extra.message, stack: extra.stack }
34
+ : extra;
35
+
36
+ try {
37
+ await log.call(app, {
38
+ body: { service: "hooks", level, message, extra: safeExtra },
39
+ });
40
+ } catch {
41
+ // Logging must not break the OpenCode TUI hook.
42
+ }
43
+ };
44
+
19
45
  const isAbsolutePath = (value: string) =>
20
46
  /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("/");
21
47
 
@@ -43,6 +69,22 @@ const resolveProjectPath = (directory: string, target?: string) => {
43
69
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
44
70
  typeof value === "object" && value !== null && !Array.isArray(value);
45
71
 
72
+ const validateConfig = (config: Record<string, unknown>) => {
73
+ for (const field of ["setupScript", "docsReminderScript", "pathContains"]) {
74
+ if (field in config && typeof config[field] !== "string") {
75
+ return `Worktree config field "${field}" must be a string.`;
76
+ }
77
+ }
78
+
79
+ if (
80
+ "reminderLines" in config &&
81
+ (!Array.isArray(config.reminderLines) ||
82
+ !config.reminderLines.every((line) => typeof line === "string"))
83
+ ) {
84
+ return 'Worktree config field "reminderLines" must be an array of strings.';
85
+ }
86
+ };
87
+
46
88
  const getPayloadWorktreePath = (payload: unknown) => {
47
89
  if (!isPlainObject(payload)) return undefined;
48
90
 
@@ -142,15 +184,14 @@ const runScript = async (
142
184
  }
143
185
 
144
186
  if (!command) {
145
- await client.app.log({
146
- body: {
147
- service: "hooks",
148
- level: "warn",
149
- message: "Unsupported worktree hook script extension",
150
- extra: { script, scriptPath },
151
- },
152
- });
153
- return { exitCode: 0, stdout: "", stderr: "", script, scriptPath };
187
+ const stderr = `Unsupported worktree hook script extension for ${scriptPath}.`;
188
+ await logToOpenCode(
189
+ client,
190
+ "warn",
191
+ "Unsupported worktree hook script extension",
192
+ { script, scriptPath },
193
+ );
194
+ return { exitCode: 1, stdout: "", stderr, script, scriptPath };
154
195
  }
155
196
 
156
197
  try {
@@ -173,35 +214,26 @@ const runScript = async (
173
214
  ]);
174
215
 
175
216
  if (exitCode !== 0) {
176
- await client.app.log({
177
- body: {
178
- service: "hooks",
179
- level: "error",
180
- message: "Worktree hook script execution failed",
181
- extra: {
182
- script,
183
- scriptPath,
184
- exitCode,
185
- stderr: stderr.trim(),
186
- stdout: stdout.trim(),
187
- },
217
+ await logToOpenCode(
218
+ client,
219
+ "error",
220
+ "Worktree hook script execution failed",
221
+ {
222
+ script,
223
+ scriptPath,
224
+ exitCode,
225
+ stderr: stderr.trim(),
226
+ stdout: stdout.trim(),
188
227
  },
189
- });
228
+ );
190
229
  }
191
230
 
192
231
  return { exitCode, stdout, stderr, script, scriptPath };
193
232
  } catch (error) {
194
- await client.app.log({
195
- body: {
196
- service: "hooks",
197
- level: "error",
198
- message: "Worktree hook script execution failed",
199
- extra: {
200
- script,
201
- scriptPath,
202
- error: error instanceof Error ? error.message : String(error),
203
- },
204
- },
233
+ await logToOpenCode(client, "error", "Worktree hook script execution failed", {
234
+ script,
235
+ scriptPath,
236
+ error: error instanceof Error ? error.message : String(error),
205
237
  });
206
238
  return {
207
239
  exitCode: 1,
@@ -285,28 +317,67 @@ const getCommandCwd = (args: Record<string, unknown>, directory: string) => {
285
317
  const replaceToken = (value: string, token: string, replacement: string) =>
286
318
  value.split(token).join(replacement);
287
319
 
320
+ const isMissingFileError = (error: unknown) => {
321
+ const code =
322
+ typeof error === "object" && error !== null && "code" in error
323
+ ? (error as { code?: unknown }).code
324
+ : undefined;
325
+ return (
326
+ code === "ENOENT" ||
327
+ (error instanceof Error && /not found|no such file/i.test(error.message))
328
+ );
329
+ };
330
+
288
331
  export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
289
332
  let config: WorktreePluginConfig = {
290
- setupScript: "scripts/setup-worktree.ps1",
291
333
  pathContains: "worktrees/",
292
334
  reminderLines: [],
293
335
  };
336
+ let configError: string | undefined;
294
337
 
295
338
  try {
296
339
  const configPath = `${directory}/.opencode/worktree.json`;
297
- const content = await (globalThis as any).Bun.file(configPath).text();
298
- config = {
299
- ...config,
300
- ...JSON.parse(content),
301
- };
302
- } catch {
303
- // Optional project config
340
+ const configFile = (globalThis as any).Bun.file(configPath);
341
+ if (
342
+ typeof configFile.exists === "function" &&
343
+ !(await configFile.exists())
344
+ ) {
345
+ // Optional project config.
346
+ } else {
347
+ const parsed = JSON.parse(await configFile.text()) as unknown;
348
+ if (!isPlainObject(parsed)) {
349
+ configError = "Worktree config must be a JSON object.";
350
+ } else {
351
+ configError = validateConfig(parsed);
352
+ if (!configError) {
353
+ config = { ...config, ...(parsed as WorktreePluginConfig) };
354
+ }
355
+ }
356
+ }
357
+ } catch (error) {
358
+ if (!isMissingFileError(error)) {
359
+ const couldNotParse = error instanceof SyntaxError;
360
+ configError = couldNotParse
361
+ ? "Worktree config could not be parsed as JSON."
362
+ : "Worktree config could not be read.";
363
+ await logToOpenCode(
364
+ client,
365
+ "warn",
366
+ couldNotParse
367
+ ? "Worktree config could not be parsed"
368
+ : "Worktree config could not be read",
369
+ error,
370
+ );
371
+ }
304
372
  }
305
373
 
306
374
  return {
307
375
  "tool.execute.after": async (input: any, output: any) => {
308
376
  try {
309
377
  const tool = (input.tool || "").toLowerCase();
378
+ const args = input.args || {};
379
+ const command = args.command || "";
380
+
310
381
  const payload = {
311
382
  event: "tool.execute.after",
312
383
  directory,
@@ -315,6 +386,11 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
315
386
  };
316
387
 
317
388
  if (tool === "enterworktree") {
389
+ if (configError) {
390
+ appendToolOutput(output, [configError]);
391
+ return;
392
+ }
393
+
318
394
  const docsReminderScript = resolveProjectPath(
319
395
  directory,
320
396
  config.docsReminderScript,
@@ -337,8 +413,6 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
337
413
 
338
414
  if (tool !== "bash") return;
339
415
 
340
- const args = input.args || {};
341
- const command = args.command || "";
342
416
  const commandLower = command.toLowerCase();
343
417
  const pathContains = toSlashes(
344
418
  config.pathContains || "worktrees/",
@@ -366,12 +440,24 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
366
440
  `worktrees/${worktreeName}`,
367
441
  );
368
442
 
369
- if (!samePath(absoluteWorktreePath, expectedWorktreePath)) {
443
+ const isCanonicalPath = samePath(
444
+ absoluteWorktreePath,
445
+ expectedWorktreePath,
446
+ );
447
+ if (!isCanonicalPath) {
370
448
  appendToolOutput(output, [
371
449
  `Worktree path is not canonical: ${absoluteWorktreePath}`,
372
450
  `Use the project-local path instead: ${expectedWorktreePath}`,
373
451
  "Canonical rule: <project-root>/worktrees/<canonical-name> or <project-root>/worktrees/<canonical-name>-prNN.",
374
452
  ]);
453
+ }
454
+
455
+ if (configError) {
456
+ appendToolOutput(output, [configError]);
457
+ return;
458
+ }
459
+
460
+ if (!isCanonicalPath) {
375
461
  return;
376
462
  }
377
463
 
@@ -430,15 +516,16 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
430
516
  appendToolOutput(output, [banner]);
431
517
  }
432
518
  } catch (error) {
433
- await client.app.log({
434
- body: {
435
- service: "hooks",
436
- level: "error",
437
- message: "Worktree plugin execution failed",
438
- extra: {
439
- error: error instanceof Error ? error.message : String(error),
440
- },
441
- },
519
+ const details = truncateMessage(
520
+ error instanceof Error ? error.message : String(error),
521
+ );
522
+ appendToolOutput(output, [
523
+ "Worktree plugin could not process this worktree command.",
524
+ "Run `git rev-parse --show-toplevel` from the project root and retry.",
525
+ ...(details ? [`Details: ${details}`] : []),
526
+ ]);
527
+ await logToOpenCode(client, "error", "Worktree plugin execution failed", {
528
+ error: error instanceof Error ? error.message : String(error),
442
529
  });
443
530
  }
444
531
  },