@mindfoldhq/trellis 0.6.7 → 0.6.9
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.d.ts.map +1 -1
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/codex.d.ts +7 -1
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +19 -2
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts +3 -1
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +1 -1
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/commands/channel/agent-loader.d.ts +2 -2
- package/dist/commands/channel/agent-loader.d.ts.map +1 -1
- package/dist/commands/channel/agent-loader.js +9 -5
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/context-loader.d.ts +1 -1
- package/dist/commands/channel/context-loader.d.ts.map +1 -1
- package/dist/commands/channel/context-loader.js +36 -15
- package/dist/commands/channel/context-loader.js.map +1 -1
- package/dist/commands/channel/context-trust.d.ts +31 -0
- package/dist/commands/channel/context-trust.d.ts.map +1 -0
- package/dist/commands/channel/context-trust.js +139 -0
- package/dist/commands/channel/context-trust.js.map +1 -0
- package/dist/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +4 -0
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts +3 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +5 -2
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +3 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +1 -0
- package/dist/commands/channel/supervisor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +30 -9
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/update.d.ts +12 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/codex.d.ts +36 -0
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +111 -13
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/grok.d.ts +22 -0
- package/dist/configurators/grok.d.ts.map +1 -0
- package/dist/configurators/grok.js +58 -0
- package/dist/configurators/grok.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +17 -2
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kimi.d.ts +29 -0
- package/dist/configurators/kimi.d.ts.map +1 -0
- package/dist/configurators/kimi.js +81 -0
- package/dist/configurators/kimi.js.map +1 -0
- package/dist/configurators/pi.js +9 -9
- package/dist/configurators/pi.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +12 -4
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/snow.d.ts +31 -0
- package/dist/configurators/snow.d.ts.map +1 -0
- package/dist/configurators/snow.js +95 -0
- package/dist/configurators/snow.js.map +1 -0
- package/dist/configurators/workflow.d.ts +14 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +37 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/configurators/zcode.d.ts +9 -4
- package/dist/configurators/zcode.d.ts.map +1 -1
- package/dist/configurators/zcode.js +41 -12
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
- package/dist/migrations/manifests/0.6.8.json +17 -0
- package/dist/migrations/manifests/0.6.9.json +9 -0
- package/dist/templates/codex/agents/trellis-check.toml +7 -12
- package/dist/templates/codex/agents/trellis-implement.toml +7 -12
- package/dist/templates/codex/agents/trellis-research.toml +15 -15
- package/dist/templates/codex/config.toml +11 -0
- package/dist/templates/codex/hooks/session-start.py +9 -3
- package/dist/templates/codex/hooks.json +12 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
- 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/platform-files/agents.md +1 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/common/skills/brainstorm.md +40 -13
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
- package/dist/templates/grok/agents/trellis-check.md +105 -0
- package/dist/templates/grok/agents/trellis-implement.md +116 -0
- package/dist/templates/grok/agents/trellis-research.md +146 -0
- package/dist/templates/grok/index.d.ts +16 -0
- package/dist/templates/grok/index.d.ts.map +1 -0
- package/dist/templates/grok/index.js +19 -0
- package/dist/templates/grok/index.js.map +1 -0
- package/dist/templates/kimi/agents/trellis-check.md +103 -0
- package/dist/templates/kimi/agents/trellis-implement.md +114 -0
- package/dist/templates/kimi/agents/trellis-research.md +144 -0
- package/dist/templates/kimi/index.d.ts +19 -0
- package/dist/templates/kimi/index.d.ts.map +1 -0
- package/dist/templates/kimi/index.js +22 -0
- package/dist/templates/kimi/index.js.map +1 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
- package/dist/templates/opencode/lib/session-utils.js +8 -2
- package/dist/templates/opencode/lib/trellis-context.js +263 -43
- package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
- package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
- package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
- package/dist/templates/pi/settings.json +0 -3
- package/dist/templates/shared-hooks/index.d.ts +11 -5
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +16 -5
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
- package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
- package/dist/templates/shared-hooks/session-start.py +26 -6
- package/dist/templates/snow/SNOW.md +86 -0
- package/dist/templates/snow/agents/trellis-check.md +135 -0
- package/dist/templates/snow/agents/trellis-implement.md +153 -0
- package/dist/templates/snow/agents/trellis-research.md +114 -0
- package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
- package/dist/templates/snow/hooks/onSessionStart.json +15 -0
- package/dist/templates/snow/hooks/onUserMessage.json +15 -0
- package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
- package/dist/templates/snow/index.d.ts +26 -0
- package/dist/templates/snow/index.d.ts.map +1 -0
- package/dist/templates/snow/index.js +36 -0
- package/dist/templates/snow/index.js.map +1 -0
- package/dist/templates/template-utils.d.ts.map +1 -1
- package/dist/templates/template-utils.js +5 -1
- package/dist/templates/template-utils.js.map +1 -1
- package/dist/templates/trellis/config.yaml +55 -7
- package/dist/templates/trellis/gitattributes.txt +9 -0
- package/dist/templates/trellis/index.d.ts +3 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +3 -1
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +94 -28
- package/dist/templates/trellis/scripts/common/active_task.py +44 -13
- package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
- package/dist/templates/trellis/scripts/common/config.py +105 -10
- package/dist/templates/trellis/scripts/common/git.py +36 -0
- package/dist/templates/trellis/scripts/common/io.py +16 -9
- package/dist/templates/trellis/scripts/common/task_context.py +101 -7
- package/dist/templates/trellis/scripts/common/task_store.py +149 -22
- package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
- package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
- package/dist/templates/trellis/scripts/task.py +106 -10
- package/dist/templates/trellis/workflow.md +20 -19
- package/dist/templates/zcode/agents/trellis-check.md +7 -0
- package/dist/templates/zcode/agents/trellis-implement.md +13 -4
- package/dist/templates/zcode/config.json +42 -0
- package/dist/templates/zcode/index.d.ts +10 -1
- package/dist/templates/zcode/index.d.ts.map +1 -1
- package/dist/templates/zcode/index.js +12 -2
- package/dist/templates/zcode/index.js.map +1 -1
- package/dist/types/ai-tools.d.ts +4 -4
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +114 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +0 -3
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi Code template module.
|
|
3
|
+
*
|
|
4
|
+
* Kimi Code is a class-2 pull-based platform:
|
|
5
|
+
* - Workflow/bundled skills go to the shared `.agents/skills/` root via the
|
|
6
|
+
* neutral resolver (byte-identical to Codex/Gemini/Pi writes).
|
|
7
|
+
* - User-invocable entry points (`trellis-start` / `trellis-continue` /
|
|
8
|
+
* `trellis-finish-work`, invoked as `/skill:trellis-<name>`) and the Trellis
|
|
9
|
+
* agent prompts live under `.kimi-code/skills/<name>/SKILL.md`.
|
|
10
|
+
*
|
|
11
|
+
* Kimi has no project-level hooks/settings file Trellis may write and no
|
|
12
|
+
* custom sub-agent definitions (built-in coder/explore/plan only), so the
|
|
13
|
+
* Trellis agent prompts ship as skills; trellis-implement / trellis-check get
|
|
14
|
+
* the pull-based prelude, trellis-research stays standalone.
|
|
15
|
+
*/
|
|
16
|
+
import { createTemplateReader } from "../template-utils.js";
|
|
17
|
+
const { listMdAgents } = createTemplateReader(import.meta.url);
|
|
18
|
+
/** Trellis agent prompts (trellis-implement, trellis-check, trellis-research), installed as Kimi skills. */
|
|
19
|
+
export function getAllAgents() {
|
|
20
|
+
return listMdAgents();
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/kimi/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,oBAAoB,EAAsB,MAAM,sBAAsB,CAAC;AAEhF,MAAM,EAAE,YAAY,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/D,4GAA4G;AAC5G,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { join, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
@@ -40,7 +40,7 @@ function buildContextKey(platformName: string, kind: string, value: string): str
|
|
|
40
40
|
return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null {
|
|
43
|
+
function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string | undefined; getSessionFile?: () => string | undefined } }): string | null {
|
|
44
44
|
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
45
45
|
if (sessionId) {
|
|
46
46
|
return buildContextKey("omp", "session", sessionId);
|
|
@@ -58,11 +58,118 @@ function isInsideRoot(root: string, candidate: string): boolean {
|
|
|
58
58
|
return rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Trusted context roots (mirrors packages/cli/src/commands/channel/context-trust.ts;
|
|
63
|
+
// standalone copy since templates don't import from the CLI package).
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const AUTO_TRUST_ENTRIES = ["tasks", "workspace"];
|
|
67
|
+
|
|
68
|
+
function stripTrustValue(s: string): string {
|
|
69
|
+
return s.trim().replace(/\s*#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseChannelTrustSection(content: string): { trustedDirs: string[]; autoTrustSymlinks?: boolean } {
|
|
73
|
+
const lines = content.split("\n");
|
|
74
|
+
const trustedDirs: string[] = [];
|
|
75
|
+
let autoTrustSymlinks: boolean | undefined;
|
|
76
|
+
let inChannel = false;
|
|
77
|
+
let inList = false;
|
|
78
|
+
|
|
79
|
+
for (const raw of lines) {
|
|
80
|
+
const line = raw.replace(/\r$/, "");
|
|
81
|
+
const trimmed = line.trimEnd();
|
|
82
|
+
if (trimmed.trim().startsWith("#")) continue;
|
|
83
|
+
|
|
84
|
+
if (/^channel:\s*$/.test(trimmed)) {
|
|
85
|
+
inChannel = true;
|
|
86
|
+
inList = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!inChannel) continue;
|
|
90
|
+
|
|
91
|
+
if (trimmed.trim() !== "" && /^\S/.test(line)) {
|
|
92
|
+
inChannel = false;
|
|
93
|
+
inList = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.trim() === "") continue;
|
|
97
|
+
|
|
98
|
+
if (inList) {
|
|
99
|
+
const item = trimmed.match(/^ {4}-\s*(.+)$/);
|
|
100
|
+
if (item) {
|
|
101
|
+
const val = stripTrustValue(item[1]!);
|
|
102
|
+
if (val) trustedDirs.push(val);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
inList = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (/^ {2}trusted_context_dirs:\s*$/.test(trimmed)) {
|
|
109
|
+
inList = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const boolMatch = trimmed.match(/^ {2}auto_trust_trellis_symlinks:\s*(.+)$/);
|
|
114
|
+
if (boolMatch) {
|
|
115
|
+
const val = stripTrustValue(boolMatch[1]!).toLowerCase();
|
|
116
|
+
if (val === "false") autoTrustSymlinks = false;
|
|
117
|
+
else if (val === "true") autoTrustSymlinks = true;
|
|
118
|
+
else process.stderr.write(`[channel] channel.auto_trust_trellis_symlinks: invalid value '${val}', ignoring\n`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { trustedDirs, autoTrustSymlinks };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveTrustedRoots(projectRoot: string): string[] {
|
|
127
|
+
const configPath = join(projectRoot, ".trellis", "config.yaml");
|
|
128
|
+
let config: { trustedDirs: string[]; autoTrustSymlinks?: boolean } = { trustedDirs: [] };
|
|
129
|
+
if (existsSync(configPath)) {
|
|
130
|
+
try {
|
|
131
|
+
config = parseChannelTrustSection(readFileSync(configPath, "utf-8"));
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const roots: string[] = [];
|
|
138
|
+
for (const entry of config.trustedDirs) {
|
|
139
|
+
try {
|
|
140
|
+
roots.push(realpathSync(resolve(projectRoot, entry)));
|
|
141
|
+
} catch {
|
|
142
|
+
// entry not found or invalid — skip
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (config.autoTrustSymlinks !== false) {
|
|
147
|
+
for (const entryName of AUTO_TRUST_ENTRIES) {
|
|
148
|
+
const entryPath = join(projectRoot, ".trellis", entryName);
|
|
149
|
+
try {
|
|
150
|
+
if (lstatSync(entryPath).isSymbolicLink()) {
|
|
151
|
+
roots.push(realpathSync(entryPath));
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// missing / broken symlink — nothing to trust
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [...new Set(roots)];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveProjectFile(
|
|
163
|
+
projectRoot: string,
|
|
164
|
+
file: string,
|
|
165
|
+
trustedRoots: string[],
|
|
166
|
+
): string | null {
|
|
62
167
|
try {
|
|
63
168
|
const rootReal = realpathSync(projectRoot);
|
|
64
169
|
const targetReal = realpathSync(resolve(projectRoot, file));
|
|
65
|
-
|
|
170
|
+
if (isInsideRoot(rootReal, targetReal)) return targetReal;
|
|
171
|
+
if (trustedRoots.some((root) => isInsideRoot(root, targetReal))) return targetReal;
|
|
172
|
+
return null;
|
|
66
173
|
} catch {
|
|
67
174
|
return null;
|
|
68
175
|
}
|
|
@@ -171,6 +278,9 @@ type AgentType = "trellis-implement" | "trellis-check" | "trellis-research" | nu
|
|
|
171
278
|
|
|
172
279
|
function buildTaskContext(projectRoot: string, taskDir: string, agentType?: AgentType): string {
|
|
173
280
|
const parts: string[] = [];
|
|
281
|
+
// Resolved once per call (not per referenced file) — avoids re-parsing
|
|
282
|
+
// config.yaml for every jsonl row.
|
|
283
|
+
const trustedRoots = resolveTrustedRoots(projectRoot);
|
|
174
284
|
|
|
175
285
|
// prd.md and info.md — always included
|
|
176
286
|
let prd = "";
|
|
@@ -212,7 +322,7 @@ function buildTaskContext(projectRoot: string, taskDir: string, agentType?: Agen
|
|
|
212
322
|
const row = JSON.parse(trimmed) as Record<string, unknown>;
|
|
213
323
|
const file = typeof row.file === "string" ? row.file.trim() : "";
|
|
214
324
|
if (!file) continue;
|
|
215
|
-
const targetPath = resolveProjectFile(projectRoot, file);
|
|
325
|
+
const targetPath = resolveProjectFile(projectRoot, file, trustedRoots);
|
|
216
326
|
if (!targetPath) continue;
|
|
217
327
|
let content = "";
|
|
218
328
|
try { content = readFileSync(targetPath, "utf-8"); } catch { }
|
|
@@ -336,7 +446,7 @@ export default function(pi: ExtensionAPI): void {
|
|
|
336
446
|
let lastCompactionTs = 0;
|
|
337
447
|
let lastInjectionTs = 0;
|
|
338
448
|
|
|
339
|
-
const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => {
|
|
449
|
+
const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string | undefined; getSessionFile?: () => string | undefined } }): string | null => {
|
|
340
450
|
const key = deriveContextKey(ctx);
|
|
341
451
|
if (!key) return null;
|
|
342
452
|
return key;
|
|
@@ -439,24 +549,38 @@ export default function(pi: ExtensionAPI): void {
|
|
|
439
549
|
messages: [
|
|
440
550
|
...event.messages,
|
|
441
551
|
{
|
|
442
|
-
role: "custom",
|
|
552
|
+
role: "custom" as const,
|
|
443
553
|
customType: "trellis-workflow-state",
|
|
444
554
|
content: cached.workflowMsg,
|
|
555
|
+
display: false,
|
|
445
556
|
timestamp: Date.now(),
|
|
446
557
|
},
|
|
447
558
|
],
|
|
448
559
|
};
|
|
449
560
|
});
|
|
450
561
|
|
|
562
|
+
// OMP passes Bash event.input through to the tool execution parameters, so
|
|
563
|
+
// inject the session key through the shell-agnostic env field. An explicit
|
|
564
|
+
// per-call value wins over the derived key.
|
|
565
|
+
pi.on("tool_call", (event, ctx) => {
|
|
566
|
+
if (event.toolName !== "bash") return;
|
|
567
|
+
const contextKey = rememberContextKey(ctx);
|
|
568
|
+
if (!contextKey) return;
|
|
569
|
+
const input = event.input as { env?: Record<string, string> };
|
|
570
|
+
input.env = {
|
|
571
|
+
TRELLIS_CONTEXT_ID: contextKey,
|
|
572
|
+
...input.env,
|
|
573
|
+
};
|
|
574
|
+
});
|
|
575
|
+
|
|
451
576
|
pi.on("input", async (_event, ctx) => {
|
|
452
577
|
if (!projectRoot) {
|
|
453
578
|
projectRoot = findProjectRoot(ctx.cwd);
|
|
454
579
|
}
|
|
455
580
|
// Resolve projectRoot on first input if session_start missed it
|
|
456
|
-
if (!projectRoot) return
|
|
581
|
+
if (!projectRoot) return;
|
|
457
582
|
const contextKey = rememberContextKey(ctx);
|
|
458
583
|
// Pre-warm the cache so before_agent_start and context can use it
|
|
459
584
|
turnCache.get(projectRoot, contextKey);
|
|
460
|
-
return { action: "continue" };
|
|
461
585
|
});
|
|
462
586
|
}
|
|
@@ -8,8 +8,14 @@ import { debugLog } from "./trellis-context.js"
|
|
|
8
8
|
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
|
|
9
9
|
|
|
10
10
|
const FIRST_REPLY_NOTICE = `<first-reply-notice>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
On the first visible assistant reply in this session, briefly acknowledge that Trellis SessionStart context loaded.
|
|
12
|
+
Choose the acknowledgment language in this order:
|
|
13
|
+
1. Use the language of the user's current request (the user message that triggered this reply).
|
|
14
|
+
2. If that request has no clear natural language, use an explicitly established project communication language.
|
|
15
|
+
3. If neither provides a language, output the language-neutral fallback exactly: \`Trellis SessionStart ✓\`.
|
|
16
|
+
Continue directly with the user's request after the acknowledgment.
|
|
17
|
+
The acknowledgment must not alter the language used for the remainder of the response.
|
|
18
|
+
This notice is one-shot: do not repeat it after the first visible assistant reply in this session.
|
|
13
19
|
</first-reply-notice>`
|
|
14
20
|
|
|
15
21
|
function hasCuratedJsonlEntry(jsonlPath) {
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* JSONL parsing, and context building capabilities.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
|
|
8
|
+
import { existsSync, readFileSync, appendFileSync, readdirSync, statSync } from "fs"
|
|
9
9
|
import { isAbsolute, join } from "path"
|
|
10
10
|
import { platform } from "os"
|
|
11
11
|
import { execSync } from "child_process"
|
|
12
12
|
import { createHash } from "crypto"
|
|
13
|
+
import { Buffer, isUtf8 } from "buffer"
|
|
13
14
|
import process from "process"
|
|
14
15
|
|
|
15
16
|
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
|
|
@@ -78,6 +79,240 @@ export function isTrellisSubagent(input) {
|
|
|
78
79
|
return TRELLIS_SUBAGENT_RE.test(agent)
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Context Injection Limits (issue #441)
|
|
84
|
+
//
|
|
85
|
+
// Notice text and behavior mirrored byte-for-byte from the shared-hooks
|
|
86
|
+
// Python sub-agent context injection hook and the Pi extension. Changing
|
|
87
|
+
// wording here requires changing it there too.
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
const DEFAULT_CONTEXT_INJECTION_LIMITS = {
|
|
91
|
+
max_file_bytes: 32768,
|
|
92
|
+
max_artifact_bytes: 65536,
|
|
93
|
+
max_total_bytes: 131072,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Truncate `buf` to at most `cap` bytes without splitting a UTF-8
|
|
98
|
+
* multi-byte sequence. `cap <= 0` means "no limit".
|
|
99
|
+
*/
|
|
100
|
+
function truncateUtf8(buf, cap) {
|
|
101
|
+
if (cap <= 0 || buf.length <= cap) return buf
|
|
102
|
+
let i = cap
|
|
103
|
+
// Back off over continuation bytes (10xxxxxx) to find the lead byte.
|
|
104
|
+
while (i > 0 && (buf[i - 1] & 0xc0) === 0x80) i--
|
|
105
|
+
if (i === 0) return Buffer.alloc(0)
|
|
106
|
+
const lead = buf[i - 1]
|
|
107
|
+
if (lead & 0x80) {
|
|
108
|
+
let seqLen = 1
|
|
109
|
+
if ((lead & 0xe0) === 0xc0) seqLen = 2
|
|
110
|
+
else if ((lead & 0xf0) === 0xe0) seqLen = 3
|
|
111
|
+
else if ((lead & 0xf8) === 0xf0) seqLen = 4
|
|
112
|
+
// Drop the lead byte too if its full sequence didn't fit.
|
|
113
|
+
if (i - 1 + seqLen > cap) i--
|
|
114
|
+
}
|
|
115
|
+
return buf.subarray(0, i)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stripInlineComment(value) {
|
|
119
|
+
let inQuote = null
|
|
120
|
+
for (let idx = 0; idx < value.length; idx++) {
|
|
121
|
+
const ch = value[idx]
|
|
122
|
+
if (inQuote) {
|
|
123
|
+
if (ch === inQuote) inQuote = null
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (ch === '"' || ch === "'") {
|
|
127
|
+
inQuote = ch
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (ch === "#" && (idx === 0 || /\s/.test(value[idx - 1]))) {
|
|
131
|
+
return value.slice(0, idx)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function unquoteYaml(s) {
|
|
138
|
+
if (s.length >= 2 && s[0] === s[s.length - 1] && (s[0] === '"' || s[0] === "'")) {
|
|
139
|
+
return s.slice(1, -1)
|
|
140
|
+
}
|
|
141
|
+
return s
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Line-based parser for ONLY the `context_injection:` block of
|
|
146
|
+
* `.trellis/config.yaml`. Not a general YAML parser — mirrors
|
|
147
|
+
* `common.config.get_context_injection_limits()` semantics for this
|
|
148
|
+
* section only (missing keys keep the default; invalid/negative values
|
|
149
|
+
* fall back to the default for that key with a debugLog warning).
|
|
150
|
+
*/
|
|
151
|
+
function readContextInjectionLimits(repoRoot) {
|
|
152
|
+
const limits = { ...DEFAULT_CONTEXT_INJECTION_LIMITS }
|
|
153
|
+
let text = null
|
|
154
|
+
try {
|
|
155
|
+
text = readFileSync(join(repoRoot, ".trellis", "config.yaml"), "utf-8")
|
|
156
|
+
} catch {
|
|
157
|
+
return limits
|
|
158
|
+
}
|
|
159
|
+
if (!text) return limits
|
|
160
|
+
|
|
161
|
+
let inSection = false
|
|
162
|
+
let sectionIndent = -1
|
|
163
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
164
|
+
const trimmed = rawLine.trim()
|
|
165
|
+
if (!inSection) {
|
|
166
|
+
if (/^context_injection\s*:\s*(#.*)?$/.test(trimmed)) {
|
|
167
|
+
inSection = true
|
|
168
|
+
sectionIndent = rawLine.length - rawLine.trimStart().length
|
|
169
|
+
}
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (!trimmed || trimmed.startsWith("#")) continue
|
|
173
|
+
const indent = rawLine.length - rawLine.trimStart().length
|
|
174
|
+
if (indent <= sectionIndent) break
|
|
175
|
+
const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/)
|
|
176
|
+
if (!m) continue
|
|
177
|
+
const key = m[1]
|
|
178
|
+
if (!(key in limits)) continue
|
|
179
|
+
const raw = unquoteYaml(stripInlineComment(m[2]).trim()).trim()
|
|
180
|
+
if (!/^-?\d+$/.test(raw) || parseInt(raw, 10) < 0) {
|
|
181
|
+
// invalid/negative -> keep default (Python warns on stderr)
|
|
182
|
+
debugLog("context", `invalid context_injection.${key} value: ${raw}; using default ${limits[key]}`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
limits[key] = parseInt(raw, 10)
|
|
186
|
+
}
|
|
187
|
+
return limits
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Tracks the running total of bytes emitted into the sub-agent context. */
|
|
191
|
+
class ContextBudget {
|
|
192
|
+
constructor(maxTotalBytes) {
|
|
193
|
+
this.maxTotalBytes = maxTotalBytes
|
|
194
|
+
this.used = 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
hasRoom(size) {
|
|
198
|
+
if (this.maxTotalBytes <= 0) return true
|
|
199
|
+
return this.used + size <= this.maxTotalBytes
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
add(size) {
|
|
203
|
+
this.used += size
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function truncateNotice(path, cap) {
|
|
208
|
+
return `\n[Trellis: truncated at ${cap} bytes — read ${path} for the full content]`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isBinaryContent(data) {
|
|
212
|
+
return data.includes(0) || !isUtf8(data)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function binaryNotice(path, size, reason) {
|
|
216
|
+
return `[Trellis: not inlined (binary file) — ${path} (${size} bytes): ${reason}]`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function indexNotice(path, size, reason) {
|
|
220
|
+
return `[Trellis: not inlined (total context limit reached) — ${path} (${size} bytes): ${reason}]`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Return an inlined `=== header ===` block, or degrade to an index
|
|
225
|
+
* notice once the total context budget is exhausted.
|
|
226
|
+
*/
|
|
227
|
+
function budgetedBlock(budget, header, plainPath, content, reason, sizeForIndex) {
|
|
228
|
+
const block = `=== ${header} ===\n${content}`
|
|
229
|
+
const blockBytes = Buffer.byteLength(block, "utf-8")
|
|
230
|
+
if (!budget.hasRoom(blockBytes)) {
|
|
231
|
+
const notice = indexNotice(plainPath, sizeForIndex, reason)
|
|
232
|
+
budget.add(Buffer.byteLength(notice, "utf-8"))
|
|
233
|
+
return notice
|
|
234
|
+
}
|
|
235
|
+
budget.add(blockBytes)
|
|
236
|
+
return block
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Read raw file bytes, return null if file doesn't exist. */
|
|
240
|
+
function readFileBytes(basePath, filePath) {
|
|
241
|
+
const fullPath = isAbsolute(filePath) ? filePath : join(basePath, filePath)
|
|
242
|
+
try {
|
|
243
|
+
if (!statSync(fullPath).isFile()) return null
|
|
244
|
+
} catch {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return readFileSync(fullPath)
|
|
249
|
+
} catch {
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Read a JSONL-referenced file, apply the per-file cap, then budget it. */
|
|
255
|
+
function materializeFile(basePath, filePath, reason, limits, budget) {
|
|
256
|
+
const data = readFileBytes(basePath, filePath)
|
|
257
|
+
if (data === null) return null
|
|
258
|
+
|
|
259
|
+
const size = data.length
|
|
260
|
+
if (isBinaryContent(data)) {
|
|
261
|
+
const notice = binaryNotice(filePath, size, reason)
|
|
262
|
+
budget.add(Buffer.byteLength(notice, "utf-8"))
|
|
263
|
+
return notice
|
|
264
|
+
}
|
|
265
|
+
const cap = limits.max_file_bytes
|
|
266
|
+
const truncated = truncateUtf8(data, cap)
|
|
267
|
+
let content = truncated.toString("utf-8")
|
|
268
|
+
if (truncated.length < size) content += truncateNotice(filePath, cap)
|
|
269
|
+
|
|
270
|
+
return budgetedBlock(budget, filePath, filePath, content, reason, size)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read all .md files in a directory, applying the same per-file and
|
|
275
|
+
* total caps as a single-file JSONL entry.
|
|
276
|
+
*/
|
|
277
|
+
function materializeDirectory(basePath, dirPath, reason, limits, budget, maxFiles = 20) {
|
|
278
|
+
const blocks = []
|
|
279
|
+
const fullPath = isAbsolute(dirPath) ? dirPath : join(basePath, dirPath)
|
|
280
|
+
|
|
281
|
+
let files
|
|
282
|
+
try {
|
|
283
|
+
if (!statSync(fullPath).isDirectory()) return blocks
|
|
284
|
+
files = readdirSync(fullPath)
|
|
285
|
+
.filter(f => f.endsWith(".md") && statSync(join(fullPath, f)).isFile())
|
|
286
|
+
.sort()
|
|
287
|
+
} catch {
|
|
288
|
+
return blocks
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const filename of files.slice(0, maxFiles)) {
|
|
292
|
+
const relativePath = join(dirPath, filename)
|
|
293
|
+
const block = materializeFile(basePath, relativePath, reason, limits, budget)
|
|
294
|
+
if (block) blocks.push(block)
|
|
295
|
+
}
|
|
296
|
+
return blocks
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read a task artifact (prd/design/implement.md), apply the per-artifact
|
|
301
|
+
* cap, then budget it.
|
|
302
|
+
*/
|
|
303
|
+
function materializeArtifact(basePath, filePath, headerLabel, reason, limits, budget) {
|
|
304
|
+
const data = readFileBytes(basePath, filePath)
|
|
305
|
+
if (data === null) return null
|
|
306
|
+
|
|
307
|
+
const size = data.length
|
|
308
|
+
const cap = limits.max_artifact_bytes
|
|
309
|
+
const truncated = truncateUtf8(data, cap)
|
|
310
|
+
let content = truncated.toString("utf-8")
|
|
311
|
+
if (truncated.length < size) content += truncateNotice(filePath, cap)
|
|
312
|
+
|
|
313
|
+
return budgetedBlock(budget, headerLabel, filePath, content, reason, size)
|
|
314
|
+
}
|
|
315
|
+
|
|
81
316
|
/**
|
|
82
317
|
* Trellis Context Manager
|
|
83
318
|
*/
|
|
@@ -282,44 +517,21 @@ export class TrellisContext {
|
|
|
282
517
|
// JSONL Reading
|
|
283
518
|
// ============================================================
|
|
284
519
|
|
|
285
|
-
readDirectoryMdFiles(dirPath, maxFiles = 20) {
|
|
286
|
-
const results = []
|
|
287
|
-
const fullPath = join(this.directory, dirPath)
|
|
288
|
-
|
|
289
|
-
if (!existsSync(fullPath)) {
|
|
290
|
-
return results
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
const files = readdirSync(fullPath)
|
|
295
|
-
.filter(f => f.endsWith(".md"))
|
|
296
|
-
.sort()
|
|
297
|
-
.slice(0, maxFiles)
|
|
298
|
-
|
|
299
|
-
for (const filename of files) {
|
|
300
|
-
const filePath = join(dirPath, filename)
|
|
301
|
-
const content = this.readProjectFile(filePath)
|
|
302
|
-
if (content) {
|
|
303
|
-
results.push({ path: filePath, content })
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
} catch {
|
|
307
|
-
// Ignore directory read errors
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return results
|
|
311
|
-
}
|
|
312
|
-
|
|
313
520
|
/**
|
|
314
|
-
* Read a JSONL file and
|
|
521
|
+
* Read a JSONL file and materialize referenced files/directories into
|
|
522
|
+
* context blocks, applying per-file caps and the shared total budget
|
|
523
|
+
* (issue #441). Mirrors Python `_materialize_jsonl_entries`.
|
|
315
524
|
* Supports:
|
|
316
525
|
* {"file": "path/to/file.md", "reason": "..."}
|
|
317
526
|
* {"file": "path/to/dir/", "type": "directory", "reason": "..."}
|
|
527
|
+
*
|
|
528
|
+
* Missing referenced files are skipped silently (Python `_materialize_file`
|
|
529
|
+
* returns None for them).
|
|
318
530
|
*/
|
|
319
|
-
readJsonlWithFiles(jsonlPath) {
|
|
320
|
-
const
|
|
531
|
+
readJsonlWithFiles(jsonlPath, limits, budget) {
|
|
532
|
+
const blocks = []
|
|
321
533
|
const content = this.readFile(jsonlPath)
|
|
322
|
-
if (!content) return
|
|
534
|
+
if (!content) return blocks
|
|
323
535
|
|
|
324
536
|
for (const line of content.split("\n")) {
|
|
325
537
|
if (!line.trim()) continue
|
|
@@ -327,28 +539,25 @@ export class TrellisContext {
|
|
|
327
539
|
const item = JSON.parse(line)
|
|
328
540
|
const file = item.file || item.path
|
|
329
541
|
const entryType = item.type || "file"
|
|
542
|
+
const reason = item.reason || "-"
|
|
330
543
|
|
|
331
544
|
if (!file) continue
|
|
332
545
|
|
|
333
546
|
if (entryType === "directory") {
|
|
334
|
-
|
|
335
|
-
results.push(...dirEntries)
|
|
547
|
+
blocks.push(...materializeDirectory(this.directory, file, reason, limits, budget))
|
|
336
548
|
} else {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
if (fileContent) {
|
|
340
|
-
results.push({ path: file, content: fileContent })
|
|
341
|
-
}
|
|
549
|
+
const block = materializeFile(this.directory, file, reason, limits, budget)
|
|
550
|
+
if (block) blocks.push(block)
|
|
342
551
|
}
|
|
343
552
|
} catch {
|
|
344
553
|
// Ignore parse errors for individual lines
|
|
345
554
|
}
|
|
346
555
|
}
|
|
347
|
-
return
|
|
556
|
+
return blocks
|
|
348
557
|
}
|
|
349
558
|
|
|
350
|
-
buildContextFromEntries(
|
|
351
|
-
return
|
|
559
|
+
buildContextFromEntries(blocks) {
|
|
560
|
+
return blocks.join("\n\n")
|
|
352
561
|
}
|
|
353
562
|
}
|
|
354
563
|
|
|
@@ -379,3 +588,14 @@ export const contextCollector = new ContextCollector()
|
|
|
379
588
|
|
|
380
589
|
// Export debug log for plugins
|
|
381
590
|
export { debugLog }
|
|
591
|
+
|
|
592
|
+
// Context injection limits (issue #441) — exported for plugins and tests
|
|
593
|
+
export {
|
|
594
|
+
DEFAULT_CONTEXT_INJECTION_LIMITS,
|
|
595
|
+
truncateUtf8,
|
|
596
|
+
readContextInjectionLimits,
|
|
597
|
+
ContextBudget,
|
|
598
|
+
materializeFile,
|
|
599
|
+
materializeDirectory,
|
|
600
|
+
materializeArtifact,
|
|
601
|
+
}
|