@mindfoldhq/trellis 0.6.5 → 0.6.7
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 +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/guard.d.ts.map +1 -1
- package/dist/commands/channel/guard.js +7 -2
- package/dist/commands/channel/guard.js.map +1 -1
- package/dist/commands/channel/store/paths.d.ts +14 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -1
- package/dist/commands/channel/store/paths.js +26 -0
- package/dist/commands/channel/store/paths.js.map +1 -1
- package/dist/commands/channel/supervisor/stdout.d.ts +7 -5
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/stdout.js +65 -18
- package/dist/commands/channel/supervisor/stdout.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +13 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +36 -16
- package/dist/commands/channel/supervisor.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.map +1 -1
- package/dist/commands/uninstall.d.ts +9 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +74 -4
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +29 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +128 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/copilot.d.ts +1 -0
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +4 -1
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +7 -1
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/omp.d.ts +11 -0
- package/dist/configurators/omp.d.ts.map +1 -0
- package/dist/configurators/omp.js +51 -0
- package/dist/configurators/omp.js.map +1 -0
- package/dist/configurators/shared.d.ts +7 -0
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +27 -0
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/zcode.d.ts +3 -3
- package/dist/configurators/zcode.js +12 -12
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/migrations/manifests/0.6.6.json +17 -0
- package/dist/migrations/manifests/0.6.7.json +9 -0
- package/dist/templates/codex/hooks/session-start.py +6 -6
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +3 -3
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +19 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/copilot/copilot-instructions.md +11 -0
- package/dist/templates/copilot/hooks/session-start.py +6 -6
- package/dist/templates/copilot/index.d.ts +6 -1
- package/dist/templates/copilot/index.d.ts.map +1 -1
- package/dist/templates/copilot/index.js +8 -1
- package/dist/templates/copilot/index.js.map +1 -1
- package/dist/templates/omp/agents/trellis-check.md +41 -0
- package/dist/templates/omp/agents/trellis-implement.md +45 -0
- package/dist/templates/omp/agents/trellis-research.md +30 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +462 -0
- package/dist/templates/omp/index.d.ts +4 -0
- package/dist/templates/omp/index.d.ts.map +1 -0
- package/dist/templates/omp/index.js +9 -0
- package/dist/templates/omp/index.js.map +1 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +47 -21
- package/dist/templates/shared-hooks/inject-workflow-state.py +5 -5
- package/dist/templates/shared-hooks/session-start.py +7 -7
- package/dist/templates/trellis/scripts/add_session.py +65 -17
- package/dist/templates/trellis/scripts/common/cli_adapter.py +36 -1
- package/dist/templates/trellis/scripts/common/config.py +29 -0
- package/dist/templates/trellis/scripts/common/io.py +20 -3
- package/dist/templates/trellis/scripts/common/session_context.py +1 -1
- package/dist/templates/trellis/scripts/common/task_store.py +90 -19
- package/dist/templates/trellis/scripts/common/task_utils.py +22 -0
- package/dist/templates/trellis/scripts/task.py +8 -2
- package/dist/templates/trellis/workflow.md +13 -13
- package/dist/templates/zcode/agents/trellis-research.md +133 -0
- package/dist/templates/zcode/index.d.ts +6 -5
- package/dist/templates/zcode/index.d.ts.map +1 -1
- package/dist/templates/zcode/index.js +6 -5
- package/dist/templates/zcode/index.js.map +1 -1
- 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 +29 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/atomic-write.d.ts +12 -0
- package/dist/utils/atomic-write.d.ts.map +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/utils/atomic-write.js.map +1 -0
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +6 -5
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/registry-config.d.ts.map +1 -1
- package/dist/utils/registry-config.js +3 -2
- package/dist/utils/registry-config.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +36 -4
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +2 -1
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +19 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-implement
|
|
3
|
+
description: |
|
|
4
|
+
Code implementation expert. Understands Trellis specs and requirements, then implements features. No git commit allowed.
|
|
5
|
+
tools: read, write, edit, bash, find, search, ast_grep, lsp
|
|
6
|
+
model: pi/task
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Implement Agent
|
|
10
|
+
|
|
11
|
+
You are the Implement Agent in the Trellis workflow.
|
|
12
|
+
|
|
13
|
+
## Recursion Guard
|
|
14
|
+
|
|
15
|
+
You are already the `trellis-implement` sub-agent that the main session dispatched.
|
|
16
|
+
Do the implementation work directly.
|
|
17
|
+
|
|
18
|
+
- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent via the `task` tool.
|
|
19
|
+
- If injected workflow-state breadcrumbs say to dispatch `trellis-implement` / `trellis-check`,
|
|
20
|
+
treat that as a main-session instruction that is already satisfied by your current role.
|
|
21
|
+
- Only the main session may dispatch Trellis implement/check agents. If more parallel work
|
|
22
|
+
is needed, report that recommendation instead of spawning.
|
|
23
|
+
|
|
24
|
+
## Core Responsibilities
|
|
25
|
+
|
|
26
|
+
1. Understand the active task requirements.
|
|
27
|
+
2. Read and follow the spec and research files listed in the task's `implement.jsonl`.
|
|
28
|
+
3. Implement the requested change using existing project patterns.
|
|
29
|
+
4. Run the relevant lint, typecheck, and focused tests for the touched code.
|
|
30
|
+
5. Report files changed and verification results.
|
|
31
|
+
|
|
32
|
+
## Forbidden Operations
|
|
33
|
+
|
|
34
|
+
Do not run:
|
|
35
|
+
- `git commit`
|
|
36
|
+
- `git push`
|
|
37
|
+
- `git merge`
|
|
38
|
+
|
|
39
|
+
## Working Rules
|
|
40
|
+
|
|
41
|
+
- Read adjacent code and tests before editing.
|
|
42
|
+
- Keep changes scoped to the task.
|
|
43
|
+
- Do not revert unrelated user or concurrent changes.
|
|
44
|
+
- Fix root causes rather than masking symptoms.
|
|
45
|
+
- Prefer existing local helpers and platform patterns over new abstractions.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-research
|
|
3
|
+
description: |
|
|
4
|
+
Code and technical research expert. Finds relevant files, patterns, docs,
|
|
5
|
+
and persists findings to the current task's research/ directory.
|
|
6
|
+
tools: read, write, bash, find, search, web_search
|
|
7
|
+
model: pi/task
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Research Agent
|
|
11
|
+
|
|
12
|
+
You are the Research Agent in the Trellis workflow.
|
|
13
|
+
|
|
14
|
+
## Core Principle
|
|
15
|
+
|
|
16
|
+
Persist every finding to a file. Chat context is temporary; files under the task
|
|
17
|
+
directory survive compaction and handoff.
|
|
18
|
+
|
|
19
|
+
## Core Responsibilities
|
|
20
|
+
|
|
21
|
+
1. Resolve the active task with `python3 ./.trellis/scripts/task.py current --source`.
|
|
22
|
+
2. Create `<task-dir>/research/` when it does not exist.
|
|
23
|
+
3. Search internal code, specs, and relevant external documentation.
|
|
24
|
+
4. Write each distinct topic to `<task-dir>/research/<topic-slug>.md`.
|
|
25
|
+
5. Report only file paths and concise summaries to the caller.
|
|
26
|
+
|
|
27
|
+
## Scope Limits
|
|
28
|
+
|
|
29
|
+
Write only under the current task's `research/` directory.
|
|
30
|
+
Do not edit code, specs, platform config, or task files outside research artifacts.
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Project root detection
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function findProjectRoot(startDir: string): string | null {
|
|
12
|
+
let current = startDir;
|
|
13
|
+
while (true) {
|
|
14
|
+
if (existsSync(join(current, ".trellis"))) return current;
|
|
15
|
+
const parent = dirname(current);
|
|
16
|
+
if (parent === current) break;
|
|
17
|
+
current = parent;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Session identity helpers (mirrors Python _sanitize_key / _hash_value / _context_key)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function sanitizeKey(raw: string): string {
|
|
27
|
+
const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, "");
|
|
28
|
+
return safe ? safe.slice(0, 160) : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hashValue(raw: string): string {
|
|
32
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildContextKey(platformName: string, kind: string, value: string): string {
|
|
36
|
+
if (kind === "transcript") {
|
|
37
|
+
return `${platformName}_transcript_${hashValue(value)}`;
|
|
38
|
+
}
|
|
39
|
+
const safeValue = sanitizeKey(value);
|
|
40
|
+
return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null {
|
|
44
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
45
|
+
if (sessionId) {
|
|
46
|
+
return buildContextKey("omp", "session", sessionId);
|
|
47
|
+
}
|
|
48
|
+
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
49
|
+
if (sessionFile) {
|
|
50
|
+
return buildContextKey("omp", "transcript", sessionFile);
|
|
51
|
+
}
|
|
52
|
+
const override = process.env.TRELLIS_CONTEXT_ID?.trim();
|
|
53
|
+
return override ? sanitizeKey(override) || hashValue(override) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isInsideRoot(root: string, candidate: string): boolean {
|
|
57
|
+
const rel = relative(root, candidate);
|
|
58
|
+
return rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveProjectFile(projectRoot: string, file: string): string | null {
|
|
62
|
+
try {
|
|
63
|
+
const rootReal = realpathSync(projectRoot);
|
|
64
|
+
const targetReal = realpathSync(resolve(projectRoot, file));
|
|
65
|
+
return isInsideRoot(rootReal, targetReal) ? targetReal : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Active task resolution
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function resolveActiveTaskStatus(
|
|
76
|
+
projectRoot: string,
|
|
77
|
+
contextKey: string | null,
|
|
78
|
+
): { status: string; taskDir: string | null; taskTitle: string | null } {
|
|
79
|
+
const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions");
|
|
80
|
+
if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null };
|
|
81
|
+
|
|
82
|
+
// --- 通过 context key 解析 session 文件 ---
|
|
83
|
+
let sessionFilePath: string | null = null;
|
|
84
|
+
|
|
85
|
+
if (contextKey) {
|
|
86
|
+
const candidate = join(sessionsDir, `${contextKey}.json`);
|
|
87
|
+
if (existsSync(candidate)) {
|
|
88
|
+
sessionFilePath = candidate;
|
|
89
|
+
} else {
|
|
90
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// No identity: use single-session fallback only when there is exactly one session file.
|
|
94
|
+
let sessionFiles: string[];
|
|
95
|
+
try {
|
|
96
|
+
sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
97
|
+
} catch {
|
|
98
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
99
|
+
}
|
|
100
|
+
if (sessionFiles.length === 1) {
|
|
101
|
+
sessionFilePath = join(sessionsDir, sessionFiles[0]);
|
|
102
|
+
} else {
|
|
103
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- 读取 session 数据 ---
|
|
108
|
+
let sessionData: Record<string, unknown>;
|
|
109
|
+
try {
|
|
110
|
+
sessionData = JSON.parse(readFileSync(sessionFilePath, "utf-8"));
|
|
111
|
+
} catch {
|
|
112
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const currentTask = sessionData.current_task;
|
|
116
|
+
if (typeof currentTask !== "string" || !currentTask)
|
|
117
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
118
|
+
|
|
119
|
+
const taskDir = join(projectRoot, currentTask);
|
|
120
|
+
const taskJsonPath = join(taskDir, "task.json");
|
|
121
|
+
if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null, taskTitle: null };
|
|
122
|
+
|
|
123
|
+
let taskData: Record<string, unknown>;
|
|
124
|
+
try {
|
|
125
|
+
taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"));
|
|
126
|
+
} catch {
|
|
127
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
status: typeof taskData.status === "string" ? taskData.status : "planning",
|
|
132
|
+
taskDir,
|
|
133
|
+
taskTitle: typeof taskData.title === "string" ? taskData.title : null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Session context — spawns get_context.py default mode (same as Claude hook)
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const SESSION_CONTEXT_TIMEOUT_MS = 5000;
|
|
142
|
+
|
|
143
|
+
function buildSessionContext(projectRoot: string, contextKey: string | null): string {
|
|
144
|
+
const script = join(projectRoot, ".trellis", "scripts", "get_context.py");
|
|
145
|
+
if (!existsSync(script)) return "";
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = spawnSync("python3", [script], {
|
|
149
|
+
cwd: projectRoot,
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
env: contextKey
|
|
152
|
+
? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
|
|
153
|
+
: process.env,
|
|
154
|
+
timeout: SESSION_CONTEXT_TIMEOUT_MS,
|
|
155
|
+
windowsHide: true,
|
|
156
|
+
});
|
|
157
|
+
if (result.status !== 0 || !result.stdout?.trim()) {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
return `<session-context>\n${result.stdout.trim()}\n</session-context>`;
|
|
161
|
+
} catch {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Task context — prd.md, info.md, and jsonl-referenced spec/research files
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
type AgentType = "trellis-implement" | "trellis-check" | "trellis-research" | null;
|
|
171
|
+
|
|
172
|
+
function buildTaskContext(projectRoot: string, taskDir: string, agentType?: AgentType): string {
|
|
173
|
+
const parts: string[] = [];
|
|
174
|
+
|
|
175
|
+
// prd.md and info.md — always included
|
|
176
|
+
let prd = "";
|
|
177
|
+
try { prd = readFileSync(join(taskDir, "prd.md"), "utf-8"); } catch { }
|
|
178
|
+
if (prd.trim()) parts.push(`## PRD\n\n${prd.trim()}`);
|
|
179
|
+
|
|
180
|
+
let info = "";
|
|
181
|
+
try { info = readFileSync(join(taskDir, "info.md"), "utf-8"); } catch { }
|
|
182
|
+
if (info.trim()) parts.push(`## Info\n\n${info.trim()}`);
|
|
183
|
+
|
|
184
|
+
// Determine which jsonl files to read based on agent type
|
|
185
|
+
let jsonlNames: string[];
|
|
186
|
+
if (agentType === "trellis-implement") {
|
|
187
|
+
jsonlNames = ["implement.jsonl"];
|
|
188
|
+
} else if (agentType === "trellis-check") {
|
|
189
|
+
jsonlNames = ["check.jsonl"];
|
|
190
|
+
} else if (agentType === "trellis-research") {
|
|
191
|
+
jsonlNames = []; // research agent gets only prd + info
|
|
192
|
+
} else {
|
|
193
|
+
jsonlNames = ["implement.jsonl", "check.jsonl"]; // main session: all
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const jsonlName of jsonlNames) {
|
|
197
|
+
const jsonlPath = join(taskDir, jsonlName);
|
|
198
|
+
if (!existsSync(jsonlPath)) continue;
|
|
199
|
+
|
|
200
|
+
let lines: string[];
|
|
201
|
+
try {
|
|
202
|
+
lines = readFileSync(jsonlPath, "utf-8").split(/\r?\n/);
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const fileChunks: string[] = [];
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (!trimmed) continue;
|
|
211
|
+
try {
|
|
212
|
+
const row = JSON.parse(trimmed) as Record<string, unknown>;
|
|
213
|
+
const file = typeof row.file === "string" ? row.file.trim() : "";
|
|
214
|
+
if (!file) continue;
|
|
215
|
+
const targetPath = resolveProjectFile(projectRoot, file);
|
|
216
|
+
if (!targetPath) continue;
|
|
217
|
+
let content = "";
|
|
218
|
+
try { content = readFileSync(targetPath, "utf-8"); } catch { }
|
|
219
|
+
if (content.trim()) {
|
|
220
|
+
fileChunks.push(`### ${file}\n\n${content.trim()}`);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// seed rows and malformed lines are non-fatal
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (fileChunks.length > 0) {
|
|
228
|
+
parts.push(`## ${jsonlName}\n\n${fileChunks.join("\n\n---\n\n")}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return parts.length > 0
|
|
233
|
+
? `<task-context>\n${parts.join("\n\n")}\n</task-context>`
|
|
234
|
+
: "";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Per-turn cache — prevents redundant workflow-state resolution within a
|
|
239
|
+
// single event cascade (input, before_agent_start, and context fire closely)
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
const SESSION_OVERVIEW_TEXT =
|
|
243
|
+
"Trellis workflow system active. Use skills and agents as directed by the workflow state.";
|
|
244
|
+
|
|
245
|
+
class TurnContextCache {
|
|
246
|
+
private key: string | null = null;
|
|
247
|
+
private timestamp = 0;
|
|
248
|
+
private workflowMsg = "";
|
|
249
|
+
private static readonly TTL_MS = 1500;
|
|
250
|
+
|
|
251
|
+
get(projectRoot: string, contextKey: string | null): { workflowMsg: string } {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const cacheKey = `${projectRoot}:${contextKey ?? ""}`;
|
|
254
|
+
if (
|
|
255
|
+
this.key === cacheKey &&
|
|
256
|
+
now - this.timestamp < TurnContextCache.TTL_MS
|
|
257
|
+
) {
|
|
258
|
+
return { workflowMsg: this.workflowMsg };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { status } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
262
|
+
|
|
263
|
+
const workflowPath = join(projectRoot, ".trellis", "workflow.md");
|
|
264
|
+
let workflowMd = "";
|
|
265
|
+
try { workflowMd = readFileSync(workflowPath, "utf-8"); } catch { }
|
|
266
|
+
|
|
267
|
+
let workflowBody = "";
|
|
268
|
+
if (workflowMd) {
|
|
269
|
+
const blocks = parseWorkflowStateBlocks(workflowMd);
|
|
270
|
+
const activeBlock = blocks.find((b) => b.status === status);
|
|
271
|
+
if (activeBlock) {
|
|
272
|
+
workflowBody = `[workflow-state:${activeBlock.status}]\n${activeBlock.content}\n[/workflow-state:${activeBlock.status}]`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!workflowBody) {
|
|
276
|
+
workflowBody = "Refer to workflow.md for current step.";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`;
|
|
280
|
+
|
|
281
|
+
this.key = cacheKey;
|
|
282
|
+
this.timestamp = now;
|
|
283
|
+
return { workflowMsg: this.workflowMsg };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Workflow-state tag parsing
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
const WORKFLOW_STATE_RE =
|
|
292
|
+
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
293
|
+
|
|
294
|
+
interface WorkflowStateBlock {
|
|
295
|
+
status: string;
|
|
296
|
+
content: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] {
|
|
300
|
+
const blocks: WorkflowStateBlock[] = [];
|
|
301
|
+
for (const match of markdown.matchAll(WORKFLOW_STATE_RE)) {
|
|
302
|
+
blocks.push({
|
|
303
|
+
status: match[1],
|
|
304
|
+
content: match[2].trim(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return blocks;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Sub-agent detection
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
const TRELLIS_AGENTS = new Set(["trellis-implement", "trellis-check", "trellis-research"]);
|
|
315
|
+
|
|
316
|
+
function detectAgentType(): AgentType {
|
|
317
|
+
const blocked = process.env.PI_BLOCKED_AGENT;
|
|
318
|
+
if (blocked && TRELLIS_AGENTS.has(blocked)) {
|
|
319
|
+
return blocked as AgentType;
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Extension entry point
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
export default function(pi: ExtensionAPI): void {
|
|
329
|
+
let projectRoot: string | null = null;
|
|
330
|
+
const turnCache = new TurnContextCache();
|
|
331
|
+
const agentType = detectAgentType();
|
|
332
|
+
const isSubAgent = agentType !== null;
|
|
333
|
+
|
|
334
|
+
// Tracks compaction boundaries — context handler skips scanning when no
|
|
335
|
+
// compaction has occurred since last injection.
|
|
336
|
+
let lastCompactionTs = 0;
|
|
337
|
+
let lastInjectionTs = 0;
|
|
338
|
+
|
|
339
|
+
const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => {
|
|
340
|
+
const key = deriveContextKey(ctx);
|
|
341
|
+
if (!key) return null;
|
|
342
|
+
return key;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
346
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
347
|
+
const contextKey = rememberContextKey(ctx);
|
|
348
|
+
|
|
349
|
+
if (!projectRoot) return;
|
|
350
|
+
|
|
351
|
+
if (isSubAgent) {
|
|
352
|
+
// Sub-agent: inject precise task context once
|
|
353
|
+
const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
354
|
+
if (taskDir) {
|
|
355
|
+
const taskContext = buildTaskContext(projectRoot, taskDir, agentType);
|
|
356
|
+
if (taskContext) {
|
|
357
|
+
await pi.sendMessage({
|
|
358
|
+
customType: "trellis-task-context",
|
|
359
|
+
content: taskContext,
|
|
360
|
+
display: false,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// Main session: inject session context (global map) + task context
|
|
366
|
+
const sessionContext = buildSessionContext(projectRoot, contextKey);
|
|
367
|
+
if (sessionContext) {
|
|
368
|
+
await pi.sendMessage({
|
|
369
|
+
customType: "trellis-session-context",
|
|
370
|
+
content: sessionContext,
|
|
371
|
+
display: false,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
376
|
+
if (taskDir) {
|
|
377
|
+
const taskContext = buildTaskContext(projectRoot, taskDir);
|
|
378
|
+
if (taskContext) {
|
|
379
|
+
await pi.sendMessage({
|
|
380
|
+
customType: "trellis-task-context",
|
|
381
|
+
content: taskContext,
|
|
382
|
+
display: false,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ctx.ui.notify("Trellis workflow system available", "info");
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
pi.on("session_before_compact", async () => {
|
|
392
|
+
lastCompactionTs = Date.now();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
396
|
+
if (!projectRoot) {
|
|
397
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
398
|
+
}
|
|
399
|
+
if (!projectRoot) return;
|
|
400
|
+
const contextKey = rememberContextKey(ctx);
|
|
401
|
+
|
|
402
|
+
// Persistent injection: workflow state for this turn
|
|
403
|
+
const cached = turnCache.get(projectRoot, contextKey);
|
|
404
|
+
lastInjectionTs = Date.now();
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
message: {
|
|
408
|
+
customType: "trellis-workflow-state",
|
|
409
|
+
content: cached.workflowMsg,
|
|
410
|
+
display: false,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// context fires before EVERY LLM API call (including tool-use continuations
|
|
416
|
+
// and post-compaction agent.continue() paths). Acts as a safety net when
|
|
417
|
+
// before_agent_start's persisted message was removed by compaction.
|
|
418
|
+
pi.on("context", async (event, ctx) => {
|
|
419
|
+
if (!projectRoot) return;
|
|
420
|
+
const contextKey = rememberContextKey(ctx);
|
|
421
|
+
|
|
422
|
+
// Fast path: no compaction since last injection — message is still present
|
|
423
|
+
if (lastInjectionTs > lastCompactionTs) return;
|
|
424
|
+
|
|
425
|
+
const cached = turnCache.get(projectRoot, contextKey);
|
|
426
|
+
if (!cached.workflowMsg) return;
|
|
427
|
+
|
|
428
|
+
// Post-compaction: reverse-scan to confirm absence before injecting
|
|
429
|
+
const messages = event.messages as { role?: string; customType?: string }[];
|
|
430
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
431
|
+
if (messages[i].role === "custom" && messages[i].customType === "trellis-workflow-state") {
|
|
432
|
+
lastInjectionTs = Date.now();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
lastInjectionTs = Date.now();
|
|
438
|
+
return {
|
|
439
|
+
messages: [
|
|
440
|
+
...event.messages,
|
|
441
|
+
{
|
|
442
|
+
role: "custom",
|
|
443
|
+
customType: "trellis-workflow-state",
|
|
444
|
+
content: cached.workflowMsg,
|
|
445
|
+
timestamp: Date.now(),
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
pi.on("input", async (_event, ctx) => {
|
|
452
|
+
if (!projectRoot) {
|
|
453
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
454
|
+
}
|
|
455
|
+
// Resolve projectRoot on first input if session_start missed it
|
|
456
|
+
if (!projectRoot) return { action: "continue" };
|
|
457
|
+
const contextKey = rememberContextKey(ctx);
|
|
458
|
+
// Pre-warm the cache so before_agent_start and context can use it
|
|
459
|
+
turnCache.get(projectRoot, contextKey);
|
|
460
|
+
return { action: "continue" };
|
|
461
|
+
});
|
|
462
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/omp/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTemplateReader } from "../template-utils.js";
|
|
2
|
+
const { listMdAgents, readTemplate } = createTemplateReader(import.meta.url);
|
|
3
|
+
export function getAllAgents() {
|
|
4
|
+
return listMdAgents();
|
|
5
|
+
}
|
|
6
|
+
export function getExtensionTemplate() {
|
|
7
|
+
return readTemplate("extensions/trellis/index.ts.txt");
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/omp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE7E,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,CAAC,iCAAiC,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -1422,16 +1422,26 @@ export default function trellisExtension(pi: {
|
|
|
1422
1422
|
};
|
|
1423
1423
|
return turnCache;
|
|
1424
1424
|
};
|
|
1425
|
-
|
|
1425
|
+
// Provider prefix caches invalidate from byte 0 whenever the system prompt
|
|
1426
|
+
// changes, so everything injected into systemPrompt is memoized per context
|
|
1427
|
+
// key and stays byte-identical for the life of the process. Volatile state
|
|
1428
|
+
// travels through persisted custom messages instead (append-only history).
|
|
1429
|
+
const startupCtxCache = new Map<string, string>();
|
|
1426
1430
|
const getStartupCtx = (
|
|
1427
1431
|
k: string | null,
|
|
1428
1432
|
turn: { ov: string },
|
|
1429
1433
|
): string => {
|
|
1430
1434
|
const key = k ?? "default";
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1435
|
+
let startup = startupCtxCache.get(key);
|
|
1436
|
+
if (startup === undefined) {
|
|
1437
|
+
startup = buildStartupContext(root, k, turn.ov);
|
|
1438
|
+
startupCtxCache.set(key, startup);
|
|
1439
|
+
}
|
|
1440
|
+
return startup;
|
|
1434
1441
|
};
|
|
1442
|
+
const taskCtxSnapshot = new Map<string, string>();
|
|
1443
|
+
const lastSentTaskCtx = new Map<string, string>();
|
|
1444
|
+
const lastSentRuntimeCtx = new Map<string, string>();
|
|
1435
1445
|
|
|
1436
1446
|
// Toggle only the latest subagent native card; do not use Pi global tool expansion.
|
|
1437
1447
|
const toggleDetail = (ctx: PiExtensionContext) => {
|
|
@@ -1645,29 +1655,45 @@ export default function trellisExtension(pi: {
|
|
|
1645
1655
|
return { isError: true };
|
|
1646
1656
|
return undefined;
|
|
1647
1657
|
});
|
|
1648
|
-
pi.on?.("input", (event, ctx) => {
|
|
1649
|
-
const k = getKey(event, ctx);
|
|
1650
|
-
const ev = event as { text?: string };
|
|
1651
|
-
if (typeof ev.text !== "string" || !ev.text.trim())
|
|
1652
|
-
return { action: "continue" };
|
|
1653
|
-
const { wf, ov } = getTurnCtx(k);
|
|
1654
|
-
const injection = [wf, ov].filter(Boolean).join("\n\n");
|
|
1655
|
-
if (!injection) return { action: "continue" };
|
|
1656
|
-
return {
|
|
1657
|
-
action: "transform",
|
|
1658
|
-
text: [ev.text, injection].join("\n\n"),
|
|
1659
|
-
};
|
|
1660
|
-
});
|
|
1661
1658
|
pi.on?.("before_agent_start", (event, ctx) => {
|
|
1662
1659
|
const k = getKey(event, ctx);
|
|
1660
|
+
const key = k ?? "default";
|
|
1663
1661
|
const cur = (event as { systemPrompt?: string }).systemPrompt ?? "";
|
|
1664
|
-
const ctxText = buildContext(root, "trellis-implement", k);
|
|
1665
1662
|
const turn = getTurnCtx(k);
|
|
1666
1663
|
const startup = getStartupCtx(k, turn);
|
|
1664
|
+
// Task context is snapshotted into systemPrompt once; later on-disk
|
|
1665
|
+
// changes are delivered as persisted messages so the prefix stays stable.
|
|
1666
|
+
const freshTaskCtx = buildContext(root, "trellis-implement", k);
|
|
1667
|
+
let taskCtx = taskCtxSnapshot.get(key);
|
|
1668
|
+
if (taskCtx === undefined) {
|
|
1669
|
+
taskCtx = freshTaskCtx;
|
|
1670
|
+
taskCtxSnapshot.set(key, taskCtx);
|
|
1671
|
+
lastSentTaskCtx.set(key, freshTaskCtx);
|
|
1672
|
+
}
|
|
1673
|
+
const updates: string[] = [];
|
|
1674
|
+
const runtimeContext = [turn.wf, turn.ov].filter(Boolean).join("\n\n");
|
|
1675
|
+
if (runtimeContext && runtimeContext !== lastSentRuntimeCtx.get(key)) {
|
|
1676
|
+
lastSentRuntimeCtx.set(key, runtimeContext);
|
|
1677
|
+
updates.push(runtimeContext);
|
|
1678
|
+
}
|
|
1679
|
+
if (freshTaskCtx !== lastSentTaskCtx.get(key)) {
|
|
1680
|
+
lastSentTaskCtx.set(key, freshTaskCtx);
|
|
1681
|
+
updates.push(
|
|
1682
|
+
"<trellis-task-context-update>\nTask context changed on disk. This supersedes the Trellis Task Context in the system prompt.\n\n" +
|
|
1683
|
+
freshTaskCtx +
|
|
1684
|
+
"\n</trellis-task-context-update>",
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const content = updates.join("\n\n");
|
|
1667
1688
|
return {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1689
|
+
message: content
|
|
1690
|
+
? {
|
|
1691
|
+
customType: "trellis-runtime-context",
|
|
1692
|
+
content,
|
|
1693
|
+
display: false,
|
|
1694
|
+
}
|
|
1695
|
+
: undefined,
|
|
1696
|
+
systemPrompt: [cur, startup, taskCtx].filter(Boolean).join("\n\n"),
|
|
1671
1697
|
};
|
|
1672
1698
|
});
|
|
1673
1699
|
pi.on?.("context", (event, ctx) => {
|