@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.
Files changed (91) hide show
  1. package/dist/cli/index.js +1 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/channel/agent-loader.d.ts +2 -2
  4. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  5. package/dist/commands/channel/agent-loader.js +9 -5
  6. package/dist/commands/channel/agent-loader.js.map +1 -1
  7. package/dist/commands/channel/context-loader.d.ts +1 -1
  8. package/dist/commands/channel/context-loader.d.ts.map +1 -1
  9. package/dist/commands/channel/context-loader.js +36 -15
  10. package/dist/commands/channel/context-loader.js.map +1 -1
  11. package/dist/commands/channel/context-trust.d.ts +31 -0
  12. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  13. package/dist/commands/channel/context-trust.js +139 -0
  14. package/dist/commands/channel/context-trust.js.map +1 -0
  15. package/dist/commands/channel/spawn.d.ts.map +1 -1
  16. package/dist/commands/channel/spawn.js +4 -2
  17. package/dist/commands/channel/spawn.js.map +1 -1
  18. package/dist/commands/init.d.ts +1 -0
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/update.d.ts.map +1 -1
  22. package/dist/commands/update.js +17 -0
  23. package/dist/commands/update.js.map +1 -1
  24. package/dist/configurators/codex.d.ts +36 -0
  25. package/dist/configurators/codex.d.ts.map +1 -1
  26. package/dist/configurators/codex.js +100 -1
  27. package/dist/configurators/codex.js.map +1 -1
  28. package/dist/configurators/index.d.ts.map +1 -1
  29. package/dist/configurators/index.js +5 -0
  30. package/dist/configurators/index.js.map +1 -1
  31. package/dist/configurators/shared.d.ts.map +1 -1
  32. package/dist/configurators/shared.js +1 -0
  33. package/dist/configurators/shared.js.map +1 -1
  34. package/dist/configurators/snow.d.ts +31 -0
  35. package/dist/configurators/snow.d.ts.map +1 -0
  36. package/dist/configurators/snow.js +95 -0
  37. package/dist/configurators/snow.js.map +1 -0
  38. package/dist/configurators/workflow.d.ts +14 -0
  39. package/dist/configurators/workflow.d.ts.map +1 -1
  40. package/dist/configurators/workflow.js +37 -1
  41. package/dist/configurators/workflow.js.map +1 -1
  42. package/dist/migrations/manifests/0.6.10.json +9 -0
  43. package/dist/migrations/manifests/0.6.9.json +9 -0
  44. package/dist/templates/codex/agents/trellis-check.toml +7 -3
  45. package/dist/templates/codex/agents/trellis-implement.toml +7 -3
  46. package/dist/templates/codex/agents/trellis-research.toml +13 -6
  47. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -1
  48. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  49. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +4 -2
  50. package/dist/templates/kimi/agents/trellis-research.md +7 -3
  51. package/dist/templates/omp/extensions/trellis/index.ts.txt +114 -4
  52. package/dist/templates/opencode/lib/trellis-context.js +263 -43
  53. package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
  54. package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
  55. package/dist/templates/pi/extensions/trellis/index.ts.txt +270 -22
  56. package/dist/templates/shared-hooks/inject-subagent-context.py +311 -83
  57. package/dist/templates/shared-hooks/inject-workflow-state.py +36 -1
  58. package/dist/templates/snow/SNOW.md +86 -0
  59. package/dist/templates/snow/agents/trellis-check.md +135 -0
  60. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  61. package/dist/templates/snow/agents/trellis-research.md +114 -0
  62. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  63. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  64. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  65. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  66. package/dist/templates/snow/index.d.ts +26 -0
  67. package/dist/templates/snow/index.d.ts.map +1 -0
  68. package/dist/templates/snow/index.js +36 -0
  69. package/dist/templates/snow/index.js.map +1 -0
  70. package/dist/templates/template-utils.d.ts.map +1 -1
  71. package/dist/templates/template-utils.js +5 -1
  72. package/dist/templates/template-utils.js.map +1 -1
  73. package/dist/templates/trellis/config.yaml +46 -0
  74. package/dist/templates/trellis/gitattributes.txt +9 -0
  75. package/dist/templates/trellis/index.d.ts +3 -1
  76. package/dist/templates/trellis/index.d.ts.map +1 -1
  77. package/dist/templates/trellis/index.js +3 -1
  78. package/dist/templates/trellis/index.js.map +1 -1
  79. package/dist/templates/trellis/scripts/add_session.py +94 -28
  80. package/dist/templates/trellis/scripts/common/active_task.py +9 -2
  81. package/dist/templates/trellis/scripts/common/config.py +89 -2
  82. package/dist/templates/trellis/scripts/common/task_context.py +84 -6
  83. package/dist/templates/trellis/scripts/common/task_store.py +63 -1
  84. package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
  85. package/dist/templates/trellis/scripts/task.py +21 -2
  86. package/dist/templates/trellis/workflow.md +14 -14
  87. package/dist/types/ai-tools.d.ts +3 -3
  88. package/dist/types/ai-tools.d.ts.map +1 -1
  89. package/dist/types/ai-tools.js +38 -0
  90. package/dist/types/ai-tools.js.map +1 -1
  91. 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 { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
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 prd = readText(join(dir, "prd.md"));
946
- const design = readText(join(dir, "design.md"));
947
- const impl = readText(join(dir, "implement.md"));
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
- let spec = "";
1176
+ const specBlocks: string[] = [];
950
1177
  if (jsonlName) {
951
- const chunks: string[] = [];
952
- for (const line of readText(join(dir, jsonlName)).split(/\r?\n/)) {
953
- const t = line.trim();
954
- if (!t) continue;
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
- "### prd.md",
971
- prd || "(missing)",
972
- design ? "\n### design.md\n" + design : "",
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
  }