@mindfoldhq/trellis 0.4.0-beta.9 → 0.4.0-rc.1
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 +3 -3
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +132 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +14 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/droid.d.ts +5 -0
- package/dist/configurators/droid.d.ts.map +1 -0
- package/dist/configurators/droid.js +48 -0
- package/dist/configurators/droid.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +13 -0
- package/dist/configurators/index.js.map +1 -1
- package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
- package/dist/templates/claude/hooks/ralph-loop.py +10 -9
- package/dist/templates/claude/hooks/session-start.py +29 -12
- package/dist/templates/claude/hooks/statusline.py +7 -0
- package/dist/templates/codex/hooks/session-start.py +29 -14
- package/dist/templates/copilot/hooks/session-start.py +29 -4
- package/dist/templates/droid/commands/trellis/before-dev.md +33 -0
- package/dist/templates/droid/commands/trellis/brainstorm.md +491 -0
- package/dist/templates/droid/commands/trellis/break-loop.md +111 -0
- package/dist/templates/droid/commands/trellis/check-cross-layer.md +157 -0
- package/dist/templates/droid/commands/trellis/check.md +29 -0
- package/dist/templates/droid/commands/trellis/create-command.md +158 -0
- package/dist/templates/droid/commands/trellis/finish-work.md +147 -0
- package/dist/templates/droid/commands/trellis/integrate-skill.md +223 -0
- package/dist/templates/droid/commands/trellis/onboard.md +362 -0
- package/dist/templates/droid/commands/trellis/record-session.md +66 -0
- package/dist/templates/droid/commands/trellis/start.md +377 -0
- package/dist/templates/droid/commands/trellis/update-spec.md +358 -0
- package/dist/templates/droid/index.d.ts +27 -0
- package/dist/templates/droid/index.d.ts.map +1 -0
- package/dist/templates/droid/index.js +47 -0
- package/dist/templates/droid/index.js.map +1 -0
- package/dist/templates/extract.d.ts +11 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +19 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/iflow/hooks/session-start.py +29 -12
- package/dist/templates/opencode/lib/trellis-context.js +4 -248
- package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
- package/dist/templates/opencode/plugins/session-start.js +143 -119
- package/dist/templates/trellis/scripts/common/cli_adapter.py +27 -2
- package/dist/templates/trellis/workflow.md +17 -4
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +9 -1
- package/dist/types/ai-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
* Trellis Session Start Plugin
|
|
4
4
|
*
|
|
5
5
|
* Injects context when user sends the first message in a session.
|
|
6
|
-
* Uses OpenCode's chat.message
|
|
7
|
-
*
|
|
8
|
-
* Compatibility:
|
|
9
|
-
* - If oh-my-opencode handles via .claude/hooks/, this plugin skips
|
|
10
|
-
* - Otherwise, this plugin handles injection
|
|
6
|
+
* Uses OpenCode's chat.message hook directly so the context persists in history.
|
|
11
7
|
*/
|
|
12
8
|
|
|
13
9
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs"
|
|
@@ -29,14 +25,12 @@ function getTaskStatus(ctx) {
|
|
|
29
25
|
return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
// Resolve task directory
|
|
33
28
|
const taskDir = ctx.resolveTaskDir(taskRef)
|
|
34
29
|
|
|
35
30
|
if (!taskDir || !existsSync(taskDir)) {
|
|
36
31
|
return `Status: STALE POINTER\nTask: ${taskRef}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
// Read task.json
|
|
40
34
|
let taskData = {}
|
|
41
35
|
const taskJsonPath = join(taskDir, "task.json")
|
|
42
36
|
if (existsSync(taskJsonPath)) {
|
|
@@ -55,7 +49,6 @@ function getTaskStatus(ctx) {
|
|
|
55
49
|
return `Status: COMPLETED\nTask: ${taskTitle}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
|
|
56
50
|
}
|
|
57
51
|
|
|
58
|
-
// Check if context is configured (jsonl files exist and non-empty)
|
|
59
52
|
let hasContext = false
|
|
60
53
|
for (const jsonlName of ["implement.jsonl", "check.jsonl", "spec.jsonl"]) {
|
|
61
54
|
const jsonlPath = join(taskDir, jsonlName)
|
|
@@ -105,7 +98,6 @@ function loadTrellisConfig(directory) {
|
|
|
105
98
|
if (data.mode !== "monorepo") {
|
|
106
99
|
return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
|
|
107
100
|
}
|
|
108
|
-
// Convert packages array to dict keyed by name
|
|
109
101
|
const pkgDict = {}
|
|
110
102
|
for (const pkg of (data.packages || [])) {
|
|
111
103
|
pkgDict[pkg.name] = pkg
|
|
@@ -135,7 +127,6 @@ function checkLegacySpec(directory, config) {
|
|
|
135
127
|
const specDir = join(directory, ".trellis", "spec")
|
|
136
128
|
if (!existsSync(specDir)) return null
|
|
137
129
|
|
|
138
|
-
// Check for legacy flat spec dirs
|
|
139
130
|
let hasLegacy = false
|
|
140
131
|
for (const name of ["backend", "frontend"]) {
|
|
141
132
|
if (existsSync(join(specDir, name, "index.md"))) {
|
|
@@ -145,7 +136,6 @@ function checkLegacySpec(directory, config) {
|
|
|
145
136
|
}
|
|
146
137
|
if (!hasLegacy) return null
|
|
147
138
|
|
|
148
|
-
// Check which packages are missing spec/<pkg>/ directory
|
|
149
139
|
const pkgNames = Object.keys(config.packages).sort()
|
|
150
140
|
const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
|
|
151
141
|
|
|
@@ -193,7 +183,6 @@ function resolveSpecScope(config) {
|
|
|
193
183
|
}
|
|
194
184
|
}
|
|
195
185
|
if (valid.size > 0) return valid
|
|
196
|
-
// All invalid: fallback
|
|
197
186
|
if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
|
|
198
187
|
if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
|
|
199
188
|
return null
|
|
@@ -209,10 +198,7 @@ function resolveSpecScope(config) {
|
|
|
209
198
|
function buildSessionContext(ctx) {
|
|
210
199
|
const directory = ctx.directory
|
|
211
200
|
const trellisDir = join(directory, ".trellis")
|
|
212
|
-
const claudeDir = join(directory, ".claude")
|
|
213
|
-
const opencodeDir = join(directory, ".opencode")
|
|
214
201
|
|
|
215
|
-
// Load config for scope filtering and legacy detection
|
|
216
202
|
const config = loadTrellisConfig(directory)
|
|
217
203
|
const allowedPkgs = resolveSpecScope(config)
|
|
218
204
|
|
|
@@ -241,11 +227,20 @@ Read and follow all instructions below carefully.
|
|
|
241
227
|
}
|
|
242
228
|
}
|
|
243
229
|
|
|
244
|
-
// 3. Workflow Guide
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
230
|
+
// 3. Workflow Guide (ToC only — lazy-load the full file on demand)
|
|
231
|
+
const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
|
|
232
|
+
if (workflowContent) {
|
|
233
|
+
const tocLines = [
|
|
234
|
+
"# Development Workflow — Section Index",
|
|
235
|
+
"Full guide: .trellis/workflow.md (read on demand)",
|
|
236
|
+
"",
|
|
237
|
+
]
|
|
238
|
+
for (const line of workflowContent.split("\n")) {
|
|
239
|
+
if (line.startsWith("## ")) tocLines.push(line)
|
|
240
|
+
}
|
|
241
|
+
tocLines.push("", "To read a section: use the Read tool on .trellis/workflow.md")
|
|
247
242
|
parts.push("<workflow>")
|
|
248
|
-
parts.push(
|
|
243
|
+
parts.push(tocLines.join("\n"))
|
|
249
244
|
parts.push("</workflow>")
|
|
250
245
|
}
|
|
251
246
|
|
|
@@ -267,7 +262,6 @@ Read and follow all instructions below carefully.
|
|
|
267
262
|
}).sort()
|
|
268
263
|
|
|
269
264
|
for (const sub of subs) {
|
|
270
|
-
// Always include guides/ regardless of scope
|
|
271
265
|
if (sub === "guides") {
|
|
272
266
|
const indexFile = join(specDir, sub, "index.md")
|
|
273
267
|
if (existsSync(indexFile)) {
|
|
@@ -281,14 +275,11 @@ Read and follow all instructions below carefully.
|
|
|
281
275
|
|
|
282
276
|
const indexFile = join(specDir, sub, "index.md")
|
|
283
277
|
if (existsSync(indexFile)) {
|
|
284
|
-
// Flat spec dir: spec/<layer>/index.md (single-repo)
|
|
285
278
|
const content = ctx.readFile(indexFile)
|
|
286
279
|
if (content) {
|
|
287
280
|
parts.push(`## ${sub}\n${content}\n`)
|
|
288
281
|
}
|
|
289
282
|
} else {
|
|
290
|
-
// Nested package dirs (monorepo): spec/<pkg>/<layer>/index.md
|
|
291
|
-
// Apply scope filter
|
|
292
283
|
if (allowedPkgs !== null && !allowedPkgs.has(sub)) {
|
|
293
284
|
continue
|
|
294
285
|
}
|
|
@@ -322,131 +313,164 @@ Read and follow all instructions below carefully.
|
|
|
322
313
|
|
|
323
314
|
parts.push("</guidelines>")
|
|
324
315
|
|
|
325
|
-
//
|
|
326
|
-
let startMd = ctx.readFile(join(claudeDir, "commands", "trellis", "start.md"))
|
|
327
|
-
if (!startMd) {
|
|
328
|
-
startMd = ctx.readFile(join(opencodeDir, "commands", "trellis", "start.md"))
|
|
329
|
-
}
|
|
330
|
-
if (startMd) {
|
|
331
|
-
parts.push("<instructions>")
|
|
332
|
-
parts.push(startMd)
|
|
333
|
-
parts.push("</instructions>")
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// 6. Task status (R2: check task state for session resume)
|
|
316
|
+
// 6. Task status
|
|
337
317
|
const taskStatus = getTaskStatus(ctx)
|
|
338
318
|
parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
|
|
339
319
|
|
|
340
|
-
// 7. Final directive
|
|
320
|
+
// 7. Final directive
|
|
341
321
|
parts.push(`<ready>
|
|
342
|
-
Context loaded.
|
|
343
|
-
|
|
322
|
+
Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
|
|
323
|
+
Wait for the user's first message, then handle it following the workflow guide.
|
|
344
324
|
If there is an active task, ask whether to continue it.
|
|
345
325
|
</ready>`)
|
|
346
326
|
|
|
347
327
|
return parts.join("\n\n")
|
|
348
328
|
}
|
|
349
329
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
330
|
+
function getTrellisMetadata(metadata) {
|
|
331
|
+
if (!metadata || typeof metadata !== "object") {
|
|
332
|
+
return {}
|
|
333
|
+
}
|
|
353
334
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const sessionID = input.sessionID
|
|
359
|
-
const agent = input.agent || "unknown"
|
|
360
|
-
debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
|
|
361
|
-
|
|
362
|
-
// Skip in non-interactive mode
|
|
363
|
-
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
|
364
|
-
debugLog("session", "Skipping - non-interactive mode")
|
|
365
|
-
return
|
|
366
|
-
}
|
|
335
|
+
const trellis = metadata.trellis
|
|
336
|
+
if (!trellis || typeof trellis !== "object") {
|
|
337
|
+
return {}
|
|
338
|
+
}
|
|
367
339
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
debugLog("session", "Skipping - omo will handle via .claude/hooks/")
|
|
371
|
-
return
|
|
372
|
-
}
|
|
340
|
+
return trellis
|
|
341
|
+
}
|
|
373
342
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
343
|
+
function markPartAsSessionStart(part) {
|
|
344
|
+
const metadata = part.metadata && typeof part.metadata === "object"
|
|
345
|
+
? part.metadata
|
|
346
|
+
: {}
|
|
347
|
+
part.metadata = {
|
|
348
|
+
...metadata,
|
|
349
|
+
trellis: {
|
|
350
|
+
...getTrellisMetadata(metadata),
|
|
351
|
+
sessionStart: true,
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
}
|
|
379
355
|
|
|
380
|
-
|
|
381
|
-
|
|
356
|
+
function hasSessionStartMarker(part) {
|
|
357
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
382
360
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
debugLog("session", "Built context, length:", context.length)
|
|
361
|
+
return getTrellisMetadata(part.metadata).sessionStart === true
|
|
362
|
+
}
|
|
386
363
|
|
|
387
|
-
|
|
388
|
-
|
|
364
|
+
export function hasInjectedTrellisContext(messages) {
|
|
365
|
+
if (!Array.isArray(messages)) {
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
389
368
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
369
|
+
return messages.some(message => {
|
|
370
|
+
if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
|
|
371
|
+
return false
|
|
372
|
+
}
|
|
394
373
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const { messages } = output
|
|
399
|
-
debugLog("session", "messages.transform called, messageCount:", messages?.length)
|
|
374
|
+
return message.parts.some(hasSessionStartMarker)
|
|
375
|
+
})
|
|
376
|
+
}
|
|
400
377
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
378
|
+
async function hasPersistedInjectedContext(client, directory, sessionID) {
|
|
379
|
+
try {
|
|
380
|
+
const response = await client.session.messages({
|
|
381
|
+
path: { id: sessionID },
|
|
382
|
+
query: { directory },
|
|
383
|
+
throwOnError: true,
|
|
384
|
+
})
|
|
385
|
+
return hasInjectedTrellisContext(response.data || [])
|
|
386
|
+
} catch (error) {
|
|
387
|
+
debugLog(
|
|
388
|
+
"session",
|
|
389
|
+
"Failed to read session history for dedupe:",
|
|
390
|
+
error instanceof Error ? error.message : String(error),
|
|
391
|
+
)
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
}
|
|
404
395
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
break
|
|
411
|
-
}
|
|
412
|
-
}
|
|
396
|
+
export default {
|
|
397
|
+
id: "trellis.session-start",
|
|
398
|
+
server: async ({ directory, client }) => {
|
|
399
|
+
const ctx = new TrellisContext(directory)
|
|
400
|
+
debugLog("session", "Plugin loaded, directory:", directory)
|
|
413
401
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
402
|
+
return {
|
|
403
|
+
// Clear in-memory dedupe after compaction so context can be re-injected.
|
|
404
|
+
event: ({ event }) => {
|
|
405
|
+
try {
|
|
406
|
+
if (event?.type === "session.compacted" && event?.properties?.sessionID) {
|
|
407
|
+
const sessionID = event.properties.sessionID
|
|
408
|
+
contextCollector.clear(sessionID)
|
|
409
|
+
debugLog("session", "Cleared processed flag after compaction for session:", sessionID)
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
debugLog(
|
|
413
|
+
"session",
|
|
414
|
+
"Error in event hook:",
|
|
415
|
+
error instanceof Error ? error.message : String(error),
|
|
416
|
+
)
|
|
417
417
|
}
|
|
418
|
+
},
|
|
418
419
|
|
|
419
|
-
|
|
420
|
-
|
|
420
|
+
// chat.message - triggered when user sends a message.
|
|
421
|
+
// Modify the message in-place so the context is persisted with updateMessage/updatePart.
|
|
422
|
+
"chat.message": async (input, output) => {
|
|
423
|
+
try {
|
|
424
|
+
const sessionID = input.sessionID
|
|
425
|
+
const agent = input.agent || "unknown"
|
|
426
|
+
debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
|
|
427
|
+
|
|
428
|
+
// Skip in non-interactive mode
|
|
429
|
+
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
|
430
|
+
debugLog("session", "Skipping - non-interactive mode")
|
|
431
|
+
return
|
|
432
|
+
}
|
|
421
433
|
|
|
422
|
-
|
|
434
|
+
// Only inject on first message
|
|
435
|
+
if (contextCollector.isProcessed(sessionID)) {
|
|
436
|
+
debugLog("session", "Skipping - session already processed")
|
|
437
|
+
return
|
|
438
|
+
}
|
|
423
439
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
440
|
+
if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) {
|
|
441
|
+
contextCollector.markProcessed(sessionID)
|
|
442
|
+
debugLog("session", "Skipping - session already contains persisted Trellis context")
|
|
443
|
+
return
|
|
444
|
+
}
|
|
428
445
|
|
|
429
|
-
|
|
430
|
-
|
|
446
|
+
// Build context
|
|
447
|
+
const context = buildSessionContext(ctx)
|
|
448
|
+
debugLog("session", "Built context, length:", context.length)
|
|
449
|
+
|
|
450
|
+
// Inject context directly into output.parts so it gets persisted by updatePart
|
|
451
|
+
const parts = output?.parts || []
|
|
452
|
+
const textPartIndex = parts.findIndex(
|
|
453
|
+
p => p.type === "text" && p.text !== undefined
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
if (textPartIndex !== -1) {
|
|
457
|
+
const originalText = parts[textPartIndex].text || ""
|
|
458
|
+
parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
|
|
459
|
+
markPartAsSessionStart(parts[textPartIndex])
|
|
460
|
+
debugLog("session", "Injected context into chat.message text part, length:", context.length)
|
|
461
|
+
} else {
|
|
462
|
+
// No existing text part: prepend a new one
|
|
463
|
+
const injectedPart = { type: "text", text: context }
|
|
464
|
+
markPartAsSessionStart(injectedPart)
|
|
465
|
+
parts.unshift(injectedPart)
|
|
466
|
+
debugLog("session", "Prepended new text part with context, length:", context.length)
|
|
467
|
+
}
|
|
431
468
|
|
|
432
|
-
|
|
433
|
-
const textPartIndex = lastUserMessage.parts?.findIndex(
|
|
434
|
-
p => p.type === "text" && p.text !== undefined
|
|
435
|
-
)
|
|
469
|
+
contextCollector.markProcessed(sessionID)
|
|
436
470
|
|
|
437
|
-
|
|
438
|
-
debugLog("session", "
|
|
439
|
-
return
|
|
471
|
+
} catch (error) {
|
|
472
|
+
debugLog("session", "Error in chat.message:", error.message, error.stack)
|
|
440
473
|
}
|
|
441
|
-
|
|
442
|
-
// Prepend context to the text part (same approach as omo)
|
|
443
|
-
const originalText = lastUserMessage.parts[textPartIndex].text || ""
|
|
444
|
-
lastUserMessage.parts[textPartIndex].text = `${pending.content}\n\n---\n\n${originalText}`
|
|
445
|
-
|
|
446
|
-
debugLog("session", "Injected context by prepending to text, length:", pending.content.length)
|
|
447
|
-
|
|
448
|
-
} catch (error) {
|
|
449
|
-
debugLog("session", "Error in messages.transform:", error.message, error.stack)
|
|
450
474
|
}
|
|
451
475
|
}
|
|
452
476
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CLI Adapter for Multi-Platform Support.
|
|
3
3
|
|
|
4
|
-
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy,
|
|
4
|
+
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, and Factory Droid interfaces.
|
|
5
5
|
|
|
6
6
|
Supported platforms:
|
|
7
7
|
- claude: Claude Code (default)
|
|
@@ -17,6 +17,7 @@ Supported platforms:
|
|
|
17
17
|
- qoder: Qoder
|
|
18
18
|
- codebuddy: CodeBuddy
|
|
19
19
|
- copilot: GitHub Copilot (VS Code)
|
|
20
|
+
- droid: Factory Droid (commands-based)
|
|
20
21
|
|
|
21
22
|
Usage:
|
|
22
23
|
from common.cli_adapter import CLIAdapter
|
|
@@ -49,6 +50,7 @@ Platform = Literal[
|
|
|
49
50
|
"qoder",
|
|
50
51
|
"codebuddy",
|
|
51
52
|
"copilot",
|
|
53
|
+
"droid",
|
|
52
54
|
]
|
|
53
55
|
|
|
54
56
|
|
|
@@ -119,6 +121,8 @@ class CLIAdapter:
|
|
|
119
121
|
return ".codebuddy"
|
|
120
122
|
elif self.platform == "copilot":
|
|
121
123
|
return ".github/copilot"
|
|
124
|
+
elif self.platform == "droid":
|
|
125
|
+
return ".factory"
|
|
122
126
|
else:
|
|
123
127
|
return ".claude"
|
|
124
128
|
|
|
@@ -241,6 +245,8 @@ class CLIAdapter:
|
|
|
241
245
|
return f".kilocode/workflows/{name}.md"
|
|
242
246
|
elif self.platform == "copilot":
|
|
243
247
|
return f".github/prompts/{name}.prompt.md"
|
|
248
|
+
elif self.platform == "droid":
|
|
249
|
+
return f".factory/commands/trellis/{name}.md"
|
|
244
250
|
else:
|
|
245
251
|
return f"{self.config_dir_name}/commands/trellis/{name}.md"
|
|
246
252
|
|
|
@@ -274,6 +280,8 @@ class CLIAdapter:
|
|
|
274
280
|
return {}
|
|
275
281
|
elif self.platform == "copilot":
|
|
276
282
|
return {}
|
|
283
|
+
elif self.platform == "droid":
|
|
284
|
+
return {}
|
|
277
285
|
else:
|
|
278
286
|
return {"CLAUDE_NON_INTERACTIVE": "1"}
|
|
279
287
|
|
|
@@ -353,6 +361,10 @@ class CLIAdapter:
|
|
|
353
361
|
raise ValueError(
|
|
354
362
|
"GitHub Copilot is IDE-only; CLI agent run is not supported."
|
|
355
363
|
)
|
|
364
|
+
elif self.platform == "droid":
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"Factory Droid CLI agent run is not yet integrated with Trellis multi-agent."
|
|
367
|
+
)
|
|
356
368
|
|
|
357
369
|
else: # claude
|
|
358
370
|
cmd = ["claude", "-p"]
|
|
@@ -413,6 +425,10 @@ class CLIAdapter:
|
|
|
413
425
|
raise ValueError(
|
|
414
426
|
"GitHub Copilot is IDE-only; CLI resume is not supported."
|
|
415
427
|
)
|
|
428
|
+
elif self.platform == "droid":
|
|
429
|
+
raise ValueError(
|
|
430
|
+
"Factory Droid CLI resume is not yet integrated with Trellis multi-agent."
|
|
431
|
+
)
|
|
416
432
|
else:
|
|
417
433
|
return ["claude", "--resume", session_id]
|
|
418
434
|
|
|
@@ -483,6 +499,8 @@ class CLIAdapter:
|
|
|
483
499
|
return "codebuddy"
|
|
484
500
|
elif self.platform == "copilot":
|
|
485
501
|
return "copilot"
|
|
502
|
+
elif self.platform == "droid":
|
|
503
|
+
return "droid"
|
|
486
504
|
else:
|
|
487
505
|
return "claude"
|
|
488
506
|
|
|
@@ -569,9 +587,10 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
|
|
|
569
587
|
"qoder",
|
|
570
588
|
"codebuddy",
|
|
571
589
|
"copilot",
|
|
590
|
+
"droid",
|
|
572
591
|
):
|
|
573
592
|
raise ValueError(
|
|
574
|
-
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', or '
|
|
593
|
+
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', or 'droid')"
|
|
575
594
|
)
|
|
576
595
|
|
|
577
596
|
return CLIAdapter(platform=platform) # type: ignore
|
|
@@ -592,6 +611,7 @@ _ALL_PLATFORM_CONFIG_DIRS = (
|
|
|
592
611
|
".qoder",
|
|
593
612
|
".codebuddy",
|
|
594
613
|
".github/copilot",
|
|
614
|
+
".factory",
|
|
595
615
|
)
|
|
596
616
|
"""All platform config directory names (used by detect_platform exclusion checks)."""
|
|
597
617
|
|
|
@@ -647,6 +667,7 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
647
667
|
"qoder",
|
|
648
668
|
"codebuddy",
|
|
649
669
|
"copilot",
|
|
670
|
+
"droid",
|
|
650
671
|
):
|
|
651
672
|
return env_platform # type: ignore
|
|
652
673
|
|
|
@@ -712,6 +733,10 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
712
733
|
if (project_root / ".github" / "copilot").is_dir():
|
|
713
734
|
return "copilot"
|
|
714
735
|
|
|
736
|
+
# Check for .factory directory (Factory Droid-specific)
|
|
737
|
+
if (project_root / ".factory").is_dir():
|
|
738
|
+
return "droid"
|
|
739
|
+
|
|
715
740
|
return "claude"
|
|
716
741
|
|
|
717
742
|
|
|
@@ -196,21 +196,30 @@ python3 ./.trellis/scripts/task.py create "<title>" --slug <task-name>
|
|
|
196
196
|
1. Create or select task
|
|
197
197
|
--> python3 ./.trellis/scripts/task.py create "<title>" --slug <name> or list
|
|
198
198
|
|
|
199
|
-
2.
|
|
199
|
+
2. Start task (mark as current)
|
|
200
|
+
--> python3 ./.trellis/scripts/task.py start <name>
|
|
201
|
+
--> Writes .trellis/.current-task; future sessions see it in <current-state>
|
|
202
|
+
|
|
203
|
+
3. Write code according to guidelines
|
|
200
204
|
--> Read .trellis/spec/ docs relevant to your task
|
|
201
205
|
--> For cross-layer: read .trellis/spec/guides/
|
|
202
206
|
|
|
203
|
-
|
|
207
|
+
4. Self-test
|
|
204
208
|
--> Run project's lint/test commands (see spec docs)
|
|
205
209
|
--> Manual feature testing
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
5. Commit code
|
|
208
212
|
--> git add <files>
|
|
209
213
|
--> git commit -m "type(scope): description"
|
|
210
214
|
Format: feat/fix/docs/refactor/test/chore
|
|
211
215
|
|
|
212
|
-
|
|
216
|
+
6. Record session (one command)
|
|
213
217
|
--> python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash"
|
|
218
|
+
|
|
219
|
+
7. Finish task (clear current)
|
|
220
|
+
--> python3 ./.trellis/scripts/task.py finish
|
|
221
|
+
--> Only when the task is fully done; otherwise leave it set so the
|
|
222
|
+
next session resumes where you left off
|
|
214
223
|
```
|
|
215
224
|
|
|
216
225
|
### Code Quality Checklist
|
|
@@ -315,11 +324,15 @@ tasks/
|
|
|
315
324
|
**Commands**:
|
|
316
325
|
```bash
|
|
317
326
|
python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] # Create task directory
|
|
327
|
+
python3 ./.trellis/scripts/task.py start <name> # Set as current task (writes .current-task, triggers after_start hooks)
|
|
328
|
+
python3 ./.trellis/scripts/task.py finish # Clear current task (triggers after_finish hooks)
|
|
318
329
|
python3 ./.trellis/scripts/task.py archive <name> # Archive to archive/{year-month}/
|
|
319
330
|
python3 ./.trellis/scripts/task.py list # List active tasks
|
|
320
331
|
python3 ./.trellis/scripts/task.py list-archive # List archived tasks
|
|
321
332
|
```
|
|
322
333
|
|
|
334
|
+
**Current task mechanism**: `task.py start <name>` writes the selected task path to `.trellis/.current-task`. The SessionStart hook reads this file to inject `## CURRENT TASK` into every new session's context, so the AI immediately knows what you're working on without being told. Run `task.py finish` when you're done — subsequent sessions will show `(none)` until you start another task.
|
|
335
|
+
|
|
323
336
|
---
|
|
324
337
|
|
|
325
338
|
## Best Practices
|
package/dist/types/ai-tools.d.ts
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Supported AI coding tools
|
|
8
8
|
*/
|
|
9
|
-
export type AITool = "claude-code" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot";
|
|
9
|
+
export type AITool = "claude-code" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot" | "droid";
|
|
10
10
|
/**
|
|
11
11
|
* Template directory categories
|
|
12
12
|
*/
|
|
13
|
-
export type TemplateDir = "common" | "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot";
|
|
13
|
+
export type TemplateDir = "common" | "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot" | "droid";
|
|
14
14
|
/**
|
|
15
15
|
* CLI flag names for platform selection (e.g., --claude, --cursor, --kilo, --kiro, --gemini, --antigravity)
|
|
16
16
|
* Must match keys in InitOptions (src/commands/init.ts)
|
|
17
17
|
*/
|
|
18
|
-
export type CliFlag = "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot";
|
|
18
|
+
export type CliFlag = "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "windsurf" | "qoder" | "codebuddy" | "copilot" | "droid";
|
|
19
19
|
/**
|
|
20
20
|
* Configuration for an AI tool
|
|
21
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-tools.d.ts","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,MAAM,GACd,aAAa,GACb,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ai-tools.d.ts","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,MAAM,GACd,aAAa,GACb,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,OAAO,CAAC;AAEZ;;;GAGG;AACH,MAAM,MAAM,OAAO,GACf,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmHjD,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAUtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAE3D"}
|
package/dist/types/ai-tools.js
CHANGED
|
@@ -48,7 +48,7 @@ export const AI_TOOLS = {
|
|
|
48
48
|
hasPythonHooks: true,
|
|
49
49
|
},
|
|
50
50
|
codex: {
|
|
51
|
-
name: "Codex",
|
|
51
|
+
name: "Codex (also writes .agents/skills/ — read by Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code)",
|
|
52
52
|
templateDirs: ["common", "codex"],
|
|
53
53
|
configDir: ".codex",
|
|
54
54
|
supportsAgentSkills: true,
|
|
@@ -121,6 +121,14 @@ export const AI_TOOLS = {
|
|
|
121
121
|
defaultChecked: false,
|
|
122
122
|
hasPythonHooks: true,
|
|
123
123
|
},
|
|
124
|
+
droid: {
|
|
125
|
+
name: "Factory Droid",
|
|
126
|
+
templateDirs: ["common", "droid"],
|
|
127
|
+
configDir: ".factory",
|
|
128
|
+
cliFlag: "droid",
|
|
129
|
+
defaultChecked: false,
|
|
130
|
+
hasPythonHooks: false,
|
|
131
|
+
},
|
|
124
132
|
};
|
|
125
133
|
/**
|
|
126
134
|
* Get the configuration for a specific AI tool
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-tools.js","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"ai-tools.js","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED