@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi/sessions/index.js +6 -0
  4. package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
  5. package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
  6. package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
  7. package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
  8. package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
  9. package/dist/executors/dag-pi-executor.js +12 -2075
  10. package/dist/executors/shell-executor.js +39 -11
  11. package/dist/worker/console/routes.js +5 -0
  12. package/dist/worker/console/static/app-icon.svg +39 -0
  13. package/dist/worker/console/static/index.html +3 -0
  14. package/dist/worker/console/static/manifest.webmanifest +19 -0
  15. package/dist/worker/observe/static/console-theme.js +38 -0
  16. package/dist/workflows/dag/checkpoint.js +686 -0
  17. package/dist/workflows/dag/frontend-repair.js +24 -0
  18. package/dist/workflows/dag/frontend-review-scopes.js +10 -2
  19. package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
  20. package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
  21. package/dist/workflows/dag/hybrid/sources.js +1812 -0
  22. package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
  23. package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
  24. package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
  25. package/dist/workflows/dag/hybrid/templates/index.js +8 -0
  26. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
  27. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
  28. package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
  29. package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
  30. package/dist/workflows/dag/hybrid/types.js +32 -0
  31. package/dist/workflows/dag/init-hybrid.js +42 -7162
  32. package/dist/workflows/dag/runner.js +11 -1178
  33. package/dist/workflows/dag/terminal-status.js +502 -0
  34. package/docs/architecture/dag-execution.md +4 -4
  35. package/package.json +1 -1
@@ -0,0 +1,309 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ /**
5
+ * Framework adapters decouple the frontend behavior-verification contract from
6
+ * any single test runner.
7
+ *
8
+ * The contract, writer admission and execution all need three things from a
9
+ * frozen verify command:
10
+ * 1. whether the runner can emit a machine-readable execution report at all
11
+ * (capability), so an unsupported runner fails closed *before* implement;
12
+ * 2. how to ask that runner for a report without clobbering the project's
13
+ * own reporter configuration;
14
+ * 3. how to read that report back as uniform file/title/status triples.
15
+ *
16
+ * Historically all three were hardcoded to Vitest (`vitest-json-v1`), while the
17
+ * command classifier was already framework-agnostic. Jest and Playwright
18
+ * commands were therefore classified into the behavior lane correctly and then
19
+ * rejected as "unsupported", surfacing a capability gap to the user as if they
20
+ * had misconfigured their project. Adapters make the two sources of truth agree.
21
+ */
22
+ /** Report protocols a frozen behavior command may be observed through. */
23
+ export const frontendTestReportSchemeSchema = z.enum([
24
+ "vitest-json-v1",
25
+ "jest-json-v1",
26
+ "playwright-json-v1",
27
+ ]);
28
+ /** Runtime guard for a persisted scheme, e.g. read back from a trace artifact. */
29
+ export const isFrontendTestReportScheme = (value) => typeof value === "string" &&
30
+ frontendTestReportSchemeSchema.options.includes(value);
31
+ const asRecord = (value) => typeof value === "object" && value !== null
32
+ ? value
33
+ : undefined;
34
+ /**
35
+ * Resolve a runner-relative path into contract workspace coordinates.
36
+ *
37
+ * A runner may report an absolute path (`config.rootDir` is absolute in a
38
+ * Playwright report) or one relative to its own root. Either way the result is
39
+ * expressed against `workspaceRoot`, which is resolved physically first: a
40
+ * workspace reached through a symlink (macOS `/var` -> `/private/var`, i.e. the
41
+ * default `os.tmpdir()`) is never a lexical prefix of the runner's canonical
42
+ * path, so comparing raw strings would yield a `../..` escape instead of a
43
+ * contract-relative file.
44
+ */
45
+ const toWorkspacePath = (raw, cwd, workspaceRoot) => {
46
+ const absolute = path.resolve(cwd, raw);
47
+ const physical = (target) => {
48
+ try {
49
+ return realpathSync(target);
50
+ }
51
+ catch {
52
+ return target;
53
+ }
54
+ };
55
+ const root = physical(path.resolve(workspaceRoot));
56
+ // The path may itself sit under the unresolved root; re-anchor it physically
57
+ // by resolving its root-relative tail against the physical root.
58
+ const anchored = absolute.startsWith(path.resolve(workspaceRoot) + path.sep)
59
+ ? path.join(root, path.relative(path.resolve(workspaceRoot), absolute))
60
+ : absolute;
61
+ return path.relative(root, physical(anchored)).replace(/\\/g, "/");
62
+ };
63
+ /**
64
+ * Strip package-manager indirection so `npx vitest run`, `pnpm exec vitest
65
+ * run`, `yarn exec vitest run` and a bare `vitest run` all reduce to the same
66
+ * runner invocation. Returns the remaining words.
67
+ */
68
+ export function resolveRunnerArgv(argv) {
69
+ const words = [...argv];
70
+ // Skip leading env assignments (`CI=1 vitest run`).
71
+ while (words.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) {
72
+ words.shift();
73
+ }
74
+ const head = words[0];
75
+ if (head === "npx" || head === "pnpx" || head === "bunx") {
76
+ words.shift();
77
+ // `npx --no-install vitest run` / `npx -y vitest run`
78
+ while (words[0]?.startsWith("-"))
79
+ words.shift();
80
+ return words;
81
+ }
82
+ if ((head === "pnpm" || head === "yarn" || head === "npm") && words[1] === "exec") {
83
+ words.splice(0, 2);
84
+ while (words[0]?.startsWith("-"))
85
+ words.shift();
86
+ return words;
87
+ }
88
+ return words;
89
+ }
90
+ /** Runner binaries each adapter claims, keyed by adapter scheme. */
91
+ const VITEST_BINARIES = new Set(["vitest"]);
92
+ const JEST_BINARIES = new Set(["jest"]);
93
+ const PLAYWRIGHT_BINARIES = new Set(["playwright"]);
94
+ const basenameOf = (value) => value.split(/[\\/]/).pop() ?? "";
95
+ /** True when `runner` invokes `binary` directly (allowing an explicit path). */
96
+ const invokesBinary = (runner, binaries) => binaries.has(basenameOf(runner[0] ?? ""));
97
+ const vitestAdapter = {
98
+ scheme: "vitest-json-v1",
99
+ runner: "vitest",
100
+ matches: ({ runner }) => invokesBinary(runner, VITEST_BINARIES),
101
+ // Vitest's json reporter writes to stdout unless an output file is given,
102
+ // and the executor reads the report from disk, so both flags are required.
103
+ reportSuffix: () => `--reporter=json --outputFile=`,
104
+ parse: (raw, cwd, workspaceRoot) => {
105
+ const schema = z.object({
106
+ testResults: z.array(z.object({
107
+ name: z.string().min(1),
108
+ assertionResults: z.array(z.object({
109
+ title: z.string(),
110
+ fullName: z.string().optional(),
111
+ ancestorTitles: z.array(z.string()).optional(),
112
+ status: z.string(),
113
+ })),
114
+ })),
115
+ });
116
+ return schema
117
+ .parse(raw)
118
+ .testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
119
+ file: toWorkspacePath(suite.name, cwd, workspaceRoot),
120
+ title: test.fullName ??
121
+ [...(test.ancestorTitles ?? []), test.title].join(" "),
122
+ status: test.status,
123
+ })));
124
+ },
125
+ };
126
+ const jestAdapter = {
127
+ scheme: "jest-json-v1",
128
+ runner: "jest",
129
+ matches: ({ runner }) => invokesBinary(runner, JEST_BINARIES),
130
+ reportSuffix: () => `--json --outputFile`,
131
+ parse: (raw, cwd, workspaceRoot) => {
132
+ // Jest's `--json` shape: testResults[] carry a per-file `name` plus
133
+ // assertionResults[] that mirror Vitest's field names.
134
+ const schema = z.object({
135
+ testResults: z.array(z.object({
136
+ name: z.string().min(1),
137
+ assertionResults: z.array(z.object({
138
+ title: z.string(),
139
+ fullName: z.string().optional(),
140
+ ancestorTitles: z.array(z.string()).optional(),
141
+ status: z.string(),
142
+ })),
143
+ })),
144
+ });
145
+ return schema
146
+ .parse(raw)
147
+ .testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
148
+ file: toWorkspacePath(suite.name, cwd, workspaceRoot),
149
+ title: test.fullName ??
150
+ [...(test.ancestorTitles ?? []), test.title].join(" "),
151
+ status: test.status,
152
+ })));
153
+ },
154
+ };
155
+ /**
156
+ * Playwright's JSON reporter emits a nested `suites` tree whose leaf `specs`
157
+ * carry `tests[].results[].status`. File paths live on the enclosing `file`
158
+ * field of each suite node, so the tree is walked while tracking it.
159
+ */
160
+ const playwrightAdapter = {
161
+ scheme: "playwright-json-v1",
162
+ runner: "playwright",
163
+ matches: ({ runner }) => {
164
+ if (!invokesBinary(runner, PLAYWRIGHT_BINARIES))
165
+ return false;
166
+ // `playwright test` runs tests; `playwright install`/`codegen` do not.
167
+ return runner[1] === "test" || runner[1] === undefined;
168
+ },
169
+ reportSuffix: () => `--reporter=json`,
170
+ // Playwright ignores an output filename on the CLI (`--output` is its
171
+ // artifacts directory); its JSON reporter goes to stdout unless this
172
+ // variable names a file.
173
+ reportPathEnv: "PLAYWRIGHT_JSON_OUTPUT_NAME",
174
+ parse: (raw, cwd, workspaceRoot) => {
175
+ const suiteSchema = z.lazy(() => z.object({
176
+ file: z.string().optional(),
177
+ title: z.string().optional(),
178
+ specs: z
179
+ .array(z.object({
180
+ title: z.string(),
181
+ ok: z.boolean(),
182
+ tests: z.array(z.object({
183
+ status: z.string().optional(),
184
+ results: z
185
+ .array(z.object({
186
+ status: z.string(),
187
+ }))
188
+ .optional(),
189
+ })),
190
+ }))
191
+ .optional(),
192
+ suites: z.array(suiteSchema).optional(),
193
+ }));
194
+ const root = z
195
+ .object({
196
+ suites: z.array(suiteSchema).optional(),
197
+ // Playwright reports each suite's `file` relative to the resolved
198
+ // `testDir` (`config.rootDir`), not to the process cwd. A project
199
+ // with `testDir: "./tests"` reports `orders.spec.js` for
200
+ // `tests/orders.spec.js`, so resolving against cwd would produce a
201
+ // path that can never match a contract target.
202
+ config: z
203
+ .object({ rootDir: z.string().optional() })
204
+ .optional(),
205
+ })
206
+ .parse(raw);
207
+ // Prefer the runner's own reported root; fall back to cwd only when the
208
+ // report omitted it (older report shapes).
209
+ const reportedRoot = root.config?.rootDir;
210
+ const baseCwd = typeof reportedRoot === "string" && reportedRoot.length > 0
211
+ ? reportedRoot
212
+ : cwd;
213
+ const out = [];
214
+ const walk = (node, inheritedFile) => {
215
+ const record = asRecord(node);
216
+ if (!record)
217
+ return;
218
+ const file = typeof record.file === "string" ? record.file : inheritedFile;
219
+ for (const spec of record.specs ?? []) {
220
+ const specRecord = asRecord(spec);
221
+ if (!specRecord)
222
+ continue;
223
+ const title = String(specRecord.title ?? "");
224
+ const tests = specRecord.tests ?? [];
225
+ // A spec with several projects/retries yields several tests; the
226
+ // spec only passes when every attempt passed.
227
+ const statuses = tests.flatMap((test) => {
228
+ const testRecord = asRecord(test);
229
+ const results = testRecord?.results ?? [];
230
+ const attempts = results
231
+ .map((result) => asRecord(result)?.status)
232
+ .filter((value) => typeof value === "string");
233
+ return attempts.length > 0
234
+ ? attempts
235
+ : [String(testRecord?.status ?? "unknown")];
236
+ });
237
+ const status = statuses.every((value) => value === "passed")
238
+ ? "passed"
239
+ : statuses.includes("passed")
240
+ ? "failed"
241
+ : (statuses[0] ?? "unknown");
242
+ out.push({
243
+ file: toWorkspacePath(file ?? "", baseCwd, workspaceRoot),
244
+ title,
245
+ status,
246
+ });
247
+ }
248
+ for (const child of record.suites ?? []) {
249
+ walk(child, file);
250
+ }
251
+ };
252
+ for (const suite of root.suites ?? [])
253
+ walk(suite, undefined);
254
+ return out;
255
+ },
256
+ };
257
+ export const FRONTEND_TEST_ADAPTERS = [
258
+ vitestAdapter,
259
+ jestAdapter,
260
+ playwrightAdapter,
261
+ ];
262
+ /**
263
+ * Runners the container classifier recognizes as behavior verification but
264
+ * that have no adapter yet. Naming them lets the capability gap read as
265
+ * "framework not adapted yet" instead of a user configuration error.
266
+ */
267
+ const KNOWN_UNADAPTED_RUNNERS = new Set([
268
+ "mocha",
269
+ "ava",
270
+ "tap",
271
+ "cypress",
272
+ "testcafe",
273
+ "nightwatch",
274
+ "wdio",
275
+ "webdriverio",
276
+ ]);
277
+ /** Resolve the adapter for a command, or `undefined` when none matches. */
278
+ export function resolveFrontendTestAdapter(context) {
279
+ // A package script owns the detail: `npm test` must resolve through the
280
+ // script body, not the wrapper, or `npm test` and `vitest run` would
281
+ // disagree about capability.
282
+ if (context.scriptArgv) {
283
+ const scriptRunner = resolveRunnerArgv(context.scriptArgv);
284
+ const fromScript = FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches({ ...context, runner: scriptRunner }));
285
+ if (fromScript)
286
+ return fromScript;
287
+ }
288
+ return FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches(context));
289
+ }
290
+ /**
291
+ * Detect a recognized-but-unadapted runner so callers can report a capability
292
+ * gap. Returns the runner name when the command is a test runner we know of
293
+ * but cannot observe.
294
+ */
295
+ export function detectUnadaptedRunner(runner) {
296
+ const binary = basenameOf(runner[0] ?? "").replace(/\.(?:js|cjs|mjs|cmd|exe)$/u, "");
297
+ if (KNOWN_UNADAPTED_RUNNERS.has(binary))
298
+ return binary;
299
+ // `node --test`, `node --test-only`, plus `deno test`/`bun test`.
300
+ if (binary === "node" && runner.includes("--test"))
301
+ return "node --test";
302
+ if (binary === "deno" && runner[1] === "test")
303
+ return "deno test";
304
+ if (binary === "bun" && runner[1] === "test")
305
+ return "bun test";
306
+ return undefined;
307
+ }
308
+ /** Empty `file` means the runner reported a spec outside the workspace. */
309
+ export const isOutsideWorkspacePath = (file) => file === ".." || file.startsWith("../") || path.isAbsolute(file);