@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
@@ -1,7 +1,7 @@
1
1
  import { collectExistingFrontendVerificationPaths } from "../workflows/dag/frontend-implementation-contract.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { inspectFrontendTestCommand, compareFrontendTestObservation, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
4
+ import { FRONTEND_TEST_OBSERVATION_FAILURES, inspectFrontendTestCommand, compareFrontendTestObservation, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
5
5
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
6
6
  import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
7
7
  import path from "node:path";
@@ -135,6 +135,13 @@ function shouldReuseSuccessfulShellCommands(shell) {
135
135
  return shell.verifyEvidence?.phase === "intermediate" || shell.verifyEvidence?.phase === "final";
136
136
  }
137
137
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
138
+ /**
139
+ * Tolerance for filesystem mtime granularity when rejecting a test report as
140
+ * stale. Reports are written by the command we just ran, so the only legitimate
141
+ * skew is coarse mtime resolution (1s on some filesystems) — not a real clock
142
+ * difference.
143
+ */
144
+ const REPORT_FRESHNESS_SKEW_MS = 2_000;
138
145
  const SUMMARY_STDOUT_MAX = 4_000;
139
146
  const SUMMARY_STDERR_MAX = 2_000;
140
147
  const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
@@ -312,7 +319,10 @@ export async function executeShellCommand(input) {
312
319
  : undefined;
313
320
  const child = spawn(resolveBashExecutable(), ["-c", input.command], {
314
321
  cwd: input.cwd,
315
- env: buildShellProcessEnv(input.envAllowlist, injectedEnv),
322
+ env: buildShellProcessEnv(input.envAllowlist, {
323
+ ...injectedEnv,
324
+ ...input.extraEnv,
325
+ }),
316
326
  ...processTreeSpawnOptions(),
317
327
  stdio: ["ignore", "pipe", "pipe"],
318
328
  });
@@ -2140,16 +2150,20 @@ async function executeFrontendVerificationBundle(input, meta) {
2140
2150
  let result = cached ? reuseCommandResult(cached, command) : undefined;
2141
2151
  if (!result) {
2142
2152
  const commandNumber = results.length + 1;
2143
- const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.vitest.json`);
2144
2153
  const observedTest = testPreflight.inspected.get(command);
2145
- const observedCommand = observedTest?.withReporter?.(reportPath);
2146
- if (observedCommand)
2154
+ // Name the report after the resolved protocol so an operator can
2155
+ // tell at a glance which runner produced it.
2156
+ const reportScheme = observedTest?.observation.scheme ?? "vitest-json-v1";
2157
+ const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.${reportScheme}.json`);
2158
+ const observedInvocation = observedTest?.withReporter?.(reportPath);
2159
+ if (observedInvocation)
2147
2160
  await mkdir(path.dirname(reportPath), { recursive: true });
2148
2161
  result = await executeShellCommand({
2149
- command: observedCommand ?? command,
2162
+ command: observedInvocation?.command ?? command,
2150
2163
  cwd,
2151
2164
  timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2152
2165
  envAllowlist: shell.envAllowlist,
2166
+ extraEnv: observedInvocation?.env,
2153
2167
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2154
2168
  reportActivity: input.reportActivity,
2155
2169
  outputArtifacts: {
@@ -2158,21 +2172,35 @@ async function executeFrontendVerificationBundle(input, meta) {
2158
2172
  },
2159
2173
  });
2160
2174
  let reportError;
2161
- if (observedCommand && result.ok) {
2175
+ if (observedInvocation && result.ok) {
2162
2176
  try {
2163
- // Vitest reports physical paths (e.g. /private/var on macOS).
2177
+ // Runners report physical paths (e.g. /private/var on macOS).
2164
2178
  // Match those in workspace coordinates even when shell.cwd is
2165
2179
  // a subdirectory or the caller used a directory alias.
2180
+ const reportStat = await stat(reportPath);
2181
+ // A stale report from an earlier run would otherwise let a
2182
+ // command that wrote nothing at all look verified.
2183
+ if (reportStat.mtimeMs < started - REPORT_FRESHNESS_SKEW_MS) {
2184
+ throw new Error(`stale test execution report (${FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol}): report predates this command`);
2185
+ }
2166
2186
  const rawReport = await readFile(reportPath, "utf8");
2167
2187
  const reportSha256 = createHash("sha256").update(rawReport).digest("hex");
2168
- commandExecutionEvidence.set(key, parseFrontendTestExecutionReport(JSON.parse(rawReport), await realpath(observedTest?.effectiveCwd ?? cwd), await realpath(input.cwd)));
2188
+ const parsedExecutions = parseFrontendTestExecutionReport(JSON.parse(rawReport), await realpath(observedTest?.effectiveCwd ?? cwd), await realpath(input.cwd), observedTest.observation.scheme);
2189
+ // Zero tests is not evidence: the runner may have matched no
2190
+ // file at all. The trace gate also rejects a target file with
2191
+ // no matching record, but failing here keeps the report itself
2192
+ // from being recorded as a successful observation.
2193
+ if (parsedExecutions.length === 0) {
2194
+ throw new Error(`test execution report contains no test cases (${FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol})`);
2195
+ }
2196
+ commandExecutionEvidence.set(key, parsedExecutions);
2169
2197
  const actual = commandExecutionEvidence.get(key);
2170
2198
  for (let offset = 0; offset < actual.length; offset += 24) {
2171
2199
  const relative = `${input.task.id}/commands/${commandNumber}-execution-${offset / 24}.json`;
2172
- const excerpt = { schemaVersion: 1, commandSha256: observedTest.observation.commandSha256, cwd: observedTest.observation.cwd, reportSha256, offset, total: actual.length, behaviorCoverage: "unconfirmed", tests: actual.slice(offset, offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) })) };
2200
+ const excerpt = { schemaVersion: 1, scheme: observedTest.observation.scheme, commandSha256: observedTest.observation.commandSha256, cwd: observedTest.observation.cwd, reportSha256, offset, total: actual.length, behaviorCoverage: "unconfirmed", tests: actual.slice(offset, offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) })) };
2173
2201
  await writeDagRunJsonArtifact(meta.runDir, relative, excerpt);
2174
2202
  const bytes = await readFile(path.join(meta.runDir, relative));
2175
- testExecutionEvidence.push({ path: relative, sha256: createHash("sha256").update(bytes).digest("hex"), reportPath: path.relative(meta.runDir, reportPath).replace(/\\/g, "/"), reportSha256 });
2203
+ testExecutionEvidence.push({ path: relative, sha256: createHash("sha256").update(bytes).digest("hex"), reportPath: path.relative(meta.runDir, reportPath).replace(/\\/g, "/"), reportSha256, scheme: observedTest.observation.scheme });
2176
2204
  }
2177
2205
  }
2178
2206
  catch (error) {
@@ -38,6 +38,11 @@ function contentTypeFor(pathname) {
38
38
  return "text/css; charset=utf-8";
39
39
  if (lower.endsWith(".json"))
40
40
  return "application/json; charset=utf-8";
41
+ // A web app manifest must carry this exact type: browsers ignore the
42
+ // document (and therefore offer no install entry) when it arrives as
43
+ // application/octet-stream.
44
+ if (lower.endsWith(".webmanifest"))
45
+ return "application/manifest+json";
41
46
  if (lower.endsWith(".svg"))
42
47
  return "image/svg+xml";
43
48
  if (lower.endsWith(".png"))
@@ -0,0 +1,39 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2
+ <title>Loop Agent App Icon</title>
3
+ <style>
4
+ .loop-ink { stroke: #26251e; }
5
+ @media (prefers-color-scheme: dark) {
6
+ .loop-ink { stroke: #f7f7f4; }
7
+ }
8
+ </style>
9
+ <defs>
10
+ <mask id="square-handoff" maskUnits="userSpaceOnUse" x="0" y="6" width="64" height="52">
11
+ <rect x="0" y="6" width="64" height="52" fill="#ffffff" />
12
+ <path
13
+ d="m31.15 32.28 2.83 3.11"
14
+ fill="none"
15
+ stroke="#000000"
16
+ stroke-linecap="round"
17
+ stroke-width="6.23"
18
+ />
19
+ </mask>
20
+ </defs>
21
+ <g transform="translate(4.8485 13.899) scale(0.56566)">
22
+ <path
23
+ class="loop-ink"
24
+ d="M12 32c0-16 16-20 28-6l17 18c11 11 27 5 27-12S68 9 57 20L39 39C28 51 12 48 12 32Z"
25
+ fill="none"
26
+ stroke-linecap="round"
27
+ stroke-linejoin="round"
28
+ stroke-width="11"
29
+ mask="url(#square-handoff)"
30
+ />
31
+ </g>
32
+ <path
33
+ d="m31.15 32.28 2.83 3.11"
34
+ fill="none"
35
+ stroke="#f54e00"
36
+ stroke-linecap="round"
37
+ stroke-width="5.37"
38
+ />
39
+ </svg>
@@ -3,7 +3,10 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#f4f2ee" />
7
+ <link rel="manifest" href="/manifest.webmanifest" />
6
8
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9
+ <link rel="apple-touch-icon" href="/app-icon.svg" />
7
10
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
8
11
  <link
9
12
  rel="stylesheet"
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "/",
3
+ "name": "Loop Operator Console",
4
+ "short_name": "Loop Console",
5
+ "description": "Loop Agent 操作台:任务、Agent DAG 运行与交付证据的只读观察与受控操作界面",
6
+ "start_url": "/",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "background_color": "#f4f2ee",
10
+ "theme_color": "#f4f2ee",
11
+ "icons": [
12
+ {
13
+ "src": "/app-icon.svg",
14
+ "sizes": "any",
15
+ "type": "image/svg+xml",
16
+ "purpose": "any"
17
+ }
18
+ ]
19
+ }
@@ -7,10 +7,46 @@ export const CONSOLE_THEMES = Object.freeze([
7
7
 
8
8
  const STORAGE_KEY = "loop-console.theme.v1";
9
9
 
10
+ /**
11
+ * Titlebar colour per theme, matching each palette's --canvas. An installed PWA
12
+ * paints the OS window frame with the theme-color meta value, so it must track
13
+ * the active theme: a fixed brand colour leaves an orange titlebar above the
14
+ * dark midnight palette. Values mirror console-theme.css --canvas.
15
+ */
16
+ const TITLEBAR_COLOR = Object.freeze({
17
+ default: "#f4f2ee",
18
+ midnight: "#1c222a",
19
+ daylight: "#eef0f2",
20
+ });
21
+
10
22
  function normalizeTheme(value) {
11
23
  return CONSOLE_THEMES.some((theme) => theme.id === value) ? value : "default";
12
24
  }
13
25
 
26
+ /**
27
+ * Project the theme onto the document's theme-color meta tag. Browsers read
28
+ * this tag live (the manifest theme_color only applies at install time), and
29
+ * an installed window repaints its frame when it changes.
30
+ *
31
+ * Strictly best-effort: the titlebar is cosmetic, so a missing or partial DOM
32
+ * (SSR, a test stub, an exotic embedder) must never break theme switching.
33
+ */
34
+ function syncTitlebarColor(theme) {
35
+ try {
36
+ const head = document?.head;
37
+ if (!head || typeof document.createElement !== "function") return;
38
+ let meta = document.querySelector?.('meta[name="theme-color"]');
39
+ if (!meta) {
40
+ meta = document.createElement("meta");
41
+ meta.setAttribute("name", "theme-color");
42
+ head.appendChild(meta);
43
+ }
44
+ meta.setAttribute("content", TITLEBAR_COLOR[theme] ?? TITLEBAR_COLOR.default);
45
+ } catch {
46
+ // The palette switch above already succeeded; the titlebar is optional.
47
+ }
48
+ }
49
+
14
50
  export function applyStoredConsoleTheme() {
15
51
  let value;
16
52
  try {
@@ -20,6 +56,7 @@ export function applyStoredConsoleTheme() {
20
56
  }
21
57
  const theme = normalizeTheme(value);
22
58
  document.documentElement.dataset.consoleTheme = theme;
59
+ syncTitlebarColor(theme);
23
60
  return theme;
24
61
  }
25
62
 
@@ -27,6 +64,7 @@ export function setConsoleTheme(value) {
27
64
  const theme = normalizeTheme(value);
28
65
  // Apply before persistence: a storage failure must not disable the picker.
29
66
  document.documentElement.dataset.consoleTheme = theme;
67
+ syncTitlebarColor(theme);
30
68
  try {
31
69
  window.localStorage.setItem(STORAGE_KEY, theme);
32
70
  } catch {