@pennixrv/trellis 0.7.1-beta.5 → 0.7.1-beta.6

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 (48) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +14 -6
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/commands/task-progress.d.ts +1 -0
  5. package/dist/commands/task-progress.d.ts.map +1 -1
  6. package/dist/commands/task-progress.js +67 -25
  7. package/dist/commands/task-progress.js.map +1 -1
  8. package/dist/commands/update.js +2 -2
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/constants/paths.d.ts +3 -10
  11. package/dist/constants/paths.d.ts.map +1 -1
  12. package/dist/constants/paths.js +4 -13
  13. package/dist/constants/paths.js.map +1 -1
  14. package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
  15. package/dist/templates/claude/hooks/statusline.py +9 -4
  16. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
  17. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
  18. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
  19. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
  20. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  21. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
  22. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
  23. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +2 -2
  24. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +1 -1
  25. package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
  26. package/dist/templates/opencode/lib/session-utils.js +301 -209
  27. package/dist/templates/snow/SNOW.md +1 -1
  28. package/dist/templates/trellis/agents/teammate.md +5 -5
  29. package/dist/templates/trellis/index.d.ts +2 -0
  30. package/dist/templates/trellis/index.d.ts.map +1 -1
  31. package/dist/templates/trellis/index.js +4 -0
  32. package/dist/templates/trellis/index.js.map +1 -1
  33. package/dist/templates/trellis/scripts/add_session.py +1 -1
  34. package/dist/templates/trellis/scripts/common/active_task.py +3 -47
  35. package/dist/templates/trellis/scripts/common/paths.py +9 -25
  36. package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
  37. package/dist/templates/trellis/scripts/common/task_context.py +11 -51
  38. package/dist/templates/trellis/scripts/common/task_continuity.py +53 -45
  39. package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
  40. package/dist/templates/trellis/scripts/common/task_move.py +345 -0
  41. package/dist/templates/trellis/scripts/common/task_store.py +28 -55
  42. package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
  43. package/dist/templates/trellis/scripts/common/task_workflow.py +28 -20
  44. package/dist/templates/trellis/scripts/common/tasks.py +6 -5
  45. package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
  46. package/dist/templates/trellis/scripts/task.py +41 -23
  47. package/dist/templates/trellis/workflow.md +4 -2
  48. package/package.json +2 -2
@@ -1,11 +1,11 @@
1
1
  /* global process */
2
- import { existsSync, readFileSync, readdirSync, statSync } from "fs"
3
- import { join } from "path"
4
- import { execFileSync } from "child_process"
5
- import { platform } from "os"
6
- import { debugLog } from "./trellis-context.js"
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
3
+ import { join } from "path";
4
+ import { execFileSync } from "child_process";
5
+ import { platform } from "os";
6
+ import { debugLog } from "./trellis-context.js";
7
7
 
8
- const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
8
+ const PYTHON_CMD = platform() === "win32" ? "python" : "python3";
9
9
 
10
10
  const FIRST_REPLY_NOTICE = `<first-reply-notice>
11
11
  On the first visible assistant reply in this session, briefly acknowledge that Trellis SessionStart context loaded.
@@ -16,83 +16,98 @@ Choose the acknowledgment language in this order:
16
16
  Continue directly with the user's request after the acknowledgment.
17
17
  The acknowledgment must not alter the language used for the remainder of the response.
18
18
  This notice is one-shot: do not repeat it after the first visible assistant reply in this session.
19
- </first-reply-notice>`
19
+ </first-reply-notice>`;
20
20
 
21
21
  function hasCuratedJsonlEntry(jsonlPath) {
22
22
  try {
23
- const content = readFileSync(jsonlPath, "utf-8")
23
+ const content = readFileSync(jsonlPath, "utf-8");
24
24
  for (const rawLine of content.split(/\r?\n/)) {
25
- const line = rawLine.trim()
26
- if (!line) continue
25
+ const line = rawLine.trim();
26
+ if (!line) continue;
27
27
  try {
28
- const row = JSON.parse(line)
29
- if (row && typeof row === "object" && typeof row.file === "string" && row.file) {
30
- return true
28
+ const row = JSON.parse(line);
29
+ if (
30
+ row &&
31
+ typeof row === "object" &&
32
+ typeof row.file === "string" &&
33
+ row.file
34
+ ) {
35
+ return true;
31
36
  }
32
37
  } catch {
33
38
  // Ignore malformed line
34
39
  }
35
40
  }
36
41
  } catch {
37
- return false
42
+ return false;
38
43
  }
39
- return false
44
+ return false;
40
45
  }
41
46
 
42
47
  function getTaskStatus(ctx, platformInput = null) {
43
- const active = ctx.getActiveTask(platformInput)
44
- const taskRef = active.taskPath
48
+ const active = ctx.getActiveTask(platformInput);
49
+ const taskRef = active.taskPath;
45
50
  if (!taskRef) {
46
51
  return (
47
52
  "Status: NO ACTIVE TASK\n" +
48
53
  "Next-Action: Classify the current turn before creating any Trellis task. " +
49
54
  "Simple conversation / small task asks only whether this turn should create a Trellis task. " +
50
55
  "Complex task asks whether task creation and planning are allowed."
51
- )
56
+ );
52
57
  }
53
58
 
54
- const taskDir = ctx.resolveTaskDir(taskRef)
59
+ const taskDir = ctx.resolveTaskDir(taskRef);
55
60
 
56
61
  if (active.stale || !taskDir || !existsSync(taskDir)) {
57
- return `Status: STALE POINTER\nTask: ${taskRef}\nNext-Action: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
62
+ return `Status: STALE POINTER\nTask: ${taskRef}\nNext-Action: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`;
58
63
  }
59
64
 
60
- let taskData = {}
61
- const taskJsonPath = join(taskDir, "task.json")
65
+ let taskData = {};
66
+ const taskJsonPath = join(taskDir, "task.json");
62
67
  if (existsSync(taskJsonPath)) {
63
68
  try {
64
- taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"))
69
+ taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"));
65
70
  } catch {
66
71
  // Ignore parse errors
67
72
  }
68
73
  }
69
74
 
70
- const taskTitle = taskData.title || taskRef
71
- const taskStatus = taskData.status || "unknown"
75
+ const taskTitle = taskData.title || taskRef;
76
+ const taskStatus = taskData.status || "unknown";
72
77
 
73
78
  if (taskStatus === "completed") {
74
- return `Status: COMPLETED\nTask: ${taskTitle}\nNext-Action: Start another task or end the session; finish-work only records the session journal.`
79
+ return `Status: COMPLETED\nTask: ${taskTitle}\nNext-Action: Start another task or end the session; finish-work only records the session journal.`;
75
80
  }
76
81
 
77
- const artifactNames = ["prd.md", "design.md", "implement.md", "implement.jsonl", "check.jsonl"]
78
- const present = artifactNames.filter(name => existsSync(join(taskDir, name)))
79
- if (existsSync(join(taskDir, "research"))) present.push("research/")
80
- const presentLine = present.length > 0 ? present.join(", ") : "(none)"
81
- const implementJsonl = join(taskDir, "implement.jsonl")
82
- const checkJsonl = join(taskDir, "check.jsonl")
82
+ const artifactNames = [
83
+ "prd.md",
84
+ "design.md",
85
+ "implement.md",
86
+ "implement.jsonl",
87
+ "check.jsonl",
88
+ ];
89
+ const present = artifactNames.filter((name) =>
90
+ existsSync(join(taskDir, name)),
91
+ );
92
+ if (existsSync(join(taskDir, "research"))) present.push("research/");
93
+ const presentLine = present.length > 0 ? present.join(", ") : "(none)";
94
+ const implementJsonl = join(taskDir, "implement.jsonl");
95
+ const checkJsonl = join(taskDir, "check.jsonl");
83
96
  const jsonlReady =
84
97
  (!existsSync(implementJsonl) || hasCuratedJsonlEntry(implementJsonl)) &&
85
- (!existsSync(checkJsonl) || hasCuratedJsonlEntry(checkJsonl))
98
+ (!existsSync(checkJsonl) || hasCuratedJsonlEntry(checkJsonl));
86
99
 
87
100
  if (taskStatus === "planning") {
88
101
  const nextBits = [
89
102
  "Follow the current per-turn workflow-state; task lifecycle status and artifact presence do not identify the business phase",
90
103
  "If no phase route is initialized, load `trellis-goal-exploration-and-requirements` and confirm the requirements baseline before initializing the route",
91
- ]
104
+ ];
92
105
  if (!jsonlReady) {
93
- nextBits.push("curate task context manifests only when a dispatched agent needs them")
106
+ nextBits.push(
107
+ "curate task context manifests only when a dispatched agent needs them",
108
+ );
94
109
  }
95
- return `Status: PLANNING\nTask: ${taskTitle}\nPresent: ${presentLine}\nNext-Action: ${nextBits.join("; ")}. Do not enter implementation until the user confirms start.`
110
+ return `Status: PLANNING\nTask: ${taskTitle}\nPresent: ${presentLine}\nNext-Action: ${nextBits.join("; ")}. Do not enter implementation until the user confirms start.`;
96
111
  }
97
112
 
98
113
  return (
@@ -100,32 +115,48 @@ function getTaskStatus(ctx, platformInput = null) {
100
115
  `Present: ${presentLine}\n` +
101
116
  "Next-Action: Follow the matching per-turn workflow-state; task lifecycle status does not identify the business phase. " +
102
117
  "Implementation/check context order is jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`."
103
- )
118
+ );
104
119
  }
105
120
 
106
121
  function loadTrellisConfig(directory, contextKey = null) {
107
- const scriptPath = join(directory, ".trellis", "scripts", "get_context.py")
122
+ const scriptPath = join(directory, ".trellis", "scripts", "get_context.py");
108
123
  if (!existsSync(scriptPath)) {
109
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
124
+ return {
125
+ isMonorepo: false,
126
+ packages: {},
127
+ specScope: null,
128
+ activeTaskPackage: null,
129
+ defaultPackage: null,
130
+ };
110
131
  }
111
132
  try {
112
- const output = execFileSync(PYTHON_CMD, [scriptPath, "--mode", "packages", "--json"], {
113
- cwd: directory,
114
- timeout: 5000,
115
- encoding: "utf-8",
116
- stdio: ["pipe", "pipe", "pipe"],
117
- env: {
118
- ...process.env,
119
- ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
133
+ const output = execFileSync(
134
+ PYTHON_CMD,
135
+ [scriptPath, "--mode", "packages", "--json"],
136
+ {
137
+ cwd: directory,
138
+ timeout: 5000,
139
+ encoding: "utf-8",
140
+ stdio: ["pipe", "pipe", "pipe"],
141
+ env: {
142
+ ...process.env,
143
+ ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
144
+ },
120
145
  },
121
- })
122
- const data = JSON.parse(output)
146
+ );
147
+ const data = JSON.parse(output);
123
148
  if (data.mode !== "monorepo") {
124
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
149
+ return {
150
+ isMonorepo: false,
151
+ packages: {},
152
+ specScope: null,
153
+ activeTaskPackage: null,
154
+ defaultPackage: null,
155
+ };
125
156
  }
126
- const pkgDict = {}
127
- for (const pkg of (data.packages || [])) {
128
- pkgDict[pkg.name] = pkg
157
+ const pkgDict = {};
158
+ for (const pkg of data.packages || []) {
159
+ pkgDict[pkg.name] = pkg;
129
160
  }
130
161
  return {
131
162
  isMonorepo: true,
@@ -133,34 +164,40 @@ function loadTrellisConfig(directory, contextKey = null) {
133
164
  specScope: data.specScope || null,
134
165
  activeTaskPackage: data.activeTaskPackage || null,
135
166
  defaultPackage: data.defaultPackage || null,
136
- }
167
+ };
137
168
  } catch (e) {
138
- debugLog("session", "loadTrellisConfig error:", e.message)
139
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
169
+ debugLog("session", "loadTrellisConfig error:", e.message);
170
+ return {
171
+ isMonorepo: false,
172
+ packages: {},
173
+ specScope: null,
174
+ activeTaskPackage: null,
175
+ defaultPackage: null,
176
+ };
140
177
  }
141
178
  }
142
179
 
143
180
  function checkLegacySpec(directory, config) {
144
181
  if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
145
- return null
182
+ return null;
146
183
  }
147
184
 
148
- const specDir = join(directory, ".trellis", "spec")
149
- if (!existsSync(specDir)) return null
185
+ const specDir = join(directory, ".trellis", "spec");
186
+ if (!existsSync(specDir)) return null;
150
187
 
151
- let hasLegacy = false
188
+ let hasLegacy = false;
152
189
  for (const name of ["backend", "frontend"]) {
153
190
  if (existsSync(join(specDir, name, "index.md"))) {
154
- hasLegacy = true
155
- break
191
+ hasLegacy = true;
192
+ break;
156
193
  }
157
194
  }
158
- if (!hasLegacy) return null
195
+ if (!hasLegacy) return null;
159
196
 
160
- const pkgNames = Object.keys(config.packages).sort()
161
- const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
197
+ const pkgNames = Object.keys(config.packages).sort();
198
+ const missing = pkgNames.filter((name) => !existsSync(join(specDir, name)));
162
199
 
163
- if (missing.length === 0) return null
200
+ if (missing.length === 0) return null;
164
201
 
165
202
  if (missing.length === pkgNames.length) {
166
203
  return (
@@ -168,84 +205,92 @@ function checkLegacySpec(directory, config) {
168
205
  `but no package-scoped \`spec/<package>/\` directories.\n` +
169
206
  `Monorepo packages: ${pkgNames.join(", ")}\n` +
170
207
  `Please reorganize: \`spec/backend/\` -> \`spec/<package>/backend/\``
171
- )
208
+ );
172
209
  }
173
210
  return (
174
211
  `[!] Partial spec migration detected: packages ${missing.join(", ")} ` +
175
212
  `still missing \`spec/<pkg>/\` directory.\n` +
176
213
  `Please complete migration for all packages.`
177
- )
214
+ );
178
215
  }
179
216
 
180
217
  function resolveSpecScope(config) {
181
218
  if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
182
- return null
219
+ return null;
183
220
  }
184
221
 
185
- const { specScope, activeTaskPackage, defaultPackage, packages } = config
186
- if (specScope == null) return null
222
+ const { specScope, activeTaskPackage, defaultPackage, packages } = config;
223
+ if (specScope == null) return null;
187
224
 
188
225
  if (specScope === "active_task") {
189
- if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
190
- if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
191
- return null
226
+ if (activeTaskPackage && activeTaskPackage in packages)
227
+ return new Set([activeTaskPackage]);
228
+ if (defaultPackage && defaultPackage in packages)
229
+ return new Set([defaultPackage]);
230
+ return null;
192
231
  }
193
232
 
194
233
  if (Array.isArray(specScope)) {
195
- const valid = new Set()
234
+ const valid = new Set();
196
235
  for (const entry of specScope) {
197
236
  if (entry in packages) {
198
- valid.add(entry)
237
+ valid.add(entry);
199
238
  }
200
239
  }
201
- if (valid.size > 0) return valid
202
- if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
203
- if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
204
- return null
240
+ if (valid.size > 0) return valid;
241
+ if (activeTaskPackage && activeTaskPackage in packages)
242
+ return new Set([activeTaskPackage]);
243
+ if (defaultPackage && defaultPackage in packages)
244
+ return new Set([defaultPackage]);
245
+ return null;
205
246
  }
206
247
 
207
- return null
248
+ return null;
208
249
  }
209
250
 
210
251
  function collectSpecIndexPaths(directory, allowedPkgs) {
211
- const specDir = join(directory, ".trellis", "spec")
212
- const paths = []
252
+ const specDir = join(directory, ".trellis", "spec");
253
+ const paths = [];
213
254
 
214
- const guidesIndex = join(specDir, "guides", "index.md")
255
+ const guidesIndex = join(specDir, "guides", "index.md");
215
256
  if (existsSync(guidesIndex)) {
216
- paths.push(".trellis/spec/guides/index.md")
257
+ paths.push(".trellis/spec/guides/index.md");
217
258
  }
218
259
 
219
- if (!existsSync(specDir)) return paths
260
+ if (!existsSync(specDir)) return paths;
220
261
 
221
262
  try {
222
- const subs = readdirSync(specDir).filter(name => {
223
- if (name.startsWith(".") || name === "guides") return false
224
- try {
225
- return statSync(join(specDir, name)).isDirectory()
226
- } catch {
227
- return false
228
- }
229
- }).sort()
263
+ const subs = readdirSync(specDir)
264
+ .filter((name) => {
265
+ if (name.startsWith(".") || name === "guides") return false;
266
+ try {
267
+ return statSync(join(specDir, name)).isDirectory();
268
+ } catch {
269
+ return false;
270
+ }
271
+ })
272
+ .sort();
230
273
 
231
274
  for (const sub of subs) {
232
- const indexFile = join(specDir, sub, "index.md")
275
+ const indexFile = join(specDir, sub, "index.md");
233
276
  if (existsSync(indexFile)) {
234
- paths.push(`.trellis/spec/${sub}/index.md`)
277
+ paths.push(`.trellis/spec/${sub}/index.md`);
235
278
  } else {
236
- if (allowedPkgs !== null && !allowedPkgs.has(sub)) continue
279
+ if (allowedPkgs !== null && !allowedPkgs.has(sub)) continue;
237
280
  try {
238
- const nested = readdirSync(join(specDir, sub)).filter(name => {
239
- try {
240
- return statSync(join(specDir, sub, name)).isDirectory()
241
- } catch {
242
- return false
243
- }
244
- }).sort()
281
+ const nested = readdirSync(join(specDir, sub))
282
+ .filter((name) => {
283
+ try {
284
+ return statSync(join(specDir, sub, name)).isDirectory();
285
+ } catch {
286
+ return false;
287
+ }
288
+ })
289
+ .sort();
245
290
  for (const layer of nested) {
246
- const nestedIndex = join(specDir, sub, layer, "index.md")
291
+ const nestedIndex = join(specDir, sub, layer, "index.md");
247
292
  if (existsSync(nestedIndex)) {
248
- paths.push(`.trellis/spec/${sub}/${layer}/index.md`)
293
+ paths.push(`.trellis/spec/${sub}/${layer}/index.md`);
249
294
  }
250
295
  }
251
296
  } catch {
@@ -257,19 +302,22 @@ function collectSpecIndexPaths(directory, allowedPkgs) {
257
302
  // Ignore spec directory read errors
258
303
  }
259
304
 
260
- return paths
305
+ return paths;
261
306
  }
262
307
 
263
308
  function readDeveloper(directory) {
264
309
  try {
265
- const content = readFileSync(join(directory, ".trellis", ".developer"), "utf-8")
310
+ const content = readFileSync(
311
+ join(directory, ".trellis", ".developer"),
312
+ "utf-8",
313
+ );
266
314
  for (const line of content.split(/\r?\n/)) {
267
- if (line.startsWith("name=")) return line.slice("name=".length).trim()
315
+ if (line.startsWith("name=")) return line.slice("name=".length).trim();
268
316
  }
269
317
  } catch {
270
318
  // Ignore missing developer file
271
319
  }
272
- return "(not initialized)"
320
+ return "(not initialized)";
273
321
  }
274
322
 
275
323
  function runGit(directory, args) {
@@ -279,63 +327,90 @@ function runGit(directory, args) {
279
327
  timeout: 3000,
280
328
  encoding: "utf-8",
281
329
  stdio: ["pipe", "pipe", "pipe"],
282
- }).trim()
330
+ }).trim();
283
331
  } catch {
284
- return ""
332
+ return "";
285
333
  }
286
334
  }
287
335
 
288
336
  function buildCompactCurrentState(ctx, platformInput, specIndexPaths) {
289
- const directory = ctx.directory
290
- const lines = []
291
- lines.push(`Developer: ${readDeveloper(directory)}`)
337
+ const directory = ctx.directory;
338
+ const lines = [];
339
+ lines.push(`Developer: ${readDeveloper(directory)}`);
292
340
 
293
- const branch = runGit(directory, ["branch", "--show-current"]) || "(detached)"
341
+ const branch =
342
+ runGit(directory, ["branch", "--show-current"]) || "(detached)";
294
343
  const dirtyCount = runGit(directory, ["status", "--porcelain"])
295
344
  .split(/\r?\n/)
296
- .filter(line => line.trim()).length
297
- lines.push(`Git: branch ${branch}; ${dirtyCount === 0 ? "clean" : `dirty ${dirtyCount} paths`}.`)
345
+ .filter((line) => line.trim()).length;
346
+ lines.push(
347
+ `Git: branch ${branch}; ${dirtyCount === 0 ? "clean" : `dirty ${dirtyCount} paths`}.`,
348
+ );
298
349
 
299
- const active = ctx.getActiveTask(platformInput)
350
+ const active = ctx.getActiveTask(platformInput);
300
351
  if (active.taskPath) {
301
- const taskDir = ctx.resolveTaskDir(active.taskPath)
302
- let status = "unknown"
352
+ const taskDir = ctx.resolveTaskDir(active.taskPath);
353
+ let status = "unknown";
303
354
  if (taskDir) {
304
355
  try {
305
- const data = JSON.parse(readFileSync(join(taskDir, "task.json"), "utf-8"))
306
- status = data.status || "unknown"
356
+ const data = JSON.parse(
357
+ readFileSync(join(taskDir, "task.json"), "utf-8"),
358
+ );
359
+ status = data.status || "unknown";
307
360
  } catch {
308
361
  // Ignore parse errors
309
362
  }
310
363
  }
311
- lines.push(`Current task: ${active.taskPath}; status=${status}.`)
364
+ lines.push(`Current task: ${active.taskPath}; status=${status}.`);
312
365
  } else {
313
- lines.push("Current task: none.")
366
+ lines.push("Current task: none.");
314
367
  }
315
368
 
316
- const tasksDir = join(directory, ".trellis", "tasks")
369
+ const tasksDir = join(directory, ".trellis", "tasks");
317
370
  if (existsSync(tasksDir)) {
318
371
  try {
319
- const activeTasks = readdirSync(tasksDir, { withFileTypes: true })
320
- .filter(entry => entry.isDirectory() && entry.name !== "archive" && existsSync(join(tasksDir, entry.name, "task.json")))
321
- lines.push(`Active tasks: ${activeTasks.length} total. Use \`python3 ./.trellis/scripts/task.py list --mine\` only if needed.`)
372
+ const result = JSON.parse(
373
+ execFileSync(
374
+ PYTHON_CMD,
375
+ [join(directory, ".trellis", "scripts", "task.py"), "list", "--json"],
376
+ {
377
+ cwd: directory,
378
+ encoding: "utf-8",
379
+ timeout: 5000,
380
+ stdio: ["ignore", "pipe", "pipe"],
381
+ },
382
+ ),
383
+ );
384
+ const activeTasks = result.tasks.filter((task) =>
385
+ ["planning", "in_progress"].includes(task.status),
386
+ );
387
+ lines.push(
388
+ `Active tasks: ${activeTasks.length} total. Use \`python3 ./.trellis/scripts/task.py list --mine\` only if needed.`,
389
+ );
322
390
  } catch {
323
391
  // Ignore task list errors
324
392
  }
325
393
  }
326
394
 
327
- const developer = readDeveloper(directory)
328
- const workspaceDir = join(directory, ".trellis", "workspace", developer)
395
+ const developer = readDeveloper(directory);
396
+ const workspaceDir = join(directory, ".trellis", "workspace", developer);
329
397
  if (developer !== "(not initialized)" && existsSync(workspaceDir)) {
330
398
  try {
331
399
  const journals = readdirSync(workspaceDir)
332
- .filter(name => /^journal-\d+\.md$/.test(name))
333
- .sort((a, b) => Number(a.match(/\d+/)?.[0] || 0) - Number(b.match(/\d+/)?.[0] || 0))
334
- const journal = journals[journals.length - 1]
400
+ .filter((name) => /^journal-\d+\.md$/.test(name))
401
+ .sort(
402
+ (a, b) =>
403
+ Number(a.match(/\d+/)?.[0] || 0) - Number(b.match(/\d+/)?.[0] || 0),
404
+ );
405
+ const journal = journals[journals.length - 1];
335
406
  if (journal) {
336
- const journalPath = join(workspaceDir, journal)
337
- const lineCount = readFileSync(journalPath, "utf-8").split(/\r?\n/).length
338
- lines.push(`Journal: .trellis/workspace/${developer}/${journal}, ${lineCount} / 2000 lines.`)
407
+ const journalPath = join(workspaceDir, journal);
408
+ const lineCount = readFileSync(journalPath, "utf-8").split(
409
+ /\r?\n/,
410
+ ).length;
411
+ lines.push(
412
+ `Journal: .trellis/workspace/${developer}/${journal}, ${lineCount} / 2000 lines.`,
413
+ );
339
414
  }
340
415
  } catch {
341
416
  // Ignore journal errors
@@ -343,180 +418,197 @@ function buildCompactCurrentState(ctx, platformInput, specIndexPaths) {
343
418
  }
344
419
 
345
420
  if (specIndexPaths.length > 0) {
346
- lines.push(`Spec indexes: ${specIndexPaths.length} available.`)
421
+ lines.push(`Spec indexes: ${specIndexPaths.length} available.`);
347
422
  }
348
423
 
349
- return lines.join("\n")
424
+ return lines.join("\n");
350
425
  }
351
426
 
352
427
  export function buildSessionContext(ctx, platformInput = null) {
353
- const directory = ctx.directory
354
- const contextKey = typeof ctx.getContextKey === "function"
355
- ? ctx.getContextKey(platformInput)
356
- : null
428
+ const directory = ctx.directory;
429
+ const contextKey =
430
+ typeof ctx.getContextKey === "function"
431
+ ? ctx.getContextKey(platformInput)
432
+ : null;
357
433
 
358
- const config = loadTrellisConfig(directory, contextKey)
359
- const allowedPkgs = resolveSpecScope(config)
360
- const paths = collectSpecIndexPaths(directory, allowedPkgs)
434
+ const config = loadTrellisConfig(directory, contextKey);
435
+ const allowedPkgs = resolveSpecScope(config);
436
+ const paths = collectSpecIndexPaths(directory, allowedPkgs);
361
437
 
362
- const parts = []
438
+ const parts = [];
363
439
 
364
440
  parts.push(`<session-context>
365
441
  Trellis compact SessionStart context. Use it to orient the session; load details on demand.
366
- </session-context>`)
367
- parts.push(FIRST_REPLY_NOTICE)
442
+ </session-context>`);
443
+ parts.push(FIRST_REPLY_NOTICE);
368
444
 
369
- const legacyWarning = checkLegacySpec(directory, config)
445
+ const legacyWarning = checkLegacySpec(directory, config);
370
446
  if (legacyWarning) {
371
- parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`)
447
+ parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`);
372
448
  }
373
449
 
374
- parts.push("<current-state>")
375
- parts.push(buildCompactCurrentState(ctx, platformInput, paths))
376
- parts.push("</current-state>")
450
+ parts.push("<current-state>");
451
+ parts.push(buildCompactCurrentState(ctx, platformInput, paths));
452
+ parts.push("</current-state>");
377
453
 
378
- const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
454
+ const workflowContent = ctx.readProjectFile(".trellis/workflow.md");
379
455
  if (workflowContent) {
380
- const allLines = workflowContent.split("\n")
456
+ const allLines = workflowContent.split("\n");
381
457
  const overviewLines = [
382
458
  "# 工作流会话摘要",
383
459
  "完整说明:.trellis/workflow.md。阶段详情:读取当前阶段提示指定的 Skill。",
384
460
  "",
385
- ]
461
+ ];
386
462
 
387
- let rangeStart = -1
388
- let rangeEnd = allLines.length
463
+ let rangeStart = -1;
464
+ let rangeEnd = allLines.length;
389
465
  for (let i = 0; i < allLines.length; i++) {
390
- const stripped = allLines[i].trim()
391
- const heading = stripped.replace(/^#{2,3}\s+/, "")
466
+ const stripped = allLines[i].trim();
467
+ const heading = stripped.replace(/^#{2,3}\s+/, "");
392
468
  const isPhaseHeading =
393
469
  /^#{2,3}\s+/.test(stripped) &&
394
- ["目标探索与需求核对阶段", "Goal Exploration and Requirements Confirmation Phase"].some(
470
+ [
471
+ "目标探索与需求核对阶段",
472
+ "Goal Exploration and Requirements Confirmation Phase",
473
+ ].some(
395
474
  (title) =>
396
475
  heading === title ||
397
476
  heading.endsWith(`:${title}`) ||
398
477
  heading.endsWith(`: ${title}`),
399
- )
400
- if (rangeStart === -1 && ["## 阶段索引", "## Phase Index"].includes(stripped)) {
401
- rangeStart = i
478
+ );
479
+ if (
480
+ rangeStart === -1 &&
481
+ ["## 阶段索引", "## Phase Index"].includes(stripped)
482
+ ) {
483
+ rangeStart = i;
402
484
  } else if (rangeStart !== -1 && isPhaseHeading) {
403
- rangeEnd = i
404
- break
485
+ rangeEnd = i;
486
+ break;
405
487
  }
406
488
  }
407
489
  if (rangeStart !== -1) {
408
490
  const strippedStateBlocks = allLines
409
491
  .slice(rangeStart, rangeEnd)
410
492
  .join("\n")
411
- .replace(/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n[\s\S]*?\n\s*\[\/workflow-state:\1\]\n?/g, "")
493
+ .replace(
494
+ /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n[\s\S]*?\n\s*\[\/workflow-state:\1\]\n?/g,
495
+ "",
496
+ )
412
497
  .replace(/<!--[\s\S]*?-->/g, "")
413
498
  .replace(/^\[(?!\/?workflow-state:)\/?[^\]\n]+\]\s*\n?/gm, "")
414
- .replace(/\n{3,}/g, "\n\n")
415
- overviewLines.push(strippedStateBlocks.trimEnd())
499
+ .replace(/\n{3,}/g, "\n\n");
500
+ overviewLines.push(strippedStateBlocks.trimEnd());
416
501
  }
417
502
 
418
- parts.push("<trellis-workflow>")
419
- parts.push(overviewLines.join("\n").trimEnd())
420
- parts.push("</trellis-workflow>")
503
+ parts.push("<trellis-workflow>");
504
+ parts.push(overviewLines.join("\n").trimEnd());
505
+ parts.push("</trellis-workflow>");
421
506
  }
422
507
 
423
- parts.push("<guidelines>")
508
+ parts.push("<guidelines>");
424
509
  parts.push(
425
510
  "Task context order for implementation/check: jsonl entries -> `prd.md` -> " +
426
- "`design.md if present` -> `implement.md if present`. Missing optional artifacts " +
427
- "are skipped for lightweight tasks.\n"
428
- )
511
+ "`design.md if present` -> `implement.md if present`. Missing optional artifacts " +
512
+ "are skipped for lightweight tasks.\n",
513
+ );
429
514
 
430
515
  if (paths.length > 0) {
431
- parts.push("## Available indexes (read on demand)")
516
+ parts.push("## Available indexes (read on demand)");
432
517
  for (const p of paths) {
433
- parts.push(`- ${p}`)
518
+ parts.push(`- ${p}`);
434
519
  }
435
- parts.push("")
520
+ parts.push("");
436
521
  }
437
522
 
438
523
  parts.push(
439
524
  "Discover more via: " +
440
- "`python3 ./.trellis/scripts/get_context.py --mode packages`"
441
- )
442
- parts.push("</guidelines>")
525
+ "`python3 ./.trellis/scripts/get_context.py --mode packages`",
526
+ );
527
+ parts.push("</guidelines>");
443
528
 
444
- const taskStatus = getTaskStatus(ctx, platformInput)
445
- parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
529
+ const taskStatus = getTaskStatus(ctx, platformInput);
530
+ parts.push(`<task-status>\n${taskStatus}\n</task-status>`);
446
531
 
447
532
  parts.push(`<ready>
448
533
  Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed.
449
- </ready>`)
534
+ </ready>`);
450
535
 
451
- return parts.join("\n\n")
536
+ return parts.join("\n\n");
452
537
  }
453
538
 
454
539
  function getTrellisMetadata(metadata) {
455
540
  if (!metadata || typeof metadata !== "object") {
456
- return {}
541
+ return {};
457
542
  }
458
543
 
459
- const trellis = metadata.trellis
544
+ const trellis = metadata.trellis;
460
545
  if (!trellis || typeof trellis !== "object") {
461
- return {}
546
+ return {};
462
547
  }
463
548
 
464
- return trellis
549
+ return trellis;
465
550
  }
466
551
 
467
552
  function markPartAsSessionStart(part) {
468
- const metadata = part.metadata && typeof part.metadata === "object"
469
- ? part.metadata
470
- : {}
553
+ const metadata =
554
+ part.metadata && typeof part.metadata === "object" ? part.metadata : {};
471
555
  part.metadata = {
472
556
  ...metadata,
473
557
  trellis: {
474
558
  ...getTrellisMetadata(metadata),
475
559
  sessionStart: true,
476
560
  },
477
- }
561
+ };
478
562
  }
479
563
 
480
564
  function hasSessionStartMarker(part) {
481
565
  if (!part || part.type !== "text" || typeof part.text !== "string") {
482
- return false
566
+ return false;
483
567
  }
484
568
 
485
- return getTrellisMetadata(part.metadata).sessionStart === true
569
+ return getTrellisMetadata(part.metadata).sessionStart === true;
486
570
  }
487
571
 
488
572
  export function hasInjectedTrellisContext(messages) {
489
573
  if (!Array.isArray(messages)) {
490
- return false
574
+ return false;
491
575
  }
492
576
 
493
- return messages.some(message => {
494
- if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
495
- return false
577
+ return messages.some((message) => {
578
+ if (
579
+ !message?.info ||
580
+ message.info.role !== "user" ||
581
+ !Array.isArray(message.parts)
582
+ ) {
583
+ return false;
496
584
  }
497
585
 
498
- return message.parts.some(hasSessionStartMarker)
499
- })
586
+ return message.parts.some(hasSessionStartMarker);
587
+ });
500
588
  }
501
589
 
502
- export async function hasPersistedInjectedContext(client, directory, sessionID) {
590
+ export async function hasPersistedInjectedContext(
591
+ client,
592
+ directory,
593
+ sessionID,
594
+ ) {
503
595
  try {
504
596
  const response = await client.session.messages({
505
597
  path: { id: sessionID },
506
598
  query: { directory },
507
599
  throwOnError: true,
508
- })
509
- return hasInjectedTrellisContext(response.data || [])
600
+ });
601
+ return hasInjectedTrellisContext(response.data || []);
510
602
  } catch (error) {
511
603
  debugLog(
512
604
  "session",
513
605
  "Failed to read session history for dedupe:",
514
606
  error instanceof Error ? error.message : String(error),
515
- )
516
- return false
607
+ );
608
+ return false;
517
609
  }
518
610
  }
519
611
 
520
612
  export function markContextInjected(part) {
521
- markPartAsSessionStart(part)
613
+ markPartAsSessionStart(part);
522
614
  }