@sentropic/track 0.85.15 → 0.85.17
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.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +119 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/install-skills.d.ts.map +1 -1
- package/dist/cli/install-skills.js +8 -6
- package/dist/cli/install-skills.js.map +1 -1
- package/dist/mcp/server.d.ts +25 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +30 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/read/contract.d.ts +3 -0
- package/dist/read/contract.d.ts.map +1 -1
- package/dist/read/contract.js +5 -0
- package/dist/read/contract.js.map +1 -1
- package/dist/read/index.d.ts +1 -0
- package/dist/read/index.d.ts.map +1 -1
- package/dist/read/index.js +1 -0
- package/dist/read/index.js.map +1 -1
- package/dist/report/ai-report.d.ts +122 -0
- package/dist/report/ai-report.d.ts.map +1 -0
- package/dist/report/ai-report.js +720 -0
- package/dist/report/ai-report.js.map +1 -0
- package/dist/report/build.d.ts +1 -1
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +6 -6
- package/dist/report/build.js.map +1 -1
- package/dist/report/directive.d.ts +1 -1
- package/dist/report/directive.d.ts.map +1 -1
- package/dist/report/directive.js +9 -9
- package/dist/report/directive.js.map +1 -1
- package/dist/report/index.d.ts +2 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +2 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/rollup.d.ts +1 -1
- package/dist/report/rollup.d.ts.map +1 -1
- package/dist/report/rollup.js +3 -3
- package/dist/report/rollup.js.map +1 -1
- package/dist/report/snapshot.d.ts +67 -0
- package/dist/report/snapshot.d.ts.map +1 -0
- package/dist/report/snapshot.js +203 -0
- package/dist/report/snapshot.js.map +1 -0
- package/package.json +1 -1
- package/skills/track-operation/SKILL.md +23 -18
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { closeSync, existsSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, realpathSync, rmSync, } from 'node:fs';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { canonicalize } from '../events/canonical.js';
|
|
7
|
+
import { ordinalCompare } from './snapshot.js';
|
|
8
|
+
const CONTEXT_LIMIT = 512 * 1024;
|
|
9
|
+
const SNAPSHOT_LIMIT = 256 * 1024;
|
|
10
|
+
const GIT_LIMIT = 100 * 1024;
|
|
11
|
+
const H2A_LIMIT = 128 * 1024;
|
|
12
|
+
const DOCUMENT_LIMIT = 64 * 1024;
|
|
13
|
+
const DOCUMENT_FILE_LIMIT = 32 * 1024;
|
|
14
|
+
const RESULT_LIMIT = 128 * 1024;
|
|
15
|
+
const ADAPTER_STDOUT_LIMIT = 256 * 1024;
|
|
16
|
+
const ADAPTER_STDERR_LIMIT = 16 * 1024;
|
|
17
|
+
const GIT_COMMIT_LIMIT = 50;
|
|
18
|
+
const GIT_PATH_LIMIT = 500;
|
|
19
|
+
export const REPORTER_TIMEOUT_DEFAULT_MS = 90_000;
|
|
20
|
+
export const REPORTER_TIMEOUT_MIN_MS = 1_000;
|
|
21
|
+
export const REPORTER_TIMEOUT_MAX_MS = 15 * 60_000;
|
|
22
|
+
const SOURCE_STATUSES = ['ok', 'timeout', 'unavailable', 'invalid', 'truncated'];
|
|
23
|
+
export const AI_SECTION_NAMES = [
|
|
24
|
+
'summary', 'facts', 'changes', 'activeWork', 'blockers', 'ownerDecisions', 'suggestions', 'uncertainty',
|
|
25
|
+
];
|
|
26
|
+
export class AiReportError extends Error {
|
|
27
|
+
constructor(reason) {
|
|
28
|
+
super(`AI report unavailable (${reason}) — use \`track snapshot\` or \`track report --raw\` for factual state`);
|
|
29
|
+
this.name = 'AiReportError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function byteLength(value) {
|
|
33
|
+
return Buffer.byteLength(value, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
function cleanOneLine(value) {
|
|
36
|
+
return normalizeAiText(value).replace(/\s+/gu, ' ').trim();
|
|
37
|
+
}
|
|
38
|
+
function truncateUtf8(value, max) {
|
|
39
|
+
if (byteLength(value) <= max)
|
|
40
|
+
return { text: value, truncated: false };
|
|
41
|
+
let out = '';
|
|
42
|
+
for (const scalar of value) {
|
|
43
|
+
if (byteLength(out + scalar) > max)
|
|
44
|
+
break;
|
|
45
|
+
out += scalar;
|
|
46
|
+
}
|
|
47
|
+
return { text: out, truncated: true };
|
|
48
|
+
}
|
|
49
|
+
const SECRET_PATTERNS = [
|
|
50
|
+
[/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/giu, '[REDACTED_PRIVATE_KEY]'],
|
|
51
|
+
[/\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{12,}\b/gu, '[REDACTED_TOKEN]'],
|
|
52
|
+
[/\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+/giu, '$1=[REDACTED]'],
|
|
53
|
+
[/\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/giu, 'Bearer [REDACTED]'],
|
|
54
|
+
];
|
|
55
|
+
export function redactText(value) {
|
|
56
|
+
let out = value;
|
|
57
|
+
for (const [pattern, replacement] of SECRET_PATTERNS)
|
|
58
|
+
out = out.replace(pattern, replacement);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function redactValue(value) {
|
|
62
|
+
if (typeof value === 'string')
|
|
63
|
+
return redactText(value);
|
|
64
|
+
if (Array.isArray(value))
|
|
65
|
+
return value.map((entry) => redactValue(entry));
|
|
66
|
+
if (value !== null && typeof value === 'object') {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [key, child] of Object.entries(value))
|
|
69
|
+
out[key] = redactValue(child);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
const ENV_ALLOWLIST = [
|
|
75
|
+
'PATH', 'HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'TMPDIR', 'LANG', 'LC_ALL',
|
|
76
|
+
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY', 'H2A_ROOT',
|
|
77
|
+
];
|
|
78
|
+
export function reporterEnvironment(source) {
|
|
79
|
+
const env = { TRACK_REPORT_AI_DEPTH: '1' };
|
|
80
|
+
for (const key of ENV_ALLOWLIST)
|
|
81
|
+
if (source[key] !== undefined)
|
|
82
|
+
env[key] = source[key];
|
|
83
|
+
return env;
|
|
84
|
+
}
|
|
85
|
+
function privateCwd(run) {
|
|
86
|
+
const dir = mkdtempSync(join(tmpdir(), 'track-report-'));
|
|
87
|
+
try {
|
|
88
|
+
return run(dir);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function spawnText(spawn, command, args, options) {
|
|
95
|
+
return spawn(command, [...args], {
|
|
96
|
+
cwd: options.cwd,
|
|
97
|
+
env: options.env,
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
shell: false,
|
|
100
|
+
timeout: options.timeout,
|
|
101
|
+
maxBuffer: options.maxBuffer,
|
|
102
|
+
...(options.input !== undefined ? { input: options.input } : {}),
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function within(root, candidate) {
|
|
107
|
+
const rel = relative(root, candidate);
|
|
108
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
109
|
+
}
|
|
110
|
+
function sourceFailure(status, detail) {
|
|
111
|
+
return { status, entries: [], omitted: 0, detail };
|
|
112
|
+
}
|
|
113
|
+
function gitRun(spawn, cwd, env, args) {
|
|
114
|
+
return spawnText(spawn, 'git', args, { cwd, env: { ...env, GIT_OPTIONAL_LOCKS: '0' }, timeout: 5_000, maxBuffer: 1024 * 1024 });
|
|
115
|
+
}
|
|
116
|
+
function repositoryRoot(spawn, cwd, env) {
|
|
117
|
+
const result = gitRun(spawn, cwd, env, ['rev-parse', '--show-toplevel']);
|
|
118
|
+
if (result.status === 0 && typeof result.stdout === 'string' && result.stdout.trim() !== '') {
|
|
119
|
+
return realpathSync(result.stdout.trim());
|
|
120
|
+
}
|
|
121
|
+
return realpathSync(cwd);
|
|
122
|
+
}
|
|
123
|
+
function pathRef(path) {
|
|
124
|
+
return `git:path:${path.replaceAll('\\', '/')}`;
|
|
125
|
+
}
|
|
126
|
+
function collectGit(spawn, root, env) {
|
|
127
|
+
const entries = [];
|
|
128
|
+
let omitted = 0;
|
|
129
|
+
let used = 0;
|
|
130
|
+
let truncated = false;
|
|
131
|
+
let pathCount = 0;
|
|
132
|
+
let unavailableDetail;
|
|
133
|
+
const add = (entry) => {
|
|
134
|
+
const clean = redactValue(entry);
|
|
135
|
+
const bytes = byteLength(canonicalize(clean));
|
|
136
|
+
if (used + bytes > GIT_LIMIT) {
|
|
137
|
+
omitted++;
|
|
138
|
+
truncated = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
entries.push(clean);
|
|
142
|
+
used += bytes;
|
|
143
|
+
};
|
|
144
|
+
const addPath = (entry) => {
|
|
145
|
+
if (pathCount >= GIT_PATH_LIMIT) {
|
|
146
|
+
omitted++;
|
|
147
|
+
truncated = true;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
pathCount++;
|
|
151
|
+
add(entry);
|
|
152
|
+
};
|
|
153
|
+
const markUnavailable = (detail) => {
|
|
154
|
+
unavailableDetail ??= detail;
|
|
155
|
+
};
|
|
156
|
+
const log = gitRun(spawn, root, env, ['log', '-n', String(GIT_COMMIT_LIMIT), '--pretty=format:%H%x09%s']);
|
|
157
|
+
if (log.status !== 0)
|
|
158
|
+
return sourceFailure('unavailable', 'git-log-unavailable');
|
|
159
|
+
const commits = log.stdout.split('\n').filter(Boolean);
|
|
160
|
+
const overReturnedCommits = Math.max(0, commits.length - GIT_COMMIT_LIMIT);
|
|
161
|
+
for (const [index, line] of commits.entries()) {
|
|
162
|
+
if (index >= GIT_COMMIT_LIMIT) {
|
|
163
|
+
truncated = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const tab = line.indexOf('\t');
|
|
167
|
+
const sha = tab >= 0 ? line.slice(0, tab) : line;
|
|
168
|
+
const text = tab >= 0 ? line.slice(tab + 1) : '';
|
|
169
|
+
add({ ref: `git:commit:${sha}`, kind: 'commit', sha, text });
|
|
170
|
+
}
|
|
171
|
+
// `git log -n 50` is the exact content cap; count separately so `omitted` remains an exact number,
|
|
172
|
+
// rather than a sentinel that could conceal a history with thousands of omitted commits.
|
|
173
|
+
const commitCount = gitRun(spawn, root, env, ['rev-list', '--count', 'HEAD']);
|
|
174
|
+
if (commitCount.status !== 0 || !/^\d+\s*$/u.test(commitCount.stdout)) {
|
|
175
|
+
omitted += overReturnedCommits;
|
|
176
|
+
markUnavailable('git-count-unavailable');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const omittedCommits = Math.max(overReturnedCommits, Number(commitCount.stdout.trim()) - GIT_COMMIT_LIMIT);
|
|
180
|
+
omitted += omittedCommits;
|
|
181
|
+
if (omittedCommits > 0)
|
|
182
|
+
truncated = true;
|
|
183
|
+
}
|
|
184
|
+
// Disable rename pairing so porcelain -z remains exactly one NUL-delimited record per path; otherwise
|
|
185
|
+
// the second rename path is a bare record and can be miscounted as an independent status entry.
|
|
186
|
+
const status = gitRun(spawn, root, env, ['status', '--porcelain=v1', '-z', '--untracked-files=normal', '--no-renames']);
|
|
187
|
+
if (status.status !== 0) {
|
|
188
|
+
markUnavailable('git-status-unavailable');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const record of status.stdout.split('\0').filter(Boolean)) {
|
|
192
|
+
const path = record.length > 3 ? record.slice(3) : record;
|
|
193
|
+
addPath({ ref: pathRef(path), kind: 'status', path, text: record.slice(0, 2) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const changed = gitRun(spawn, root, env, ['diff', '--name-only', '-z', '--no-ext-diff', 'HEAD']);
|
|
197
|
+
if (changed.status === 0) {
|
|
198
|
+
for (const path of changed.stdout.split('\0').filter(Boolean)) {
|
|
199
|
+
addPath({ ref: pathRef(path), kind: 'changed-path', path, text: 'worktree-change' });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
markUnavailable('git-diff-unavailable');
|
|
204
|
+
}
|
|
205
|
+
const stat = gitRun(spawn, root, env, ['diff', '--stat', '--no-ext-diff', 'HEAD']);
|
|
206
|
+
if (stat.status === 0 && stat.stdout.trim() !== '') {
|
|
207
|
+
add({ ref: 'git:diff-stat:worktree', kind: 'diff-stat', text: stat.stdout.trim() });
|
|
208
|
+
}
|
|
209
|
+
else if (stat.status !== 0) {
|
|
210
|
+
markUnavailable('git-diff-stat-unavailable');
|
|
211
|
+
}
|
|
212
|
+
if (unavailableDetail !== undefined)
|
|
213
|
+
return { status: 'unavailable', entries, omitted, detail: unavailableDetail };
|
|
214
|
+
return { status: truncated ? 'truncated' : 'ok', entries, omitted, ...(truncated ? { detail: 'git-cap' } : {}) };
|
|
215
|
+
}
|
|
216
|
+
function readPrefix(path, limit) {
|
|
217
|
+
const fd = openSync(path, 'r');
|
|
218
|
+
try {
|
|
219
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
220
|
+
const count = readSync(fd, buffer, 0, buffer.length, 0);
|
|
221
|
+
return { text: buffer.subarray(0, Math.min(count, limit)).toString('utf8'), truncated: count > limit };
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
closeSync(fd);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function collectDocuments(root) {
|
|
228
|
+
const candidates = [
|
|
229
|
+
['README.md', 'readme'], ['README', 'readme'], ['AGENTS.md', 'agents'], ['BRANCH.md', 'branch'],
|
|
230
|
+
];
|
|
231
|
+
const entries = [];
|
|
232
|
+
let used = 0;
|
|
233
|
+
let omitted = 0;
|
|
234
|
+
let truncated = false;
|
|
235
|
+
for (const [name, kind] of candidates) {
|
|
236
|
+
const path = join(root, name);
|
|
237
|
+
if (!existsSync(path))
|
|
238
|
+
continue;
|
|
239
|
+
const stat = lstatSync(path);
|
|
240
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
241
|
+
return sourceFailure('invalid', 'document-not-regular');
|
|
242
|
+
const real = realpathSync(path);
|
|
243
|
+
if (!within(root, real))
|
|
244
|
+
return sourceFailure('invalid', 'document-outside-repository');
|
|
245
|
+
const read = readPrefix(real, DOCUMENT_FILE_LIMIT);
|
|
246
|
+
const clipped = truncateUtf8(redactText(read.text), Math.max(0, DOCUMENT_LIMIT - used));
|
|
247
|
+
if (clipped.text === '' && read.text !== '') {
|
|
248
|
+
omitted++;
|
|
249
|
+
truncated = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const entry = {
|
|
253
|
+
ref: `doc:${name}:chunk:1`, kind, path: name, chunk: 1, text: clipped.text, untrusted: true,
|
|
254
|
+
};
|
|
255
|
+
entries.push(entry);
|
|
256
|
+
used += byteLength(clipped.text);
|
|
257
|
+
if (read.truncated || clipped.truncated)
|
|
258
|
+
truncated = true;
|
|
259
|
+
}
|
|
260
|
+
return { status: truncated ? 'truncated' : 'ok', entries, omitted, ...(truncated ? { detail: 'document-cap' } : {}) };
|
|
261
|
+
}
|
|
262
|
+
function collectH2a(spawn, root, env) {
|
|
263
|
+
return privateCwd((cwd) => {
|
|
264
|
+
const result = spawnText(spawn, 'h2a', ['report-context', '--workspace-root', root], {
|
|
265
|
+
cwd, env, timeout: 5_000, maxBuffer: H2A_LIMIT,
|
|
266
|
+
});
|
|
267
|
+
if (result.error !== undefined) {
|
|
268
|
+
const timedOut = result.error.code === 'ETIMEDOUT';
|
|
269
|
+
return sourceFailure(timedOut ? 'timeout' : 'unavailable', timedOut ? 'h2a-timeout' : 'h2a-unavailable');
|
|
270
|
+
}
|
|
271
|
+
if (byteLength(result.stderr) > 16 * 1024)
|
|
272
|
+
return sourceFailure('invalid', 'h2a-stderr-cap');
|
|
273
|
+
if (result.status !== 0)
|
|
274
|
+
return sourceFailure('unavailable', 'h2a-nonzero');
|
|
275
|
+
try {
|
|
276
|
+
const decoded = JSON.parse(result.stdout);
|
|
277
|
+
if (!isRecord(decoded) || Object.keys(decoded).sort(ordinalCompare).join(',') !== 'entries,omitted,schema,storeRoot,workspaceRoot' ||
|
|
278
|
+
decoded['schema'] !== 'h2a.report-context/v1' || typeof decoded['storeRoot'] !== 'string' ||
|
|
279
|
+
typeof decoded['workspaceRoot'] !== 'string' || !Array.isArray(decoded['entries']) ||
|
|
280
|
+
!Number.isInteger(decoded['omitted']) || Number(decoded['omitted']) < 0) {
|
|
281
|
+
return sourceFailure('invalid', 'h2a-invalid-envelope');
|
|
282
|
+
}
|
|
283
|
+
if (!isAbsolute(decoded['storeRoot']) || !isAbsolute(decoded['workspaceRoot'])) {
|
|
284
|
+
return sourceFailure('invalid', 'h2a-root-not-absolute');
|
|
285
|
+
}
|
|
286
|
+
let storeRoot;
|
|
287
|
+
let envelopeRoot;
|
|
288
|
+
try {
|
|
289
|
+
storeRoot = realpathSync(decoded['storeRoot']);
|
|
290
|
+
envelopeRoot = realpathSync(decoded['workspaceRoot']);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return sourceFailure('invalid', 'h2a-root-invalid');
|
|
294
|
+
}
|
|
295
|
+
if (envelopeRoot !== root)
|
|
296
|
+
return sourceFailure('invalid', 'h2a-workspace-root-mismatch');
|
|
297
|
+
const expectedStorePath = env['H2A_ROOT'] ?? join(env['HOME'] ?? homedir(), 'h2a-workspace', '.h2a');
|
|
298
|
+
let expectedStoreRoot;
|
|
299
|
+
try {
|
|
300
|
+
expectedStoreRoot = realpathSync(expectedStorePath);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return sourceFailure('invalid', 'h2a-expected-store-root-invalid');
|
|
304
|
+
}
|
|
305
|
+
if (storeRoot !== expectedStoreRoot)
|
|
306
|
+
return sourceFailure('invalid', 'h2a-store-root-mismatch');
|
|
307
|
+
const raw = decoded['entries'];
|
|
308
|
+
const entries = [];
|
|
309
|
+
let used = 0;
|
|
310
|
+
let omitted = Number(decoded['omitted']);
|
|
311
|
+
const refs = new Set();
|
|
312
|
+
for (const value of raw) {
|
|
313
|
+
if (!isRecord(value) || Object.keys(value).sort(ordinalCompare).join(',') !== 'kind,ref,text,workspace') {
|
|
314
|
+
return sourceFailure('invalid', 'h2a-invalid-entry');
|
|
315
|
+
}
|
|
316
|
+
const ref = value['ref'];
|
|
317
|
+
const kind = value['kind'];
|
|
318
|
+
const workspace = value['workspace'];
|
|
319
|
+
const text = value['text'];
|
|
320
|
+
if (typeof ref !== 'string' || !ref.startsWith('h2a:') || typeof workspace !== 'string' || typeof text !== 'string' ||
|
|
321
|
+
!['loop', 'session', 'blockage', 'inbox-metadata'].includes(String(kind))) {
|
|
322
|
+
return sourceFailure('invalid', 'h2a-invalid-entry');
|
|
323
|
+
}
|
|
324
|
+
if (refs.has(ref))
|
|
325
|
+
return sourceFailure('invalid', 'h2a-duplicate-ref');
|
|
326
|
+
refs.add(ref);
|
|
327
|
+
if (!isAbsolute(workspace))
|
|
328
|
+
return sourceFailure('invalid', 'h2a-workspace-not-absolute');
|
|
329
|
+
let workspaceReal;
|
|
330
|
+
try {
|
|
331
|
+
workspaceReal = realpathSync(workspace);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return sourceFailure('invalid', 'h2a-workspace-invalid');
|
|
335
|
+
}
|
|
336
|
+
if (!within(root, workspaceReal))
|
|
337
|
+
return sourceFailure('invalid', 'h2a-cross-workspace');
|
|
338
|
+
const entry = { ref, kind: kind, workspace: workspaceReal, text: redactText(text) };
|
|
339
|
+
const bytes = byteLength(canonicalize(entry));
|
|
340
|
+
if (entries.length >= 100 || used + bytes > H2A_LIMIT) {
|
|
341
|
+
omitted++;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
entries.push(entry);
|
|
345
|
+
used += bytes;
|
|
346
|
+
}
|
|
347
|
+
entries.sort((a, b) => ordinalCompare(a.ref, b.ref));
|
|
348
|
+
return { status: omitted > 0 ? 'truncated' : 'ok', entries, omitted, ...(omitted > 0 ? { detail: 'h2a-cap' } : {}) };
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return sourceFailure('invalid', 'h2a-invalid-json');
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function wpRefs(nodes, out) {
|
|
356
|
+
for (const node of nodes) {
|
|
357
|
+
out.push({ ref: `track:wp:${node.id}`, kind: 'workpackage', state: 'fact' });
|
|
358
|
+
wpRefs(node.children, out);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function referencesOf(snapshot, git, h2a, documents) {
|
|
362
|
+
const refs = [];
|
|
363
|
+
for (const bucket of Object.values(snapshot.report.buckets)) {
|
|
364
|
+
for (const row of bucket)
|
|
365
|
+
refs.push({ ref: `track:item:${row.id}`, kind: 'item', state: 'fact' });
|
|
366
|
+
}
|
|
367
|
+
for (const decision of snapshot.report.decisions ?? []) {
|
|
368
|
+
refs.push({
|
|
369
|
+
ref: `track:decision:${decision.id}`,
|
|
370
|
+
kind: 'decision',
|
|
371
|
+
state: decision.outcome === 'pending' ? 'open' : 'closed',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
wpRefs(snapshot.report.wpTree ?? [], refs);
|
|
375
|
+
for (const event of snapshot.recentEvents) {
|
|
376
|
+
refs.push({ ref: `track:event:${event.position}`, kind: 'event', state: 'fact' });
|
|
377
|
+
// SnapshotV1 intentionally carries no separate blocker table. The allowlisted recent-event projection
|
|
378
|
+
// still exposes stable blocker refs for blocker events inside its 200-event window; older blockers remain
|
|
379
|
+
// citeable through their affected item/decision, never through a fabricated blocker ref.
|
|
380
|
+
if (event.kind === 'blocker.opened' || event.kind === 'blocker.resolved') {
|
|
381
|
+
refs.push({ ref: `track:blocker:${event.aggregateId}`, kind: 'blocker', state: 'fact' });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const entry of git.entries)
|
|
385
|
+
refs.push({ ref: entry.ref, kind: entry.kind, state: 'fact' });
|
|
386
|
+
for (const entry of h2a.entries)
|
|
387
|
+
refs.push({ ref: entry.ref, kind: entry.kind, state: 'fact' });
|
|
388
|
+
for (const entry of documents.entries)
|
|
389
|
+
refs.push({ ref: entry.ref, kind: entry.kind, state: 'fact' });
|
|
390
|
+
for (const [name, source] of [['git', git], ['h2a', h2a], ['documents', documents]]) {
|
|
391
|
+
refs.push({ ref: `source:${name}`, kind: 'source-status', state: source.status === 'ok' ? 'fact' : 'degraded' });
|
|
392
|
+
}
|
|
393
|
+
const unique = new Map();
|
|
394
|
+
for (const ref of refs)
|
|
395
|
+
if (!unique.has(ref.ref))
|
|
396
|
+
unique.set(ref.ref, ref);
|
|
397
|
+
return [...unique.values()].sort((a, b) => ordinalCompare(a.ref, b.ref));
|
|
398
|
+
}
|
|
399
|
+
export function buildReportContext(options, deps = {}) {
|
|
400
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
401
|
+
const sourceEnv = options.env ?? process.env;
|
|
402
|
+
const childEnv = reporterEnvironment(sourceEnv);
|
|
403
|
+
const root = repositoryRoot(spawn, options.cwd, childEnv);
|
|
404
|
+
const snapshot = redactValue(options.reader.snapshot({
|
|
405
|
+
baselineInput: options.request.baselineInput,
|
|
406
|
+
resolvedCommit: options.request.baselineCommit,
|
|
407
|
+
requireAccepted: options.request.requireAccepted,
|
|
408
|
+
}));
|
|
409
|
+
if (byteLength(canonicalize(snapshot)) > SNAPSHOT_LIMIT)
|
|
410
|
+
throw new AiReportError('snapshot-cap');
|
|
411
|
+
const git = collectGit(spawn, root, childEnv);
|
|
412
|
+
const h2a = collectH2a(spawn, root, childEnv);
|
|
413
|
+
const documents = collectDocuments(root);
|
|
414
|
+
const body = {
|
|
415
|
+
schema: 'track.ai-report.context-body/v1',
|
|
416
|
+
request: options.request,
|
|
417
|
+
workspace: { repoRoot: root },
|
|
418
|
+
track: { snapshot },
|
|
419
|
+
git,
|
|
420
|
+
h2a,
|
|
421
|
+
documents,
|
|
422
|
+
references: referencesOf(snapshot, git, h2a, documents),
|
|
423
|
+
};
|
|
424
|
+
const contextBytes = canonicalize(body);
|
|
425
|
+
if (byteLength(contextBytes) > CONTEXT_LIMIT)
|
|
426
|
+
throw new AiReportError('context-cap');
|
|
427
|
+
const contextDigest = createHash('sha256').update(contextBytes, 'utf8').digest('hex');
|
|
428
|
+
return { schema: 'track.ai-report.context-envelope/v1', context: body, contextDigest };
|
|
429
|
+
}
|
|
430
|
+
function configPath(env) {
|
|
431
|
+
const xdg = env['XDG_CONFIG_HOME'];
|
|
432
|
+
const home = env['HOME'];
|
|
433
|
+
const base = xdg !== undefined && xdg.length > 0 && isAbsolute(xdg)
|
|
434
|
+
? xdg
|
|
435
|
+
: join(home !== undefined && home.length > 0 && isAbsolute(home) ? home : homedir(), '.config');
|
|
436
|
+
return join(base, 'track', 'report-ai.json');
|
|
437
|
+
}
|
|
438
|
+
export function resolveReporterConfig(env = process.env) {
|
|
439
|
+
if (env['TRACK_REPORT_AI_DEPTH'] !== undefined && env['TRACK_REPORT_AI_DEPTH'] !== '0') {
|
|
440
|
+
throw new AiReportError('recursive-adapter');
|
|
441
|
+
}
|
|
442
|
+
const envRaw = env['TRACK_REPORT_AI_ARGV'];
|
|
443
|
+
const path = configPath(env);
|
|
444
|
+
let raw;
|
|
445
|
+
let fromEnvironment = false;
|
|
446
|
+
if (envRaw !== undefined) {
|
|
447
|
+
raw = envRaw;
|
|
448
|
+
fromEnvironment = true;
|
|
449
|
+
}
|
|
450
|
+
else if (existsSync(path)) {
|
|
451
|
+
try {
|
|
452
|
+
raw = readFileSync(path, 'utf8');
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
throw new AiReportError('unreadable-configuration');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (raw === undefined)
|
|
459
|
+
throw new AiReportError('missing-configuration');
|
|
460
|
+
let value;
|
|
461
|
+
try {
|
|
462
|
+
value = JSON.parse(raw);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
throw new AiReportError('invalid-configuration-json');
|
|
466
|
+
}
|
|
467
|
+
if (fromEnvironment && !Array.isArray(value))
|
|
468
|
+
throw new AiReportError('invalid-configuration-env-shape');
|
|
469
|
+
if (!fromEnvironment && (!isRecord(value) || !Object.hasOwn(value, 'argv') ||
|
|
470
|
+
!['argv', 'argv,timeoutMs'].includes(Object.keys(value).sort().join(',')))) {
|
|
471
|
+
throw new AiReportError('invalid-configuration-file-shape');
|
|
472
|
+
}
|
|
473
|
+
const argv = fromEnvironment ? value : value['argv'];
|
|
474
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
|
|
475
|
+
throw new AiReportError('invalid-configuration-argv');
|
|
476
|
+
}
|
|
477
|
+
const configuredTimeout = !fromEnvironment && isRecord(value) && Object.hasOwn(value, 'timeoutMs')
|
|
478
|
+
? value['timeoutMs']
|
|
479
|
+
: REPORTER_TIMEOUT_DEFAULT_MS;
|
|
480
|
+
if (!Number.isInteger(configuredTimeout) || Number(configuredTimeout) < REPORTER_TIMEOUT_MIN_MS ||
|
|
481
|
+
Number(configuredTimeout) > REPORTER_TIMEOUT_MAX_MS) {
|
|
482
|
+
throw new AiReportError('invalid-configuration-timeout');
|
|
483
|
+
}
|
|
484
|
+
return { argv: argv, timeoutMs: Number(configuredTimeout) };
|
|
485
|
+
}
|
|
486
|
+
export function resolveReporterArgv(env = process.env) {
|
|
487
|
+
return resolveReporterConfig(env).argv;
|
|
488
|
+
}
|
|
489
|
+
function isRecord(value) {
|
|
490
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
491
|
+
}
|
|
492
|
+
function exactKeys(value, allowed, label) {
|
|
493
|
+
const actual = Object.keys(value).sort();
|
|
494
|
+
const expected = [...allowed].sort();
|
|
495
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
496
|
+
throw new AiReportError(`invalid-${label}-shape`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function assertMaxDepth(value, depth = 0) {
|
|
500
|
+
if (depth > 16)
|
|
501
|
+
throw new AiReportError('result-depth-cap');
|
|
502
|
+
if (Array.isArray(value)) {
|
|
503
|
+
for (const child of value)
|
|
504
|
+
assertMaxDepth(child, depth + 1);
|
|
505
|
+
}
|
|
506
|
+
else if (isRecord(value)) {
|
|
507
|
+
for (const child of Object.values(value))
|
|
508
|
+
assertMaxDepth(child, depth + 1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function stringField(value, label, optional = false) {
|
|
512
|
+
if (value === undefined && optional)
|
|
513
|
+
return undefined;
|
|
514
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('\uFFFD'))
|
|
515
|
+
throw new AiReportError(`invalid-${label}`);
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
function adapterMetadataField(value, label, optional = false) {
|
|
519
|
+
const raw = stringField(value, label, optional);
|
|
520
|
+
if (raw === undefined)
|
|
521
|
+
return undefined;
|
|
522
|
+
if ([...raw].length > 128)
|
|
523
|
+
throw new AiReportError(`invalid-${label}-cap`);
|
|
524
|
+
const normalized = cleanOneLine(raw);
|
|
525
|
+
if (normalized.length === 0)
|
|
526
|
+
throw new AiReportError(`invalid-${label}-normalized`);
|
|
527
|
+
return normalized;
|
|
528
|
+
}
|
|
529
|
+
export function normalizeAiText(value) {
|
|
530
|
+
const ansi = value.replace(/\u001b(?:\[[0-?]*[ -\/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/gu, '');
|
|
531
|
+
return ansi
|
|
532
|
+
.replace(/\r\n?/gu, '\n')
|
|
533
|
+
.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/gu, '')
|
|
534
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
535
|
+
.trim();
|
|
536
|
+
}
|
|
537
|
+
function validateResult(raw, envelope) {
|
|
538
|
+
if (byteLength(raw) > RESULT_LIMIT)
|
|
539
|
+
throw new AiReportError('result-cap');
|
|
540
|
+
if (raw.includes('\uFFFD'))
|
|
541
|
+
throw new AiReportError('invalid-result-utf8');
|
|
542
|
+
let decoded;
|
|
543
|
+
try {
|
|
544
|
+
decoded = JSON.parse(raw);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
throw new AiReportError('invalid-result-json');
|
|
548
|
+
}
|
|
549
|
+
assertMaxDepth(decoded);
|
|
550
|
+
if (!isRecord(decoded))
|
|
551
|
+
throw new AiReportError('invalid-result-object');
|
|
552
|
+
exactKeys(decoded, ['schema', 'adapter', 'sections'], 'result');
|
|
553
|
+
if (decoded['schema'] !== 'track.ai-report.result/v1')
|
|
554
|
+
throw new AiReportError('invalid-result-schema');
|
|
555
|
+
if (!isRecord(decoded['adapter']))
|
|
556
|
+
throw new AiReportError('invalid-adapter');
|
|
557
|
+
const adapterRaw = decoded['adapter'];
|
|
558
|
+
const adapterKeys = Object.keys(adapterRaw);
|
|
559
|
+
if (!adapterKeys.every((key) => ['provider', 'model', 'effort', 'resolvedModel', 'identity'].includes(key)) ||
|
|
560
|
+
!['provider', 'model', 'identity'].every((key) => adapterKeys.includes(key))) {
|
|
561
|
+
throw new AiReportError('invalid-adapter-shape');
|
|
562
|
+
}
|
|
563
|
+
if (adapterRaw['identity'] !== 'adapter-reported')
|
|
564
|
+
throw new AiReportError('invalid-adapter-identity');
|
|
565
|
+
const provider = adapterMetadataField(adapterRaw['provider'], 'adapter-provider');
|
|
566
|
+
const model = adapterMetadataField(adapterRaw['model'], 'adapter-model');
|
|
567
|
+
const effort = adapterMetadataField(adapterRaw['effort'], 'adapter-effort', true);
|
|
568
|
+
const resolvedModel = adapterMetadataField(adapterRaw['resolvedModel'], 'adapter-resolved-model', true);
|
|
569
|
+
if (!isRecord(decoded['sections']))
|
|
570
|
+
throw new AiReportError('invalid-sections');
|
|
571
|
+
exactKeys(decoded['sections'], AI_SECTION_NAMES, 'sections');
|
|
572
|
+
const refs = new Map(envelope.context.references.map((ref) => [ref.ref, ref]));
|
|
573
|
+
const seenIds = new Set();
|
|
574
|
+
const sections = {};
|
|
575
|
+
for (const name of AI_SECTION_NAMES) {
|
|
576
|
+
const values = decoded['sections'][name];
|
|
577
|
+
if (!Array.isArray(values) || values.length > 20)
|
|
578
|
+
throw new AiReportError(`invalid-section-${name}`);
|
|
579
|
+
sections[name] = values.map((value) => {
|
|
580
|
+
if (!isRecord(value))
|
|
581
|
+
throw new AiReportError(`invalid-entry-${name}`);
|
|
582
|
+
exactKeys(value, ['id', 'text', 'citations'], `entry-${name}`);
|
|
583
|
+
const id = stringField(value['id'], 'entry-id');
|
|
584
|
+
if (seenIds.has(id))
|
|
585
|
+
throw new AiReportError('duplicate-entry-id');
|
|
586
|
+
seenIds.add(id);
|
|
587
|
+
const rawText = stringField(value['text'], 'entry-text');
|
|
588
|
+
if ([...rawText].length > 1_000)
|
|
589
|
+
throw new AiReportError('entry-text-cap');
|
|
590
|
+
if (!Array.isArray(value['citations']) || value['citations'].length < 1 || value['citations'].length > 8) {
|
|
591
|
+
throw new AiReportError('invalid-entry-citations');
|
|
592
|
+
}
|
|
593
|
+
const citations = value['citations'].map((citation) => {
|
|
594
|
+
if (!isRecord(citation))
|
|
595
|
+
throw new AiReportError('invalid-citation');
|
|
596
|
+
exactKeys(citation, ['ref'], 'citation');
|
|
597
|
+
const ref = stringField(citation['ref'], 'citation-ref');
|
|
598
|
+
if (!refs.has(ref))
|
|
599
|
+
throw new AiReportError('forged-citation');
|
|
600
|
+
return { ref };
|
|
601
|
+
});
|
|
602
|
+
if (name === 'ownerDecisions') {
|
|
603
|
+
const decisionCitations = citations.filter((citation) => refs.get(citation.ref)?.kind === 'decision');
|
|
604
|
+
if (decisionCitations.length === 0 || decisionCitations.some((citation) => refs.get(citation.ref)?.state !== 'open')) {
|
|
605
|
+
throw new AiReportError('closed-owner-decision-citation');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const text = normalizeAiText(rawText);
|
|
609
|
+
if (text.length === 0)
|
|
610
|
+
throw new AiReportError('empty-normalized-entry-text');
|
|
611
|
+
return { id, text, citations };
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
schema: 'track.ai-report.result/v1',
|
|
616
|
+
adapter: {
|
|
617
|
+
provider,
|
|
618
|
+
model,
|
|
619
|
+
...(effort !== undefined ? { effort } : {}),
|
|
620
|
+
...(resolvedModel !== undefined ? { resolvedModel } : {}),
|
|
621
|
+
identity: 'adapter-reported',
|
|
622
|
+
},
|
|
623
|
+
sections,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const SECTION_LABELS = {
|
|
627
|
+
summary: 'SUMMARY', facts: 'FACTS', changes: 'RECENT CHANGES', activeWork: 'ACTIVE WORK', blockers: 'BLOCKERS',
|
|
628
|
+
ownerDecisions: 'OWNER DECISIONS', suggestions: 'AI SUGGESTIONS', uncertainty: 'UNCERTAINTY',
|
|
629
|
+
};
|
|
630
|
+
function markdownEscape(value) {
|
|
631
|
+
return cleanOneLine(value).replace(/([\\`*_{}\[\]()#+\-.!|<>~])/gu, '\\$1');
|
|
632
|
+
}
|
|
633
|
+
function htmlEscape(value) {
|
|
634
|
+
return value.replace(/&/gu, '&').replace(/</gu, '<').replace(/>/gu, '>').replace(/"/gu, '"').replace(/'/gu, ''');
|
|
635
|
+
}
|
|
636
|
+
function citations(entry, escape) {
|
|
637
|
+
return entry.citations.map((citation) => escape(citation.ref)).join(', ');
|
|
638
|
+
}
|
|
639
|
+
function metadata(result, envelope) {
|
|
640
|
+
const adapter = result.adapter;
|
|
641
|
+
const degraded = ['git', 'h2a', 'documents']
|
|
642
|
+
.filter((key) => envelope.context[key].status !== 'ok')
|
|
643
|
+
.map((key) => `${key}:${envelope.context[key].status}`);
|
|
644
|
+
return [
|
|
645
|
+
`adapter-reported: ${adapter.provider}/${adapter.model}${adapter.resolvedModel !== undefined ? ` -> ${adapter.resolvedModel}` : ''}${adapter.effort !== undefined ? ` (${adapter.effort})` : ''}`,
|
|
646
|
+
`contextDigest: ${envelope.contextDigest}`,
|
|
647
|
+
`degraded sources: ${degraded.length > 0 ? degraded.join(', ') : 'none'}`,
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
export function renderAiReport(result, envelope, format, width = 100) {
|
|
651
|
+
if (format === 'html') {
|
|
652
|
+
const sections = AI_SECTION_NAMES.map((name) => {
|
|
653
|
+
const items = result.sections[name].map((entry) => `<li><span class="track-ai-text">${htmlEscape(cleanOneLine(entry.text))}</span> ` +
|
|
654
|
+
`<cite>[${htmlEscape(citations(entry, cleanOneLine))}]</cite></li>`).join('');
|
|
655
|
+
return `<section class="track-ai-section" data-section="${htmlEscape(name)}"><h2>${htmlEscape(SECTION_LABELS[name])}</h2><ul>${items}</ul></section>`;
|
|
656
|
+
}).join('');
|
|
657
|
+
const meta = metadata(result, envelope).map((line) => `<li>${htmlEscape(line)}</li>`).join('');
|
|
658
|
+
return `<article class="track-ai-report" data-kind="ai-report"><header><h1>Track AI report</h1><p>AI-prepared interpretation</p><ul>${meta}</ul></header>${sections}</article>\n`;
|
|
659
|
+
}
|
|
660
|
+
if (format === 'inline') {
|
|
661
|
+
const cap = Math.min(240, Math.max(40, width));
|
|
662
|
+
const truncate = (line) => line.length <= cap ? line : `${line.slice(0, Math.max(0, cap - 1))}…`;
|
|
663
|
+
const lines = ['TRACK AI REPORT', ...metadata(result, envelope).map(truncate)];
|
|
664
|
+
for (const name of AI_SECTION_NAMES) {
|
|
665
|
+
const entries = result.sections[name];
|
|
666
|
+
if (entries.length === 0)
|
|
667
|
+
continue;
|
|
668
|
+
lines.push(truncate(`${SECTION_LABELS[name]}:`));
|
|
669
|
+
for (const entry of entries.slice(0, 2))
|
|
670
|
+
lines.push(truncate(`- ${cleanOneLine(entry.text)} [${citations(entry, cleanOneLine)}]`));
|
|
671
|
+
if (entries.length > 2)
|
|
672
|
+
lines.push(truncate(`- +${entries.length - 2} omitted`));
|
|
673
|
+
}
|
|
674
|
+
return `${lines.join('\n')}\n`;
|
|
675
|
+
}
|
|
676
|
+
const md = format === 'md';
|
|
677
|
+
const escape = md ? markdownEscape : cleanOneLine;
|
|
678
|
+
const lines = [md ? '# Track AI report' : 'TRACK AI REPORT', 'AI-prepared interpretation', ...metadata(result, envelope).map(escape), ''];
|
|
679
|
+
for (const name of AI_SECTION_NAMES) {
|
|
680
|
+
lines.push(md ? `## ${SECTION_LABELS[name]}` : SECTION_LABELS[name]);
|
|
681
|
+
const entries = result.sections[name];
|
|
682
|
+
if (entries.length === 0)
|
|
683
|
+
lines.push('- none');
|
|
684
|
+
for (const entry of entries)
|
|
685
|
+
lines.push(`- ${escape(entry.text)} [${citations(entry, escape)}]`);
|
|
686
|
+
lines.push('');
|
|
687
|
+
}
|
|
688
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
689
|
+
}
|
|
690
|
+
export function generateAiReport(options, deps = {}) {
|
|
691
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
692
|
+
const sourceEnv = options.env ?? process.env;
|
|
693
|
+
const reporter = resolveReporterConfig(sourceEnv);
|
|
694
|
+
const argv = reporter.argv;
|
|
695
|
+
const envelope = buildReportContext(options, { spawn });
|
|
696
|
+
const input = canonicalize(envelope);
|
|
697
|
+
const childEnv = reporterEnvironment(sourceEnv);
|
|
698
|
+
const result = privateCwd((cwd) => spawnText(spawn, argv[0], argv.slice(1), {
|
|
699
|
+
cwd,
|
|
700
|
+
env: childEnv,
|
|
701
|
+
timeout: reporter.timeoutMs,
|
|
702
|
+
maxBuffer: ADAPTER_STDOUT_LIMIT,
|
|
703
|
+
input,
|
|
704
|
+
}));
|
|
705
|
+
if (byteLength(result.stderr) > ADAPTER_STDERR_LIMIT)
|
|
706
|
+
throw new AiReportError('adapter-stderr-cap');
|
|
707
|
+
if (result.error !== undefined) {
|
|
708
|
+
const code = result.error.code;
|
|
709
|
+
throw new AiReportError(code === 'ETIMEDOUT' ? 'adapter-timeout' : code === 'ENOBUFS' ? 'adapter-output-cap' : 'adapter-spawn');
|
|
710
|
+
}
|
|
711
|
+
if (result.status !== 0)
|
|
712
|
+
throw new AiReportError('adapter-nonzero');
|
|
713
|
+
const validated = validateResult(result.stdout, envelope);
|
|
714
|
+
return {
|
|
715
|
+
output: renderAiReport(validated, envelope, options.request.format, options.width),
|
|
716
|
+
envelope,
|
|
717
|
+
result: validated,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=ai-report.js.map
|