@mindfoldhq/trellis 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -0
- package/dist/cli/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/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +4 -2
- package/dist/commands/channel/spawn.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/update.d.ts.map +1 -1
- package/dist/commands/update.js +17 -0
- 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 +100 -1
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +5 -0
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +1 -0
- 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/migrations/manifests/0.6.10.json +9 -0
- package/dist/migrations/manifests/0.6.9.json +9 -0
- package/dist/templates/codex/agents/trellis-check.toml +7 -3
- package/dist/templates/codex/agents/trellis-implement.toml +7 -3
- package/dist/templates/codex/agents/trellis-research.toml +13 -6
- 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 +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +4 -2
- package/dist/templates/kimi/agents/trellis-research.md +7 -3
- package/dist/templates/omp/extensions/trellis/index.ts.txt +114 -4
- 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 +270 -22
- package/dist/templates/shared-hooks/inject-subagent-context.py +311 -83
- package/dist/templates/shared-hooks/inject-workflow-state.py +36 -1
- 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 +46 -0
- 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 +9 -2
- package/dist/templates/trellis/scripts/common/config.py +89 -2
- package/dist/templates/trellis/scripts/common/task_context.py +84 -6
- package/dist/templates/trellis/scripts/common/task_store.py +63 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
- package/dist/templates/trellis/scripts/task.py +21 -2
- package/dist/templates/trellis/workflow.md +14 -14
- 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 +38 -0
- package/dist/types/ai-tools.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
delimiter,
|
|
5
|
+
dirname,
|
|
6
|
+
isAbsolute,
|
|
7
|
+
join,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
} from "node:path";
|
|
4
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
|
+
import { isUtf8 } from "node:buffer";
|
|
5
13
|
|
|
6
14
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
7
15
|
type JsonObject = Record<string, unknown>;
|
|
@@ -722,6 +730,222 @@ class BBC {
|
|
|
722
730
|
}
|
|
723
731
|
}
|
|
724
732
|
|
|
733
|
+
// ── Context Injection Limits (issue #441) ───────────────────────────────
|
|
734
|
+
//
|
|
735
|
+
// Notice text and behavior mirrored byte-for-byte from the shared-hooks
|
|
736
|
+
// Python sub-agent context injection hook. Changing wording there requires
|
|
737
|
+
// changing it here too.
|
|
738
|
+
interface ContextInjectionLimits {
|
|
739
|
+
max_file_bytes: number;
|
|
740
|
+
max_artifact_bytes: number;
|
|
741
|
+
max_total_bytes: number;
|
|
742
|
+
}
|
|
743
|
+
const DEFAULT_CONTEXT_INJECTION_LIMITS: ContextInjectionLimits = {
|
|
744
|
+
max_file_bytes: 32768,
|
|
745
|
+
max_artifact_bytes: 65536,
|
|
746
|
+
max_total_bytes: 131072,
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
function truncateUtf8(buf: Buffer, cap: number): Buffer {
|
|
750
|
+
if (cap <= 0 || buf.length <= cap) return buf;
|
|
751
|
+
let i = cap;
|
|
752
|
+
// Back off over continuation bytes (10xxxxxx) to find the lead byte.
|
|
753
|
+
while (i > 0 && (buf[i - 1]! & 0xc0) === 0x80) i--;
|
|
754
|
+
if (i === 0) return Buffer.alloc(0);
|
|
755
|
+
const lead = buf[i - 1]!;
|
|
756
|
+
if (lead & 0x80) {
|
|
757
|
+
let seqLen = 1;
|
|
758
|
+
if ((lead & 0xe0) === 0xc0) seqLen = 2;
|
|
759
|
+
else if ((lead & 0xf0) === 0xe0) seqLen = 3;
|
|
760
|
+
else if ((lead & 0xf8) === 0xf0) seqLen = 4;
|
|
761
|
+
// Drop the lead byte too if its full sequence didn't fit.
|
|
762
|
+
if (i - 1 + seqLen > cap) i--;
|
|
763
|
+
}
|
|
764
|
+
return buf.subarray(0, i);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function stripInlineComment(value: string): string {
|
|
768
|
+
let inQuote: string | null = null;
|
|
769
|
+
for (let idx = 0; idx < value.length; idx++) {
|
|
770
|
+
const ch = value[idx]!;
|
|
771
|
+
if (inQuote) {
|
|
772
|
+
if (ch === inQuote) inQuote = null;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (ch === '"' || ch === "'") {
|
|
776
|
+
inQuote = ch;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (ch === "#" && (idx === 0 || /\s/.test(value[idx - 1]!)))
|
|
780
|
+
return value.slice(0, idx);
|
|
781
|
+
}
|
|
782
|
+
return value;
|
|
783
|
+
}
|
|
784
|
+
function unquoteYaml(s: string): string {
|
|
785
|
+
if (s.length >= 2 && s[0] === s[s.length - 1] && (s[0] === '"' || s[0] === "'"))
|
|
786
|
+
return s.slice(1, -1);
|
|
787
|
+
return s;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Line-based parser for ONLY the `context_injection:` block of
|
|
791
|
+
* `.trellis/config.yaml`. Not a general YAML parser — mirrors
|
|
792
|
+
* `common.config.get_context_injection_limits()` semantics for this
|
|
793
|
+
* section only (missing keys keep the default; invalid/negative values
|
|
794
|
+
* fall back to the default for that key). */
|
|
795
|
+
function readContextInjectionLimits(repoRoot: string): ContextInjectionLimits {
|
|
796
|
+
const limits: ContextInjectionLimits = { ...DEFAULT_CONTEXT_INJECTION_LIMITS };
|
|
797
|
+
const text = readText(join(repoRoot, ".trellis", "config.yaml"));
|
|
798
|
+
if (!text) return limits;
|
|
799
|
+
|
|
800
|
+
let inSection = false;
|
|
801
|
+
let sectionIndent = -1;
|
|
802
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
803
|
+
const trimmed = rawLine.trim();
|
|
804
|
+
if (!inSection) {
|
|
805
|
+
if (/^context_injection\s*:\s*(#.*)?$/.test(trimmed)) {
|
|
806
|
+
inSection = true;
|
|
807
|
+
sectionIndent = rawLine.length - rawLine.trimStart().length;
|
|
808
|
+
}
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
812
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
813
|
+
if (indent <= sectionIndent) break;
|
|
814
|
+
const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/);
|
|
815
|
+
if (!m) continue;
|
|
816
|
+
const key = m[1]!;
|
|
817
|
+
if (!(key in limits)) continue;
|
|
818
|
+
const raw = unquoteYaml(stripInlineComment(m[2]!).trim()).trim();
|
|
819
|
+
if (!/^-?\d+$/.test(raw)) continue; // invalid -> keep default
|
|
820
|
+
const value = parseInt(raw, 10);
|
|
821
|
+
if (value < 0) continue; // negative -> keep default
|
|
822
|
+
(limits as unknown as Record<string, number>)[key] = value;
|
|
823
|
+
}
|
|
824
|
+
return limits;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class ContextBudget {
|
|
828
|
+
used = 0;
|
|
829
|
+
constructor(private maxTotalBytes: number) {}
|
|
830
|
+
hasRoom(size: number): boolean {
|
|
831
|
+
if (this.maxTotalBytes <= 0) return true;
|
|
832
|
+
return this.used + size <= this.maxTotalBytes;
|
|
833
|
+
}
|
|
834
|
+
add(size: number): void {
|
|
835
|
+
this.used += size;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function truncateNotice(path: string, cap: number): string {
|
|
840
|
+
return `\n[Trellis: truncated at ${cap} bytes — read ${path} for the full content]`;
|
|
841
|
+
}
|
|
842
|
+
function isBinaryContent(data: Buffer): boolean {
|
|
843
|
+
return data.includes(0) || !isUtf8(data);
|
|
844
|
+
}
|
|
845
|
+
function binaryNotice(path: string, size: number, reason: string): string {
|
|
846
|
+
return `[Trellis: not inlined (binary file) — ${path} (${size} bytes): ${reason}]`;
|
|
847
|
+
}
|
|
848
|
+
function indexNotice(path: string, size: number, reason: string): string {
|
|
849
|
+
return `[Trellis: not inlined (total context limit reached) — ${path} (${size} bytes): ${reason}]`;
|
|
850
|
+
}
|
|
851
|
+
function budgetedBlock(
|
|
852
|
+
budget: ContextBudget,
|
|
853
|
+
header: string,
|
|
854
|
+
plainPath: string,
|
|
855
|
+
content: string,
|
|
856
|
+
reason: string,
|
|
857
|
+
sizeForIndex: number,
|
|
858
|
+
): string {
|
|
859
|
+
const block = `=== ${header} ===\n${content}`;
|
|
860
|
+
const blockBytes = Buffer.byteLength(block, "utf-8");
|
|
861
|
+
if (!budget.hasRoom(blockBytes)) {
|
|
862
|
+
const notice = indexNotice(plainPath, sizeForIndex, reason);
|
|
863
|
+
budget.add(Buffer.byteLength(notice, "utf-8"));
|
|
864
|
+
return notice;
|
|
865
|
+
}
|
|
866
|
+
budget.add(blockBytes);
|
|
867
|
+
return block;
|
|
868
|
+
}
|
|
869
|
+
function readFileBytes(basePath: string, filePath: string): Buffer | null {
|
|
870
|
+
const full = join(basePath, filePath);
|
|
871
|
+
try {
|
|
872
|
+
if (!statSync(full).isFile()) return null;
|
|
873
|
+
} catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
return readFileSync(full);
|
|
878
|
+
} catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function materializeFile(
|
|
883
|
+
basePath: string,
|
|
884
|
+
filePath: string,
|
|
885
|
+
reason: string,
|
|
886
|
+
limits: ContextInjectionLimits,
|
|
887
|
+
budget: ContextBudget,
|
|
888
|
+
): string | null {
|
|
889
|
+
const data = readFileBytes(basePath, filePath);
|
|
890
|
+
if (data === null) return null;
|
|
891
|
+
const size = data.length;
|
|
892
|
+
if (isBinaryContent(data)) {
|
|
893
|
+
const notice = binaryNotice(filePath, size, reason);
|
|
894
|
+
budget.add(Buffer.byteLength(notice, "utf-8"));
|
|
895
|
+
return notice;
|
|
896
|
+
}
|
|
897
|
+
const cap = limits.max_file_bytes;
|
|
898
|
+
const truncated = truncateUtf8(data, cap);
|
|
899
|
+
let content = truncated.toString("utf-8");
|
|
900
|
+
if (truncated.length < size) content += truncateNotice(filePath, cap);
|
|
901
|
+
return budgetedBlock(budget, filePath, filePath, content, reason, size);
|
|
902
|
+
}
|
|
903
|
+
function materializeArtifact(
|
|
904
|
+
basePath: string,
|
|
905
|
+
filePath: string,
|
|
906
|
+
headerLabel: string,
|
|
907
|
+
reason: string,
|
|
908
|
+
limits: ContextInjectionLimits,
|
|
909
|
+
budget: ContextBudget,
|
|
910
|
+
): string | null {
|
|
911
|
+
const data = readFileBytes(basePath, filePath);
|
|
912
|
+
if (data === null) return null;
|
|
913
|
+
const size = data.length;
|
|
914
|
+
const cap = limits.max_artifact_bytes;
|
|
915
|
+
const truncated = truncateUtf8(data, cap);
|
|
916
|
+
let content = truncated.toString("utf-8");
|
|
917
|
+
if (truncated.length < size) content += truncateNotice(filePath, cap);
|
|
918
|
+
return budgetedBlock(budget, headerLabel, filePath, content, reason, size);
|
|
919
|
+
}
|
|
920
|
+
interface JsonlEntry {
|
|
921
|
+
file: string;
|
|
922
|
+
type: string;
|
|
923
|
+
reason: string;
|
|
924
|
+
}
|
|
925
|
+
function readJsonlEntries(basePath: string, jsonlPath: string): JsonlEntry[] {
|
|
926
|
+
const text = readText(join(basePath, jsonlPath));
|
|
927
|
+
if (!text) return [];
|
|
928
|
+
const entries: JsonlEntry[] = [];
|
|
929
|
+
for (const line of text.split(/\r?\n/)) {
|
|
930
|
+
const t = line.trim();
|
|
931
|
+
if (!t) continue;
|
|
932
|
+
try {
|
|
933
|
+
const item = JSON.parse(t) as JsonObject;
|
|
934
|
+
const filePath =
|
|
935
|
+
(typeof item.file === "string" && item.file) ||
|
|
936
|
+
(typeof item.path === "string" && item.path) ||
|
|
937
|
+
"";
|
|
938
|
+
if (!filePath) continue;
|
|
939
|
+
entries.push({
|
|
940
|
+
file: filePath,
|
|
941
|
+
type: typeof item.type === "string" ? item.type : "file",
|
|
942
|
+
reason: (typeof item.reason === "string" && item.reason) || "-",
|
|
943
|
+
});
|
|
944
|
+
} catch {}
|
|
945
|
+
}
|
|
946
|
+
return entries;
|
|
947
|
+
}
|
|
948
|
+
|
|
725
949
|
// ── Trellis Context ────────────────────────────────────────────────────
|
|
726
950
|
function findRoot(start: string): string {
|
|
727
951
|
let c = resolve(start);
|
|
@@ -942,35 +1166,59 @@ function buildContext(root: string, agent: string, key: string | null): string {
|
|
|
942
1166
|
const dir = readTaskDir(root, key);
|
|
943
1167
|
if (!dir)
|
|
944
1168
|
return "No active Trellis task found. Read .trellis/ before proceeding.";
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
const
|
|
1169
|
+
const relTaskDir = relative(root, dir).replace(/\\/g, "/");
|
|
1170
|
+
const limits = readContextInjectionLimits(root);
|
|
1171
|
+
const budget = new ContextBudget(limits.max_total_bytes);
|
|
1172
|
+
|
|
1173
|
+
// 1. Curated spec/research files from {agent}.jsonl (same order, budget
|
|
1174
|
+
// processed first, matching Python's get_agent_context()).
|
|
948
1175
|
const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
|
|
949
|
-
|
|
1176
|
+
const specBlocks: string[] = [];
|
|
950
1177
|
if (jsonlName) {
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
const
|
|
954
|
-
if (
|
|
955
|
-
try {
|
|
956
|
-
const r = JSON.parse(t) as JsonObject;
|
|
957
|
-
const f = typeof r.file === "string" ? r.file : "";
|
|
958
|
-
if (f) {
|
|
959
|
-
const c = readText(join(root, f));
|
|
960
|
-
if (c) chunks.push(`## ${f}\n\n${c}`);
|
|
961
|
-
}
|
|
962
|
-
} catch {}
|
|
1178
|
+
for (const entry of readJsonlEntries(dir, jsonlName)) {
|
|
1179
|
+
if (entry.type === "directory") continue;
|
|
1180
|
+
const block = materializeFile(root, entry.file, entry.reason, limits, budget);
|
|
1181
|
+
if (block) specBlocks.push(block);
|
|
963
1182
|
}
|
|
964
|
-
spec = chunks.join("\n\n---\n\n");
|
|
965
1183
|
}
|
|
1184
|
+
const spec = specBlocks.join("\n\n");
|
|
1185
|
+
|
|
1186
|
+
// 2-4. Task artifacts, in order: prd.md -> design.md -> implement.md.
|
|
1187
|
+
const prd = materializeArtifact(
|
|
1188
|
+
root,
|
|
1189
|
+
`${relTaskDir}/prd.md`,
|
|
1190
|
+
`${relTaskDir}/prd.md (Requirements)`,
|
|
1191
|
+
"Requirements document",
|
|
1192
|
+
limits,
|
|
1193
|
+
budget,
|
|
1194
|
+
);
|
|
1195
|
+
const design = materializeArtifact(
|
|
1196
|
+
root,
|
|
1197
|
+
`${relTaskDir}/design.md`,
|
|
1198
|
+
`${relTaskDir}/design.md (Technical Design)`,
|
|
1199
|
+
"Technical design document",
|
|
1200
|
+
limits,
|
|
1201
|
+
budget,
|
|
1202
|
+
);
|
|
1203
|
+
const impl = materializeArtifact(
|
|
1204
|
+
root,
|
|
1205
|
+
`${relTaskDir}/implement.md`,
|
|
1206
|
+
`${relTaskDir}/implement.md (Execution Plan)`,
|
|
1207
|
+
"Execution plan document",
|
|
1208
|
+
limits,
|
|
1209
|
+
budget,
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
// prd/design/impl already carry their own "=== path (label) ===" header
|
|
1213
|
+
// (from materializeArtifact) — no extra "### x.md" wrapper needed, that
|
|
1214
|
+
// would just double the header.
|
|
966
1215
|
return [
|
|
967
1216
|
`## Trellis Task Context`,
|
|
968
1217
|
`Task directory: ${dir}`,
|
|
969
1218
|
"",
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
impl ? "\n### implement.md\n" + impl : "",
|
|
1219
|
+
prd ?? `(missing) ${relTaskDir}/prd.md`,
|
|
1220
|
+
design ? "\n" + design : "",
|
|
1221
|
+
impl ? "\n" + impl : "",
|
|
974
1222
|
spec ? "\n### Curated Spec / Research Context\n" + spec : "",
|
|
975
1223
|
].join("\n");
|
|
976
1224
|
}
|