agenttwin 0.3.10 → 0.3.11

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/README.md CHANGED
@@ -154,3 +154,9 @@ A run receipt's **Fix & retest** tab exports an evidence-based task for your cod
154
154
  The website’s Settings page saves workspace name, default execution limit (1–300 seconds for new templates), automatic refresh, compact lists and reduced motion in this browser. Existing reviewed bundles retain their limits. The account section supports token visibility, connection status, manual workspace refresh and disconnect; tokens are never persisted with preferences.
155
155
 
156
156
  Top-right initials open account actions. The sidebar closes into a full-width layout. See `docs-site/src/dashboard.mdx` for the complete guide. The website/API UI has received additional fixes after npm 0.2.2; the published package remains 0.2.2.
157
+
158
+ ### Workflow discovery boundaries
159
+
160
+ Discovery identifies individual request branches in Fetch/Bun-style URL dispatch, Express-style callbacks, queue workers and exported provider operations. It follows direct local function calls and relative named imports, including aliases, and recognizes provider hosts in literal HTTP calls. Each candidate retains its source entrypoint and evidence. Router containers and pure exported helpers are not treated as complete workflows.
161
+
162
+ These are possible static code paths, not executed or verified business workflows. Dynamic dispatch, computed destinations, external packages, re-exports and runtime-only integrations can remain unresolved. Prepare inputs, operation coverage and correctness checks before running a candidate. Refresh discovery and sync it to update an already-connected dashboard.
package/dist/cli.js CHANGED
@@ -1929,7 +1929,7 @@ import { join as join34 } from "path";
1929
1929
  // package.json
1930
1930
  var package_default = {
1931
1931
  name: "agenttwin",
1932
- version: "0.3.10",
1932
+ version: "0.3.11",
1933
1933
  description: "Give your agent a test world before it touches the real one.",
1934
1934
  keywords: [
1935
1935
  "agent",
@@ -6720,15 +6720,15 @@ function factoryProviderBindings(source) {
6720
6720
  constructors.set(specifier.local.name, provider.id);
6721
6721
  }
6722
6722
  }
6723
- const walk = (node, visit, skipFunctions = false) => {
6723
+ const walk2 = (node, visit, skipFunctions = false) => {
6724
6724
  if (!node || typeof node !== "object") return;
6725
6725
  const value = node;
6726
6726
  if (skipFunctions && ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(value.type)) return;
6727
6727
  visit(value);
6728
6728
  for (const [key, child] of Object.entries(value)) {
6729
6729
  if (["loc", "start", "end", "comments"].includes(key)) continue;
6730
- if (Array.isArray(child)) child.forEach((item) => walk(item, visit, skipFunctions));
6731
- else if (child && typeof child === "object") walk(child, visit, skipFunctions);
6730
+ if (Array.isArray(child)) child.forEach((item) => walk2(item, visit, skipFunctions));
6731
+ else if (child && typeof child === "object") walk2(child, visit, skipFunctions);
6732
6732
  }
6733
6733
  };
6734
6734
  const factories = /* @__PURE__ */ new Map();
@@ -6737,7 +6737,7 @@ function factoryProviderBindings(source) {
6737
6737
  if (node?.type !== "FunctionDeclaration" || !node.id || node.async || node.generator || node.params.length) continue;
6738
6738
  const providers = /* @__PURE__ */ new Set();
6739
6739
  let unknown = false;
6740
- walk(node.body, (child) => {
6740
+ walk2(node.body, (child) => {
6741
6741
  if (child.type !== "ReturnStatement") return;
6742
6742
  const argument = child.argument;
6743
6743
  if (argument?.type === "NullLiteral") return;
@@ -6749,18 +6749,18 @@ function factoryProviderBindings(source) {
6749
6749
  }
6750
6750
  const declarations = /* @__PURE__ */ new Map();
6751
6751
  const count = (name) => declarations.set(name, (declarations.get(name) ?? 0) + 1);
6752
- walk(ast.program, (node) => {
6752
+ walk2(ast.program, (node) => {
6753
6753
  if (["FunctionDeclaration", "ClassDeclaration"].includes(node.type) && node.id?.name) count(node.id.name);
6754
6754
  if (node.type === "VariableDeclarator" && node.id.type === "Identifier") count(node.id.name);
6755
6755
  if (["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(node.type)) {
6756
- for (const parameter of node.params ?? []) walk(parameter, (part) => {
6756
+ for (const parameter of node.params ?? []) walk2(parameter, (part) => {
6757
6757
  if (part.type === "Identifier") count(part.name);
6758
6758
  });
6759
6759
  }
6760
6760
  });
6761
6761
  for (const name of factories.keys()) if ((declarations.get(name) ?? 0) > 1) factories.delete(name);
6762
6762
  const ambiguous = /* @__PURE__ */ new Set();
6763
- walk(ast.program, (node) => {
6763
+ walk2(ast.program, (node) => {
6764
6764
  if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier" || node.init?.type !== "CallExpression" || node.init.callee.type !== "Identifier") return;
6765
6765
  const provider = factories.get(node.init.callee.name);
6766
6766
  if (!provider) return;
@@ -6776,6 +6776,195 @@ function factoryProviderBindings(source) {
6776
6776
  init_entrypoints();
6777
6777
  import { createHash as createHash12 } from "crypto";
6778
6778
 
6779
+ // src/inventory/workflow-graph.ts
6780
+ init_catalog();
6781
+ import { parse as parse3 } from "@babel/parser";
6782
+ import { posix } from "path";
6783
+ function walk(node, visit, skipFunctions = false) {
6784
+ if (!node || typeof node !== "object") return;
6785
+ if (skipFunctions && /^(FunctionDeclaration|FunctionExpression|ArrowFunctionExpression)$/.test(node.type)) return;
6786
+ visit(node);
6787
+ for (const [key, child] of Object.entries(node)) {
6788
+ if (["loc", "start", "end", "comments", "tokens"].includes(key)) continue;
6789
+ if (Array.isArray(child)) child.forEach((item) => walk(item, visit, skipFunctions));
6790
+ else if (child && typeof child === "object") walk(child, visit, skipFunctions);
6791
+ }
6792
+ }
6793
+ function workflowGraph(sources, operations, tools) {
6794
+ const modules = /* @__PURE__ */ new Map();
6795
+ const gaps = [];
6796
+ const resolve19 = (path2, specifier) => {
6797
+ const base = posix.normalize(posix.join(posix.dirname(path2), specifier));
6798
+ return [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].map((ext) => base + ext), base.replace(/\.js$/, ".ts"), ...[".ts", ".js"].map((ext) => base + "/index" + ext)].find((candidate) => sources.has(candidate));
6799
+ };
6800
+ for (const [path2, source] of sources) {
6801
+ if (!/\.[cm]?[jt]sx?$/.test(path2)) continue;
6802
+ let ast;
6803
+ try {
6804
+ ast = parse3(source, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
6805
+ } catch {
6806
+ gaps.push(`Workflow syntax could not be parsed: ${path2}`);
6807
+ continue;
6808
+ }
6809
+ const mod = { functions: /* @__PURE__ */ new Map(), imports: /* @__PURE__ */ new Map(), routes: [], ast };
6810
+ modules.set(path2, mod);
6811
+ const unit = (name, node, exported, trigger = false) => ({ path: path2, name, node, exported, trigger, startLine: node.loc.start.line, endLine: node.loc.end.line });
6812
+ for (const item of ast.program.body) {
6813
+ if (item.type === "ImportDeclaration" && item.importKind !== "type" && item.source.value.startsWith(".")) {
6814
+ const target = resolve19(path2, item.source.value);
6815
+ if (target) for (const spec of item.specifiers) {
6816
+ if (spec.type === "ImportSpecifier" && spec.importKind !== "type") mod.imports.set(spec.local.name, { path: target, name: spec.imported.name ?? spec.imported.value });
6817
+ if (spec.type === "ImportDefaultSpecifier") mod.imports.set(spec.local.name, { path: target, name: "default" });
6818
+ }
6819
+ }
6820
+ const exported = item.type === "ExportNamedDeclaration" || item.type === "ExportDefaultDeclaration";
6821
+ const declaration = exported ? item.declaration : item;
6822
+ if (declaration?.type === "FunctionDeclaration") {
6823
+ const name = declaration.id?.name ?? "default";
6824
+ mod.functions.set(name, { ...unit(name, declaration.body, exported), parameters: declaration.params });
6825
+ if (item.type === "ExportDefaultDeclaration") mod.functions.set("default", mod.functions.get(name));
6826
+ }
6827
+ if (declaration?.type === "VariableDeclaration") for (const variable of declaration.declarations) {
6828
+ if (variable.id.type !== "Identifier") continue;
6829
+ const fn = variable.init;
6830
+ if (fn && ["ArrowFunctionExpression", "FunctionExpression"].includes(fn.type)) mod.functions.set(variable.id.name, { ...unit(variable.id.name, fn.body, exported), parameters: fn.params });
6831
+ }
6832
+ }
6833
+ walk(ast.program, (fn) => {
6834
+ if (!["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(fn.type) || fn.body?.type !== "BlockStatement") return;
6835
+ const paths = /* @__PURE__ */ new Set();
6836
+ const patterns = /* @__PURE__ */ new Map();
6837
+ walk(fn.body, (node) => {
6838
+ if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier") return;
6839
+ if (node.init?.type === "MemberExpression" && node.init.property.name === "pathname") paths.add(node.id.name);
6840
+ const call = node.init;
6841
+ if (call?.type === "CallExpression" && call.callee.type === "MemberExpression" && paths.has(call.callee.object.name) && call.callee.property.name === "match" && call.arguments[0]?.type === "RegExpLiteral") patterns.set(node.id.name, "/" + call.arguments[0].pattern + "/");
6842
+ }, true);
6843
+ if (paths.size === 0) return;
6844
+ walk(fn.body, (node) => {
6845
+ if (node.type !== "IfStatement") return;
6846
+ const routes = /* @__PURE__ */ new Set();
6847
+ const methods = /* @__PURE__ */ new Set();
6848
+ walk(node.test, (test) => {
6849
+ if (test.type === "Identifier" && patterns.has(test.name)) routes.add(patterns.get(test.name));
6850
+ if (test.type !== "BinaryExpression" || !["===", "=="].includes(test.operator)) return;
6851
+ for (const [left, right] of [[test.left, test.right], [test.right, test.left]]) {
6852
+ if (right.type !== "StringLiteral") continue;
6853
+ if (left.type === "Identifier" && paths.has(left.name) && right.value.startsWith("/")) routes.add(right.value);
6854
+ if (left.type === "MemberExpression" && left.property.name === "method" && /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(right.value)) methods.add(right.value);
6855
+ }
6856
+ });
6857
+ let handlesRequest = false;
6858
+ walk(node.consequent, (part) => {
6859
+ if (part.type === "CallExpression" || part.type === "NewExpression" || part.type === "ReturnStatement" && part.argument && part.argument.type !== "NullLiteral") handlesRequest = true;
6860
+ }, true);
6861
+ if (!handlesRequest) return;
6862
+ for (const route of routes) {
6863
+ const method = methods.size === 1 ? [...methods][0] : "HTTP";
6864
+ mod.routes.push({ ...unit(`route:${method}:${route}@${node.loc.start.line}`, node.consequent, false, true), parameters: fn.params });
6865
+ }
6866
+ }, true);
6867
+ });
6868
+ walk(ast.program, (node) => {
6869
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier" || node.callee.name !== "fetch") return;
6870
+ const url = node.arguments[0];
6871
+ const value = url?.type === "StringLiteral" ? url.value : url?.type === "TemplateLiteral" ? url.quasis[0]?.value.cooked : void 0;
6872
+ if (!value || !/^https?:\/\//.test(value)) return;
6873
+ let host;
6874
+ try {
6875
+ host = new URL(value).hostname;
6876
+ } catch {
6877
+ return;
6878
+ }
6879
+ const provider = PROVIDERS.find((p) => p.signatures.hosts?.some((h2) => host === h2 || host.endsWith("." + h2)));
6880
+ const toolId = provider && tools.get(provider.id);
6881
+ if (!toolId) return;
6882
+ const methodNode = node.arguments[1]?.properties?.find((p) => p.key?.name === "method" || p.key?.value === "method")?.value;
6883
+ const method = methodNode ? methodNode.type === "StringLiteral" ? methodNode.value.toUpperCase() : "UNKNOWN" : "GET";
6884
+ const line = node.loc.start.line;
6885
+ operations.push({ path: path2, line, toolId, kind: method === "GET" || method === "HEAD" ? "reads" : "writes", label: `HTTP ${method} ${host}`, resource: provider.id, ref: `${path2}:${line}` });
6886
+ });
6887
+ }
6888
+ const adjacency = /* @__PURE__ */ new Map();
6889
+ const resolveCalls = (entry) => {
6890
+ const cached = adjacency.get(entry);
6891
+ if (cached) return cached;
6892
+ const found = /* @__PURE__ */ new Set();
6893
+ const boundNames = (nodes) => {
6894
+ const names = /* @__PURE__ */ new Set();
6895
+ for (const node of nodes) walk(node, (part) => {
6896
+ if (part.type === "Identifier") names.add(part.name);
6897
+ });
6898
+ return names;
6899
+ };
6900
+ const scan = (node, shadowed) => {
6901
+ if (!node || typeof node !== "object") return;
6902
+ if (node.type === "FunctionDeclaration") return;
6903
+ if (["ArrowFunctionExpression", "FunctionExpression"].includes(node.type)) {
6904
+ scan(node.body, /* @__PURE__ */ new Set([...shadowed, ...boundNames(node.params)]));
6905
+ return;
6906
+ }
6907
+ if (node.type === "BlockStatement") {
6908
+ shadowed = new Set(shadowed);
6909
+ for (const statement of node.body) {
6910
+ if (statement.type === "VariableDeclaration") for (const declaration of statement.declarations) for (const name of boundNames([declaration.id])) shadowed.add(name);
6911
+ if (statement.type === "FunctionDeclaration" && statement.id) shadowed.add(statement.id.name);
6912
+ }
6913
+ }
6914
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && !shadowed.has(node.callee.name)) {
6915
+ const mod = modules.get(entry.path);
6916
+ const local = mod.functions.get(node.callee.name);
6917
+ const imported = mod.imports.get(node.callee.name);
6918
+ const target = local ?? (imported ? modules.get(imported.path)?.functions.get(imported.name) : void 0);
6919
+ if (target && (local || target.exported)) found.add(target);
6920
+ }
6921
+ for (const [key, child] of Object.entries(node)) {
6922
+ if (["loc", "start", "end", "comments", "tokens"].includes(key)) continue;
6923
+ if (Array.isArray(child)) child.forEach((item) => scan(item, shadowed));
6924
+ else if (child && typeof child === "object") scan(child, shadowed);
6925
+ }
6926
+ };
6927
+ scan(entry.node, boundNames(entry.parameters ?? []));
6928
+ const result = [...found];
6929
+ adjacency.set(entry, result);
6930
+ return result;
6931
+ };
6932
+ const byPath = /* @__PURE__ */ new Map();
6933
+ for (const operation of operations) {
6934
+ const list2 = byPath.get(operation.path) ?? [];
6935
+ list2.push(operation);
6936
+ byPath.set(operation.path, list2);
6937
+ }
6938
+ const directEffects = /* @__PURE__ */ new Map();
6939
+ const effects = (unit) => {
6940
+ let result = directEffects.get(unit);
6941
+ if (!result) {
6942
+ result = (byPath.get(unit.path) ?? []).filter((op) => op.line >= unit.startLine && op.line <= unit.endLine);
6943
+ directEffects.set(unit, result);
6944
+ }
6945
+ return result;
6946
+ };
6947
+ const entries = [];
6948
+ for (const mod of modules.values()) {
6949
+ for (const entry of [...new Set(mod.functions.values()), ...mod.routes]) {
6950
+ if (!entry.exported && !entry.trigger) continue;
6951
+ if (!entry.trigger && mod.routes.some((route) => route.startLine >= entry.startLine && route.endLine <= entry.endLine)) continue;
6952
+ const visited = /* @__PURE__ */ new Set();
6953
+ const pending = [entry];
6954
+ while (pending.length) {
6955
+ const current = pending.pop();
6956
+ if (visited.has(current)) continue;
6957
+ visited.add(current);
6958
+ pending.push(...resolveCalls(current));
6959
+ }
6960
+ const calls = [...new Set([...visited].flatMap(effects))];
6961
+ if (!entry.trigger && !calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
6962
+ entries.push({ ...entry, calls, evidence: [...visited].map((u) => ({ source: "static", ref: `${u.path}:${u.startLine}` })) });
6963
+ }
6964
+ }
6965
+ return { entries, gaps };
6966
+ }
6967
+
6779
6968
  // src/inventory/operations.ts
6780
6969
  var WRITE_VERBS = /* @__PURE__ */ new Set([
6781
6970
  "create",
@@ -7194,32 +7383,47 @@ async function buildRelationships(apps, tools, files, decisions) {
7194
7383
  verified: false
7195
7384
  });
7196
7385
  }
7197
- for (const [path2, text] of sourceText) {
7386
+ const allowedCalls = entrypointCalls.filter((call) => decidedRecords.some((record2) => record2.toolId === call.toolId && record2.kind === call.kind && record2.evidence.some((e) => e.ref === call.ref)));
7387
+ const graph = workflowGraph(sourceText, allowedCalls, new Map(providerTools.map((t) => [t.providerId, t.id])));
7388
+ gaps.push(...graph.gaps);
7389
+ const entries = graph.entries.map((entry) => ({ ...entry, evidence: entry.evidence }));
7390
+ for (const [path2, text] of sourceText) for (const entry of exportedEntrypoints(text)) {
7391
+ if (!/^(route|job):/.test(entry.name)) continue;
7392
+ entries.push({
7393
+ ...entry,
7394
+ path: path2,
7395
+ node: {},
7396
+ exported: false,
7397
+ trigger: true,
7398
+ calls: allowedCalls.filter((call) => call.path === path2 && call.line >= entry.startLine && call.line <= entry.endLine),
7399
+ evidence: [{ source: "static", ref: `${path2}:${entry.startLine}` }]
7400
+ });
7401
+ }
7402
+ for (const entry of entries) {
7403
+ const { path: path2 } = entry;
7198
7404
  const app = appForPath(apps, path2);
7199
- for (const entry of exportedEntrypoints(text)) {
7200
- const calls = entrypointCalls.filter((call) => call.path === path2 && 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)));
7201
- if (!calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
7202
- const dependencies = [...new Set(calls.map((call) => call.toolId))];
7203
- const evidence = [...new Set(calls.map((call) => call.ref))].map((ref) => ({ source: "static", ref }));
7204
- workflows.push({
7205
- id: "entry:" + createHash12("sha256").update(path2 + ":" + entry.name).digest("hex").slice(0, 20),
7206
- title: entry.name,
7207
- entrypoint: path2 + "#" + entry.name,
7208
- apps: [app.id],
7209
- tools: dependencies,
7210
- steps: dependencies.map((tool2) => ({ app: app.id, tool: tool2, operations: [...new Set(calls.filter((c) => c.toolId === tool2).map((c) => c.label))], evidence })),
7211
- sharedEntities: [...new Set(calls.map((call) => singularize(call.resource)))],
7212
- evidence,
7213
- confidence: "medium",
7214
- risk: "high",
7215
- rank: 0,
7216
- supported: false,
7217
- hypothesis: true,
7218
- linkingSignals: ["callable export or route/job handler containing binding-resolved side effects; invocation and correctness require review"],
7219
- scopeUncertain: true,
7220
- verified: false
7221
- });
7222
- }
7405
+ const calls = entry.calls.filter((call) => !rejected.has(opRelId(appForPath(apps, call.path).id, call.toolId, call.kind)));
7406
+ if (!entry.trigger && !calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
7407
+ const dependencies = [...new Set(calls.map((call) => call.toolId))];
7408
+ const evidence = [...new Map([...entry.evidence, ...calls.map((call) => ({ source: "static", ref: call.ref }))].map((ref) => [ref.ref, ref])).values()];
7409
+ workflows.push({
7410
+ id: "entry:" + createHash12("sha256").update(path2 + ":" + entry.name).digest("hex").slice(0, 20),
7411
+ title: entry.name,
7412
+ entrypoint: path2 + "#" + entry.name.replace(/\\/g, "%5C"),
7413
+ apps: [app.id],
7414
+ tools: dependencies,
7415
+ steps: dependencies.map((tool2) => ({ app: app.id, tool: tool2, operations: [...new Set(calls.filter((c) => c.toolId === tool2).map((c) => c.label))], evidence })),
7416
+ sharedEntities: [...new Set(calls.map((call) => singularize(call.resource)))],
7417
+ evidence,
7418
+ confidence: "medium",
7419
+ risk: "high",
7420
+ rank: 0,
7421
+ supported: false,
7422
+ hypothesis: true,
7423
+ linkingSignals: [entry.trigger ? "explicit request or background-job trigger" : "exported operation with statically linked provider effects", "local helper call chains are possible paths, not proof of runtime execution; invocation and correctness require review"],
7424
+ scopeUncertain: true,
7425
+ verified: false
7426
+ });
7223
7427
  }
7224
7428
  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.");
7225
7429
  const finalWorkflows = workflows.filter((w) => !rejected.has(w.id)).map(
@@ -7878,6 +8082,10 @@ function scenariosFor(kind) {
7878
8082
  return [...new Set(list2)];
7879
8083
  }
7880
8084
  function assertionsFor(candidate) {
8085
+ if (candidate.entrypoint) return [
8086
+ `define the expected response/output for ${candidate.entrypoint}`,
8087
+ "review required final state and forbidden effects; discovery does not infer business correctness"
8088
+ ];
7881
8089
  const entity = candidate.sharedEntities[0] ?? "record";
7882
8090
  return [
7883
8091
  `exactly one ${entity} write is applied (no duplicates)`,
@@ -7900,7 +8108,7 @@ function buildWorkflowPlans(candidates, _tools) {
7900
8108
  ),
7901
8109
  sharedEntities: candidate.sharedEntities,
7902
8110
  permissions: permissionsFor(kind, candidate.sharedEntities),
7903
- asyncEvents: eventsFor(kind, candidate.sharedEntities),
8111
+ asyncEvents: candidate.entrypoint ? [] : eventsFor(kind, candidate.sharedEntities),
7904
8112
  failureScenarios: scenariosFor(kind),
7905
8113
  outcomeAssertions: assertionsFor(candidate),
7906
8114
  unknowns: [