@jjanczur/tyran 0.1.0
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/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +22 -0
- package/CHANGELOG.md +245 -0
- package/LICENSE +201 -0
- package/README.md +468 -0
- package/agents/.gitkeep +0 -0
- package/agents/implementer.md +60 -0
- package/agents/retro.md +88 -0
- package/agents/reviewer.md +55 -0
- package/agents/scout.md +60 -0
- package/bin/tyran.mjs +172 -0
- package/hooks/HOOK-CONTRACT-MEASURED.md +377 -0
- package/hooks/hooks.json +83 -0
- package/hooks/scripts/.gitkeep +0 -0
- package/hooks/scripts/evidence-gate.mjs +705 -0
- package/hooks/scripts/hook-io.mjs +813 -0
- package/hooks/scripts/policy-gate.mjs +1402 -0
- package/hooks/scripts/pre-compact.mjs +211 -0
- package/hooks/scripts/retro-gate.mjs +191 -0
- package/hooks/scripts/secrets-gate.mjs +1683 -0
- package/hooks/scripts/session-start.mjs +358 -0
- package/hooks/scripts/write-guard.mjs +475 -0
- package/package.json +52 -0
- package/scripts/desc-budget.mjs +139 -0
- package/scripts/doctor.mjs +1267 -0
- package/scripts/hooks-check.mjs +1312 -0
- package/scripts/invisible.mjs +346 -0
- package/scripts/journal.mjs +747 -0
- package/scripts/project.mjs +981 -0
- package/scripts/scan-control-chars.mjs +547 -0
- package/scripts/scan-repo.mjs +287 -0
- package/scripts/schema.mjs +467 -0
- package/scripts/stop-check.mjs +89 -0
- package/scripts/tiers.mjs +324 -0
- package/scripts/yaml-lite.mjs +383 -0
- package/skills/browser-check/SKILL.md +118 -0
- package/skills/code-review/SKILL.md +83 -0
- package/skills/deslop/SKILL.md +97 -0
- package/skills/doctor/SKILL.md +35 -0
- package/skills/fidelity-gate/SKILL.md +132 -0
- package/skills/hello/SKILL.md +16 -0
- package/skills/pr-feedback/SKILL.md +85 -0
- package/skills/prompt-tuning/SKILL.md +102 -0
- package/skills/retro/SKILL.md +69 -0
- package/skills/root-cause/SKILL.md +89 -0
- package/skills/run/SKILL.md +285 -0
- package/skills/setup/SKILL.md +79 -0
- package/skills/skill-writing/SKILL.md +99 -0
- package/skills/status/SKILL.md +31 -0
- package/templates/.gitkeep +0 -0
- package/templates/config.yaml +44 -0
- package/templates/knowledge.yaml +23 -0
- package/templates/policies/autonomy.yaml +61 -0
- package/templates/project-command/SKILL.md +16 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session-start — the probe that hands a resumed session its own state back.
|
|
4
|
+
*
|
|
5
|
+
* A conductor who reopens a repo two days later has no memory of it, and the
|
|
6
|
+
* state that would tell them is spread across a journal, two projections, a
|
|
7
|
+
* doctor run and a lock directory. This injects the two-kilobyte version:
|
|
8
|
+
* where we are, what to do next, what is still open, and who is still
|
|
9
|
+
* holding what.
|
|
10
|
+
*
|
|
11
|
+
* It is a PROBE, not a gate, and the distinction is enforced in the runtime
|
|
12
|
+
* rather than in this comment: `SessionStart` has no way to refuse anything
|
|
13
|
+
* (ADR-22), so nothing here may be a check. Every failure — no state, an
|
|
14
|
+
* unreadable journal, a doctor that will not run — degrades to a shorter
|
|
15
|
+
* message or to nothing at all, and the session starts anyway. Costing a
|
|
16
|
+
* user their session to report that a summary was unavailable would be a
|
|
17
|
+
* worse bug than the missing summary.
|
|
18
|
+
*/
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
21
|
+
import { cpus, totalmem } from 'node:os';
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { checkHooks } from '../../scripts/hooks-check.mjs';
|
|
26
|
+
import { readJournal } from '../../scripts/journal.mjs';
|
|
27
|
+
import { fold, inlinePlain, progressLine } from '../../scripts/project.mjs';
|
|
28
|
+
import { field, main, runProbe } from './hook-io.mjs';
|
|
29
|
+
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const DOCTOR = resolve(HERE, '..', '..', 'scripts', 'doctor.mjs');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* This probe's own budget, deliberately far below the `timeout` its
|
|
35
|
+
* hooks.json entry declares (ADR-22 point 2). A unit test reads both numbers
|
|
36
|
+
* and refuses to let them cross; the relation is not maintained by anyone
|
|
37
|
+
* remembering it.
|
|
38
|
+
*/
|
|
39
|
+
export const DEADLINE_MS = 4000;
|
|
40
|
+
|
|
41
|
+
/** Doctor gets the smaller half: it is one of several things that can hang. */
|
|
42
|
+
export const DOCTOR_TIMEOUT_MS = 2000;
|
|
43
|
+
|
|
44
|
+
/** Working target for the injection. The platform's hard ceiling is 10 000. */
|
|
45
|
+
export const CONTEXT_BUDGET = 2000;
|
|
46
|
+
|
|
47
|
+
/** Keep the summary skimmable; long lists are what makes context unread. */
|
|
48
|
+
const MAX_ROWS = 5;
|
|
49
|
+
const MAX_STEPS = 3;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Which directory the host repo lives in.
|
|
53
|
+
*
|
|
54
|
+
* `cwd` comes from the platform, but it is still input: it is used to build
|
|
55
|
+
* filesystem paths and a child-process argument, so it is checked for shape
|
|
56
|
+
* (absolute, existing, a directory) instead of trusted. It never reaches a
|
|
57
|
+
* shell — the doctor call below uses execFile with an argument vector, so
|
|
58
|
+
* even a path full of shell metacharacters is inert.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveRepoRoot(input, env = process.env, cwd = process.cwd()) {
|
|
61
|
+
const candidates = [field(input, 'cwd'), env.CLAUDE_PROJECT_DIR, cwd];
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (typeof candidate !== 'string' || candidate === '' || !isAbsolute(candidate)) continue;
|
|
64
|
+
try {
|
|
65
|
+
if (statSync(candidate).isDirectory()) return candidate;
|
|
66
|
+
} catch {
|
|
67
|
+
/* try the next candidate */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Cores and memory, so the conductor sizes its team to the machine it is on. */
|
|
74
|
+
export function hardwareLine() {
|
|
75
|
+
const cores = cpus().length;
|
|
76
|
+
const gib = Math.round(totalmem() / 1024 ** 3);
|
|
77
|
+
// A rough ceiling, not a promise: heavy agents are memory-bound long
|
|
78
|
+
// before they are core-bound, so both numbers are shown and the smaller
|
|
79
|
+
// of the two derived limits wins.
|
|
80
|
+
const ceiling = Math.max(1, Math.min(Math.floor(cores / 2), Math.floor(gib / 6)));
|
|
81
|
+
return `${cores} cores · ${gib} GiB RAM · suggested parallel heavy agents: ${ceiling}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Doctor's verdict for this repo.
|
|
86
|
+
*
|
|
87
|
+
* `--now` is passed on purpose and is load-bearing. Without it doctor takes
|
|
88
|
+
* its reference clock from each journal's own last event, so an agent that
|
|
89
|
+
* died three days ago is compared against the timestamp of its own spawn and
|
|
90
|
+
* is never stale — the dead-agent check would exist and never fire. A unit
|
|
91
|
+
* test pins the flag into the argument vector for exactly that reason.
|
|
92
|
+
*
|
|
93
|
+
* Exit 1 means "findings", which is the normal case, not an error.
|
|
94
|
+
*/
|
|
95
|
+
export function runDoctor(stateDir, nowIso, { exec = execFileSync } = {}) {
|
|
96
|
+
const args = [DOCTOR, '--state', '--json', '--dir', stateDir, '--now', nowIso];
|
|
97
|
+
let stdout;
|
|
98
|
+
try {
|
|
99
|
+
stdout = exec(process.execPath, args, {
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
timeout: DOCTOR_TIMEOUT_MS,
|
|
102
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// status 1 is "doctor found something", and its report is on stdout.
|
|
107
|
+
if (err && err.status === 1 && typeof err.stdout === 'string') stdout = err.stdout;
|
|
108
|
+
else return { available: false, reason: err?.message ?? 'doctor did not run' };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(stdout);
|
|
112
|
+
return { available: true, counts: parsed.counts, findings: parsed.findings ?? [] };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return { available: false, reason: `doctor output was not JSON: ${err.message}` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Every initiative under `.tyran/state`, folded, worst failures swallowed. */
|
|
119
|
+
export function readInitiatives(stateDir) {
|
|
120
|
+
const root = join(stateDir, 'state');
|
|
121
|
+
let names;
|
|
122
|
+
try {
|
|
123
|
+
names = readdirSync(root).sort();
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const name of names) {
|
|
129
|
+
const journal = join(root, name, 'journal.jsonl');
|
|
130
|
+
try {
|
|
131
|
+
if (!statSync(join(root, name)).isDirectory()) continue;
|
|
132
|
+
const read = readJournal(journal);
|
|
133
|
+
out.push({ name, state: fold(read), events: read.events.length });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
out.push({ name, state: null, error: err.message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function rows(list, render) {
|
|
142
|
+
const lines = list.slice(0, MAX_ROWS).map(render);
|
|
143
|
+
if (list.length > MAX_ROWS) lines.push(`- ... and ${list.length - MAX_ROWS} more`);
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render the summary. Pure, so the whole shape of the injected context is
|
|
149
|
+
* testable without a filesystem, a clock or a child process.
|
|
150
|
+
*
|
|
151
|
+
* EVERY journal-derived value goes through `inlinePlain`. This text is
|
|
152
|
+
* injected straight into the conductor's context — it is the LAST step of the
|
|
153
|
+
* attack path ADR-19 describes (foreign repo -> subagent report -> journal ->
|
|
154
|
+
* projection -> conductor), so it is the place where a raw byte costs the most.
|
|
155
|
+
*
|
|
156
|
+
* It did not do this until a security review measured it: 400 hostile trees
|
|
157
|
+
* put 35 819 invisible codepoints into this function's output, 400 of 400
|
|
158
|
+
* leaking, with "IGNORE PRIOR" reconstructable from the TAG characters. The
|
|
159
|
+
* process was safe only because hook-io sanitizes one floor up — and that is
|
|
160
|
+
* caller discipline, the mechanism class this project distrusts on principle
|
|
161
|
+
* and rejects by name in journal.mjs and doctor.mjs. Worse than redundant
|
|
162
|
+
* defence: there was ZERO here and ONE at the consumer, and no test at this
|
|
163
|
+
* level held any of it.
|
|
164
|
+
*
|
|
165
|
+
* Sanitizing here also restores the budget below. `fitBudget` measured the
|
|
166
|
+
* text BEFORE hook-io expanded it, so a hostile journal produced 9 880
|
|
167
|
+
* characters of injected context against a 2 000-character budget — 4.9x over,
|
|
168
|
+
* 120 characters under the platform's hard ceiling. Escaping first means the
|
|
169
|
+
* length fitBudget sees is the length that ships.
|
|
170
|
+
*/
|
|
171
|
+
export function renderContext({ repoRoot, initiatives, doctor, hardware, nowIso }) {
|
|
172
|
+
if (initiatives.length === 0) return '';
|
|
173
|
+
const lines = [
|
|
174
|
+
'## Tyran state (injected by the session-start probe)',
|
|
175
|
+
'',
|
|
176
|
+
`Repo: ${inlinePlain(repoRoot)} · as of ${inlinePlain(nowIso)}`,
|
|
177
|
+
`Machine: ${inlinePlain(hardware)}`,
|
|
178
|
+
'',
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const { name, state, error } of initiatives) {
|
|
182
|
+
lines.push(`### Initiative \`${inlinePlain(name)}\``);
|
|
183
|
+
if (state === null) {
|
|
184
|
+
lines.push(`- journal unreadable: ${inlinePlain(error)}`);
|
|
185
|
+
lines.push('');
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
lines.push(`- ${progressLine(state)}`);
|
|
189
|
+
if (state.checkpoint) {
|
|
190
|
+
lines.push(
|
|
191
|
+
`- Checkpoint: ${inlinePlain(state.checkpoint.phase ?? '(no phase)')} at ${inlinePlain(state.checkpoint.ts)} by ${inlinePlain(state.checkpoint.actor)}`,
|
|
192
|
+
);
|
|
193
|
+
const steps = Array.isArray(state.checkpoint.nextSteps) ? state.checkpoint.nextSteps : [];
|
|
194
|
+
if (steps.length > 0) {
|
|
195
|
+
lines.push(`- First ${Math.min(MAX_STEPS, steps.length)} step(s) on resume:`);
|
|
196
|
+
steps.slice(0, MAX_STEPS).forEach((step, i) => lines.push(` ${i + 1}. ${inlinePlain(step)}`));
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
lines.push('- No checkpoint yet — this initiative has never been paused deliberately.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const openGates = state.openGates ?? [];
|
|
203
|
+
if (openGates.length > 0) {
|
|
204
|
+
lines.push(`- Open gates (${openGates.length}):`);
|
|
205
|
+
lines.push(...rows(openGates, (g) => ` - ${inlinePlain(g.kind)}: ${inlinePlain(g.result ?? 'open')} (${inlinePlain(g.ts)})`));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const leases = [...(state.leases?.values() ?? [])];
|
|
209
|
+
if (leases.length > 0) {
|
|
210
|
+
lines.push(`- Open leases (${leases.length}) — do NOT touch these resources:`);
|
|
211
|
+
lines.push(...rows(leases, (l) => ` - ${inlinePlain(l.resource)} held by ${inlinePlain(l.holder ?? '(unknown)')} since ${inlinePlain(l.ts)}`));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const working = (state.agents ?? []).filter((a) => a.status === 'running');
|
|
215
|
+
if (working.length > 0) {
|
|
216
|
+
lines.push(`- Agents the journal still believes are working (${working.length}):`);
|
|
217
|
+
lines.push(
|
|
218
|
+
...rows(working, (a) => ` - ${inlinePlain(a.agent)} (${inlinePlain(a.role ?? 'no role')}) since ${inlinePlain(a.spawnTs ?? '?')}`),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (doctor.available) {
|
|
225
|
+
const c = doctor.counts ?? { error: 0, warning: 0, info: 0 };
|
|
226
|
+
lines.push(`### Doctor: ${c.error} error(s) · ${c.warning} warning(s) · ${c.info} info`);
|
|
227
|
+
const loud = (doctor.findings ?? []).filter((f) => f.severity !== 'info');
|
|
228
|
+
lines.push(...rows(loud, (f) => `- [${inlinePlain(f.code)}] ${inlinePlain(f.where)}`));
|
|
229
|
+
if (loud.length === 0) lines.push('- nothing above info level');
|
|
230
|
+
} else {
|
|
231
|
+
lines.push(`### Doctor did not run: ${inlinePlain(doctor.reason)}`);
|
|
232
|
+
lines.push('- run `node scripts/doctor.mjs --state --now "$(date -u +%FT%TZ)"` by hand');
|
|
233
|
+
}
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push('Run `/tyran` to resume, or read `.tyran/state/<init>/STATE.md` in full.');
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The plugin's own gates, checked at session start — the ONE thing this probe
|
|
241
|
+
* says even in a repository that has no Tyran state at all.
|
|
242
|
+
*
|
|
243
|
+
* Silent when healthy, and that is the whole design. A note on every session
|
|
244
|
+
* saying "your gates are fine" is noise that trains the reader to skip the
|
|
245
|
+
* section, and this text has to be read on the one day it is not fine.
|
|
246
|
+
*
|
|
247
|
+
* It goes FIRST in the injected context on purpose: `fitBudget` drops whole
|
|
248
|
+
* sections from the END, so anything placed after the initiative summaries
|
|
249
|
+
* would be the first thing lost in exactly the repositories that have the
|
|
250
|
+
* most state — and a warning that disappears when the report gets long is a
|
|
251
|
+
* warning that is absent when it matters.
|
|
252
|
+
*
|
|
253
|
+
* This is DETECTION, not enforcement, and the text says so to the reader as
|
|
254
|
+
* well as to us: `SessionStart` has no refusal channel (ADR-22), so the probe
|
|
255
|
+
* cannot stop a session whose gates are dead. It can only make sure that the
|
|
256
|
+
* person and the agent both know.
|
|
257
|
+
*/
|
|
258
|
+
export function renderHookWarning(result) {
|
|
259
|
+
if (result === null) return '';
|
|
260
|
+
const loud = result.findings.filter((f) => f.severity !== 'info');
|
|
261
|
+
if (loud.length === 0) return '';
|
|
262
|
+
const lines = [
|
|
263
|
+
'### WARNING: this plugin has gates that cannot fire',
|
|
264
|
+
'',
|
|
265
|
+
`\`tyran doctor --hooks\` finds ${result.counts.error} error(s) and ${result.counts.warning} warning(s) ` +
|
|
266
|
+
'in the hook registration. A hook whose file is missing, is not executable, or whose matcher can ' +
|
|
267
|
+
'never match is not a weaker control — the platform fails OPEN, so the action it was meant to ' +
|
|
268
|
+
'check simply proceeds, with nothing printed anywhere.',
|
|
269
|
+
'',
|
|
270
|
+
];
|
|
271
|
+
lines.push(...rows(loud, (f) => `- [${inlinePlain(f.code)}] ${inlinePlain(f.where)}`));
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push('Run `node scripts/doctor.mjs --hooks` for the full reason and the fix for each one.');
|
|
274
|
+
lines.push(
|
|
275
|
+
'This probe can only REPORT it: SessionStart has no way to refuse, so nothing here stops a session ' +
|
|
276
|
+
'whose gates are dead.',
|
|
277
|
+
);
|
|
278
|
+
lines.push('');
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Never let a broken check break a session — it is a probe (ADR-22). */
|
|
283
|
+
export function hookHealth({ check = checkHooks } = {}) {
|
|
284
|
+
try {
|
|
285
|
+
return check();
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Trim to the working budget on a SECTION boundary, keeping the header and
|
|
293
|
+
* saying what was dropped. The runtime enforces the platform's hard 10 000
|
|
294
|
+
* ceiling on top of this; this one exists so the injection stays short
|
|
295
|
+
* enough to actually be read.
|
|
296
|
+
*/
|
|
297
|
+
export function fitBudget(text, budget = CONTEXT_BUDGET) {
|
|
298
|
+
if (text.length <= budget) return text;
|
|
299
|
+
const sections = text.split('\n### ');
|
|
300
|
+
let out = sections[0];
|
|
301
|
+
let kept = 0;
|
|
302
|
+
for (const section of sections.slice(1)) {
|
|
303
|
+
const candidate = `${out}\n### ${section}`;
|
|
304
|
+
if (candidate.length > budget) break;
|
|
305
|
+
out = candidate;
|
|
306
|
+
kept++;
|
|
307
|
+
}
|
|
308
|
+
const dropped = sections.length - 1 - kept;
|
|
309
|
+
return `${out}\n\n[tyran session-start: ${dropped} further section(s) omitted to stay inside the ${budget}-character working budget; read .tyran/state/ for the rest]`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function buildContext({ input, now = new Date(), env = process.env, health = hookHealth } = {}) {
|
|
313
|
+
// Said even in a repository that has nothing to do with Tyran: the claim
|
|
314
|
+
// being corrected is about the PLUGIN the user installed, not about this
|
|
315
|
+
// project's state, and a user whose gates are dead needs to hear it in the
|
|
316
|
+
// session where they are relying on them.
|
|
317
|
+
const warning = renderHookWarning(health());
|
|
318
|
+
const repoRoot = resolveRepoRoot(input, env);
|
|
319
|
+
if (repoRoot === null) return fitBudget(warning);
|
|
320
|
+
const stateDir = join(repoRoot, '.tyran');
|
|
321
|
+
if (!existsSync(stateDir)) return fitBudget(warning); // not a Tyran repo — but a dead gate still counts
|
|
322
|
+
const nowIso = now.toISOString();
|
|
323
|
+
return fitBudget(
|
|
324
|
+
warning +
|
|
325
|
+
renderContext({
|
|
326
|
+
repoRoot,
|
|
327
|
+
initiatives: readInitiatives(stateDir),
|
|
328
|
+
doctor: runDoctor(stateDir, nowIso),
|
|
329
|
+
hardware: hardwareLine(),
|
|
330
|
+
nowIso,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** See journal.mjs — both sides canonicalized, or a symlinked path no-ops. */
|
|
336
|
+
function canonicalPath(path) {
|
|
337
|
+
const abs = resolve(path);
|
|
338
|
+
try {
|
|
339
|
+
return realpathSync(abs);
|
|
340
|
+
} catch {
|
|
341
|
+
return abs;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isMainModule(moduleUrl) {
|
|
346
|
+
if (!process.argv[1]) return false;
|
|
347
|
+
return canonicalPath(process.argv[1]) === canonicalPath(fileURLToPath(moduleUrl));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isMainModule(import.meta.url)) {
|
|
351
|
+
await main(() =>
|
|
352
|
+
runProbe({
|
|
353
|
+
event: 'SessionStart',
|
|
354
|
+
deadlineMs: DEADLINE_MS,
|
|
355
|
+
handler: ({ input }) => buildContext({ input }),
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
}
|