@tea-agent/loop-agent 0.35.1-beta.2 → 0.35.1

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 (48) hide show
  1. package/AGENTS.md +0 -2
  2. package/CHANGELOG.md +25 -24
  3. package/bin/loop-agent.js +1 -37
  4. package/dist/application/dag/generate-task-dag.js +4 -1
  5. package/dist/application/task-lifecycle/advance.js +14 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/commands/task-advance.js +1 -0
  8. package/dist/executors/dag-pi-executor.js +0 -44
  9. package/dist/shared/package-metadata.js +0 -42
  10. package/dist/task/config-types.js +2 -0
  11. package/dist/task/contract/project.js +3 -0
  12. package/dist/task/contract/schema.js +1 -0
  13. package/dist/task/source-prepare/build-draft.js +7 -0
  14. package/dist/task/source-prepare/semantic-intake.js +37 -10
  15. package/dist/task/task-demand-routing.js +10 -0
  16. package/dist/worker/console/operator-actions.js +72 -6
  17. package/dist/worker/console/prd-intake-bridge.js +10 -3
  18. package/dist/worker/console/prd-reference-discovery.js +124 -0
  19. package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
  20. package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
  24. package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
  25. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  26. package/dist/worker/observability/read-model.js +0 -20
  27. package/dist/worker/preflight.js +1 -2
  28. package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
  29. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  30. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  32. package/dist/workflows/dag/frontend-repair.js +18 -219
  33. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  34. package/dist/workflows/dag/init-hybrid.js +26 -41
  35. package/dist/workflows/dag/node-execution.js +0 -89
  36. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  37. package/dist/workflows/dag/runner.js +11 -245
  38. package/dist/workflows/dag/scheduler.js +3 -257
  39. package/dist/workflows/dag/types.js +2 -130
  40. package/package.json +2 -2
  41. package/dist/build-stamp.json +0 -6
  42. package/dist/workflows/dag/contract-output-registry.js +0 -14
  43. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  44. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  45. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  46. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  47. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  48. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <link rel="stylesheet" href="/inspect/operator-chrome.css" />
8
8
  <title>Loop 操作台 · Operator Console</title>
9
- <script type="module" crossorigin src="/assets/index-CvsQgALl.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-hJqCPs_g.css">
9
+ <script type="module" crossorigin src="/assets/index-M0BLEBfh.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-HX1pbOyl.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -2,6 +2,21 @@ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
3
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
4
4
  import { confirmationToken, CONFIRMATION_HEADER, LONG_RUNNING_ACTIONS, postOperator, waitForOperation, } from "./console-types.js";
5
+ /** Human-readable in-progress copy for sync actions that take tens of
6
+ * seconds server-side (imports + intake). Prevents the "page looks frozen"
7
+ * gap between click and the terminal flash message. */
8
+ const ACTION_PROGRESS_COPY = {
9
+ bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
10
+ importPrd: "正在导入 PRD:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
11
+ interviewStart: "正在启动需求访谈…",
12
+ interviewTurn: "正在提交访谈回答…",
13
+ interviewSkip: "正在跳过访谈并生成评估…",
14
+ contractValidate: "正在校验契约草稿…",
15
+ contractDiff: "正在计算契约差异…",
16
+ contractApply: "正在应用契约(写入需求与执行约束)…",
17
+ spineCheck: "正在运行 Spine 审计…",
18
+ dagGenerate: "正在生成 DAG…",
19
+ };
5
20
  /** Central operator-action dispatcher: POST /api/operator/v1/operations with
6
21
  * polling, long-running background waits, interview payload handoff and the
7
22
  * per-operation SSE follow log. */
@@ -18,7 +33,10 @@ export function useOperatorActions(params) {
18
33
  setBusy(true);
19
34
  setOpError(null);
20
35
  setOpErrorCode(null);
21
- setOpMessage(null);
36
+ // Show an immediate progress hint instead of a blank flash region:
37
+ // sync actions (imports/intake) block for tens of seconds with no
38
+ // other UI feedback, which reads as a frozen page.
39
+ setOpMessage(ACTION_PROGRESS_COPY[action] ?? null);
22
40
  try {
23
41
  const { status, body } = await postOperator(action, actionParams, opts?.clientRequestId);
24
42
  if (status === 412 || body.error?.code === "PI_SETUP_REQUIRED") {
@@ -22,7 +22,6 @@ export function useRecoveryConsole(params) {
22
22
  const [recoveryNodesLoading, setRecoveryNodesLoading] = useState(false);
23
23
  const [recoveryRunStatus, setRecoveryRunStatus] = useState(null);
24
24
  const [recoveryFailureCategory, setRecoveryFailureCategory] = useState(null);
25
- const [frontendRecovery, setFrontendRecovery] = useState(null);
26
25
  const [manualRunId, setManualRunId] = useState(false);
27
26
  const [manualNodeId, setManualNodeId] = useState(false);
28
27
  const [recoveryReport, setRecoveryReport] = useState(null);
@@ -125,7 +124,6 @@ export function useRecoveryConsole(params) {
125
124
  setRecoveryNodes([]);
126
125
  setRecoveryRunStatus(null);
127
126
  setRecoveryFailureCategory(null);
128
- setFrontendRecovery(null);
129
127
  return;
130
128
  }
131
129
  setRecoveryNodesLoading(true);
@@ -169,13 +167,11 @@ export function useRecoveryConsole(params) {
169
167
  n.failureCategory &&
170
168
  n.failureCategory !== "success")?.failureCategory;
171
169
  setRecoveryFailureCategory(runFailure ?? nodeFailure ?? null);
172
- setFrontendRecovery(body.frontendRecovery ?? null);
173
170
  }
174
171
  catch {
175
172
  setRecoveryNodes([]);
176
173
  setRecoveryRunStatus(null);
177
174
  setRecoveryFailureCategory(null);
178
- setFrontendRecovery(null);
179
175
  }
180
176
  finally {
181
177
  setRecoveryNodesLoading(false);
@@ -327,7 +323,6 @@ export function useRecoveryConsole(params) {
327
323
  recoveryRunsLoading,
328
324
  recoveryNodesLoading,
329
325
  recoveryRunStatus,
330
- frontendRecovery,
331
326
  manualRunId,
332
327
  setManualRunId,
333
328
  manualNodeId,
@@ -208,12 +208,24 @@ export function useTaskWizard(params) {
208
208
  const semanticNote = result.semanticIntake?.applied
209
209
  ? "semantic"
210
210
  : (result.semanticIntake?.code ?? "no-semantic");
211
+ const autoImported = (result.autoImportedReferences ?? []).filter((ref) => ref.repoRelative);
212
+ const unresolvedRefs = (result.unresolvedReferences ?? []).filter((ref) => ref.token);
211
213
  setOpMessage([
212
214
  `已创建任务「${result.title ?? ""}」(${result.taskId ?? ""})`,
213
215
  `来源:${result.identitySource ?? "prd"}`,
214
216
  `解析:${parseLabel}(${semanticNote})`,
215
217
  objectiveHint ? `目标:${objectiveHint.slice(0, 120)}` : null,
216
218
  gapCount > 0 ? `缺口 ${gapCount} 项待确认` : null,
219
+ autoImported.length > 0
220
+ ? `已自动导入 ${autoImported.length} 个引用文档(${autoImported
221
+ .map((ref) => `${ref.repoRelative}${ref.role ? `:${ref.role}` : ""}`)
222
+ .join("、")})`
223
+ : null,
224
+ unresolvedRefs.length > 0
225
+ ? `有 ${unresolvedRefs.length} 个引用未能解析:${unresolvedRefs
226
+ .map((ref) => `${ref.token}(${ref.reason ?? "未知原因"})`)
227
+ .join("、")}`
228
+ : null,
217
229
  ]
218
230
  .filter(Boolean)
219
231
  .join(" · "));
@@ -2,15 +2,11 @@ import { spawn } from "node:child_process";
2
2
  import { appendFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, normalizeCliVersionLabel, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
5
+ import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
6
6
  import { parseCommandJson } from "./parse-json.js";
7
7
  export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
8
8
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
9
9
  export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
10
- /** Grace window between the SIGTERM and SIGKILL stages of a hard timeout, so a
11
- * runner can persist terminal state (see executeDagCheckpoint) before being
12
- * force-killed. */
13
- export const HARD_TIMEOUT_KILL_GRACE_MS = 10_000;
14
10
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
15
11
  /**
16
12
  * Bind evidence supplied by an orchestration layer to the identity exposed by
@@ -182,11 +178,10 @@ export class LoopAgentClient {
182
178
  if (!this._identity) {
183
179
  throw new Error("controller identity: cannot record a reported version without a pinned identity");
184
180
  }
185
- const normalized = normalizeCliVersionLabel(reportedVersion);
186
- if (normalized !== this._identity.packageVersion) {
181
+ if (reportedVersion !== this._identity.packageVersion) {
187
182
  throw new Error(`controller identity: reported version ${reportedVersion} does not match package version ${this._identity.packageVersion}`);
188
183
  }
189
- this._identity = { ...this._identity, reportedVersion: normalized };
184
+ this._identity = { ...this._identity, reportedVersion };
190
185
  return this._identity;
191
186
  }
192
187
  async run(args, options) {
@@ -386,17 +381,10 @@ function spawnCommand(input) {
386
381
  });
387
382
  };
388
383
  let hardTimeout;
389
- let killGraceTimeout;
390
384
  if (input.hardTimeoutMs !== undefined && input.hardTimeoutMs > 0) {
391
385
  hardTimeout = setTimeout(() => {
392
386
  timedOut = true;
393
- // Two-stage termination: SIGTERM first so the runner can persist a
394
- // terminal state (see executeDagCheckpoint signal handler), then
395
- // SIGKILL only after the grace window expires.
396
387
  child.kill("SIGTERM");
397
- killGraceTimeout = setTimeout(() => {
398
- child.kill("SIGKILL");
399
- }, HARD_TIMEOUT_KILL_GRACE_MS);
400
388
  }, input.hardTimeoutMs);
401
389
  }
402
390
  let heartbeatTimer;
@@ -406,8 +394,6 @@ function spawnCommand(input) {
406
394
  const clearTimers = () => {
407
395
  if (hardTimeout !== undefined)
408
396
  clearTimeout(hardTimeout);
409
- if (killGraceTimeout !== undefined)
410
- clearTimeout(killGraceTimeout);
411
397
  if (heartbeatTimer !== undefined) {
412
398
  clearInterval(heartbeatTimer);
413
399
  heartbeatTimer = undefined;
@@ -1491,7 +1491,6 @@ function mergeDagRun(existing, incoming) {
1491
1491
  failureCategory: incoming.failureCategory ?? existing.failureCategory,
1492
1492
  stateConsistent: incoming.stateConsistent ?? existing.stateConsistent,
1493
1493
  recoveryEligibility: incoming.recoveryEligibility ?? existing.recoveryEligibility,
1494
- frontendRecovery: incoming.frontendRecovery ?? existing.frontendRecovery,
1495
1494
  title: incoming.title ?? existing.title,
1496
1495
  startedAt,
1497
1496
  finishedAt,
@@ -1791,23 +1790,6 @@ async function loadFrontendLintProjection(runDir) {
1791
1790
  blockingReasons: parsed.data.blockingReasons,
1792
1791
  };
1793
1792
  }
1794
- function projectFrontendRecovery(state) {
1795
- const s = state.frontendRecoveryState;
1796
- const r = state.frontendRecoveryResult;
1797
- if (!s && !r)
1798
- return undefined;
1799
- return {
1800
- ...(s?.recoveryRootRunId ? { recoveryRootRunId: s.recoveryRootRunId } : {}),
1801
- ...(s?.parentRunId ? { parentRunId: s.parentRunId } : {}),
1802
- ...(s?.childRunId ? { childRunId: s.childRunId } : {}),
1803
- ...(s?.attemptIndex !== undefined ? { attemptIndex: s.attemptIndex } : {}),
1804
- ...(s?.continuationCount !== undefined
1805
- ? { continuationCount: s.continuationCount }
1806
- : {}),
1807
- ...(s?.phase ? { phase: s.phase } : {}),
1808
- ...(r?.outcome ? { outcome: r.outcome } : {}),
1809
- };
1810
- }
1811
1793
  async function parseDagStateFile(statePath, now) {
1812
1794
  try {
1813
1795
  if (!existsSync(statePath))
@@ -1878,7 +1860,6 @@ async function parseDagStateFile(statePath, now) {
1878
1860
  loadBackendTestProjection(runDir, nodes),
1879
1861
  loadFrontendLintProjection(runDir),
1880
1862
  ]);
1881
- const frontendRecovery = projectFrontendRecovery(state);
1882
1863
  const continuationRecord = state.continuation &&
1883
1864
  typeof state.continuation.parentRunId === "string" &&
1884
1865
  typeof state.continuation.effectiveFromNodeId === "string" &&
@@ -1937,7 +1918,6 @@ async function parseDagStateFile(statePath, now) {
1937
1918
  ...(continuation ? { continuation } : {}),
1938
1919
  ...(backendTest ? { backendTest } : {}),
1939
1920
  ...(frontendLint ? { frontendLint } : {}),
1940
- ...(frontendRecovery ? { frontendRecovery } : {}),
1941
1921
  };
1942
1922
  }
1943
1923
  catch {
@@ -1,6 +1,5 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { normalizeCliVersionLabel } from "../shared/package-metadata.js";
4
3
  import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
5
4
  export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
6
5
  export async function preflightTargetRepo(input) {
@@ -48,7 +47,7 @@ export async function preflightTargetRepo(input) {
48
47
  controllerIdentity: identity,
49
48
  };
50
49
  }
51
- const reportedVersion = normalizeCliVersionLabel(version.stdout);
50
+ const reportedVersion = version.stdout.trim();
52
51
  let controllerIdentity = identity
53
52
  ? { ...identity, reportedVersion }
54
53
  : undefined;
@@ -724,40 +724,30 @@ export function deterministicRewriteScenarioParam(input) {
724
724
  return { ok: true, source: nextSource, detail: "rewritten" };
725
725
  }
726
726
  function validatePythonSyntaxWithoutExecution(source) {
727
- // Prefer `python3` (present on modern POSIX images and macOS); fall back to
728
- // `python` for older or Windows layouts where only the unversioned shim exists.
729
- // A missing interpreter must not silently roll back an otherwise-valid
730
- // deterministic rewrite.
731
- const candidates = ["python3", "python"];
732
- let lastError;
733
- for (const interpreter of candidates) {
734
- const result = spawnSync(interpreter, ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
735
- input: source,
736
- encoding: "utf8",
737
- env: {
738
- ...process.env,
739
- PYTHONDONTWRITEBYTECODE: "1",
740
- PYTHONUTF8: "1",
741
- },
742
- timeout: 15_000,
743
- });
744
- if (result.error) {
745
- lastError = result.error.message;
746
- continue;
747
- }
748
- if (result.status !== 0) {
749
- const detail = String(result.stderr || result.stdout || "invalid Python syntax")
750
- .trim()
751
- .replace(/\s+/g, " ")
752
- .slice(0, 500);
753
- return { ok: false, detail: `python syntax validation failed: ${detail}` };
754
- }
755
- return { ok: true, detail: "python ast.parse PASS" };
727
+ const result = spawnSync("python", ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
728
+ input: source,
729
+ encoding: "utf8",
730
+ env: {
731
+ ...process.env,
732
+ PYTHONDONTWRITEBYTECODE: "1",
733
+ PYTHONUTF8: "1",
734
+ },
735
+ timeout: 15_000,
736
+ });
737
+ if (result.error) {
738
+ return {
739
+ ok: false,
740
+ detail: `python syntax validation unavailable: ${result.error.message}`,
741
+ };
756
742
  }
757
- return {
758
- ok: false,
759
- detail: `python syntax validation unavailable: ${lastError ?? "python/python3 not found"}`,
760
- };
743
+ if (result.status !== 0) {
744
+ const detail = String(result.stderr || result.stdout || "invalid Python syntax")
745
+ .trim()
746
+ .replace(/\s+/g, " ")
747
+ .slice(0, 500);
748
+ return { ok: false, detail: `python syntax validation failed: ${detail}` };
749
+ }
750
+ return { ok: true, detail: "python ast.parse PASS" };
761
751
  }
762
752
  export async function applyDeterministicScenarioParamRepairs(input) {
763
753
  const repairable = input.facts.entries.filter((entry) => entry.status === "MISMATCH" && entry.repairability === "repairable");
@@ -31,15 +31,7 @@ export function getNodeOutputAsJson(state, nodeId) {
31
31
  export function normalizeVerdictCandidateLine(value) {
32
32
  const trimmed = String(value).trim();
33
33
  const emphasized = trimmed.match(/^\*{1,3}\s*(VERDICT:[^*]+?)\s*\*{1,3}$/);
34
- const line = (emphasized ? emphasized[1] : trimmed).trim();
35
- // Canonicalize the colon separator and trailing sentence punctuation so
36
- // "VERDICT:pass", "VERDICT: pass", or "VERDICT: pass." match the declared
37
- // enum lines without accepting free-text verdicts. Conflicting verdicts
38
- // still canonicalize to distinct enum lines and remain fail-closed.
39
- const verdictMatch = line.match(/^VERDICT:\s*(pass|request-revision)\s*[.!?。!?]*$/i);
40
- if (verdictMatch)
41
- return `VERDICT: ${verdictMatch[1].toLowerCase()}`;
42
- return line;
34
+ return (emphasized ? emphasized[1] : trimmed).trim();
43
35
  }
44
36
  /** First line matching /^VERDICT:/ from node assistantText/stdout (markdown emphasis OK). */
45
37
  export function extractFirstVerdictLineFromNodeText(text) {