@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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/task-progress.d.ts +1 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +67 -25
- package/dist/commands/task-progress.js.map +1 -1
- package/dist/commands/update.js +2 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/constants/paths.d.ts +3 -10
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -13
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
- package/dist/templates/claude/hooks/statusline.py +9 -4
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +1 -1
- package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
- package/dist/templates/opencode/lib/session-utils.js +301 -209
- package/dist/templates/snow/SNOW.md +1 -1
- package/dist/templates/trellis/agents/teammate.md +5 -5
- package/dist/templates/trellis/index.d.ts +2 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +4 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/active_task.py +3 -47
- package/dist/templates/trellis/scripts/common/paths.py +9 -25
- package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
- package/dist/templates/trellis/scripts/common/task_context.py +11 -51
- package/dist/templates/trellis/scripts/common/task_continuity.py +53 -45
- package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
- package/dist/templates/trellis/scripts/common/task_move.py +345 -0
- package/dist/templates/trellis/scripts/common/task_store.py +28 -55
- package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
- package/dist/templates/trellis/scripts/common/task_workflow.py +28 -20
- package/dist/templates/trellis/scripts/common/tasks.py +6 -5
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
- package/dist/templates/trellis/scripts/task.py +41 -23
- package/dist/templates/trellis/workflow.md +4 -2
- 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 (
|
|
30
|
-
|
|
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 = [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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)
|
|
190
|
-
|
|
191
|
-
|
|
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)
|
|
203
|
-
|
|
204
|
-
|
|
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)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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))
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
306
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
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(
|
|
334
|
-
|
|
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(
|
|
338
|
-
|
|
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 =
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
[
|
|
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 (
|
|
401
|
-
rangeStart
|
|
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(
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
495
|
-
|
|
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(
|
|
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
|
}
|