jorgex-stack 1.0.12 → 1.0.14
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
|
@@ -71,7 +71,7 @@ Operational state:
|
|
|
71
71
|
|
|
72
72
|
## Status
|
|
73
73
|
|
|
74
|
-
The CLI is complete and the real migration has been executed
|
|
74
|
+
The CLI is complete and the real migration has been executed; the stack is the only configuration source. Versions are published automatically to [npm](https://www.npmjs.com/package/jorgex-stack) according to the flow described in [Publishing](#publishing).
|
|
75
75
|
|
|
76
76
|
## Publishing
|
|
77
77
|
|
|
@@ -79,12 +79,10 @@ Releases are triggered by push/merge to `main` and GitHub Actions; there is also
|
|
|
79
79
|
|
|
80
80
|
- **Automatic patch**: if the push to `main` contains publishable changes and the current `package.json` version already exists on npm, the workflow finds the first free patch (`x+1`, `x+2`, ...), commits `chore(release): bump version to v...`, and publishes. If tag `v<package.version>` already exists, it uses that point as the accumulated base; otherwise, it falls back to `github.event.before`. Obsolete runs are aborted after `git fetch origin main --tags` if `origin/main` no longer matches `GITHUB_SHA`.
|
|
81
81
|
- **Manual recovery**: a manual run on `main` with `release_sha` publishes that SHA if it does not exist on npm yet, without bumping again; if the version already exists on npm but tag `v<version>` is missing, the workflow fails and forces a rerun with `release_sha=<published sha>` to avoid tagging `origin/main`. `release_sha` must be a full 40-hex SHA and belong to `main`; mutable refs (`main`, tags, `main~1`) are rejected. If you do not pass `release_sha`, `validate` resolves `origin/main` once, exposes it as `target_sha`, and `bump` uses that validated SHA. Recovery does not bypass the `.github/workflows/*` guard: if the diff mixes workflows with publishable changes, split the release or perform the tag/publish manually with elevated permissions. If there is no reachable previous release tag to reconstruct the range, the workflow fails closed and requires manual intervention.
|
|
82
|
-
- **No release**: changes only in `work/`, `worktrees/`, tests, or
|
|
82
|
+
- **No release**: changes only in `work/`, `worktrees/`, tests, or docs (`README.md`, `docs/`) do not create a release. The publishable set that does trigger one is `src/`, `stack/`, `upstreams.json`, `package.json`, `pnpm-lock.yaml`, `tsconfig.json`, and `tsup.config.ts`.
|
|
83
83
|
- **Manual minor and major**: explicit bump in `package.json` in the PR (the workflow detects that the next patch already exists on npm and requires the bump).
|
|
84
84
|
- **OIDC / trusted publishing**: the publishing job uses `id-token: write` and `setup-node` `registry-url`; the bump/push job only has `contents: write`; `tag-release` only writes `contents` and does not use OIDC. There is no `NPM_TOKEN` or `NODE_AUTH_TOKEN` in any secret. `tag-release` only runs if `publish` was `success` or `skipped` with `tag_needed=true`, and keeps its SHA validation as the final defense. The only exception to the "always pnpm" rule is `npm pack --dry-run --ignore-scripts` and `npm publish --ignore-scripts --provenance` in the final step, for registry compatibility and hardening.
|
|
85
85
|
|
|
86
|
-
Design details are in [PRD §7.6](PRD.md).
|
|
87
|
-
|
|
88
86
|
## Development
|
|
89
87
|
|
|
90
88
|
Requirements: Node >= 22.5 and pnpm (never npm). Goal Mode uses `node:sqlite` in tests/Node CLI and OpenCode uses `bun:sqlite` at runtime.
|
package/dist/cli.js
CHANGED
|
@@ -855,7 +855,7 @@ ${agent.body}`,
|
|
|
855
855
|
continue;
|
|
856
856
|
}
|
|
857
857
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
858
|
-
servers[name] = { command, args: server.args ?? [] };
|
|
858
|
+
servers[name] = { type: "stdio", command, args: server.args ?? [] };
|
|
859
859
|
} else {
|
|
860
860
|
const previous = servers[name];
|
|
861
861
|
const headers = {};
|
package/package.json
CHANGED
|
@@ -116,7 +116,9 @@ Every subagent ends with a **Result contract** (Status / Delegations / Risks). P
|
|
|
116
116
|
|
|
117
117
|
### Worktree
|
|
118
118
|
|
|
119
|
-
Before the first task, create a git worktree for this work
|
|
119
|
+
Before the first task, create a git worktree for this work and run the ENTIRE execution inside it — implementation, tests, commits and pushes happen there, never on the user's main checkout.
|
|
120
|
+
|
|
121
|
+
Canonical location is mandatory: resolve the project root with `git rev-parse --show-toplevel`, ensure `worktrees/` is ignored in the repo-local `.git/info/exclude`, create `worktrees/` inside that root if needed, and create the worktree at `<project-root>/worktrees/<canonical-name>` (branch = canonical name). Do not create worktrees next to the repo, in the repo root, under `work/`, or in any external temp/shared folder.
|
|
120
122
|
|
|
121
123
|
Every delegation prompt must state the worktree path as the ONLY allowed write root. After each writer subagent finishes, verify the user's main checkout is still clean (`git status` there); if the subagent wrote outside the worktree, STOP, move those changes into the worktree (patch/apply) and restore the main checkout before continuing. Subagent obedience is not a safety boundary — this check is.
|
|
122
124
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
interface WorktreePluginConfig {
|
|
4
5
|
setupScript?: string;
|
|
@@ -19,12 +20,22 @@ interface ScriptResult {
|
|
|
19
20
|
const isAbsolutePath = (value: string) =>
|
|
20
21
|
/^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("/");
|
|
21
22
|
|
|
23
|
+
const isWindowsPath = (value: string) => /^[a-zA-Z]:[\\/]/.test(value);
|
|
24
|
+
|
|
25
|
+
const toSlashes = (value: string) => value.replace(/\\/g, "/");
|
|
26
|
+
|
|
22
27
|
const joinProjectPath = (directory: string, target: string) => {
|
|
23
|
-
const normalizedDirectory = directory.replace(/[\\/]+$/, "");
|
|
24
|
-
const normalizedTarget = target.replace(/^[\\/]+/, "")
|
|
28
|
+
const normalizedDirectory = toSlashes(directory).replace(/[\\/]+$/, "");
|
|
29
|
+
const normalizedTarget = toSlashes(target).replace(/^[\\/]+/, "");
|
|
25
30
|
return `${normalizedDirectory}/${normalizedTarget}`;
|
|
26
31
|
};
|
|
27
32
|
|
|
33
|
+
const resolvePath = (base: string, target: string) => {
|
|
34
|
+
const api = isWindowsPath(base) || isWindowsPath(target) ? path.win32 : path.posix;
|
|
35
|
+
const resolved = api.resolve(base, target);
|
|
36
|
+
return toSlashes(resolved);
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
const resolveProjectPath = (directory: string, target?: string) => {
|
|
29
40
|
if (!target) return undefined;
|
|
30
41
|
return isAbsolutePath(target) ? target : joinProjectPath(directory, target);
|
|
@@ -250,10 +261,28 @@ const parseWorktreePath = (command: string) => {
|
|
|
250
261
|
};
|
|
251
262
|
|
|
252
263
|
const getWorktreeName = (worktreePath: string) => {
|
|
253
|
-
const segments = worktreePath
|
|
264
|
+
const segments = toSlashes(worktreePath).split("/").filter(Boolean);
|
|
254
265
|
return segments[segments.length - 1] || null;
|
|
255
266
|
};
|
|
256
267
|
|
|
268
|
+
const normalizePath = (value: string) =>
|
|
269
|
+
toSlashes(value).replace(/\/+$/, "");
|
|
270
|
+
|
|
271
|
+
const samePath = (left: string, right: string) => {
|
|
272
|
+
const normalizedLeft = normalizePath(left);
|
|
273
|
+
const normalizedRight = normalizePath(right);
|
|
274
|
+
if (isWindowsPath(normalizedLeft) || isWindowsPath(normalizedRight)) {
|
|
275
|
+
return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
|
|
276
|
+
}
|
|
277
|
+
return normalizedLeft === normalizedRight;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const getCommandCwd = (args: Record<string, unknown>, directory: string) => {
|
|
281
|
+
const cwd = args.workdir || args.cwd;
|
|
282
|
+
if (typeof cwd !== "string" || cwd.length === 0) return directory;
|
|
283
|
+
return isAbsolutePath(cwd) ? cwd : resolvePath(directory, cwd);
|
|
284
|
+
};
|
|
285
|
+
|
|
257
286
|
const replaceToken = (value: string, token: string, replacement: string) =>
|
|
258
287
|
value.split(token).join(replacement);
|
|
259
288
|
|
|
@@ -313,14 +342,11 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
|
|
|
313
342
|
const args = input.args || {};
|
|
314
343
|
const command = args.command || "";
|
|
315
344
|
const commandLower = command.toLowerCase();
|
|
316
|
-
const pathContains = (
|
|
317
|
-
config.pathContains || "worktrees/"
|
|
345
|
+
const pathContains = toSlashes(
|
|
346
|
+
config.pathContains || "worktrees/",
|
|
318
347
|
).toLowerCase();
|
|
319
348
|
|
|
320
|
-
if (
|
|
321
|
-
!commandLower.includes("git worktree add") ||
|
|
322
|
-
!commandLower.includes(pathContains)
|
|
323
|
-
) {
|
|
349
|
+
if (!commandLower.includes("git worktree add")) {
|
|
324
350
|
return;
|
|
325
351
|
}
|
|
326
352
|
|
|
@@ -334,9 +360,25 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
|
|
|
334
360
|
|
|
335
361
|
const gitRoot = await $`git rev-parse --show-toplevel`.text();
|
|
336
362
|
const projectRoot = String(gitRoot).trim().replace(/\\/g, "/");
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
363
|
+
const commandCwd = getCommandCwd(args, directory);
|
|
364
|
+
const absoluteWorktreePath = resolvePath(commandCwd, parsedWorktreePath);
|
|
365
|
+
const expectedWorktreePath = joinProjectPath(
|
|
366
|
+
projectRoot,
|
|
367
|
+
`worktrees/${worktreeName}`,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (!samePath(absoluteWorktreePath, expectedWorktreePath)) {
|
|
371
|
+
appendToolOutput(output, [
|
|
372
|
+
`Worktree path is not canonical: ${absoluteWorktreePath}`,
|
|
373
|
+
`Use the project-local path instead: ${expectedWorktreePath}`,
|
|
374
|
+
"Canonical rule: <project-root>/worktrees/<canonical-name>.",
|
|
375
|
+
]);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!normalizePath(absoluteWorktreePath).toLowerCase().includes(pathContains)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
340
382
|
|
|
341
383
|
const setupScript = resolveProjectPath(directory, config.setupScript);
|
|
342
384
|
if (setupScript) {
|
|
@@ -33,7 +33,8 @@ Every piece of work gets a **canonical kebab-case name** when it starts (e.g. `c
|
|
|
33
33
|
|
|
34
34
|
## Executing
|
|
35
35
|
|
|
36
|
-
- Execution happens inside a git worktree created for the work
|
|
36
|
+
- Execution happens inside a git worktree created for the work; the user's main checkout stays untouched until merge.
|
|
37
|
+
- Worktree path is fixed: first resolve the project root with `git rev-parse --show-toplevel`, ensure `worktrees/` is ignored in the repo-local `.git/info/exclude`, then create/use `<project-root>/worktrees/<canonical-name>` (branch = canonical name). Never place worktrees in the repo root, next to the repo, under `work/`, or outside the project.
|
|
37
38
|
- Delegation handoff: the subagent receives its **topic_key + task title**, never the task content inline. It retrieves the spec itself (`mem_search` → `mem_get_observation`).
|
|
38
39
|
- The subagent saves its phase outcome under the topic_key the orchestrator gave it (`work/{name}/{phase}`) BEFORE its final report.
|
|
39
40
|
- Task status lives ONLY in the plan.md table: flip it (⬜ → ✅) with a surgical edit when the task closes. Do not mirror statuses into memory, and do not re-read the whole plan after every task — it is already in context; re-read it on resume.
|
|
@@ -91,6 +91,7 @@ docs/
|
|
|
91
91
|
Every piece of information about a piece of work has exactly ONE home — never two. The `work-lifecycle` skill is the single source of this flow.
|
|
92
92
|
|
|
93
93
|
- In-progress work lives in `work/{name}/` (gitignored): `PRD.md` + `plan.md`. plan.md is the ONLY task status board — update statuses with surgical edits. An empty `work/` means nothing is half-done.
|
|
94
|
+
- Execution worktrees always live inside the current project's root under `worktrees/{name}`. Resolve the root with `git rev-parse --show-toplevel`, ensure `worktrees/` is ignored in the repo-local `.git/info/exclude`, then create/use `<project-root>/worktrees/<canonical-name>`; never create worktrees next to the repo, in the repo root, under `work/`, or in external temp/shared folders.
|
|
94
95
|
- Full task specs, phase outcomes and history live in Engram: `work/{name}/task/{NN}`, `work/{name}/{phase}`, `work/{name}/done`. Subagents receive a topic_key + title, never the task content inline.
|
|
95
96
|
- Pending work: the project's single `work/backlog` topic_key (one upserted list — never one key per idea), or issues (`to-issues`) if the project uses a tracker. Never a TODOs folder.
|
|
96
97
|
- On close: save the outcome under `work/{name}/done`, move the PRD to the project's docs only if it has lasting value, then delete `work/{name}/`. History is memory + git — no archive folders.
|