agenttwin 0.2.0 → 0.2.2

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.js CHANGED
@@ -53,22 +53,13 @@ var init_answers = __esm({
53
53
 
54
54
  // src/cli.ts
55
55
  import { readFileSync as readFileSync3 } from "fs";
56
- import { dirname as dirname4, join as join15 } from "path";
56
+ import { dirname as dirname4, join as join17 } from "path";
57
57
  import { fileURLToPath as fileURLToPath3 } from "url";
58
58
  import { parseArgs } from "util";
59
59
 
60
- // src/commands/prepare.ts
61
- import { mkdir as mkdir2, mkdtemp as mkdtemp2, readFile as readFile4, writeFile as writeFile2, rm as rm2 } from "fs/promises";
62
- import { join as join4, resolve as resolve2, dirname as dirname2 } from "path";
63
- import { tmpdir as tmpdir2 } from "os";
64
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
65
-
66
- // src/commands/run.ts
67
- import { mkdtemp, mkdir, readFile as readFile3, writeFile, realpath, lstat, rm, copyFile } from "fs/promises";
68
- import { tmpdir } from "os";
69
- import { dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
70
- import { fileURLToPath } from "url";
71
- import { createHash, randomUUID } from "crypto";
60
+ // src/commands/sync.ts
61
+ import { readFile as readFile4 } from "fs/promises";
62
+ import { join as join4 } from "path";
72
63
 
73
64
  // src/runtime/remote.ts
74
65
  import { readFile } from "fs/promises";
@@ -172,19 +163,206 @@ async function executeRemote(stage, spec, log) {
172
163
  throw new CliError(`Timed out waiting for hosted run ${job.id}; inspect this run in the API before retrying`);
173
164
  }
174
165
 
166
+ // src/commands/session.ts
167
+ import { spawn, spawnSync } from "child_process";
168
+ import { writeFile as writeFile2, readFile as readFile3 } from "fs/promises";
169
+ import { join as join3 } from "path";
170
+
171
+ // src/inventory/dashboard-transfer.ts
172
+ function discoveryTransfer(value) {
173
+ const input = value;
174
+ if (!input || input.schema !== "company-inventory/v1" || typeof input.name !== "string" || !Array.isArray(input.tools) || !Array.isArray(input.workflows) || input.tools.length > 1e3 || input.workflows.length > 5e3) throw new Error("Invalid discovery metadata");
175
+ const text = (v, limit = 240) => {
176
+ if (typeof v !== "string" || v.length > limit || /[\x00-\x1f]/.test(v)) throw new Error("Invalid discovery string");
177
+ return v;
178
+ };
179
+ const strings = (v) => {
180
+ if (!Array.isArray(v) || v.length > 1e3) throw new Error("Invalid discovery list");
181
+ return v.map((s) => text(s));
182
+ };
183
+ const entrypoint = (v) => {
184
+ const path = text(v, 500);
185
+ if (path.startsWith("/") || path.includes("\\") || /(^|[\/])\.\.([\/#]|$)/.test(path) || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(path)) throw new Error("Entrypoint must be relative to the project");
186
+ return path;
187
+ };
188
+ const tools = input.tools.map((raw) => {
189
+ const t = raw;
190
+ return { id: text(t.id), title: text(t.title), kind: text(t.kind), category: text(t.category), confidence: text(t.confidence), interface: text(t.interface) };
191
+ });
192
+ const workflows = input.workflows.map((raw) => {
193
+ const w = raw;
194
+ if (!Array.isArray(w.steps) || w.steps.length > 1e3) throw new Error("Invalid workflow steps");
195
+ return { id: text(w.id), title: text(w.title), confidence: text(w.confidence), supported: w.supported === true, verified: false, tools: strings(w.tools), steps: w.steps.map((rawStep) => {
196
+ const step = rawStep;
197
+ return { tool: text(step.tool), operations: strings(step.operations) };
198
+ }), ...w.entrypoint ? { entrypoint: entrypoint(w.entrypoint) } : {} };
199
+ });
200
+ if (new Set(tools.map((t) => t.id)).size !== tools.length || new Set(workflows.map((w) => w.id)).size !== workflows.length) throw new Error("Duplicate discovery IDs");
201
+ const ids = new Set(tools.map((t) => t.id));
202
+ if (workflows.some((w) => w.tools.some((id) => !ids.has(id)) || w.steps.some((s) => !ids.has(s.tool)))) throw new Error("Unknown workflow tool");
203
+ const result = { schema: "company-inventory/v1", name: text(input.name), generatedAt: input.generatedAt ? text(input.generatedAt) : "", tools, workflows };
204
+ if (Buffer.byteLength(JSON.stringify(result)) > 2 * 1024 * 1024) throw new Error("Discovery exceeds 2 MiB");
205
+ return result;
206
+ }
207
+
208
+ // src/commands/dashboard.ts
209
+ import { cp, mkdir, readFile as readFile2, writeFile } from "fs/promises";
210
+ import { basename, join as join2 } from "path";
211
+ import { fileURLToPath } from "url";
212
+
213
+ // src/inspect/sanitize.ts
214
+ var URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
215
+ var TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
216
+ var EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
217
+ function stripUrlCredentials(value) {
218
+ return value.replace(URL_CREDENTIALS, "$1");
219
+ }
220
+ function sanitizeOutput(value) {
221
+ return stripUrlCredentials(value).replace(TOKENISH, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
222
+ }
223
+ function excerpt(value, max = 200) {
224
+ const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
225
+ return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
226
+ }
227
+
228
+ // src/commands/dashboard.ts
229
+ function dashboardInventory(cwd, inventory) {
230
+ return JSON.parse(sanitizeOutput(JSON.stringify({
231
+ schema: "company-inventory/v1",
232
+ generatedAt: inventory.generatedAt,
233
+ name: basename(cwd),
234
+ tools: inventory.tools.map((t) => ({
235
+ id: t.id,
236
+ title: t.title,
237
+ kind: t.kind,
238
+ category: t.category,
239
+ confidence: t.confidence ?? "low",
240
+ interface: t.kind === "runtime" ? "terminal" : "api"
241
+ })),
242
+ workflows: inventory.workflows.map((w) => ({ id: w.id, title: w.title ?? w.id, confidence: w.confidence ?? "low", supported: w.supported, verified: false, ...w.entrypoint ? { entrypoint: w.entrypoint } : {}, tools: w.tools, steps: w.steps.map((s) => ({ tool: s.tool, operations: s.operations })) }))
243
+ })));
244
+ }
245
+ async function writeDashboard(cwd, inventory) {
246
+ const directory = join2(cwd, ".agenttwin", "dashboard");
247
+ let bundle = fileURLToPath(new URL("./dashboard/", import.meta.url));
248
+ try {
249
+ await readFile2(join2(bundle, "index.html"));
250
+ } catch {
251
+ bundle = fileURLToPath(new URL("../../../../site/", import.meta.url));
252
+ }
253
+ await mkdir(join2(directory, "assets", "dashboard"), { recursive: true });
254
+ const html = await readFile2(join2(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
255
+ for (const folder of ["dashboard", "icons", "logo", "fonts"]) {
256
+ await cp(join2(bundle, "assets", folder), join2(directory, "assets", folder), { recursive: true });
257
+ }
258
+ await cp(join2(bundle, "assets", "brand.css"), join2(directory, "assets", "brand.css"));
259
+ const data = await loadDashboardDiscovery(cwd, inventory);
260
+ const payload = JSON.stringify(data).replace(/</g, "\\u003c");
261
+ const page2 = html.replace('type="module"', "defer").replace('<script src="assets/dashboard/dashboard.js', `<script id="agenttwin-discovery" type="application/json">${payload}</script><script src="assets/dashboard/dashboard.js`).replace(/href="index.html"/g, 'href="../report.html"').replace(/href="docs\/"/g, 'href="../report.html"').replace("Docs \u2197", "Discovery report \u2197").replace("Runtime documentation \u2197", "Coverage report \u2197").replace("Back to website \u2197", "Discovery report \u2197");
262
+ await writeFile(join2(directory, "index.html"), page2, "utf8");
263
+ await writeFile(join2(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
264
+ await writeFile(join2(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
265
+ return join2(directory, "index.html");
266
+ }
267
+
268
+ // src/commands/session.ts
269
+ function sessionTask() {
270
+ return `# Prepare this repository for AgentTwin
271
+
272
+ Read project instructions and .agenttwin/inventory.json first. Inspect tracked application source and configuration; skip secrets, .env values, private data, dependencies and build outputs. Use your configured coding-agent provider and normal permission controls.
273
+
274
+ Find distinct business workflows across the repository, including routes, queues, schedules, agent tools and exported functions. Do not cap discovery at a handful of examples. Trace callers and side effects across files. For each candidate record its real relative entrypoint, intent, inputs, dependency operations, expected observable outcome and plausible production failures. Distinguish observed code from hypotheses. Do not claim discovery is exhaustive; report skipped files and unresolved dynamic paths.
275
+
276
+ Write a human-readable .agenttwin/WORKFLOWS.md. For dashboard handoff, write .agenttwin/agent-discovery.json using the metadata-only company-inventory/v1 schema below. Start with the tools in .agenttwin/dashboard/assets/dashboard/inventory.json and retain their IDs; add candidates to workflows. Include no source bodies, credentials, PII or absolute paths. Never mark a candidate verified or executable merely because it was discovered.
277
+
278
+ Example shape (replace these descriptive placeholders with evidence from this repo):
279
+ {"schema":"company-inventory/v1","name":"project","tools":[],"workflows":[{"id":"agent:unique_workflow","title":"Workflow intent","entrypoint":"src/path.ts#function","confidence":"medium","supported":false,"verified":false,"tools":[],"steps":[]}]}
280
+ Each tool must have id, title, kind, category, confidence, interface. Each step must have tool (an existing tool ID) and operations (string array).
281
+
282
+ Before execution, ask the user for missing input, expected outcomes and any required synthetic fixtures. Prepare one real workflow at a time: an executable Node entrypoint plus its required application files, reviewed dependency mappings, synthetic starting state, assertions and an explicit fault when appropriate. Read agenttwin prepare --help and the existing preparation schema rather than guessing fields. The current hosted runtime supports Node, PostgreSQL, Redis, bounded Slack operations and explicit custom JSON collections. Unsupported vendor SDKs, browser and desktop interactions are blockers; never represent generic JSON endpoints as full vendor twins.
283
+
284
+ Do not run application workflows, Docker or containers locally. Do not access production accounts or copy production data. Do not print credentials. Execute reviewed workflows only through npx agenttwin run in a fresh hosted sandbox with AgentTwin account credentials or E2B credentials. Upload application files only after the user reviews the bundle. When a run fails, use the actual run receipt, logs, failed assertions and source digests to propose a fix. Explain uncertainty, edit the approved code, rebuild the bundle and retest in a new sandbox; never report success without passing evidence.
285
+
286
+ After review, npx agenttwin sync sends discovery metadata to the account dashboard using AGENTTWIN_TOKEN (and optional AGENTTWIN_API_URL). It does not upload application source. The dashboard will list all candidate workflows and populate the environment map from these findings.
287
+ `;
288
+ }
289
+ function installedOpenCode() {
290
+ return spawnSync("opencode", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
291
+ }
292
+ async function writeSessionTask(cwd) {
293
+ const path = join3(cwd, ".agenttwin/AGENT-TASK.md");
294
+ await writeFile2(path, sessionTask(), "utf8");
295
+ return path;
296
+ }
297
+ async function launchSession(cwd) {
298
+ if (!installedOpenCode()) throw new Error("OpenCode is not installed. Open .agenttwin/AGENT-TASK.md with your coding agent, or install OpenCode and retry init --agent opencode.");
299
+ const prompt = "Read .agenttwin/AGENT-TASK.md and follow it to discover and prepare this project for AgentTwin. Start by analyzing the repository and presenting workflow findings and missing execution requirements.";
300
+ await new Promise((resolve5, reject) => {
301
+ const child = spawn("opencode", ["--prompt", prompt], { cwd, stdio: "inherit", shell: false });
302
+ child.once("error", reject);
303
+ child.once("exit", (code, signal) => code === 0 ? resolve5() : reject(new Error(`Coding session ended ${signal || `with exit code ${code}`}. Discovery files remain available.`)));
304
+ });
305
+ }
306
+ async function loadDashboardDiscovery(cwd, inventory) {
307
+ const base = discoveryTransfer(dashboardInventory(cwd, inventory));
308
+ let raw;
309
+ try {
310
+ raw = await readFile3(join3(cwd, ".agenttwin/agent-discovery.json"), "utf8");
311
+ } catch (error) {
312
+ if (error.code === "ENOENT") return base;
313
+ throw error;
314
+ }
315
+ if (Buffer.byteLength(raw) > 2 * 1024 * 1024) throw new Error("Agent discovery exceeds 2 MiB");
316
+ const extra = discoveryTransfer(JSON.parse(raw));
317
+ const tools = new Map(base.tools.map((t) => [t.id, t]));
318
+ for (const tool of extra.tools) if (!tools.has(tool.id)) tools.set(tool.id, tool);
319
+ const workflows = new Map(base.workflows.map((w) => [w.id, w]));
320
+ for (const workflow of extra.workflows) if (!workflows.has(workflow.id)) workflows.set(workflow.id, { ...workflow, supported: false, verified: false });
321
+ const result = discoveryTransfer({ ...base, tools: [...tools.values()], workflows: [...workflows.values()] });
322
+ const ids = new Set(result.tools.map((t) => t.id));
323
+ if (result.workflows.some((w) => w.tools.some((id) => !ids.has(id)) || w.steps.some((s) => !ids.has(s.tool)))) throw new Error("Agent discovery references unknown tools. Correct .agenttwin/agent-discovery.json before syncing.");
324
+ return result;
325
+ }
326
+
327
+ // src/commands/sync.ts
328
+ async function runSync(cwd) {
329
+ const token = process.env.AGENTTWIN_TOKEN;
330
+ if (!token || !/^at_user_[a-f0-9]{64}$/.test(token)) throw new Error("Set AGENTTWIN_TOKEN to your individual account token.");
331
+ const base = apiBase(process.env.AGENTTWIN_API_URL || "https://agenttwin-api.212.56.40.125.sslip.io");
332
+ const inventory = JSON.parse(await readFile4(join4(cwd, ".agenttwin/inventory.json"), "utf8"));
333
+ const payload = await loadDashboardDiscovery(cwd, inventory);
334
+ const response = await fetch(base + "/v1/discovery", { method: "POST", headers: { authorization: "Bearer " + token, "content-type": "application/json" }, body: JSON.stringify(payload), redirect: "error", signal: AbortSignal.timeout(2e4) });
335
+ const result = await response.json();
336
+ if (!response.ok) throw new Error(result.error || "Discovery sync failed: " + response.status);
337
+ return { synced: true, tools: payload.tools.length, workflows: payload.workflows.length, dashboard: "https://agenttwin.212.56.40.125.sslip.io/site/dashboard.html", uploaded: "discovery metadata only; no source files or credentials" };
338
+ }
339
+
340
+ // src/commands/prepare.ts
341
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile7, writeFile as writeFile4, rm as rm2 } from "fs/promises";
342
+ import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
343
+ import { tmpdir as tmpdir2 } from "os";
344
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
345
+
346
+ // src/commands/run.ts
347
+ import { mkdtemp, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile3, realpath, lstat, rm, copyFile } from "fs/promises";
348
+ import { tmpdir } from "os";
349
+ import { dirname, isAbsolute, join as join6, relative, resolve, sep } from "path";
350
+ import { fileURLToPath as fileURLToPath2 } from "url";
351
+ import { createHash, randomUUID } from "crypto";
352
+
175
353
  // src/runtime/e2b.ts
176
354
  import { Sandbox } from "e2b";
177
- import { readFile as readFile2 } from "fs/promises";
355
+ import { readFile as readFile5 } from "fs/promises";
178
356
  import { randomBytes } from "crypto";
179
357
  import { homedir } from "os";
180
- import { join as join2 } from "path";
358
+ import { join as join5 } from "path";
181
359
  async function executeE2B(stage, spec, log, signal) {
182
360
  signal?.throwIfAborted();
183
361
  const apiKey = process.env.E2B_API_KEY;
184
362
  if (!apiKey) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Set E2B_API_KEY in your shell. It remains on the host and is never passed to the application." });
185
363
  let saved = {};
186
364
  try {
187
- saved = JSON.parse(await readFile2(join2(homedir(), ".agenttwin/runtime.json"), "utf8"));
365
+ saved = JSON.parse(await readFile5(join5(homedir(), ".agenttwin/runtime.json"), "utf8"));
188
366
  } catch {
189
367
  }
190
368
  const template = process.env.E2B_TEMPLATE_ID ?? saved.templateId;
@@ -203,7 +381,7 @@ async function executeE2B(stage, spec, log, signal) {
203
381
  signal?.throwIfAborted();
204
382
  await sandbox.commands.run("mkdir -p /input/app && chmod 755 /input /input/app", { user: "root" });
205
383
  for (const file of ["world.json", "worker.js", ...spec.files.map((f) => `app/${f}`)]) {
206
- await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile2(join2(stage, file))).buffer, { user: "root" });
384
+ await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile5(join5(stage, file))).buffer, { user: "root" });
207
385
  }
208
386
  await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
209
387
  const runtimeEnv = {};
@@ -326,12 +504,12 @@ async function stageApplication(root, files, destination) {
326
504
  const source = await realpath(resolve(canonicalRoot, file));
327
505
  const rel = relative(canonicalRoot, source);
328
506
  if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel) || source !== resolve(canonicalRoot, file) || !(await lstat(source)).isFile()) throw new CliError(`Files must be regular files within the project, without symlinks: ${file}`, { code: EXIT.USAGE });
329
- const contents = await readFile3(source);
507
+ const contents = await readFile6(source);
330
508
  bytes += contents.length;
331
509
  if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
332
- const target = join3(destination, file);
333
- await mkdir(dirname(target), { recursive: true });
334
- await writeFile(target, contents, { mode: 420 });
510
+ const target = join6(destination, file);
511
+ await mkdir2(dirname(target), { recursive: true });
512
+ await writeFile3(target, contents, { mode: 420 });
335
513
  digests[file] = createHash("sha256").update(contents).digest("hex");
336
514
  }
337
515
  return digests;
@@ -340,14 +518,14 @@ async function runWorld(cwd, opts) {
340
518
  const file = resolve(cwd, opts.file ?? "agenttwin.world.json");
341
519
  let spec;
342
520
  try {
343
- spec = parseWorld(JSON.parse(await readFile3(file, "utf8")));
521
+ spec = parseWorld(JSON.parse(await readFile6(file, "utf8")));
344
522
  } catch (error) {
345
523
  throw new CliError(String(error), { code: EXIT.USAGE, action: "Create agenttwin.world.json; run agenttwin example --out ./agenttwin-example for a working example." });
346
524
  }
347
525
  if (spec.workflowId) {
348
526
  let inventory;
349
527
  try {
350
- inventory = JSON.parse(await readFile3(join3(cwd, ".agenttwin/inventory.json"), "utf8"));
528
+ inventory = JSON.parse(await readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8"));
351
529
  } catch {
352
530
  throw new CliError("Linked workflow needs discovery inventory; run init first", { code: EXIT.BLOCKED });
353
531
  }
@@ -356,12 +534,12 @@ async function runWorld(cwd, opts) {
356
534
  const runtime = opts.runtime ?? (process.env.AGENTTWIN_API_URL ? "hosted" : "e2b");
357
535
  if (!["e2b", "hosted"].includes(runtime)) throw new CliError("runtime must be hosted or e2b; no local application execution", { code: EXIT.USAGE });
358
536
  if (runtime === "e2b" && !process.env.E2B_API_KEY) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Configure your E2B key in the shell; it will not be uploaded." });
359
- const stage = await mkdtemp(join3(tmpdir(), "agenttwin-world-"));
537
+ const stage = await mkdtemp(join6(tmpdir(), "agenttwin-world-"));
360
538
  try {
361
- const sourceDigests = await stageApplication(dirname(file), spec.files, join3(stage, "app"));
362
- await writeFile(join3(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
363
- const worker = join3(dirname(fileURLToPath(import.meta.url)), "worker.js");
364
- await copyFile(worker, join3(stage, "worker.js"));
539
+ const sourceDigests = await stageApplication(dirname(file), spec.files, join6(stage, "app"));
540
+ await writeFile3(join6(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
541
+ const worker = join6(dirname(fileURLToPath2(import.meta.url)), "worker.js");
542
+ await copyFile(worker, join6(stage, "worker.js"));
365
543
  if (runtime === "hosted") {
366
544
  const receipt2 = await executeRemote(stage, spec, opts.log);
367
545
  if (receipt2.schema !== "agenttwin/run-v1" || typeof receipt2.runId !== "string" || !/^[a-f0-9-]{36}$/.test(receipt2.runId) || !["passed", "failed", "error", "incomplete"].includes(String(receipt2.status))) throw new CliError("Invalid hosted receipt");
@@ -369,11 +547,11 @@ async function runWorld(cwd, opts) {
369
547
  const reportedSources = receipt2.sourceDigests;
370
548
  if (receipt2.specDigest !== expectedSpec || !reportedSources || Object.keys(reportedSources).length !== Object.keys(sourceDigests).length || Object.entries(sourceDigests).some(([path, digest]) => reportedSources[path] !== digest)) throw new CliError("Hosted receipt does not match the submitted source/specification");
371
549
  const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
372
- await mkdir(out2, { recursive: true });
373
- const evidencePath = join3(out2, "run.json");
374
- const reportPath = join3(out2, "report.md");
375
- await writeFile(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
376
- await writeFile(reportPath, `# ${spec.name}: ${receipt2.status}
550
+ await mkdir2(out2, { recursive: true });
551
+ const evidencePath = join6(out2, "run.json");
552
+ const reportPath = join6(out2, "report.md");
553
+ await writeFile3(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
554
+ await writeFile3(reportPath, `# ${spec.name}: ${receipt2.status}
377
555
 
378
556
  Run: ${receipt2.runId}
379
557
 
@@ -405,12 +583,12 @@ See run.json for assertions, service state, application output and isolation met
405
583
  isolation
406
584
  };
407
585
  const out = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
408
- await mkdir(out, { recursive: true });
409
- await writeFile(join3(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
410
- await writeFile(join3(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
586
+ await mkdir2(out, { recursive: true });
587
+ await writeFile3(join6(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
588
+ await writeFile3(join6(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
411
589
  const summary = [`# ${spec.name}: ${report.status}`, "", `Run: ${runId}`, `Image: ${image}`, "", "## Assertions", "", ...report.assertions.map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}${a.path}: expected ${a.count}, observed ${a.actual}`), "", "## Infrastructure assertions", "", ...(report.infrastructureAssertions ?? []).map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}: ${JSON.stringify(a.actual)}`), "", "## Coverage", "", report.coverage, "", "External networking disabled. Only explicitly listed source files were uploaded. New state and tokens are created for each run.", "", "See run.json for application output, faults, events, and initial/final state.", ""].join("\n");
412
- await writeFile(join3(out, "report.md"), summary);
413
- return { ...receipt, reportPath: join3(out, "report.md"), evidencePath: join3(out, "run.json") };
590
+ await writeFile3(join6(out, "report.md"), summary);
591
+ return { ...receipt, reportPath: join6(out, "report.md"), evidencePath: join6(out, "run.json") };
414
592
  } finally {
415
593
  await rm(stage, { recursive: true, force: true });
416
594
  }
@@ -463,30 +641,36 @@ function reviewPreparation(inventory, input) {
463
641
  async function runPrepare(cwd, opts) {
464
642
  if (!opts.file) throw new CliError("prepare requires --file preparation.json", { code: EXIT.USAGE });
465
643
  const file = resolve2(cwd, opts.file);
466
- const inventory = JSON.parse(await readFile4(join4(cwd, ".agenttwin/inventory.json"), "utf8"));
467
- const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile4(file, "utf8")));
644
+ const inventory = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8"));
645
+ const discovered = await loadDashboardDiscovery(cwd, inventory);
646
+ const known = new Set(inventory.workflows.map((w) => w.id));
647
+ for (const w of discovered.workflows) if (!known.has(w.id)) {
648
+ inventory.workflows.push({ ...w, confidence: "low", apps: [], sharedEntities: [], evidence: [{ source: "agent-analysis", ref: ".agenttwin/agent-discovery.json" }], steps: w.steps.map((s) => ({ ...s, app: "", evidence: [] })), risk: "high", rank: 0, supported: false, hypothesis: true, linkingSignals: ["Coding-agent analysis requires user review"], scopeUncertain: true, verified: false });
649
+ }
650
+ for (const t of discovered.tools) if (!inventory.tools.some((tool) => tool.id === t.id)) throw new Error("Agent-discovered dependencies must be classified in init before preparation. Review the additional tool, then rerun init.");
651
+ const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile7(file, "utf8")));
468
652
  const directory = resolve2(cwd, opts.out ?? `.agenttwin/prepared/${spec.name}-${randomUUID2().slice(0, 8)}`);
469
- const stage = await mkdtemp2(join4(tmpdir2(), "agenttwin-prepare-"));
653
+ const stage = await mkdtemp2(join7(tmpdir2(), "agenttwin-prepare-"));
470
654
  try {
471
655
  const sourceDigests = await stageApplication(dirname2(file), spec.files, stage);
472
656
  const files = /* @__PURE__ */ Object.create(null);
473
- for (const name of spec.files) files[name] = (await readFile4(join4(stage, name))).toString("base64");
657
+ for (const name of spec.files) files[name] = (await readFile7(join7(stage, name))).toString("base64");
474
658
  const manifest = { schema: "agenttwin/prepared-v1", workflowId: workflow.id, invocation: spec.command, sourceDigests, specDigest: createHash2("sha256").update(JSON.stringify(spec)).digest("hex"), mappings, provenance: "user-reviewed invocation and operation contracts; discovery does not verify runtime fidelity", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
475
- await writeFile2(join4(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
476
- await writeFile2(join4(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
477
- await writeFile2(join4(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
478
- await mkdir2(join4(stage, ".agenttwin"), { recursive: true });
479
- await writeFile2(join4(stage, ".agenttwin/inventory.json"), JSON.stringify({ workflows: [{ id: workflow.id }] }), { mode: 384 });
480
- await mkdir2(dirname2(directory), { recursive: true });
481
- await mkdir2(directory, { mode: 448 });
659
+ await writeFile4(join7(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
660
+ await writeFile4(join7(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
661
+ await writeFile4(join7(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
662
+ await mkdir3(join7(stage, ".agenttwin"), { recursive: true });
663
+ await writeFile4(join7(stage, ".agenttwin/inventory.json"), JSON.stringify({ workflows: [{ id: workflow.id }] }), { mode: 384 });
664
+ await mkdir3(dirname2(directory), { recursive: true });
665
+ await mkdir3(directory, { mode: 448 });
482
666
  try {
483
667
  const { cp: cp2, readdir: readdir2 } = await import("fs/promises");
484
- for (const entry of await readdir2(stage)) await cp2(join4(stage, entry), join4(directory, entry), { recursive: true, force: false, errorOnExist: true });
668
+ for (const entry of await readdir2(stage)) await cp2(join7(stage, entry), join7(directory, entry), { recursive: true, force: false, errorOnExist: true });
485
669
  } catch (error) {
486
670
  await rm2(directory, { recursive: true, force: true });
487
671
  throw error;
488
672
  }
489
- return { directory, worldPath: join4(directory, "agenttwin.world.json"), bundlePath: join4(directory, "agenttwin.bundle.json"), manifest };
673
+ return { directory, worldPath: join7(directory, "agenttwin.world.json"), bundlePath: join7(directory, "agenttwin.bundle.json"), manifest };
490
674
  } finally {
491
675
  await rm2(stage, { recursive: true, force: true });
492
676
  }
@@ -494,9 +678,9 @@ async function runPrepare(cwd, opts) {
494
678
 
495
679
  // src/commands/setup.ts
496
680
  import { Template } from "e2b";
497
- import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
681
+ import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
498
682
  import { homedir as homedir2 } from "os";
499
- import { join as join5 } from "path";
683
+ import { join as join8 } from "path";
500
684
  async function runSetup(log) {
501
685
  if (!process.env.E2B_API_KEY) throw new CliError("Set E2B_API_KEY before creating your hosted runtime template", { code: EXIT.BLOCKED });
502
686
  log("Building the AgentTwin template on E2B (Node 22, PostgreSQL 15, Redis). No local containers are used.");
@@ -504,16 +688,16 @@ async function runSetup(log) {
504
688
  const result = await Template.build(template, "agenttwin-node-v2", { apiKey: process.env.E2B_API_KEY, cpuCount: 2, memoryMB: 2048, onBuildLogs: (entry) => {
505
689
  if (!entry.message.includes("[stdout]") && !entry.message.includes("[stderr]")) log(entry.message);
506
690
  } });
507
- const directory = join5(homedir2(), ".agenttwin");
508
- await mkdir3(directory, { recursive: true, mode: 448 });
509
- const path = join5(directory, "runtime.json");
510
- await writeFile3(path, JSON.stringify({ provider: "e2b", templateId: result.templateId, buildId: result.buildId }, null, 2) + "\n", { mode: 384 });
691
+ const directory = join8(homedir2(), ".agenttwin");
692
+ await mkdir4(directory, { recursive: true, mode: 448 });
693
+ const path = join8(directory, "runtime.json");
694
+ await writeFile5(path, JSON.stringify({ provider: "e2b", templateId: result.templateId, buildId: result.buildId }, null, 2) + "\n", { mode: 384 });
511
695
  return { provider: "e2b", templateId: result.templateId, buildId: result.buildId, configPath: path };
512
696
  }
513
697
 
514
698
  // src/commands/example.ts
515
- import { mkdir as mkdir4, writeFile as writeFile4, stat } from "fs/promises";
516
- import { resolve as resolve3, join as join6 } from "path";
699
+ import { mkdir as mkdir5, writeFile as writeFile6, stat } from "fs/promises";
700
+ import { resolve as resolve3, join as join9 } from "path";
517
701
  var exampleSource = `// Replace this file with your own bundled Node workflow.
518
702
  const safe = process.argv.includes('--safe');
519
703
  const crm = process.env.AGENTTWIN_CRM_URL;
@@ -557,27 +741,27 @@ function exampleWorld(safe = false) {
557
741
  async function runExample(cwd, out) {
558
742
  const directory = resolve3(cwd, out ?? "agenttwin-example");
559
743
  if (await stat(directory).then(() => true, () => false)) throw new CliError(`Destination exists: ${directory}`, { code: EXIT.USAGE, action: "Choose a new --out directory." });
560
- await mkdir4(directory, { recursive: true });
561
- await writeFile4(join6(directory, "workflow.mjs"), exampleSource);
562
- await writeFile4(join6(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
563
- await writeFile4(join6(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
564
- await writeFile4(join6(directory, "README.md"), `# Support workflow
744
+ await mkdir5(directory, { recursive: true });
745
+ await writeFile6(join9(directory, "workflow.mjs"), exampleSource);
746
+ await writeFile6(join9(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
747
+ await writeFile6(join9(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
748
+ await writeFile6(join9(directory, "README.md"), `# Support workflow
565
749
 
566
750
  Run with agenttwin run --file agenttwin.world.json (expected FAIL), then --file safe.world.json (expected PASS). Requires AGENTTWIN_API_URL and AGENTTWIN_TOKEN, or your own E2B_API_KEY plus a template from agenttwin setup. Each run creates a fresh hosted sandbox and deletes it afterward.
567
751
 
568
752
  This is actual application code using a synthetic custom ticket API plus the supported Slack JSON subset. It is not a clone of a CRM vendor. Replace workflow.mjs with your own built code and edit the explicit file list, endpoints, seed records and assertions. No npm install runs in the sandbox; dependencies must be bundled into your application.
569
753
  `);
570
- return { directory, worldPath: join6(directory, "agenttwin.world.json") };
754
+ return { directory, worldPath: join9(directory, "agenttwin.world.json") };
571
755
  }
572
756
 
573
757
  // src/commands/init.ts
574
758
  import { readFileSync as readFileSync2 } from "fs";
575
- import { writeFile as writeFile9 } from "fs/promises";
576
- import { join as join14 } from "path";
759
+ import { writeFile as writeFile10 } from "fs/promises";
760
+ import { join as join16 } from "path";
577
761
 
578
762
  // src/lib/files.ts
579
- import { readdir, readFile as readFile5, stat as stat2 } from "fs/promises";
580
- import { join as join7, relative as relative2, sep as sep2 } from "path";
763
+ import { readdir, readFile as readFile8, stat as stat2 } from "fs/promises";
764
+ import { join as join10, relative as relative2, sep as sep2 } from "path";
581
765
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
582
766
  ".git",
583
767
  ".agenttwin",
@@ -635,7 +819,7 @@ async function walkDir(root, maxFiles, budgetMs) {
635
819
  }
636
820
  entries.sort((a, b) => a.name.localeCompare(b.name));
637
821
  for (const entry of entries) {
638
- const full = join7(dir, entry.name);
822
+ const full = join10(dir, entry.name);
639
823
  if (entry.isDirectory()) {
640
824
  if (!shouldIgnoreDir(entry.name)) stack.push(full);
641
825
  } else if (entry.isFile()) {
@@ -669,7 +853,7 @@ var RepoFileIndex = class {
669
853
  }
670
854
  async exists(path) {
671
855
  try {
672
- await stat2(join7(this.cwd, path));
856
+ await stat2(join10(this.cwd, path));
673
857
  return true;
674
858
  } catch {
675
859
  return false;
@@ -678,7 +862,7 @@ var RepoFileIndex = class {
678
862
  async read(path, maxBytes = MAX_BYTES) {
679
863
  const cached = this.cache.contents.get(path);
680
864
  if (cached !== void 0) return cached;
681
- const full = join7(this.cwd, path);
865
+ const full = join10(this.cwd, path);
682
866
  try {
683
867
  const info2 = await stat2(full);
684
868
  if (!info2.isFile()) return void 0;
@@ -686,7 +870,7 @@ var RepoFileIndex = class {
686
870
  this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
687
871
  return void 0;
688
872
  }
689
- const text = await readFile5(full, "utf8");
873
+ const text = await readFile8(full, "utf8");
690
874
  this.cache.contents.set(path, text);
691
875
  return text;
692
876
  } catch {
@@ -1546,13 +1730,13 @@ var containerDetector = {
1546
1730
  // src/discovery/detectors/cloud-creds.ts
1547
1731
  import { stat as stat3 } from "fs/promises";
1548
1732
  import { homedir as homedir3 } from "os";
1549
- import { join as join8 } from "path";
1733
+ import { join as join11 } from "path";
1550
1734
  var CREDENTIALS = [
1551
- { path: join8(homedir3(), ".aws", "config"), subject: "aws", title: "AWS" },
1552
- { path: join8(homedir3(), ".aws", "credentials"), subject: "aws", title: "AWS" },
1553
- { path: join8(homedir3(), ".config", "gcloud"), subject: "gcp", title: "Google Cloud" },
1554
- { path: join8(homedir3(), ".azure"), subject: "azure", title: "Azure" },
1555
- { path: join8(homedir3(), ".kube", "config"), subject: "kubernetes", title: "Kubernetes" }
1735
+ { path: join11(homedir3(), ".aws", "config"), subject: "aws", title: "AWS" },
1736
+ { path: join11(homedir3(), ".aws", "credentials"), subject: "aws", title: "AWS" },
1737
+ { path: join11(homedir3(), ".config", "gcloud"), subject: "gcp", title: "Google Cloud" },
1738
+ { path: join11(homedir3(), ".azure"), subject: "azure", title: "Azure" },
1739
+ { path: join11(homedir3(), ".kube", "config"), subject: "kubernetes", title: "Kubernetes" }
1556
1740
  ];
1557
1741
  async function exists(path) {
1558
1742
  try {
@@ -1603,13 +1787,13 @@ function registerDetectors() {
1603
1787
  }
1604
1788
 
1605
1789
  // src/discovery/masking.ts
1606
- var EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
1607
- var TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
1790
+ var EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
1791
+ var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
1608
1792
  function maskSecret(value) {
1609
- return value.replace(TOKENISH, (match) => `${match.slice(0, 4)}***`);
1793
+ return value.replace(TOKENISH2, (match) => `${match.slice(0, 4)}***`);
1610
1794
  }
1611
1795
  function maskEmail(value) {
1612
- return value.replace(EMAIL, "$1***$2");
1796
+ return value.replace(EMAIL2, "$1***$2");
1613
1797
  }
1614
1798
  function maskUrl(value) {
1615
1799
  try {
@@ -2111,12 +2295,12 @@ var SimpleDetectorRegistry = class {
2111
2295
  };
2112
2296
 
2113
2297
  // src/discovery/passes/runtime.ts
2114
- import { spawn } from "child_process";
2298
+ import { spawn as spawn2 } from "child_process";
2115
2299
  import { createServer } from "http";
2116
2300
  import { connect } from "net";
2117
2301
  import { readFileSync, rmSync, writeFileSync } from "fs";
2118
2302
  import { tmpdir as tmpdir3 } from "os";
2119
- import { join as join9 } from "path";
2303
+ import { join as join12 } from "path";
2120
2304
  var EGRESS_HOOK = `
2121
2305
  const fs = require('fs');
2122
2306
  const dns = require('dns');
@@ -2186,8 +2370,8 @@ function startProxy(onHost) {
2186
2370
  async function observeEgress(opts) {
2187
2371
  const hosts = /* @__PURE__ */ new Set();
2188
2372
  const { server, url } = await startProxy((host) => hosts.add(host));
2189
- const logFile = join9(tmpdir3(), `agenttwin-egress-${process.pid}.log`);
2190
- const hookFile = join9(tmpdir3(), `agenttwin-egress-hook-${process.pid}.cjs`);
2373
+ const logFile = join12(tmpdir3(), `agenttwin-egress-${process.pid}.log`);
2374
+ const hookFile = join12(tmpdir3(), `agenttwin-egress-hook-${process.pid}.cjs`);
2191
2375
  try {
2192
2376
  writeFileSync(hookFile, EGRESS_HOOK, "utf8");
2193
2377
  writeFileSync(logFile, "", "utf8");
@@ -2196,7 +2380,7 @@ async function observeEgress(opts) {
2196
2380
  let child;
2197
2381
  if (opts.run) {
2198
2382
  opts.log(`Observing egress: spawning \`${opts.run}\` through ${url}`);
2199
- child = spawn(opts.run, {
2383
+ child = spawn2(opts.run, {
2200
2384
  shell: true,
2201
2385
  env: {
2202
2386
  ...process.env,
@@ -2654,6 +2838,32 @@ function singularize(word) {
2654
2838
  return word;
2655
2839
  }
2656
2840
 
2841
+ // src/inventory/relationships.ts
2842
+ import { createHash as createHash4 } from "crypto";
2843
+
2844
+ // src/inventory/entrypoints.ts
2845
+ function codeOnly(source) {
2846
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
2847
+ }
2848
+ function exportedEntrypoints(source) {
2849
+ const code = codeOnly(source);
2850
+ const pattern = /\bexport\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)[^{;]*\{|\bexport\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::[^=;]+)?=>\s*\{/g;
2851
+ const result = [];
2852
+ let match;
2853
+ while (match = pattern.exec(code)) {
2854
+ let depth = 1, end = pattern.lastIndex;
2855
+ while (end < code.length && depth) {
2856
+ if (code[end] === "{") depth++;
2857
+ if (code[end] === "}") depth--;
2858
+ end++;
2859
+ }
2860
+ if (depth) continue;
2861
+ result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
2862
+ pattern.lastIndex = end;
2863
+ }
2864
+ return result;
2865
+ }
2866
+
2657
2867
  // src/inventory/operations.ts
2658
2868
  var WRITE_VERBS = /* @__PURE__ */ new Set([
2659
2869
  "create",
@@ -2708,7 +2918,7 @@ function scanOperations(text, path) {
2708
2918
  seen.add(key);
2709
2919
  out.push({ receiver, resource, verb, kind, ref: `${path}:${line}` });
2710
2920
  }
2711
- return out.slice(0, 40);
2921
+ return out;
2712
2922
  }
2713
2923
 
2714
2924
  // src/inventory/relationships.ts
@@ -2888,9 +3098,12 @@ async function buildRelationships(apps, tools, files, decisions) {
2888
3098
  const roles = /* @__PURE__ */ new Map();
2889
3099
  const fileEntities = /* @__PURE__ */ new Map();
2890
3100
  const uncertainOperations = [];
3101
+ const entrypointCalls = [];
3102
+ const sourceText = /* @__PURE__ */ new Map();
2891
3103
  for (const path of sourceFiles) {
2892
3104
  const text = await files.read(path, 2e5);
2893
3105
  if (!text) continue;
3106
+ sourceText.set(path, text);
2894
3107
  const app = appForPath(apps, path);
2895
3108
  const role = roles.get(app.id) ?? { produced: /* @__PURE__ */ new Set(), consumed: /* @__PURE__ */ new Set() };
2896
3109
  collect(PRODUCE, text, role.produced);
@@ -2923,6 +3136,7 @@ async function buildRelationships(apps, tools, files, decisions) {
2923
3136
  evidence: []
2924
3137
  };
2925
3138
  const label = `${op.resource}.${op.verb}`;
3139
+ entrypointCalls.push({ path, line: Number(op.ref.slice(path.length + 1)), providerId, toolId: tool.id, kind: op.kind, label, resource: op.resource, ref: op.ref });
2926
3140
  if (!record2.operations.includes(label)) record2.operations.push(label);
2927
3141
  if (!record2.resources.includes(op.resource)) record2.resources.push(op.resource);
2928
3142
  if (!record2.evidence.some((e) => e.ref === op.ref)) record2.evidence.push({ source: "static", ref: op.ref });
@@ -3066,6 +3280,34 @@ async function buildRelationships(apps, tools, files, decisions) {
3066
3280
  verified: false
3067
3281
  });
3068
3282
  }
3283
+ for (const [path, text] of sourceText) {
3284
+ const app = appForPath(apps, path);
3285
+ for (const entry of exportedEntrypoints(text)) {
3286
+ const calls = entrypointCalls.filter((call) => call.path === path && call.line >= entry.startLine && call.line <= entry.endLine && decidedRecords.some((record2) => record2.toolId === call.toolId && record2.kind === call.kind && record2.evidence.some((e) => e.ref === call.ref)));
3287
+ if (!calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
3288
+ const dependencies = [...new Set(calls.map((call) => call.toolId))];
3289
+ const evidence = [...new Set(calls.map((call) => call.ref))].map((ref) => ({ source: "static", ref }));
3290
+ workflows.push({
3291
+ id: "entry:" + createHash4("sha256").update(path + ":" + entry.name).digest("hex").slice(0, 20),
3292
+ title: entry.name,
3293
+ entrypoint: path + "#" + entry.name,
3294
+ apps: [app.id],
3295
+ tools: dependencies,
3296
+ steps: dependencies.map((tool) => ({ app: app.id, tool, operations: [...new Set(calls.filter((c) => c.toolId === tool).map((c) => c.label))], evidence })),
3297
+ sharedEntities: [...new Set(calls.map((call) => singularize(call.resource)))],
3298
+ evidence,
3299
+ confidence: "medium",
3300
+ risk: "high",
3301
+ rank: 0,
3302
+ supported: false,
3303
+ hypothesis: true,
3304
+ linkingSignals: ["exported function containing binding-resolved side effects; invocation and correctness require review"],
3305
+ scopeUncertain: true,
3306
+ verified: false
3307
+ });
3308
+ }
3309
+ }
3310
+ if (sourceFiles.length === MAX_FILES3) gaps.push("Source scan is limited to 2500 files; split large repositories into explicit roots. Not every workflow can be proven by static discovery.");
3069
3311
  const finalWorkflows = workflows.filter((w) => !rejected.has(w.id)).map(
3070
3312
  (w) => confirmed.has(w.id) ? { ...w, supported: true, hypothesis: false, linkingSignals: [...w.linkingSignals, "confirmed by decision"] } : w
3071
3313
  );
@@ -3379,8 +3621,8 @@ async function buildInventory({
3379
3621
  }
3380
3622
 
3381
3623
  // src/inventory/store.ts
3382
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
3383
- import { join as join10 } from "path";
3624
+ import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
3625
+ import { join as join13 } from "path";
3384
3626
  var STORE_DIR = ".agenttwin";
3385
3627
  var STORE_GITIGNORE = [
3386
3628
  "inventory.json",
@@ -3396,24 +3638,24 @@ var STORE_GITIGNORE = [
3396
3638
  ""
3397
3639
  ].join("\n");
3398
3640
  function storeDir(cwd) {
3399
- return join10(cwd, STORE_DIR);
3641
+ return join13(cwd, STORE_DIR);
3400
3642
  }
3401
3643
  function storePath(cwd, name) {
3402
- return join10(storeDir(cwd), name);
3644
+ return join13(storeDir(cwd), name);
3403
3645
  }
3404
3646
  async function ensureStore(cwd) {
3405
- await mkdir5(storeDir(cwd), { recursive: true });
3406
- await writeFile5(storePath(cwd, ".gitignore"), STORE_GITIGNORE, "utf8");
3647
+ await mkdir6(storeDir(cwd), { recursive: true });
3648
+ await writeFile7(storePath(cwd, ".gitignore"), STORE_GITIGNORE, "utf8");
3407
3649
  }
3408
3650
  async function readJson(path) {
3409
3651
  try {
3410
- return JSON.parse(await readFile6(path, "utf8"));
3652
+ return JSON.parse(await readFile9(path, "utf8"));
3411
3653
  } catch {
3412
3654
  return void 0;
3413
3655
  }
3414
3656
  }
3415
3657
  async function writeJson(path, value) {
3416
- await writeFile5(path, `${JSON.stringify(value, null, 2)}
3658
+ await writeFile7(path, `${JSON.stringify(value, null, 2)}
3417
3659
  `, "utf8");
3418
3660
  }
3419
3661
  var readInventory = (cwd) => readJson(storePath(cwd, "inventory.json"));
@@ -3616,30 +3858,13 @@ var REGISTRY = [
3616
3858
  ];
3617
3859
 
3618
3860
  // src/inspect/runner.ts
3619
- import { spawn as spawn2 } from "child_process";
3620
-
3621
- // src/inspect/sanitize.ts
3622
- var URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
3623
- var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
3624
- var EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
3625
- function stripUrlCredentials(value) {
3626
- return value.replace(URL_CREDENTIALS, "$1");
3627
- }
3628
- function sanitizeOutput(value) {
3629
- return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL2, "$1***$2");
3630
- }
3631
- function excerpt(value, max = 200) {
3632
- const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
3633
- return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
3634
- }
3635
-
3636
- // src/inspect/runner.ts
3861
+ import { spawn as spawn3 } from "child_process";
3637
3862
  function runCommand(spec, cwd, signal) {
3638
3863
  return new Promise((resolve5) => {
3639
3864
  if (signal.aborted) {
3640
3865
  return resolve5({ code: null, stdout: "", stderr: "", missing: false, timedOut: false, cancelled: true });
3641
3866
  }
3642
- const child = spawn2(spec.cmd, spec.args, {
3867
+ const child = spawn3(spec.cmd, spec.args, {
3643
3868
  cwd,
3644
3869
  stdio: ["ignore", "pipe", "pipe"],
3645
3870
  env: process.env
@@ -4177,12 +4402,12 @@ async function runExport(opts) {
4177
4402
  const plan = await readPlan(opts.cwd);
4178
4403
  const bundle = { schema: "export/v1", inventory, plan: plan ?? null };
4179
4404
  if (opts.out) {
4180
- const { mkdir: mkdir8, writeFile: writeFile10 } = await import("fs/promises");
4181
- const { join: join16 } = await import("path");
4405
+ const { mkdir: mkdir8, writeFile: writeFile11 } = await import("fs/promises");
4406
+ const { join: join18 } = await import("path");
4182
4407
  await mkdir8(opts.out, { recursive: true });
4183
- await writeFile10(join16(opts.out, "inventory.json"), `${JSON.stringify(inventory, null, 2)}
4408
+ await writeFile11(join18(opts.out, "inventory.json"), `${JSON.stringify(inventory, null, 2)}
4184
4409
  `, "utf8");
4185
- if (plan) await writeFile10(join16(opts.out, "plan.json"), `${JSON.stringify(plan, null, 2)}
4410
+ if (plan) await writeFile11(join18(opts.out, "plan.json"), `${JSON.stringify(plan, null, 2)}
4186
4411
  `, "utf8");
4187
4412
  return { schema: "export/v1", out: opts.out, wrote: ["inventory.json", ...plan ? ["plan.json"] : []] };
4188
4413
  }
@@ -4582,16 +4807,16 @@ async function askQuestions(questions) {
4582
4807
  init_answers();
4583
4808
 
4584
4809
  // src/lib/config.ts
4585
- import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
4586
- import { join as join11 } from "path";
4810
+ import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
4811
+ import { join as join14 } from "path";
4587
4812
  var CONFIG_FILENAME = "agenttwin.config.json";
4588
4813
  var CONFIG_VERSION = 2;
4589
4814
  function configPath(cwd) {
4590
- return join11(cwd, CONFIG_FILENAME);
4815
+ return join14(cwd, CONFIG_FILENAME);
4591
4816
  }
4592
4817
  async function readConfig(cwd) {
4593
4818
  try {
4594
- const text = await readFile7(configPath(cwd), "utf8");
4819
+ const text = await readFile10(configPath(cwd), "utf8");
4595
4820
  return JSON.parse(text);
4596
4821
  } catch {
4597
4822
  return void 0;
@@ -4627,57 +4852,14 @@ function buildConfigFromInventory(inventory, existing) {
4627
4852
  }
4628
4853
  async function writeConfig(cwd, config, _options = {}) {
4629
4854
  const path = configPath(cwd);
4630
- await writeFile6(path, `${JSON.stringify(config, null, 2)}
4855
+ await writeFile8(path, `${JSON.stringify(config, null, 2)}
4631
4856
  `, "utf8");
4632
4857
  return { path };
4633
4858
  }
4634
4859
 
4635
- // src/commands/dashboard.ts
4636
- import { cp, mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
4637
- import { basename, join as join12 } from "path";
4638
- import { fileURLToPath as fileURLToPath2 } from "url";
4639
- function dashboardInventory(cwd, inventory) {
4640
- return JSON.parse(sanitizeOutput(JSON.stringify({
4641
- schema: "company-inventory/v1",
4642
- generatedAt: inventory.generatedAt,
4643
- name: basename(cwd),
4644
- tools: inventory.tools.map((t) => ({
4645
- id: t.id,
4646
- title: t.title,
4647
- kind: t.kind,
4648
- category: t.category,
4649
- confidence: t.confidence,
4650
- interface: t.kind === "runtime" ? "terminal" : "api"
4651
- })),
4652
- workflows: inventory.workflows.map((w) => ({ id: w.id, title: w.title, confidence: w.confidence, supported: w.supported, verified: false, tools: w.tools, steps: w.steps.map((s) => ({ tool: s.tool, operations: s.operations })) }))
4653
- })));
4654
- }
4655
- async function writeDashboard(cwd, inventory) {
4656
- const directory = join12(cwd, ".agenttwin", "dashboard");
4657
- let bundle = fileURLToPath2(new URL("./dashboard/", import.meta.url));
4658
- try {
4659
- await readFile8(join12(bundle, "index.html"));
4660
- } catch {
4661
- bundle = fileURLToPath2(new URL("../../../../site/", import.meta.url));
4662
- }
4663
- await mkdir6(join12(directory, "assets", "dashboard"), { recursive: true });
4664
- const html = await readFile8(join12(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
4665
- for (const folder of ["dashboard", "icons", "logo", "fonts"]) {
4666
- await cp(join12(bundle, "assets", folder), join12(directory, "assets", folder), { recursive: true });
4667
- }
4668
- await cp(join12(bundle, "assets", "brand.css"), join12(directory, "assets", "brand.css"));
4669
- const data = dashboardInventory(cwd, inventory);
4670
- const payload = JSON.stringify(data).replace(/</g, "\\u003c");
4671
- const page2 = html.replace('type="module"', "defer").replace('<script src="assets/dashboard/dashboard.js"', `<script id="agenttwin-discovery" type="application/json">${payload}</script><script src="assets/dashboard/dashboard.js"`).replace(/href="index.html"/g, 'href="../report.html"').replace(/href="docs\/"/g, 'href="../report.html"').replace("Docs \u2197", "Discovery report \u2197").replace("Runtime documentation \u2197", "Coverage report \u2197").replace("Back to website \u2197", "Discovery report \u2197");
4672
- await writeFile7(join12(directory, "index.html"), page2, "utf8");
4673
- await writeFile7(join12(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
4674
- await writeFile7(join12(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
4675
- return join12(directory, "index.html");
4676
- }
4677
-
4678
4860
  // src/commands/preview.ts
4679
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
4680
- import { dirname as dirname3, join as join13, resolve as resolve4 } from "path";
4861
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
4862
+ import { dirname as dirname3, join as join15, resolve as resolve4 } from "path";
4681
4863
 
4682
4864
  // src/preview/html.ts
4683
4865
  import { randomBytes as randomBytes2 } from "crypto";
@@ -4743,7 +4925,7 @@ async function writeBrowserReport(cwd, inventory, plan, out) {
4743
4925
  await mkdir7(dirname3(path), { recursive: true });
4744
4926
  const cleanInventory = JSON.parse(sanitizeOutput(JSON.stringify(inventory)));
4745
4927
  const cleanPlan = JSON.parse(sanitizeOutput(JSON.stringify(plan)));
4746
- await writeFile8(path, renderBrowserReport(cleanInventory, cleanPlan), "utf8");
4928
+ await writeFile9(path, renderBrowserReport(cleanInventory, cleanPlan), "utf8");
4747
4929
  return path;
4748
4930
  }
4749
4931
  async function runReport(cwd, out) {
@@ -4758,13 +4940,13 @@ async function runFeedback(cwd, note) {
4758
4940
  const path = storePath(cwd, "feedback.json");
4759
4941
  let entries = [];
4760
4942
  try {
4761
- const existing = JSON.parse(await readFile9(path, "utf8"));
4943
+ const existing = JSON.parse(await readFile11(path, "utf8"));
4762
4944
  entries = Array.isArray(existing.items) ? existing.items : [];
4763
4945
  } catch (error) {
4764
4946
  if (error.code !== "ENOENT") throw new CliError("Existing feedback could not be read; it was not overwritten");
4765
4947
  }
4766
4948
  entries.push({ createdAt: (/* @__PURE__ */ new Date()).toISOString(), note: sanitizeOutput(note.trim()) });
4767
- await writeFile8(path, `${JSON.stringify({ schema: "preview-feedback/v1", items: entries }, null, 2)}
4949
+ await writeFile9(path, `${JSON.stringify({ schema: "preview-feedback/v1", items: entries }, null, 2)}
4768
4950
  `, "utf8");
4769
4951
  return { schema: "feedback-result/v1", saved: true, uploaded: false, path, notes: entries.length };
4770
4952
  }
@@ -4853,6 +5035,8 @@ function mergeDecisions(existing, incoming) {
4853
5035
  return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
4854
5036
  }
4855
5037
  async function runInit(opts) {
5038
+ if (opts.agent && opts.agent !== "opencode") throw new Error("Supported launcher: --agent opencode. Use .agenttwin/AGENT-TASK.md with any other coding agent.");
5039
+ if (opts.agent && (opts.dryRun || opts.json)) throw new Error("A coding session requires a writing, interactive init (omit --dry-run and --json).");
4856
5040
  setQuiet(opts.json);
4857
5041
  const interactive = !opts.json && !opts.yes && !opts.dryRun && (opts.interactive === true || !opts.json && process.stdin.isTTY === true && process.stdout.isTTY === true);
4858
5042
  const log = (message) => {
@@ -4915,8 +5099,8 @@ async function runInit(opts) {
4915
5099
  await writeInventory(opts.cwd, finalInventory);
4916
5100
  await writePlan(opts.cwd, plan);
4917
5101
  await writeConfig(opts.cwd, config, { force: opts.force });
4918
- await writeFile9(join14(opts.cwd, "ENVIRONMENT.md"), renderEnvironment(finalInventory), "utf8");
4919
- await writeFile9(join14(opts.cwd, "REPLICATION.md"), renderReplication(finalInventory, plan), "utf8");
5102
+ await writeFile10(join16(opts.cwd, "ENVIRONMENT.md"), renderEnvironment(finalInventory), "utf8");
5103
+ await writeFile10(join16(opts.cwd, "REPLICATION.md"), renderReplication(finalInventory, plan), "utf8");
4920
5104
  reportPath = await writeBrowserReport(opts.cwd, finalInventory, plan);
4921
5105
  dashboardPath = await writeDashboard(opts.cwd, finalInventory);
4922
5106
  wrote = true;
@@ -4960,6 +5144,19 @@ async function runInit(opts) {
4960
5144
  if (dashboardPath) info(`Open ${dashboardPath} to review detected connectors and workflows.`);
4961
5145
  if (reportPath) info(`Open ${reportPath} in your browser. To save review notes: ${bold('agenttwin feedback --note "..."')}`);
4962
5146
  }
5147
+ if (!opts.dryRun) {
5148
+ const taskPath = await writeSessionTask(opts.cwd);
5149
+ if (!opts.json) info(`Coding-agent task: ${taskPath}`);
5150
+ const launch = opts.agent === "opencode" || interactive && installedOpenCode() && await confirm("Start OpenCode with your configured provider to analyze workflows? Repository context may be sent to that provider.", false);
5151
+ if (launch) {
5152
+ await launchSession(opts.cwd);
5153
+ await writeDashboard(opts.cwd, finalInventory);
5154
+ }
5155
+ if (opts.sync) {
5156
+ const synced = await runSync(opts.cwd);
5157
+ if (!opts.json) ok(`Synced ${synced.workflows} candidates: ${synced.dashboard}`);
5158
+ } else if (!opts.json) info("Send reviewed discovery to your account: npx agenttwin sync (requires AGENTTWIN_TOKEN).");
5159
+ }
4963
5160
  process.exitCode = result.exitCode;
4964
5161
  }
4965
5162
 
@@ -4967,7 +5164,7 @@ async function runInit(opts) {
4967
5164
  function readVersion() {
4968
5165
  const here = dirname4(fileURLToPath3(import.meta.url));
4969
5166
  const pkg = JSON.parse(
4970
- readFileSync3(join15(here, "..", "package.json"), "utf8")
5167
+ readFileSync3(join17(here, "..", "package.json"), "utf8")
4971
5168
  );
4972
5169
  return pkg.version ?? "0.0.0";
4973
5170
  }
@@ -4977,12 +5174,14 @@ function helpText() {
4977
5174
  "",
4978
5175
  bold("Usage"),
4979
5176
  " $ npx agenttwin <command> [options]",
5177
+ " $ npx agenttwin init --agent opencode [--sync]",
4980
5178
  "",
4981
5179
  bold("Commands"),
4982
5180
  ` ${"setup".padEnd(12)}Build your hosted E2B runtime template (once per account)`,
4983
5181
  ` ${"prepare".padEnd(12)}Bundle a reviewed discovered workflow (--file preparation.json)`,
4984
5182
  ` ${"run".padEnd(12)}Execute your Node workflow in a hosted E2B sandbox (--file world.json)`,
4985
5183
  ` ${"example".padEnd(12)}Write an editable multi-service workflow (--out directory)`,
5184
+ ` ${"sync".padEnd(12)}Send discovery metadata to your account dashboard`,
4986
5185
  ` ${"init".padEnd(12)}Discover and write agenttwin.config.json + reports`,
4987
5186
  ` ${"discover".padEnd(12)}Build the company inventory (writes .agenttwin/inventory.json)`,
4988
5187
  ` ${"decide".padEnd(12)}Apply structured decisions (--file or --stdin)`,
@@ -5029,6 +5228,8 @@ async function main() {
5029
5228
  answers: { type: "string" },
5030
5229
  "max-questions": { type: "string" },
5031
5230
  interactive: { type: "boolean" },
5231
+ agent: { type: "string" },
5232
+ sync: { type: "boolean" },
5032
5233
  "non-interactive": { type: "boolean" },
5033
5234
  note: { type: "string" },
5034
5235
  incremental: { type: "boolean" },
@@ -5115,6 +5316,13 @@ Evidence: ${result.evidencePath}`);
5115
5316
  Run: agenttwin run --file "${result.worldPath}"`);
5116
5317
  return;
5117
5318
  }
5319
+ case "sync": {
5320
+ const result = await runSync(cwd);
5321
+ printResult(result, `Synced ${result.tools} resources and ${result.workflows} workflow candidates.
5322
+ ${result.dashboard}
5323
+ ${result.uploaded}`);
5324
+ return;
5325
+ }
5118
5326
  case "init":
5119
5327
  await runInit({
5120
5328
  cwd,
@@ -5127,7 +5335,9 @@ Run: agenttwin run --file "${result.worldPath}"`);
5127
5335
  observeRun: str("observe-run"),
5128
5336
  answers: str("answers"),
5129
5337
  maxQuestions: Number(str("max-questions") ?? 8) || 8,
5130
- interactive: flag("interactive")
5338
+ interactive: flag("interactive"),
5339
+ agent: str("agent"),
5340
+ sync: flag("sync")
5131
5341
  });
5132
5342
  return;
5133
5343
  case "discover": {
@@ -5147,7 +5357,7 @@ Run: agenttwin run --file "${result.worldPath}"`);
5147
5357
  result,
5148
5358
  [
5149
5359
  `Discovered ${counts.tools} tools (${counts.classified} classified) \xB7 ${counts.unclassified} unclassified \xB7 ${counts.withTwin} with twins`,
5150
- `Wrote ${join15(".agenttwin", "inventory.json")}`,
5360
+ `Wrote ${join17(".agenttwin", "inventory.json")}`,
5151
5361
  result.blocked.length > 0 ? `Blocked: ${result.blocked.join("; ")}` : "",
5152
5362
  result.opportunities.length > 0 ? `Opportunities: ${result.opportunities.join("; ")}` : "",
5153
5363
  result.reconciliation.splits.length > 0 ? `Splits: ${result.reconciliation.splits.map((s) => s.from).join(", ")}` : ""
@@ -5170,7 +5380,7 @@ Run: agenttwin run --file "${result.worldPath}"`);
5170
5380
  printResult(
5171
5381
  result,
5172
5382
  `Plan: ${result.plan.items.length} tools \xB7 ${Object.entries(byStrategy).map(([k, v]) => `${k} ${v}`).join(", ")}
5173
- Wrote ${join15(".agenttwin", "plan.json")}`
5383
+ Wrote ${join17(".agenttwin", "plan.json")}`
5174
5384
  );
5175
5385
  return;
5176
5386
  }