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 +6 -4
- package/dist/cli.js +6 -6
- package/package.json +1 -1
- package/stack/plugins/opencode/worktree.ts +140 -53
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
5146
|
-
source: "npm:jorgex-pi@0.
|
|
5145
|
+
version: "0.8.0",
|
|
5146
|
+
source: "npm:jorgex-pi@0.8.0"
|
|
5147
5147
|
},
|
|
5148
5148
|
provenance: {
|
|
5149
|
-
commit: "
|
|
5149
|
+
commit: "9f999747df3e335947a61d38e581555367973b09"
|
|
5150
5150
|
},
|
|
5151
5151
|
tarball: {
|
|
5152
|
-
bytes:
|
|
5153
|
-
sha256: "
|
|
5154
|
-
sha512: "
|
|
5152
|
+
bytes: 89128340,
|
|
5153
|
+
sha256: "b001f9dea23669b7211af228df6ea6442bcd90a9a928aa1a47c3e7132966f989",
|
|
5154
|
+
sha512: "a591bf223e2d48ddecc89341253dc0ac41a6f843796c04ca62f47dc7587b3ec9afafdffba63e52fe99f833aec5ac0b34926eaf3c26f2755003bbf8e6891f6bd9"
|
|
5155
5155
|
},
|
|
5156
5156
|
pi: {
|
|
5157
5157
|
testedVersions: ["0.84.2"]
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
},
|