@tea-agent/loop-agent 0.16.25 → 0.17.0

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 (115) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -3
  3. package/dist/cli/command-definitions.js +43 -0
  4. package/dist/cli/program.js +26 -0
  5. package/dist/commands/dag-approve.js +4 -0
  6. package/dist/commands/dag-resume.js +1 -0
  7. package/dist/commands/dag-validate.js +6 -0
  8. package/dist/commands/operator.js +44 -0
  9. package/dist/commands/task-contract.js +271 -0
  10. package/dist/executors/dag-pi-executor.js +118 -16
  11. package/dist/executors/pi-executor.js +206 -13
  12. package/dist/executors/pi-sdk-executor.js +21 -6
  13. package/dist/executors/shell-executor.js +85 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/executors/shell-write-guard.js +64 -2
  16. package/dist/shared/operator/capabilities.js +255 -0
  17. package/dist/shared/operator/envelope.js +59 -0
  18. package/dist/shared/operator/index.js +4 -0
  19. package/dist/shared/operator/registry.js +38 -0
  20. package/dist/shared/operator/types.js +5 -0
  21. package/dist/task/contract/adopt.js +166 -0
  22. package/dist/task/contract/apply.js +326 -0
  23. package/dist/task/contract/canonicalize.js +60 -0
  24. package/dist/task/contract/constants.js +29 -0
  25. package/dist/task/contract/diff.js +177 -0
  26. package/dist/task/contract/hash.js +42 -0
  27. package/dist/task/contract/import-revision.js +96 -0
  28. package/dist/task/contract/index.js +17 -0
  29. package/dist/task/contract/journal.js +155 -0
  30. package/dist/task/contract/lock.js +153 -0
  31. package/dist/task/contract/observe.js +296 -0
  32. package/dist/task/contract/paths.js +19 -0
  33. package/dist/task/contract/project.js +170 -0
  34. package/dist/task/contract/recover.js +312 -0
  35. package/dist/task/contract/request-ledger.js +37 -0
  36. package/dist/task/contract/schema.js +151 -0
  37. package/dist/task/contract/transaction.js +160 -0
  38. package/dist/task/contract/types.js +1 -0
  39. package/dist/task/contract/validate-draft.js +106 -0
  40. package/dist/task/index.js +3 -0
  41. package/dist/task/operator/capabilities.js +6 -0
  42. package/dist/task/operator/envelope.js +2 -0
  43. package/dist/task/operator/index.js +5 -0
  44. package/dist/task/operator/registry.js +2 -0
  45. package/dist/task/operator/types.js +1 -0
  46. package/dist/task/runtime.js +5 -1
  47. package/dist/task/source-references.js +7 -0
  48. package/dist/worker/cli.js +150 -32
  49. package/dist/worker/console/app-data.js +185 -0
  50. package/dist/worker/console/dag-confirmation.js +313 -0
  51. package/dist/worker/console/doctor.js +169 -0
  52. package/dist/worker/console/draft-store.js +80 -0
  53. package/dist/worker/console/index.js +15 -0
  54. package/dist/worker/console/interview/assessment.js +67 -0
  55. package/dist/worker/console/interview/session.js +100 -0
  56. package/dist/worker/console/interview/tools.js +109 -0
  57. package/dist/worker/console/loopback.js +16 -0
  58. package/dist/worker/console/observe-health-match.js +174 -0
  59. package/dist/worker/console/observe-link.js +33 -0
  60. package/dist/worker/console/operation-runner.js +166 -0
  61. package/dist/worker/console/operation-sse.js +158 -0
  62. package/dist/worker/console/operation-store.js +147 -0
  63. package/dist/worker/console/operator-actions.js +769 -0
  64. package/dist/worker/console/pi-readiness.js +94 -0
  65. package/dist/worker/console/recovery-cta.js +133 -0
  66. package/dist/worker/console/repo-fingerprint.js +29 -0
  67. package/dist/worker/console/resource-loader.js +95 -0
  68. package/dist/worker/console/routes.js +368 -0
  69. package/dist/worker/console/security.js +126 -0
  70. package/dist/worker/console/server.js +149 -0
  71. package/dist/worker/console/sibling-controller.js +28 -0
  72. package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
  73. package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
  74. package/dist/worker/console/static/index.html +13 -0
  75. package/dist/worker/console/vite.config.js +27 -0
  76. package/dist/worker/delivery/git-transaction.js +43 -8
  77. package/dist/worker/observe/health.js +57 -0
  78. package/dist/worker/observe/paths.js +81 -0
  79. package/dist/worker/observe/routes.js +142 -27
  80. package/dist/worker/observe/spec-evidence.js +84 -0
  81. package/dist/worker/observe/static/api.js +23 -0
  82. package/dist/worker/observe/static/state.js +26 -0
  83. package/dist/worker/observe/static/styles.css +10 -0
  84. package/dist/worker/observe/static/views/dag-inspector.js +173 -6
  85. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  86. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  87. package/dist/workflows/dag/frontend-repair.js +1 -10
  88. package/dist/workflows/dag/init-hybrid.js +372 -115
  89. package/dist/workflows/dag/node-execution.js +57 -6
  90. package/dist/workflows/dag/project-governance-context.js +508 -0
  91. package/dist/workflows/dag/prompt.js +46 -1
  92. package/dist/workflows/dag/retry-policy.js +16 -1
  93. package/dist/workflows/dag/runner.js +9 -0
  94. package/dist/workflows/dag/skill-snapshot.js +1 -0
  95. package/dist/workflows/dag/task-contract-binding.js +138 -0
  96. package/dist/workflows/dag/types.js +84 -10
  97. package/dist/workflows/dag/validate.js +53 -7
  98. package/docs/README.md +2 -0
  99. package/docs/architecture/evolution.md +2 -0
  100. package/docs/architecture/system-overview.md +6 -0
  101. package/docs/architecture/worker-and-feature.md +7 -0
  102. package/docs/templates/agent-dag.schema.json +64 -2
  103. package/docs/templates/agent-dag.supervised-implementation.json +1 -0
  104. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  105. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  106. package/docs/templates/backend-test-dag.json +26 -154
  107. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  108. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  109. package/package.json +8 -2
  110. package/skills/agent-worker/SKILL.md +1 -0
  111. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  112. package/skills/frontend-design-review/SKILL.md +25 -16
  113. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  114. package/skills/loop-agent/references/command-reference.md +48 -1
  115. package/skills/loop-agent/references/hybrid-dag.md +4 -4
@@ -10,12 +10,14 @@ import {
10
10
  UI_TEXT,
11
11
  } from "../constants.js";
12
12
  import { clearNode, el } from "../dom.js";
13
- import { fetchJson } from "../api.js";
13
+ import { fetchJson, fetchSpecEvidenceFile } from "../api.js";
14
14
  import { badge } from "../format.js";
15
15
  import { renderMarkdown } from "../markdown-render.js";
16
16
  import {
17
17
  uiState,
18
18
  makeSessionEventIdentity,
19
+ openSpecEvidenceDetail,
20
+ closeSpecEvidenceDetail,
19
21
  updateDagTimelineViewportState,
20
22
  updateDagOutputViewportState,
21
23
  computeFollowLatest,
@@ -44,6 +46,10 @@ export function selectDagNode(dagRunId, nodeId) {
44
46
  if (nodeChanged) {
45
47
  uiState.sessionEventOffset = 0;
46
48
  uiState.sessionEvents = [];
49
+ // Drop any open spec-evidence preview so a stale A detail cannot survive
50
+ // into node B's spec-evidence panel (AC-007 regression guard).
51
+ uiState.specEvidenceDetail = null;
52
+ uiState.specEvidenceListEvidence = null;
47
53
  // Bump the session-event identity token so any in-flight response for the
48
54
  // previous node is rejected before it can mutate the current node cache.
49
55
  uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
@@ -188,15 +194,20 @@ export function captureDagTimelineViewportState(panel) {
188
194
  }
189
195
  }
190
196
 
191
- async function renderSpecEvidence(content, dagRunId, nodeId) {
192
- const evidence = await fetchJson(
197
+ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
198
+ const evidence = cachedEvidence ?? await fetchJson(
193
199
  `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/spec-evidence`,
194
200
  );
195
201
  if (!content.isConnected || content.dataset.dagNodeId !== String(nodeId)) return;
202
+ if (uiState.specEvidenceDetail) {
203
+ renderSpecEvidenceDetail(content, dagRunId, nodeId);
204
+ return;
205
+ }
196
206
  if (!evidence) {
197
207
  content.appendChild(el("p", "empty", "无法加载规范证据。"));
198
208
  return;
199
209
  }
210
+ uiState.specEvidenceListEvidence = evidence;
200
211
 
201
212
  const statusLabels = {
202
213
  "spec-read": "已读取规范",
@@ -241,9 +252,24 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
241
252
  });
242
253
  appendListSection("已绑定任务源", evidence.sourceBinding?.sources, (source) => {
243
254
  const item = el("li", null);
255
+ const button = document.createElement("button");
256
+ button.type = "button";
257
+ button.className = "spec-evidence-file-btn";
258
+ button.setAttribute("data-source", "binding");
259
+ button.setAttribute("data-path", source.path);
260
+ button.id = `spec-evidence-btn-binding-${source.path}`;
244
261
  const icon = el("i", "ri-links-line");
245
262
  icon.setAttribute("aria-hidden", "true");
246
- item.append(icon, document.createTextNode(` ${source.kind}: `), el("code", null, source.path), document.createTextNode(` · sha256 ${source.sha256.slice(0, 12)}…`));
263
+ button.append(
264
+ icon,
265
+ document.createTextNode(` ${source.kind}: `),
266
+ el("code", null, source.path),
267
+ document.createTextNode(` · sha256 ${source.sha256.slice(0, 12)}…`),
268
+ );
269
+ button.addEventListener("click", () =>
270
+ onSpecEvidenceFileClick(content, dagRunId, nodeId, "binding", source.path, button),
271
+ );
272
+ item.appendChild(button);
247
273
  return item;
248
274
  });
249
275
  appendListSection("显式需求编号", evidence.sourceBinding?.requirementIds, (id) => el("li", null, id));
@@ -252,11 +278,20 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
252
278
  evidence.specReads,
253
279
  (read) => {
254
280
  const item = el("li", null);
281
+ const button = document.createElement("button");
282
+ button.type = "button";
283
+ button.className = "spec-evidence-file-btn";
284
+ button.setAttribute("data-source", "read");
285
+ button.setAttribute("data-path", read.path);
286
+ if (read.timestamp) {
287
+ button.setAttribute("data-read-at", read.timestamp);
288
+ }
289
+ button.id = `spec-evidence-btn-read-${read.path}`;
255
290
  const icon = el("i", "ri-file-text-line");
256
291
  icon.setAttribute("aria-hidden", "true");
257
- item.append(icon, el("code", null, read.path));
292
+ button.append(icon, el("code", null, read.path));
258
293
  if (read.timestamp) {
259
- item.appendChild(
294
+ button.appendChild(
260
295
  el(
261
296
  "span",
262
297
  "spec-evidence-time",
@@ -264,6 +299,10 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
264
299
  ),
265
300
  );
266
301
  }
302
+ button.addEventListener("click", () =>
303
+ onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
304
+ );
305
+ item.appendChild(button);
267
306
  return item;
268
307
  },
269
308
  );
@@ -308,7 +347,135 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
308
347
  }
309
348
  }
310
349
 
350
+ /**
351
+ * Click handler for spec-evidence file buttons (AC-001/002). Opens the detail
352
+ * view in the same spec-evidence panel (no modal), records the trigger button
353
+ * so the back action can restore keyboard focus (AC-005).
354
+ */
355
+ function onSpecEvidenceFileClick(content, dagRunId, nodeId, source, relPath, button) {
356
+ openSpecEvidenceDetail(source, relPath, button);
357
+ clearNode(content);
358
+ const detail = uiState.specEvidenceDetail;
359
+ renderSpecEvidenceDetail(content, dagRunId, nodeId);
360
+ void (async () => {
361
+ try {
362
+ const data = await fetchSpecEvidenceFile(dagRunId, nodeId, source, relPath);
363
+ if (
364
+ !content.isConnected ||
365
+ content.dataset.dagNodeId !== String(nodeId) ||
366
+ !uiState.specEvidenceDetail ||
367
+ uiState.specEvidenceDetail.source !== source ||
368
+ uiState.specEvidenceDetail.path !== relPath
369
+ ) {
370
+ return;
371
+ }
372
+ detail.loading = false;
373
+ detail.data = data;
374
+ } catch (error) {
375
+ if (
376
+ !content.isConnected ||
377
+ content.dataset.dagNodeId !== String(nodeId) ||
378
+ !uiState.specEvidenceDetail ||
379
+ uiState.specEvidenceDetail.source !== source ||
380
+ uiState.specEvidenceDetail.path !== relPath
381
+ ) {
382
+ return;
383
+ }
384
+ detail.loading = false;
385
+ detail.error = error instanceof Error ? error.message : String(error);
386
+ }
387
+ renderSpecEvidenceDetail(content, dagRunId, nodeId);
388
+ })();
389
+ }
390
+
391
+ /**
392
+ * Render the in-inspector file preview (AC-005). Single scroll container for
393
+ * the body, always-visible back button, and accessible loading/error states.
394
+ */
395
+ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
396
+ const detail = uiState.specEvidenceDetail;
397
+ clearNode(content);
398
+ if (!detail) {
399
+ renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
400
+ return;
401
+ }
402
+
403
+ const wrapper = el("div", "spec-evidence-detail");
404
+ const back = document.createElement("button");
405
+ back.type = "button";
406
+ back.className = "spec-evidence-back";
407
+ back.setAttribute("aria-label", "返回规范证据");
408
+ const backIcon = el("i", "ri-arrow-left-line");
409
+ backIcon.setAttribute("aria-hidden", "true");
410
+ back.append(backIcon, document.createTextNode(" 返回规范证据"));
411
+ back.addEventListener("click", () => {
412
+ const closed = closeSpecEvidenceDetail();
413
+ clearNode(content);
414
+ // Restore the list synchronously from the cached evidence so the user
415
+ // sees no flicker and the trigger button is immediately focusable.
416
+ renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
417
+ if (closed?.triggerId) {
418
+ const trigger = document.getElementById(closed.triggerId);
419
+ trigger?.focus();
420
+ }
421
+ });
422
+ wrapper.appendChild(back);
423
+
424
+ const meta = el("div", "spec-evidence-detail-meta");
425
+ meta.appendChild(el("code", null, detail.path));
426
+ if (detail.source === "binding" && detail.data) {
427
+ meta.appendChild(
428
+ el(
429
+ "span",
430
+ "spec-evidence-detail-hash",
431
+ `绑定 sha256 ${String(detail.data.sha256 ?? "").slice(0, 12)}… · 当前 ${detail.data.hashMatch ? "一致" : "不一致"}`,
432
+ ),
433
+ );
434
+ } else if (detail.source === "read" && detail.data?.readAt) {
435
+ meta.appendChild(
436
+ el(
437
+ "span",
438
+ "spec-evidence-detail-time",
439
+ `读取于 ${formatSessionEventTime({ timestamp: detail.data.readAt })}`,
440
+ ),
441
+ );
442
+ }
443
+ wrapper.appendChild(meta);
444
+
445
+ if (detail.loading) {
446
+ const loading = el("p", "muted", "正在加载文件内容…");
447
+ loading.setAttribute("role", "status");
448
+ loading.setAttribute("aria-live", "polite");
449
+ wrapper.appendChild(loading);
450
+ } else if (detail.error) {
451
+ const error = el("p", "spec-evidence-detail-error", `加载失败:${detail.error}`);
452
+ error.setAttribute("role", "alert");
453
+ wrapper.appendChild(error);
454
+ } else if (detail.data) {
455
+ const body = el("pre", "spec-evidence-detail-content");
456
+ body.textContent = detail.data.content ?? "";
457
+ wrapper.appendChild(body);
458
+ if (detail.data.truncated) {
459
+ const note = el(
460
+ "p",
461
+ "spec-evidence-detail-truncated",
462
+ `已截断,仅显示前 ${detail.data.maxBytes ?? 0} 字节。`,
463
+ );
464
+ wrapper.appendChild(note);
465
+ }
466
+ }
467
+ content.appendChild(wrapper);
468
+ }
469
+
311
470
  function fillDagInspectorContent(content, dagRunId, node) {
471
+ const previousRenderedNode = content.dataset.dagNodeId ?? "";
472
+ const nodeChanged = previousRenderedNode !== String(node.nodeId ?? "");
473
+ if (nodeChanged) {
474
+ // Drop any open spec-evidence preview + cached list so a stale A detail
475
+ // cannot survive into node B (AC-007 regression guard).
476
+ uiState.specEvidenceDetail = null;
477
+ uiState.specEvidenceListEvidence = null;
478
+ }
312
479
  const activeTab = ["timeline", "spec-evidence"].includes(
313
480
  uiState.dagInspectorTab,
314
481
  )
@@ -356,20 +356,47 @@ function coerceOptionalEvidence(value, kind) {
356
356
  out.sourceRef = sourceRef;
357
357
  return out;
358
358
  }
359
+ const EVIDENCE_GAP_INPUT_KEYS = new Set([
360
+ "description",
361
+ "text",
362
+ "requirementId",
363
+ "acId",
364
+ "id",
365
+ "sourceRef",
366
+ "sourceRefs",
367
+ ]);
359
368
  function coerceEvidenceGap(value) {
369
+ if (typeof value === "string") {
370
+ if (!value.trim())
371
+ throw new Error("evidence gap string must be non-empty");
372
+ return { description: value.trim() };
373
+ }
360
374
  const record = asRecord(value);
361
375
  if (!record)
362
- return null;
376
+ throw new Error("evidence gap must be a string or object");
377
+ for (const key of Object.keys(record)) {
378
+ if (!EVIDENCE_GAP_INPUT_KEYS.has(key))
379
+ throw new Error(`evidence gap has unrecognized key: ${key}`);
380
+ }
381
+ for (const key of ["description", "text", "requirementId", "acId", "id", "sourceRef"]) {
382
+ if (record[key] !== undefined && typeof record[key] !== "string")
383
+ throw new Error(`evidence gap ${key} must be a string`);
384
+ }
385
+ if (record.sourceRefs !== undefined && (!Array.isArray(record.sourceRefs) || record.sourceRefs.some((item) => typeof item !== "string"))) {
386
+ throw new Error("evidence gap sourceRefs must be a string array");
387
+ }
363
388
  const description = (typeof record.description === "string" && record.description.trim()) ||
364
389
  (typeof record.text === "string" && record.text.trim()) ||
365
390
  "";
366
391
  if (!description)
367
- return null;
392
+ throw new Error("evidence gap description must be non-empty");
368
393
  const out = { description };
369
- if (typeof record.requirementId === "string" && record.requirementId.trim())
370
- out.requirementId = record.requirementId.trim();
371
- else if (typeof record.acId === "string" && record.acId.trim())
372
- out.requirementId = record.acId.trim();
394
+ const requirementId = (typeof record.requirementId === "string" && record.requirementId.trim()) ||
395
+ (typeof record.acId === "string" && record.acId.trim()) ||
396
+ (typeof record.id === "string" && record.id.trim()) ||
397
+ "";
398
+ if (requirementId)
399
+ out.requirementId = requirementId;
373
400
  const sourceRef = firstSourceRef(record, "");
374
401
  if (sourceRef)
375
402
  out.sourceRef = sourceRef;
@@ -403,9 +430,7 @@ export function coerceBackendTestAnalysisInput(value) {
403
430
  next.risks = asArray(record.risks)
404
431
  .map((item) => coerceOptionalEvidence(item, "risk"))
405
432
  .filter((item) => item !== null);
406
- next.evidenceGaps = asArray(record.evidenceGaps)
407
- .map(coerceEvidenceGap)
408
- .filter((item) => item !== null);
433
+ next.evidenceGaps = asArray(record.evidenceGaps).map(coerceEvidenceGap);
409
434
  return next;
410
435
  }
411
436
  function normalizeAnalysis(value) {
@@ -5,6 +5,7 @@ export function freshNodeRecord(task) {
5
5
  status: "PENDING",
6
6
  executor: task.executor,
7
7
  complexity: task.complexity,
8
+ ...(task.outputMode ? { outputMode: task.outputMode } : {}),
8
9
  };
9
10
  }
10
11
  export function sha256Json(value) {
@@ -320,16 +320,7 @@ export async function runFrontendRepairContractGate(input) {
320
320
  return { ok: true, assessment };
321
321
  }
322
322
  export function formatFrontendFailureAssessStdout(assessment) {
323
- return [
324
- "Frontend failure assess: ok",
325
- `eligible=${assessment.eligible}`,
326
- `failureClass=${assessment.failureClass}`,
327
- `failedNodeIds=${assessment.failedNodeIds.join(",") || "(none)"}`,
328
- `attempt=${assessment.attempt}/${assessment.maxAttempts}`,
329
- `reason=${assessment.reason}`,
330
- `browser=${assessment.browserStatus}`,
331
- `artifact=contracts/frontend-repair-assessment.json`,
332
- ].join("\n");
323
+ return JSON.stringify(assessment);
333
324
  }
334
325
  export function formatFrontendRepairContractStdout(assessment) {
335
326
  return [