trellis 1.0.8 → 2.0.6

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -83
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Decision Auto-Capture Middleware
3
+ *
4
+ * Wraps MCP tool handlers to automatically emit vcs:decisionRecord ops
5
+ * for every tool invocation. Pre/post hooks can enrich the trace with
6
+ * rationale, alternatives, and prompt context.
7
+ */
8
+
9
+ import type { HookRegistry } from './hooks.js';
10
+ import type { DecisionInput, DecisionContext } from './types.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** A generic MCP tool handler: receives params, returns a result. */
17
+ export type ToolHandler = (params: Record<string, unknown>) => Promise<unknown>;
18
+
19
+ /** Callback invoked after auto-capture builds the DecisionInput. */
20
+ export type DecisionRecorder = (decision: DecisionInput) => Promise<void>;
21
+
22
+ export interface AutoCaptureOpts {
23
+ /** The hook registry for pre/post enrichment. */
24
+ hooks: HookRegistry;
25
+ /** Called to persist the decision as a VcsOp. */
26
+ recorder: DecisionRecorder;
27
+ /** Tool names to exclude from auto-capture (e.g. read-only queries). */
28
+ exclude?: Set<string>;
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Wrapper
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Wrap an MCP tool handler for automatic decision trace capture.
37
+ */
38
+ export function wrapToolHandler(
39
+ toolName: string,
40
+ handler: ToolHandler,
41
+ opts: AutoCaptureOpts,
42
+ ): ToolHandler {
43
+ return async (params: Record<string, unknown>) => {
44
+ // Skip excluded tools
45
+ if (opts.exclude?.has(toolName)) {
46
+ return handler(params);
47
+ }
48
+
49
+ // Run pre-hooks
50
+ const preContext: DecisionContext = await opts.hooks.runPreHooks(
51
+ toolName,
52
+ params,
53
+ );
54
+
55
+ // Execute the actual tool
56
+ const result = await handler(params);
57
+
58
+ // Run post-hooks
59
+ const enrichment = await opts.hooks.runPostHooks(
60
+ toolName,
61
+ params,
62
+ result,
63
+ preContext,
64
+ );
65
+
66
+ // Build the decision input
67
+ const decision: DecisionInput = {
68
+ toolName,
69
+ input: sanitizeInput(params),
70
+ outputSummary: summarize(result),
71
+ context: preContext.prompt ?? preContext.conversationId,
72
+ rationale: enrichment.rationale,
73
+ alternatives: enrichment.alternatives,
74
+ confidence: enrichment.confidence,
75
+ relatedEntities: enrichment.relatedEntities,
76
+ custom: {
77
+ ...preContext.custom,
78
+ ...enrichment.custom,
79
+ agentModel: preContext.agentModel,
80
+ },
81
+ };
82
+
83
+ // Record asynchronously — don't block tool response
84
+ opts.recorder(decision).catch(() => {
85
+ // Silently ignore recording failures
86
+ });
87
+
88
+ return result;
89
+ };
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Helpers
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Remove potentially large/sensitive fields from tool input before storing.
98
+ */
99
+ function sanitizeInput(
100
+ params: Record<string, unknown>,
101
+ ): Record<string, unknown> {
102
+ const sanitized: Record<string, unknown> = {};
103
+ for (const [key, value] of Object.entries(params)) {
104
+ if (typeof value === 'string' && value.length > 2000) {
105
+ sanitized[key] = value.slice(0, 2000) + '…';
106
+ } else {
107
+ sanitized[key] = value;
108
+ }
109
+ }
110
+ return sanitized;
111
+ }
112
+
113
+ /**
114
+ * Summarize a tool result to a concise string (max 500 chars).
115
+ */
116
+ function summarize(result: unknown): string {
117
+ if (result === null || result === undefined) return '';
118
+
119
+ // MCP-style { content: [{ type: 'text', text: '...' }] }
120
+ if (typeof result === 'object' && result !== null && 'content' in result) {
121
+ const content = (result as any).content;
122
+ if (Array.isArray(content)) {
123
+ const texts = content
124
+ .filter((c: any) => c.type === 'text')
125
+ .map((c: any) => c.text)
126
+ .join('\n');
127
+ return texts.length > 500 ? texts.slice(0, 500) + '…' : texts;
128
+ }
129
+ }
130
+
131
+ const str =
132
+ typeof result === 'string' ? result : JSON.stringify(result, null, 0);
133
+ return str.length > 500 ? str.slice(0, 500) + '…' : str;
134
+ }
135
+
136
+ export { summarize as _summarizeForTest };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Decision Hook Registry
3
+ *
4
+ * External agent harnesses register pre/post hooks to enrich decision traces
5
+ * with rationale, alternatives, prompt context, etc.
6
+ */
7
+
8
+ import type {
9
+ DecisionPreHook,
10
+ DecisionPostHook,
11
+ DecisionContext,
12
+ DecisionEnrichment,
13
+ } from './types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Hook Registry
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export class HookRegistry {
20
+ private preHooks: DecisionPreHook[] = [];
21
+ private postHooks: DecisionPostHook[] = [];
22
+
23
+ /**
24
+ * Register a pre-hook that runs before a tool handler.
25
+ */
26
+ registerPreHook(hook: DecisionPreHook): void {
27
+ this.preHooks.push(hook);
28
+ }
29
+
30
+ /**
31
+ * Register a post-hook that runs after a tool handler.
32
+ */
33
+ registerPostHook(hook: DecisionPostHook): void {
34
+ this.postHooks.push(hook);
35
+ }
36
+
37
+ /**
38
+ * Remove a pre-hook by name.
39
+ */
40
+ removePreHook(name: string): void {
41
+ this.preHooks = this.preHooks.filter((h) => h.name !== name);
42
+ }
43
+
44
+ /**
45
+ * Remove a post-hook by name.
46
+ */
47
+ removePostHook(name: string): void {
48
+ this.postHooks = this.postHooks.filter((h) => h.name !== name);
49
+ }
50
+
51
+ /**
52
+ * Get all pre-hooks matching a tool name.
53
+ */
54
+ getPreHooks(toolName: string): DecisionPreHook[] {
55
+ return this.preHooks.filter((h) => matchesPattern(h.toolPattern, toolName));
56
+ }
57
+
58
+ /**
59
+ * Get all post-hooks matching a tool name.
60
+ */
61
+ getPostHooks(toolName: string): DecisionPostHook[] {
62
+ return this.postHooks.filter((h) =>
63
+ matchesPattern(h.toolPattern, toolName),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Run all matching pre-hooks and merge their contexts.
69
+ */
70
+ async runPreHooks(
71
+ toolName: string,
72
+ input: Record<string, unknown>,
73
+ ): Promise<DecisionContext> {
74
+ const hooks = this.getPreHooks(toolName);
75
+ const merged: DecisionContext = {};
76
+
77
+ for (const hook of hooks) {
78
+ try {
79
+ const ctx = await hook.handler(toolName, input);
80
+ Object.assign(merged, ctx);
81
+ if (ctx.custom) {
82
+ merged.custom = { ...merged.custom, ...ctx.custom };
83
+ }
84
+ } catch {
85
+ // Hooks should not break tool execution
86
+ }
87
+ }
88
+
89
+ return merged;
90
+ }
91
+
92
+ /**
93
+ * Run all matching post-hooks and merge their enrichments.
94
+ */
95
+ async runPostHooks(
96
+ toolName: string,
97
+ input: Record<string, unknown>,
98
+ output: unknown,
99
+ preContext: DecisionContext,
100
+ ): Promise<DecisionEnrichment> {
101
+ const hooks = this.getPostHooks(toolName);
102
+ const merged: DecisionEnrichment = {};
103
+
104
+ for (const hook of hooks) {
105
+ try {
106
+ const enrichment = await hook.handler(
107
+ toolName,
108
+ input,
109
+ output,
110
+ preContext,
111
+ );
112
+ if (enrichment.rationale) merged.rationale = enrichment.rationale;
113
+ if (enrichment.alternatives)
114
+ merged.alternatives = enrichment.alternatives;
115
+ if (enrichment.confidence !== undefined)
116
+ merged.confidence = enrichment.confidence;
117
+ if (enrichment.relatedEntities) {
118
+ merged.relatedEntities = [
119
+ ...(merged.relatedEntities ?? []),
120
+ ...enrichment.relatedEntities,
121
+ ];
122
+ }
123
+ if (enrichment.custom) {
124
+ merged.custom = { ...merged.custom, ...enrichment.custom };
125
+ }
126
+ } catch {
127
+ // Hooks should not break tool execution
128
+ }
129
+ }
130
+
131
+ return merged;
132
+ }
133
+
134
+ /**
135
+ * Clear all hooks.
136
+ */
137
+ clear(): void {
138
+ this.preHooks = [];
139
+ this.postHooks = [];
140
+ }
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Match a tool name against a glob-like pattern or RegExp.
149
+ * Supports `*` as a wildcard in string patterns (e.g. "trellis_issue_*").
150
+ */
151
+ function matchesPattern(pattern: string | RegExp, toolName: string): boolean {
152
+ if (pattern instanceof RegExp) {
153
+ return pattern.test(toolName);
154
+ }
155
+ if (pattern === '*') return true;
156
+ if (!pattern.includes('*')) return pattern === toolName;
157
+
158
+ // Convert glob to regex: escape special chars, replace * with .*
159
+ const escaped = pattern
160
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
161
+ .replace(/\*/g, '.*');
162
+ return new RegExp(`^${escaped}$`).test(toolName);
163
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Decision Traces Module
3
+ *
4
+ * Auto-captures agent decision traces from MCP tool invocations as
5
+ * first-class Decision entities in the EAV store.
6
+ *
7
+ * Public API:
8
+ * - recordDecision() — persist a decision as a vcs:decisionRecord op
9
+ * - queryDecisions() — filter/list decisions from the EAV store
10
+ * - getDecisionChain() — all decisions that affected a given entity
11
+ * - HookRegistry — pre/post hook registration for external harnesses
12
+ * - wrapToolHandler() — MCP middleware for auto-capture
13
+ */
14
+
15
+ export { HookRegistry } from './hooks.js';
16
+ export {
17
+ wrapToolHandler,
18
+ type ToolHandler,
19
+ type DecisionRecorder,
20
+ type AutoCaptureOpts,
21
+ } from './auto-capture.js';
22
+ export type {
23
+ Decision,
24
+ DecisionInput,
25
+ DecisionFilter,
26
+ DecisionContext,
27
+ DecisionEnrichment,
28
+ DecisionPreHook,
29
+ DecisionPostHook,
30
+ } from './types.js';
31
+
32
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
33
+ import { join, dirname } from 'path';
34
+ import { createVcsOp } from '../vcs/ops.js';
35
+ import { decisionEntityId } from '../vcs/types.js';
36
+ import type { VcsOp } from '../vcs/types.js';
37
+ import type { EngineContext } from '../vcs/engine-context.js';
38
+ import type { Decision, DecisionInput, DecisionFilter } from './types.js';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Sequential Decision ID (file-based, matching issue counter pattern)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function getDecisionCounterPath(rootPath: string): string {
45
+ return join(rootPath, '.trellis', 'decision-counter.json');
46
+ }
47
+
48
+ function nextDecisionId(rootPath: string): string {
49
+ const counterPath = getDecisionCounterPath(rootPath);
50
+ let counter = 0;
51
+ if (existsSync(counterPath)) {
52
+ try {
53
+ counter = JSON.parse(readFileSync(counterPath, 'utf-8')).counter ?? 0;
54
+ } catch {}
55
+ }
56
+ counter++;
57
+ const dir = dirname(counterPath);
58
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
59
+ writeFileSync(counterPath, JSON.stringify({ counter }, null, 2));
60
+ return `DEC-${counter}`;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Record
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Record a decision trace as a vcs:decisionRecord op.
69
+ */
70
+ export async function recordDecision(
71
+ ctx: EngineContext,
72
+ rootPath: string,
73
+ input: DecisionInput,
74
+ ): Promise<VcsOp> {
75
+ const id = nextDecisionId(rootPath);
76
+
77
+ const op = await createVcsOp('vcs:decisionRecord', {
78
+ agentId: ctx.agentId,
79
+ previousHash: ctx.getLastOp()?.hash,
80
+ vcs: {
81
+ decisionId: id,
82
+ decisionToolName: input.toolName,
83
+ decisionToolInput: input.input ? JSON.stringify(input.input) : undefined,
84
+ decisionToolOutput: input.outputSummary,
85
+ decisionContext: input.context,
86
+ decisionRationale: input.rationale,
87
+ decisionAlternatives: input.alternatives
88
+ ? JSON.stringify(input.alternatives)
89
+ : undefined,
90
+ },
91
+ });
92
+ ctx.applyOp(op);
93
+
94
+ // Store related entity links
95
+ if (input.relatedEntities) {
96
+ const eid = decisionEntityId(id);
97
+ const links = input.relatedEntities.map((target) => ({
98
+ e1: eid,
99
+ a: 'relatedTo',
100
+ e2: target,
101
+ }));
102
+ if (links.length > 0) {
103
+ ctx.store.addLinks(links);
104
+ }
105
+ }
106
+
107
+ return op;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Query
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Build a Decision from EAV facts for a given entity ID.
116
+ */
117
+ function buildDecision(ctx: EngineContext, entityId: string): Decision {
118
+ const facts = ctx.store.getFactsByEntity(entityId);
119
+ const get = (a: string) => {
120
+ const matches = facts.filter((f) => f.a === a);
121
+ return matches.length > 0
122
+ ? (matches[matches.length - 1].v as string)
123
+ : undefined;
124
+ };
125
+
126
+ // Gather related entity links
127
+ const links = ctx.store.getLinksByAttribute('relatedTo');
128
+ const related = links.filter((l) => l.e1 === entityId).map((l) => l.e2);
129
+
130
+ const bareId = entityId.replace(/^decision:/, '');
131
+
132
+ const alternativesRaw = get('alternatives');
133
+ let alternatives: string[] | undefined;
134
+ if (alternativesRaw) {
135
+ try {
136
+ alternatives = JSON.parse(alternativesRaw);
137
+ } catch {
138
+ alternatives = [alternativesRaw];
139
+ }
140
+ }
141
+
142
+ const confidenceRaw = get('confidence');
143
+ const confidence =
144
+ confidenceRaw !== undefined ? parseFloat(confidenceRaw) : undefined;
145
+
146
+ return {
147
+ id: bareId,
148
+ toolName: get('toolName') ?? '',
149
+ outputSummary: get('outputSummary'),
150
+ context: get('context'),
151
+ rationale: get('rationale'),
152
+ alternatives,
153
+ confidence,
154
+ createdAt: get('createdAt'),
155
+ createdBy: get('createdBy'),
156
+ relatedEntities: related,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Query decisions with optional filters.
162
+ */
163
+ export function queryDecisions(
164
+ ctx: EngineContext,
165
+ filter?: DecisionFilter,
166
+ ): Decision[] {
167
+ const decisionFacts = ctx.store
168
+ .getFactsByAttribute('type')
169
+ .filter((f) => f.v === 'Decision');
170
+
171
+ let decisions = decisionFacts.map((f) => buildDecision(ctx, f.e));
172
+
173
+ if (filter?.toolPattern) {
174
+ const pattern = filter.toolPattern;
175
+ const regex = pattern.includes('*')
176
+ ? new RegExp(
177
+ `^${pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`,
178
+ )
179
+ : null;
180
+ decisions = decisions.filter((d) =>
181
+ regex ? regex.test(d.toolName) : d.toolName === pattern,
182
+ );
183
+ }
184
+
185
+ if (filter?.agentId) {
186
+ decisions = decisions.filter((d) => d.createdBy === filter.agentId);
187
+ }
188
+
189
+ if (filter?.since) {
190
+ const since = new Date(filter.since).getTime();
191
+ decisions = decisions.filter(
192
+ (d) => d.createdAt && new Date(d.createdAt).getTime() >= since,
193
+ );
194
+ }
195
+
196
+ if (filter?.until) {
197
+ const until = new Date(filter.until).getTime();
198
+ decisions = decisions.filter(
199
+ (d) => d.createdAt && new Date(d.createdAt).getTime() <= until,
200
+ );
201
+ }
202
+
203
+ if (filter?.entityId) {
204
+ decisions = decisions.filter((d) =>
205
+ d.relatedEntities.includes(filter.entityId!),
206
+ );
207
+ }
208
+
209
+ // Sort by createdAt descending (newest first)
210
+ decisions.sort((a, b) => {
211
+ if (!a.createdAt || !b.createdAt) return 0;
212
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
213
+ });
214
+
215
+ if (filter?.limit && filter.limit > 0) {
216
+ decisions = decisions.slice(0, filter.limit);
217
+ }
218
+
219
+ return decisions;
220
+ }
221
+
222
+ /**
223
+ * Get all decisions that affected a given entity (the decision chain).
224
+ */
225
+ export function getDecisionChain(
226
+ ctx: EngineContext,
227
+ entityId: string,
228
+ ): Decision[] {
229
+ // Find all decision entities linked to this entity
230
+ const allLinks = ctx.store.getLinksByAttribute('relatedTo');
231
+ const decisionEids = new Set(
232
+ allLinks
233
+ .filter((l) => l.e2 === entityId)
234
+ .map((l) => l.e1)
235
+ .filter((e) => e.startsWith('decision:')),
236
+ );
237
+
238
+ const decisions = Array.from(decisionEids).map((eid) =>
239
+ buildDecision(ctx, eid),
240
+ );
241
+
242
+ // Sort chronologically
243
+ decisions.sort((a, b) => {
244
+ if (!a.createdAt || !b.createdAt) return 0;
245
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
246
+ });
247
+
248
+ return decisions;
249
+ }
250
+
251
+ /**
252
+ * Get a single decision by ID.
253
+ */
254
+ export function getDecision(ctx: EngineContext, id: string): Decision | null {
255
+ const eid = decisionEntityId(id);
256
+ const typeFact = ctx.store
257
+ .getFactsByEntity(eid)
258
+ .find((f) => f.a === 'type' && f.v === 'Decision');
259
+ if (!typeFact) return null;
260
+ return buildDecision(ctx, eid);
261
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Decision Trace Types
3
+ *
4
+ * Types for auto-captured agent decision traces. Decisions are first-class
5
+ * entities in the EAV store, emitted automatically from MCP tool calls
6
+ * and enrichable via pre/post hooks.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Decision Entity
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface Decision {
14
+ id: string;
15
+ toolName: string;
16
+ input?: Record<string, unknown>;
17
+ outputSummary?: string;
18
+ context?: string;
19
+ rationale?: string;
20
+ alternatives?: string[];
21
+ confidence?: number;
22
+ createdAt?: string;
23
+ createdBy?: string;
24
+ /** IDs of related entities (issues, files, milestones) affected by this decision */
25
+ relatedEntities: string[];
26
+ custom?: Record<string, unknown>;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Decision Input (for recording)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export interface DecisionInput {
34
+ toolName: string;
35
+ input?: Record<string, unknown>;
36
+ outputSummary?: string;
37
+ context?: string;
38
+ rationale?: string;
39
+ alternatives?: string[];
40
+ confidence?: number;
41
+ /** Entity IDs this decision relates to (e.g. "issue:TRL-5") */
42
+ relatedEntities?: string[];
43
+ custom?: Record<string, unknown>;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Decision Filters (for querying)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface DecisionFilter {
51
+ /** Filter by MCP tool name (glob-like, e.g. "trellis_issue_*") */
52
+ toolPattern?: string;
53
+ /** Filter by agent ID */
54
+ agentId?: string;
55
+ /** Only decisions after this ISO timestamp */
56
+ since?: string;
57
+ /** Only decisions before this ISO timestamp */
58
+ until?: string;
59
+ /** Only decisions referencing this entity */
60
+ entityId?: string;
61
+ /** Max results */
62
+ limit?: number;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Hook Types
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export interface DecisionContext {
70
+ prompt?: string;
71
+ conversationId?: string;
72
+ agentModel?: string;
73
+ custom?: Record<string, unknown>;
74
+ }
75
+
76
+ export interface DecisionEnrichment {
77
+ rationale?: string;
78
+ alternatives?: string[];
79
+ confidence?: number;
80
+ relatedEntities?: string[];
81
+ custom?: Record<string, unknown>;
82
+ }
83
+
84
+ export interface DecisionPreHook {
85
+ name: string;
86
+ /** Which tools to intercept — string (glob) or RegExp */
87
+ toolPattern: string | RegExp;
88
+ handler: (
89
+ toolName: string,
90
+ input: Record<string, unknown>,
91
+ ) => Promise<DecisionContext>;
92
+ }
93
+
94
+ export interface DecisionPostHook {
95
+ name: string;
96
+ toolPattern: string | RegExp;
97
+ handler: (
98
+ toolName: string,
99
+ input: Record<string, unknown>,
100
+ output: unknown,
101
+ preContext: DecisionContext,
102
+ ) => Promise<DecisionEnrichment>;
103
+ }