jorgex-stack 1.0.13 → 1.0.15
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 +2 -4
- package/dist/cli.js +1 -0
- package/package.json +1 -1
- package/stack/agents/orchestrator.md +3 -1
- package/stack/plugins/opencode/engram.ts +54 -27
- package/stack/plugins/opencode/worktree.ts +54 -12
- package/stack/skills/work-lifecycle/SKILL.md +2 -1
- package/stack/system-prompt/AGENTS.md +1 -0
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
|
@@ -1323,6 +1323,7 @@ function planPlugins(adapter, ctx) {
|
|
|
1323
1323
|
);
|
|
1324
1324
|
content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
|
|
1325
1325
|
}
|
|
1326
|
+
content = content.replace(/(from\s+["'])(\.{1,2}\/[^"']+?)\.js(["'])/g, "$1$2.ts$3");
|
|
1326
1327
|
if (content === raw) return { kind: "copy", source: sourceFile, target };
|
|
1327
1328
|
return { kind: "write", target, content };
|
|
1328
1329
|
});
|
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
|
|
|
@@ -16,12 +16,30 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
18
18
|
|
|
19
|
+
declare const Bun: {
|
|
20
|
+
which?: (bin: string) => string | null
|
|
21
|
+
spawnSync: (args: string[]) => { exitCode: number; stdout?: { toString(): string } | string }
|
|
22
|
+
spawn: (args: string[], options?: Record<string, unknown>) => unknown
|
|
23
|
+
file: (path: string) => { exists: () => Promise<boolean> }
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
20
27
|
|
|
21
28
|
const ENGRAM_PORT = parseInt(process.env.ENGRAM_PORT ?? "7437")
|
|
22
29
|
const ENGRAM_URL = `http://127.0.0.1:${ENGRAM_PORT}`
|
|
23
30
|
// "{{ENGRAM_BIN}}" lo resuelve el instalador con el binario detectado (D7).
|
|
24
|
-
const ENGRAM_BIN =
|
|
31
|
+
const ENGRAM_BIN = "{{ENGRAM_BIN}}"
|
|
32
|
+
|
|
33
|
+
export function resolveEngramBin(installerBin = ENGRAM_BIN): string {
|
|
34
|
+
const envBin = process.env.ENGRAM_BIN
|
|
35
|
+
if (envBin) return envBin
|
|
36
|
+
|
|
37
|
+
const bun = globalThis as typeof globalThis & { Bun?: { which?: (bin: string) => string | null } }
|
|
38
|
+
const bunBin = bun.Bun?.which?.("engram")
|
|
39
|
+
if (bunBin) return bunBin
|
|
40
|
+
|
|
41
|
+
return installerBin !== "{{ENGRAM_BIN}}" ? installerBin : "engram"
|
|
42
|
+
}
|
|
25
43
|
|
|
26
44
|
// Engram's own MCP tools — don't count these as "tool calls" for session stats
|
|
27
45
|
const ENGRAM_TOOLS = new Set([
|
|
@@ -60,6 +78,7 @@ async function engramFetch(
|
|
|
60
78
|
headers: opts.body ? { "Content-Type": "application/json" } : undefined,
|
|
61
79
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
62
80
|
})
|
|
81
|
+
if (!res.ok) return null
|
|
63
82
|
return await res.json()
|
|
64
83
|
} catch {
|
|
65
84
|
// Engram server not running — silently fail
|
|
@@ -126,6 +145,7 @@ function stripPrivateTags(str: string): string {
|
|
|
126
145
|
export const Engram: Plugin = async (ctx) => {
|
|
127
146
|
const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
|
|
128
147
|
const project = extractProjectName(ctx.directory)
|
|
148
|
+
const engramBin = resolveEngramBin()
|
|
129
149
|
|
|
130
150
|
// Track tool counts per session (in-memory only, not critical)
|
|
131
151
|
const toolCounts = new Map<string, number>()
|
|
@@ -145,12 +165,12 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
145
165
|
*
|
|
146
166
|
* Silently skips sub-agent sessions (tracked in `subAgentSessions`).
|
|
147
167
|
*/
|
|
148
|
-
async function ensureSession(sessionId: string): Promise<
|
|
149
|
-
if (!sessionId
|
|
168
|
+
async function ensureSession(sessionId: string): Promise<boolean> {
|
|
169
|
+
if (!sessionId) return false
|
|
170
|
+
if (knownSessions.has(sessionId)) return true
|
|
150
171
|
// Do not register sub-agent sessions in Engram (issue #116).
|
|
151
|
-
if (subAgentSessions.has(sessionId)) return
|
|
152
|
-
|
|
153
|
-
await engramFetch("/sessions", {
|
|
172
|
+
if (subAgentSessions.has(sessionId)) return false
|
|
173
|
+
const session = await engramFetch("/sessions", {
|
|
154
174
|
method: "POST",
|
|
155
175
|
body: {
|
|
156
176
|
id: sessionId,
|
|
@@ -158,13 +178,18 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
158
178
|
directory: ctx.directory,
|
|
159
179
|
},
|
|
160
180
|
})
|
|
181
|
+
|
|
182
|
+
if (session === null) return false
|
|
183
|
+
|
|
184
|
+
knownSessions.add(sessionId)
|
|
185
|
+
return true
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
// Try to start engram server if not running
|
|
164
189
|
const running = await isEngramRunning()
|
|
165
190
|
if (!running) {
|
|
166
191
|
try {
|
|
167
|
-
Bun.spawn([
|
|
192
|
+
Bun.spawn([engramBin, "serve"], {
|
|
168
193
|
stdout: "ignore",
|
|
169
194
|
stderr: "ignore",
|
|
170
195
|
stdin: "ignore",
|
|
@@ -192,7 +217,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
192
217
|
const manifestFile = `${ctx.directory}/.engram/manifest.json`
|
|
193
218
|
const file = Bun.file(manifestFile)
|
|
194
219
|
if (await file.exists()) {
|
|
195
|
-
Bun.spawn([
|
|
220
|
+
Bun.spawn([engramBin, "sync", "--import"], {
|
|
196
221
|
cwd: ctx.directory,
|
|
197
222
|
stdout: "ignore",
|
|
198
223
|
stderr: "ignore",
|
|
@@ -206,7 +231,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
206
231
|
return {
|
|
207
232
|
// ─── Event Listeners ───────────────────────────────────────────
|
|
208
233
|
|
|
209
|
-
event: async ({ event }) => {
|
|
234
|
+
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
210
235
|
// --- Session Created ---
|
|
211
236
|
if (event.type === "session.created") {
|
|
212
237
|
// Bug fix (#116): session data is nested under event.properties.info,
|
|
@@ -254,7 +279,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
254
279
|
// output.message is typed as UserMessage (role:"user" already guaranteed).
|
|
255
280
|
// output.parts contains TextPart[] with the actual message text.
|
|
256
281
|
|
|
257
|
-
"chat.message": async (input, output) => {
|
|
282
|
+
"chat.message": async (input: any, output: any) => {
|
|
258
283
|
// Skip sub-agent sessions — they inflate session counts (issue #116)
|
|
259
284
|
if (subAgentSessions.has(input.sessionID)) return
|
|
260
285
|
|
|
@@ -262,8 +287,8 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
262
287
|
|
|
263
288
|
// Extract text from parts (type:"text")
|
|
264
289
|
const content = output.parts
|
|
265
|
-
.filter((p) => p.type === "text")
|
|
266
|
-
.map((p) =>
|
|
290
|
+
.filter((p: any) => p.type === "text")
|
|
291
|
+
.map((p: any) => p.text ?? "")
|
|
267
292
|
.join("\n")
|
|
268
293
|
.trim()
|
|
269
294
|
|
|
@@ -276,15 +301,17 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
276
301
|
|
|
277
302
|
// Only capture non-trivial prompts (>10 chars)
|
|
278
303
|
if (finalContent.length > 10) {
|
|
279
|
-
await ensureSession(sessionId)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
304
|
+
const sessionReady = await ensureSession(sessionId)
|
|
305
|
+
if (sessionReady) {
|
|
306
|
+
await engramFetch("/prompts", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: {
|
|
309
|
+
session_id: sessionId,
|
|
310
|
+
content: stripPrivateTags(truncate(finalContent, 2000)),
|
|
311
|
+
project,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
}
|
|
288
315
|
}
|
|
289
316
|
},
|
|
290
317
|
|
|
@@ -294,13 +321,13 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
294
321
|
// Passive capture: when a Task tool completes, POST its output to
|
|
295
322
|
// the passive capture endpoint so the server extracts learnings.
|
|
296
323
|
|
|
297
|
-
"tool.execute.after": async (input, output) => {
|
|
324
|
+
"tool.execute.after": async (input: any, output: any) => {
|
|
298
325
|
if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
|
|
299
326
|
|
|
300
327
|
// input.sessionID comes from OpenCode — always available
|
|
301
328
|
const sessionId = input.sessionID
|
|
302
|
-
|
|
303
|
-
|
|
329
|
+
const sessionReady = sessionId ? await ensureSession(sessionId) : false
|
|
330
|
+
if (sessionReady && sessionId) {
|
|
304
331
|
toolCounts.set(sessionId, (toolCounts.get(sessionId) ?? 0) + 1)
|
|
305
332
|
}
|
|
306
333
|
|
|
@@ -308,7 +335,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
308
335
|
// (OpenCode reports the tool name in lowercase: "task")
|
|
309
336
|
if (input.tool.toLowerCase() === "task" && output && sessionId) {
|
|
310
337
|
const text = typeof output === "string" ? output : JSON.stringify(output)
|
|
311
|
-
if (text.length > 50) {
|
|
338
|
+
if (text.length > 50 && sessionReady) {
|
|
312
339
|
await engramFetch("/observations/passive", {
|
|
313
340
|
method: "POST",
|
|
314
341
|
body: {
|
|
@@ -332,7 +359,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
332
359
|
// block at the beginning. By concatenating, we avoid adding extra system
|
|
333
360
|
// messages that would break these models. See: GitHub issue #23.
|
|
334
361
|
|
|
335
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
362
|
+
"experimental.chat.system.transform": async (_input: any, output: any) => {
|
|
336
363
|
if (output.system.length > 0) {
|
|
337
364
|
output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
|
|
338
365
|
} else {
|
|
@@ -348,7 +375,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
348
375
|
// 2. Inject context from previous sessions into the compaction prompt
|
|
349
376
|
// 3. Tell the compressor to remind the new agent to save memories
|
|
350
377
|
|
|
351
|
-
"experimental.session.compacting": async (input, output) => {
|
|
378
|
+
"experimental.session.compacting": async (input: any, output: any) => {
|
|
352
379
|
if (input.sessionID) {
|
|
353
380
|
await ensureSession(input.sessionID)
|
|
354
381
|
}
|
|
@@ -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.
|