agenttwin 0.3.10 → 0.3.12
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 +7 -1
- package/dist/api.js +42 -2
- package/dist/cli.js +348 -47
- package/dist/dashboard/assets/next/static/chunks/9819-f4092ab03f2f8081.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-256847cee82d65f9.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/layout-936e4569a1d1bf81.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-0a9d4a8730592fce.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/b468fba8.437b212aae4abcf8.js +2 -0
- package/dist/dashboard/assets/next/static/chunks/webpack-7c1fd2c82a2ee8c9.js +1 -0
- package/dist/dashboard/assets/next/static/css/dd3fb06005e18149.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/gateway.js +42 -2
- package/package.json +1 -1
- package/dist/dashboard/assets/next/static/chunks/1848-a6938ad74a8e409f.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-03aac4c01adcda82.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/layout-dd5d4772c11d4cbb.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-a99a9eeb03937d53.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/webpack-351162512698e56b.js +0 -1
- package/dist/dashboard/assets/next/static/css/f9c2780fff15fd22.css +0 -1
- /package/dist/dashboard/assets/next/static/{puuDlswFkm304y1LsNJ98 → 1OFptRW9eKBlZCmD85D2C}/_buildManifest.js +0 -0
- /package/dist/dashboard/assets/next/static/{puuDlswFkm304y1LsNJ98 → 1OFptRW9eKBlZCmD85D2C}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Node.js 22.12+ is required. Run discovery from your project; npx installs the CL
|
|
|
12
12
|
npx agenttwin init
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`init` discovers your repository
|
|
15
|
+
`init` discovers and connects your repository, then shows one recommended next step: set up your first test, or run the test you already selected. You can search and page through detected workflows, load a reviewed test file, and view the last result. Fault exploration, replay, patch verification, regressions, CI setup and runtime selection live under Advanced options. Bare `npx agenttwin` opens the same menu. `npx agenttwin refresh` updates discovery without executing application code. Existing commands remain available to CI and coding agents. Selecting a discovered workflow saves your choice and provides a coding-agent preparation request; it does not automatically create or execute a test.
|
|
16
16
|
|
|
17
17
|
Discovery writes the inventory, workflow candidates, `agenttwin.config.json`, `ENVIRONMENT.md`, `REPLICATION.md`, and an offline browser report. A discovered candidate is not an executable specification: the menu accepts a reviewed `agenttwin.world.json` with explicit bundled Node files, synthetic services, invocation and required outcomes. The optional demo is clearly labeled as synthetic. Redirected input and `--yes` only perform discovery; they never start the interactive execution loop. `init --json --dry-run` writes nothing. Interactive init connects and syncs discovery after the first browser approval; saved connections sync without another approval. Noninteractive init never uploads automatically.
|
|
18
18
|
|
|
@@ -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/api.js
CHANGED
|
@@ -1596,7 +1596,7 @@ var challengeStore = (map, key, value, ttlMs) => {
|
|
|
1596
1596
|
init_dashboard_transfer();
|
|
1597
1597
|
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
|
|
1598
1598
|
import { createServer as createServer2 } from "http";
|
|
1599
|
-
import { createHash as
|
|
1599
|
+
import { createHash as createHash9, randomBytes as randomBytes6, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
1600
1600
|
import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile9, rename, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
|
|
1601
1601
|
import { resolve as resolve4, join as join13, dirname as dirname2, extname } from "path";
|
|
1602
1602
|
import { tmpdir as tmpdir3 } from "os";
|
|
@@ -4106,8 +4106,43 @@ See run.json for assertions, service state, application output and isolation met
|
|
|
4106
4106
|
}
|
|
4107
4107
|
}
|
|
4108
4108
|
|
|
4109
|
+
// src/runtime/analytics.ts
|
|
4110
|
+
import { createHash as createHash8 } from "crypto";
|
|
4111
|
+
function analyticsConfiguration() {
|
|
4112
|
+
const key = process.env.POSTHOG_API_KEY;
|
|
4113
|
+
const host = process.env.POSTHOG_API_HOST || "https://us.i.posthog.com";
|
|
4114
|
+
return { enabled: Boolean(key), host: host.endsWith("/") ? host.slice(0, -1) : host, key: key ?? "" };
|
|
4115
|
+
}
|
|
4116
|
+
var FLUSH_SINK = "/capture/";
|
|
4117
|
+
function captureAnalytics(event) {
|
|
4118
|
+
const config = analyticsConfiguration();
|
|
4119
|
+
if (!config.enabled) return;
|
|
4120
|
+
const body2 = {
|
|
4121
|
+
api_key: config.key,
|
|
4122
|
+
event: event.event,
|
|
4123
|
+
distinct_id: event.distinctId,
|
|
4124
|
+
properties: {
|
|
4125
|
+
$lib: "agenttwin-gateway",
|
|
4126
|
+
$lib_version: "0.1",
|
|
4127
|
+
...event.properties
|
|
4128
|
+
},
|
|
4129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4130
|
+
};
|
|
4131
|
+
const headers = { "content-type": "application/json" };
|
|
4132
|
+
const timeout = AbortSignal.timeout(4e3);
|
|
4133
|
+
void fetch(config.host + FLUSH_SINK, {
|
|
4134
|
+
method: "POST",
|
|
4135
|
+
headers,
|
|
4136
|
+
body: JSON.stringify(body2),
|
|
4137
|
+
signal: timeout
|
|
4138
|
+
}).then((response) => {
|
|
4139
|
+
if (!response.ok) throw new Error("analytics_http_" + response.status);
|
|
4140
|
+
}).catch(() => {
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4109
4144
|
// src/runtime/gateway.ts
|
|
4110
|
-
var hash2 = (value) =>
|
|
4145
|
+
var hash2 = (value) => createHash9("sha256").update(value).digest("hex");
|
|
4111
4146
|
function uploadPath(path2) {
|
|
4112
4147
|
if (!path2 || path2.startsWith("/") || path2.includes("\\") || path2.includes("\0") || path2.split("/").some((p) => !p || p === "." || p === ".." || /^(\.env.*|\.git|\.ssh|\.aws|\.azure|\.config|\.npmrc|\.netrc|credentials.*)$/i.test(p)) || /\.(pem|key|p12|pfx)$/i.test(path2)) throw new Error("Unsafe uploaded file path");
|
|
4113
4148
|
return path2;
|
|
@@ -4227,10 +4262,12 @@ async function startGateway(opts) {
|
|
|
4227
4262
|
const { reportPath: _report, evidencePath: _evidence, ...portable } = receipt;
|
|
4228
4263
|
item.job.receipt = { ...portable, git: item.git, project: item.project, apiJobId: item.job.id, provenance: "client-supplied workflow declaration" };
|
|
4229
4264
|
item.job.status = "completed";
|
|
4265
|
+
captureAnalytics({ event: "run_completed", distinctId: item.job.accountId, properties: { jobId: item.job.id, ok: true } });
|
|
4230
4266
|
} catch (error) {
|
|
4231
4267
|
item.job.status = controllers.get(item.job.id)?.signal.aborted ? "cancelled" : "error";
|
|
4232
4268
|
const message = error instanceof Error ? error.message : String(error);
|
|
4233
4269
|
item.job.error = message.split(process.env.E2B_API_KEY ?? "\0").join("[redacted]").replace(/(?:postgresql|redis):\/\/[^\s]+/g, "[test-connection]").slice(0, 2e3);
|
|
4270
|
+
if (item.job.status !== "cancelled") captureAnalytics({ event: "run_failed", distinctId: item.job.accountId, properties: { jobId: item.job.id } });
|
|
4234
4271
|
} finally {
|
|
4235
4272
|
try {
|
|
4236
4273
|
if (workspace) await rm4(workspace, { recursive: true, force: true });
|
|
@@ -4451,6 +4488,7 @@ async function startGateway(opts) {
|
|
|
4451
4488
|
if (store.accounts.length >= 1e4) throw new Error("account_capacity");
|
|
4452
4489
|
owner = { id: randomUUID2(), tokenHash: hash2(randomBytes6(32).toString("hex")), label: identity.name + "'s workspace", active: true, usageDay: "", runs: 0, identity: { email: identity.email, name: identity.name }, externalIdentities: [{ provider: identity.provider, issuer: identity.issuer, subject: identity.subject }], ...identity.organization ? { organization: identity.organization } : {} };
|
|
4453
4490
|
store.accounts.push(owner);
|
|
4491
|
+
captureAnalytics({ event: "account_created", distinctId: owner.id, properties: { method: identity.provider, realm: identity.organization || identity.issuer } });
|
|
4454
4492
|
}
|
|
4455
4493
|
if (!owner.active) throw new Error("account_inactive");
|
|
4456
4494
|
const secret = randomBytes6(32).toString("hex");
|
|
@@ -4671,6 +4709,7 @@ async function startGateway(opts) {
|
|
|
4671
4709
|
}
|
|
4672
4710
|
a = { id: randomUUID2(), tokenHash: hash2(randomBytes6(32).toString("hex")), label: workspace, active: true, usageDay: "", runs: 0, identity: { email: parsed.email, name, passwordHash: encoded } };
|
|
4673
4711
|
store.accounts.push(a);
|
|
4712
|
+
captureAnalytics({ event: "account_created", distinctId: a.id, properties: { method: "email", realm: "password" } });
|
|
4674
4713
|
} else {
|
|
4675
4714
|
const matches2 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
|
|
4676
4715
|
if (!a?.active || !a.identity?.passwordHash || !matches2) {
|
|
@@ -5111,6 +5150,7 @@ async function startGateway(opts) {
|
|
|
5111
5150
|
account.runs++;
|
|
5112
5151
|
store.jobs.push(job);
|
|
5113
5152
|
await save();
|
|
5153
|
+
captureAnalytics({ event: "run_submitted", distinctId: account.id, properties: { jobId: job.id, project } });
|
|
5114
5154
|
controllers.set(job.id, new AbortController());
|
|
5115
5155
|
queue.push({ job, spec, files: input.files, git, project });
|
|
5116
5156
|
schedule();
|
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.
|
|
1932
|
+
version: "0.3.12",
|
|
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
|
|
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) =>
|
|
6731
|
-
else if (child && typeof child === "object")
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? [])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
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: [
|
|
@@ -9072,7 +9280,7 @@ async function runInit(opts) {
|
|
|
9072
9280
|
// src/commands/control.ts
|
|
9073
9281
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
9074
9282
|
import { readFile as readFile23, writeFile as writeFile22 } from "fs/promises";
|
|
9075
|
-
import { join as join27, resolve as resolve14 } from "path";
|
|
9283
|
+
import { basename as basename4, join as join27, resolve as resolve14 } from "path";
|
|
9076
9284
|
init_store();
|
|
9077
9285
|
init_contracts();
|
|
9078
9286
|
|
|
@@ -9173,7 +9381,7 @@ var CONTROL_ACTIONS = [
|
|
|
9173
9381
|
async function readState(cwd) {
|
|
9174
9382
|
try {
|
|
9175
9383
|
const state = JSON.parse(await readFile23(join27(cwd, ".agenttwin/control.json"), "utf8"));
|
|
9176
|
-
if (state.schema === "agenttwin/control-v1" && ["world", "exploration", "receipt"].every((key) => state[key] === void 0 || typeof state[key] === "string") && (state.runtime === void 0 || ["local", "hosted", "e2b"].includes(state.runtime))) return state;
|
|
9384
|
+
if (state.schema === "agenttwin/control-v1" && ["world", "exploration", "receipt", "workflowId", "workflowTitle"].every((key) => state[key] === void 0 || typeof state[key] === "string") && (state.runtime === void 0 || ["local", "hosted", "e2b"].includes(state.runtime))) return state;
|
|
9177
9385
|
} catch {
|
|
9178
9386
|
}
|
|
9179
9387
|
return { schema: "agenttwin/control-v1" };
|
|
@@ -9188,7 +9396,7 @@ async function runControl(opts) {
|
|
|
9188
9396
|
const ask = async (prompt) => (await rl.question(prompt + " ")).trim();
|
|
9189
9397
|
const save = () => writeFile22(join27(opts.cwd, ".agenttwin/control.json"), JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
9190
9398
|
const requireWorld = () => {
|
|
9191
|
-
if (!state.world) throw new Error("
|
|
9399
|
+
if (!state.world) throw new Error("Set up a test first. A detected workflow needs application files, test data and expected outcomes before it can run.");
|
|
9192
9400
|
return state.world;
|
|
9193
9401
|
};
|
|
9194
9402
|
const connectAndShare = async () => {
|
|
@@ -9199,20 +9407,110 @@ async function runControl(opts) {
|
|
|
9199
9407
|
const result = await runSync(opts.cwd);
|
|
9200
9408
|
log(`Project connected: ${result.tools} services and ${result.workflows} workflow candidates synced. ${result.dashboard}`);
|
|
9201
9409
|
};
|
|
9410
|
+
const chooseWorkflow = async () => {
|
|
9411
|
+
const inventory = await readInventory(opts.cwd);
|
|
9412
|
+
const workflows = inventory?.workflows ?? [];
|
|
9413
|
+
if (!workflows.length) {
|
|
9414
|
+
log("No workflow candidates found. Refresh discovery in Advanced options, or load an existing test file.");
|
|
9415
|
+
return;
|
|
9416
|
+
}
|
|
9417
|
+
log(`${workflows.length} workflow candidates found. These are code paths, not tested workflows yet.`);
|
|
9418
|
+
const query = (await ask("Find a workflow (for example mail, calendar or billing; Enter shows all):")).toLowerCase();
|
|
9419
|
+
const matches2 = workflows.filter((w) => `${w.title} ${w.entrypoint ?? ""}`.toLowerCase().includes(query));
|
|
9420
|
+
if (!matches2.length) {
|
|
9421
|
+
log("No matching workflows. Try another search.");
|
|
9422
|
+
return;
|
|
9423
|
+
}
|
|
9424
|
+
let offset = 0;
|
|
9425
|
+
while (true) {
|
|
9426
|
+
const page2 = matches2.slice(offset, offset + 10);
|
|
9427
|
+
page2.forEach((w, i) => log(` ${i + 1}. ${w.title}${w.entrypoint ? ` \xB7 ${w.entrypoint}` : ""}`));
|
|
9428
|
+
log(`Showing ${offset + 1}\u2013${offset + page2.length} of ${matches2.length}. n: next page \xB7 p: previous page \xB7 0: back`);
|
|
9429
|
+
const selected = await ask("Choose a workflow [0]:");
|
|
9430
|
+
if (!selected || selected === "0") return;
|
|
9431
|
+
if (selected === "n") {
|
|
9432
|
+
offset = offset + 10 < matches2.length ? offset + 10 : 0;
|
|
9433
|
+
continue;
|
|
9434
|
+
}
|
|
9435
|
+
if (selected === "p") {
|
|
9436
|
+
offset = Math.max(0, offset - 10);
|
|
9437
|
+
continue;
|
|
9438
|
+
}
|
|
9439
|
+
const workflow = /^\d+$/.test(selected) ? page2[Number(selected) - 1] : void 0;
|
|
9440
|
+
if (!workflow) {
|
|
9441
|
+
log("Choose a workflow number from this page.");
|
|
9442
|
+
continue;
|
|
9443
|
+
}
|
|
9444
|
+
state.workflowId = workflow.id;
|
|
9445
|
+
state.workflowTitle = workflow.title;
|
|
9446
|
+
delete state.world;
|
|
9447
|
+
delete state.exploration;
|
|
9448
|
+
delete state.receipt;
|
|
9449
|
+
await save();
|
|
9450
|
+
log(`Selected: ${workflow.title}`);
|
|
9451
|
+
log("Next: prepare its test. AgentTwin cannot yet turn every detected code path into a runnable test automatically.");
|
|
9452
|
+
log("Give your coding agent this request:");
|
|
9453
|
+
log(`Read .agenttwin/AGENT-TASK.md and .agenttwin/inventory.json. Prepare workflow ${JSON.stringify(workflow.id)} (${JSON.stringify(workflow.title)}) as agenttwin.world.json, with bundled application files, supported service operations, synthetic records and reviewed expected outcomes. Report unsupported dependencies; do not replace the application with a demo.`);
|
|
9454
|
+
log("Then return here and choose Load a test file. Selecting a workflow has not run your application.");
|
|
9455
|
+
return;
|
|
9456
|
+
}
|
|
9457
|
+
};
|
|
9202
9458
|
try {
|
|
9203
9459
|
try {
|
|
9204
9460
|
await connectAndShare();
|
|
9205
9461
|
} catch (error) {
|
|
9206
|
-
log(`Connection stopped: ${error instanceof Error ? error.message : String(error)}. Local discovery is saved. Retry
|
|
9462
|
+
log(`Connection stopped: ${error instanceof Error ? error.message : String(error)}. Local discovery is saved. Retry Connect project in Advanced options.`);
|
|
9207
9463
|
}
|
|
9208
9464
|
while (true) {
|
|
9209
|
-
log(
|
|
9210
|
-
|
|
9211
|
-
|
|
9465
|
+
log(`
|
|
9466
|
+
AgentTwin \xB7 ${basename4(opts.cwd)}`);
|
|
9467
|
+
if (state.world) log(`Ready to test: ${state.world} \xB7 ${state.runtime ?? "local"}`);
|
|
9468
|
+
else if (state.workflowTitle) log(`Setup needed: ${state.workflowTitle}`);
|
|
9469
|
+
else log("Next: choose a workflow to test.");
|
|
9470
|
+
log(` 1. ${state.world ? "Run my test" : state.workflowTitle ? "Continue test setup" : "Set up my first test"} \u2190 next step`);
|
|
9471
|
+
log(" 2. View last result");
|
|
9472
|
+
log(" 3. Change workflow or test");
|
|
9473
|
+
log(" 4. Advanced options");
|
|
9212
9474
|
log(" 0. Exit");
|
|
9213
|
-
|
|
9214
|
-
if (
|
|
9475
|
+
let choice = await ask("Choose [1]:");
|
|
9476
|
+
if (choice === "0") break;
|
|
9215
9477
|
try {
|
|
9478
|
+
if (!choice || choice === "1") choice = state.world ? "3" : "setup";
|
|
9479
|
+
else if (choice === "2") choice = "13";
|
|
9480
|
+
else if (choice === "3") choice = "setup";
|
|
9481
|
+
else if (choice === "4") {
|
|
9482
|
+
log("\nAdvanced options");
|
|
9483
|
+
CONTROL_ACTIONS.forEach((action, index) => log(` ${index + 1}. ${action}`));
|
|
9484
|
+
log(" 0. Back");
|
|
9485
|
+
choice = await ask("Advanced action [0]:");
|
|
9486
|
+
if (!choice || choice === "0") continue;
|
|
9487
|
+
if (!/^\d+$/.test(choice) || Number(choice) < 1 || Number(choice) > CONTROL_ACTIONS.length) {
|
|
9488
|
+
log("Choose a listed action.");
|
|
9489
|
+
continue;
|
|
9490
|
+
}
|
|
9491
|
+
} else {
|
|
9492
|
+
log("Choose 1, 2, 3, 4 or 0.");
|
|
9493
|
+
continue;
|
|
9494
|
+
}
|
|
9495
|
+
if (choice === "setup") {
|
|
9496
|
+
log("\nSet up a test");
|
|
9497
|
+
log(" 1. Choose a workflow from my repository");
|
|
9498
|
+
log(" 2. Load a test file I already have");
|
|
9499
|
+
log(" 3. Try a sample test (not my application)");
|
|
9500
|
+
log(" 0. Back");
|
|
9501
|
+
const setup = await ask("Choose [1]:");
|
|
9502
|
+
if (!setup || setup === "1") {
|
|
9503
|
+
await chooseWorkflow();
|
|
9504
|
+
continue;
|
|
9505
|
+
}
|
|
9506
|
+
if (setup === "0") continue;
|
|
9507
|
+
if (setup === "2") choice = "2";
|
|
9508
|
+
else if (setup === "3") choice = "12";
|
|
9509
|
+
else {
|
|
9510
|
+
log("Choose a listed action.");
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9216
9514
|
switch (choice) {
|
|
9217
9515
|
case "1": {
|
|
9218
9516
|
const inventory = await readInventory(opts.cwd);
|
|
@@ -9220,7 +9518,7 @@ async function runControl(opts) {
|
|
|
9220
9518
|
break;
|
|
9221
9519
|
}
|
|
9222
9520
|
case "2": {
|
|
9223
|
-
let file = await ask("
|
|
9521
|
+
let file = await ask("Test file [agenttwin.world.json]:") || "agenttwin.world.json";
|
|
9224
9522
|
const { parseWorld: parseWorld2 } = await Promise.resolve().then(() => (init_spec(), spec_exports));
|
|
9225
9523
|
let input = JSON.parse(await readFile23(resolve14(opts.cwd, file), "utf8"));
|
|
9226
9524
|
if (input.schema === "agenttwin/preparation-v1") {
|
|
@@ -9232,8 +9530,11 @@ async function runControl(opts) {
|
|
|
9232
9530
|
const spec = parseWorld2(input);
|
|
9233
9531
|
log(`${spec.name}: ${spec.files.length} explicit application files, ${spec.assertions.length} required outcomes, ${(spec.forbiddenEffects?.length ?? 0) + (spec.preservedState?.length ?? 0)} safety checks.`);
|
|
9234
9532
|
log("Review the listed files, command, synthetic state and checks. Local execution runs application code on this machine.");
|
|
9235
|
-
state.world = file;
|
|
9533
|
+
state.world = resolve14(opts.cwd, file);
|
|
9534
|
+
delete state.receipt;
|
|
9535
|
+
delete state.exploration;
|
|
9236
9536
|
await save();
|
|
9537
|
+
log("Test ready. Choose Run my test to execute it.");
|
|
9237
9538
|
break;
|
|
9238
9539
|
}
|
|
9239
9540
|
case "3": {
|
|
@@ -9253,12 +9554,12 @@ async function runControl(opts) {
|
|
|
9253
9554
|
const result = await runExploration(opts.cwd, captured.id, { log });
|
|
9254
9555
|
state.receipt = (result.results.find((r) => r.status !== "passed") ?? result.results[0]).evidencePath;
|
|
9255
9556
|
await save();
|
|
9256
|
-
log(`${result.status.toUpperCase()} \xB7 saved ${result.results.length} trial receipts. Replay
|
|
9557
|
+
log(`${result.status.toUpperCase()} \xB7 saved ${result.results.length} trial receipts. Replay or verify a patch in Advanced options.`);
|
|
9257
9558
|
break;
|
|
9258
9559
|
}
|
|
9259
9560
|
case "5":
|
|
9260
9561
|
case "6": {
|
|
9261
|
-
if (!state.exploration) throw new Error("
|
|
9562
|
+
if (!state.exploration) throw new Error("Choose Explore seeded faults in Advanced options first.");
|
|
9262
9563
|
const result = await runExploration(opts.cwd, state.exploration, { patched: choice === "6", log });
|
|
9263
9564
|
state.receipt = (result.results.find((r) => r.status !== "passed") ?? result.results[0]).evidencePath;
|
|
9264
9565
|
await save();
|