@tea-agent/loop-agent 0.16.26 → 0.17.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 (104) hide show
  1. package/CHANGELOG.md +45 -1
  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 +55 -7
  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 +55 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/shared/operator/capabilities.js +255 -0
  16. package/dist/shared/operator/envelope.js +59 -0
  17. package/dist/shared/operator/index.js +4 -0
  18. package/dist/shared/operator/registry.js +38 -0
  19. package/dist/shared/operator/types.js +5 -0
  20. package/dist/task/contract/adopt.js +166 -0
  21. package/dist/task/contract/apply.js +326 -0
  22. package/dist/task/contract/canonicalize.js +60 -0
  23. package/dist/task/contract/constants.js +29 -0
  24. package/dist/task/contract/diff.js +177 -0
  25. package/dist/task/contract/hash.js +42 -0
  26. package/dist/task/contract/import-revision.js +96 -0
  27. package/dist/task/contract/index.js +17 -0
  28. package/dist/task/contract/journal.js +155 -0
  29. package/dist/task/contract/lock.js +153 -0
  30. package/dist/task/contract/observe.js +296 -0
  31. package/dist/task/contract/paths.js +19 -0
  32. package/dist/task/contract/project.js +170 -0
  33. package/dist/task/contract/recover.js +312 -0
  34. package/dist/task/contract/request-ledger.js +37 -0
  35. package/dist/task/contract/schema.js +151 -0
  36. package/dist/task/contract/transaction.js +160 -0
  37. package/dist/task/contract/types.js +1 -0
  38. package/dist/task/contract/validate-draft.js +106 -0
  39. package/dist/task/index.js +3 -0
  40. package/dist/task/operator/capabilities.js +6 -0
  41. package/dist/task/operator/envelope.js +2 -0
  42. package/dist/task/operator/index.js +5 -0
  43. package/dist/task/operator/registry.js +2 -0
  44. package/dist/task/operator/types.js +1 -0
  45. package/dist/task/runtime.js +5 -1
  46. package/dist/task/source-references.js +7 -0
  47. package/dist/worker/cli.js +150 -32
  48. package/dist/worker/console/app-data.js +185 -0
  49. package/dist/worker/console/dag-confirmation.js +313 -0
  50. package/dist/worker/console/doctor.js +169 -0
  51. package/dist/worker/console/draft-store.js +80 -0
  52. package/dist/worker/console/index.js +15 -0
  53. package/dist/worker/console/interview/assessment.js +67 -0
  54. package/dist/worker/console/interview/session.js +100 -0
  55. package/dist/worker/console/interview/tools.js +109 -0
  56. package/dist/worker/console/loopback.js +16 -0
  57. package/dist/worker/console/observe-health-match.js +174 -0
  58. package/dist/worker/console/observe-link.js +33 -0
  59. package/dist/worker/console/operation-runner.js +166 -0
  60. package/dist/worker/console/operation-sse.js +158 -0
  61. package/dist/worker/console/operation-store.js +147 -0
  62. package/dist/worker/console/operator-actions.js +769 -0
  63. package/dist/worker/console/pi-readiness.js +266 -0
  64. package/dist/worker/console/recovery-cta.js +133 -0
  65. package/dist/worker/console/repo-fingerprint.js +29 -0
  66. package/dist/worker/console/resource-loader.js +95 -0
  67. package/dist/worker/console/routes.js +368 -0
  68. package/dist/worker/console/security.js +126 -0
  69. package/dist/worker/console/server.js +149 -0
  70. package/dist/worker/console/sibling-controller.js +28 -0
  71. package/dist/worker/console/static/assets/index-BEIdBogJ.js +9 -0
  72. package/dist/worker/console/static/assets/index-Rt0TqimP.css +1 -0
  73. package/dist/worker/console/static/index.html +13 -0
  74. package/dist/worker/console/vite.config.js +27 -0
  75. package/dist/worker/materialize/harness-task-materializer.js +34 -0
  76. package/dist/worker/observe/health.js +57 -0
  77. package/dist/worker/observe/routes.js +16 -9
  78. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  79. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  80. package/dist/workflows/dag/frontend-repair.js +1 -10
  81. package/dist/workflows/dag/init-hybrid.js +269 -116
  82. package/dist/workflows/dag/node-execution.js +25 -1
  83. package/dist/workflows/dag/retry-policy.js +16 -1
  84. package/dist/workflows/dag/runner.js +9 -0
  85. package/dist/workflows/dag/task-contract-binding.js +138 -0
  86. package/dist/workflows/dag/types.js +74 -10
  87. package/dist/workflows/dag/validate.js +25 -7
  88. package/docs/README.md +2 -0
  89. package/docs/architecture/evolution.md +2 -0
  90. package/docs/architecture/system-overview.md +6 -0
  91. package/docs/architecture/worker-and-feature.md +7 -0
  92. package/docs/templates/agent-dag.schema.json +49 -2
  93. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  94. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  95. package/docs/templates/backend-test-dag.json +26 -154
  96. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  97. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  98. package/package.json +8 -2
  99. package/skills/agent-worker/SKILL.md +1 -0
  100. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  101. package/skills/frontend-design-review/SKILL.md +25 -16
  102. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  103. package/skills/loop-agent/references/command-reference.md +48 -1
  104. package/skills/loop-agent/references/hybrid-dag.md +4 -4
@@ -0,0 +1 @@
1
+ :root{--canvas: #f7f7f4;--canvas-soft: #fafaf7;--surface: #ffffff;--surface-muted: #f0efe9;--ink: #26251e;--body: #5a5852;--muted: #807d72;--muted-soft: #a09c92;--hairline: #e6e5e0;--hairline-strong: #cfcdc4;--orange: #f54e00;--orange-active: #d04200;--blue: #3f6ea4;--green: #1f8a65;--amber: #a66a00;--red: #c72d53;--peach: #f1d8cb;--mint: #d8ead5;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--transition-fast: .16s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%);color-scheme:light;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;min-height:100vh;min-width:320px;background:var(--canvas);color:var(--ink);font-size:14px}button,input,select,textarea{font:inherit}a{color:inherit}code,.cmd,.kv dd code{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace}.console-shell{width:min(100% - 48px,1080px);margin:0 auto;padding:var(--space-8) 0 var(--space-12, 48px)}.console-header{display:grid;gap:var(--space-5);grid-template-columns:1.4fr 1fr;align-items:start;margin-bottom:var(--space-6);padding-bottom:var(--space-5);border-bottom:1px solid var(--hairline)}.eyebrow{margin:0 0 var(--space-2);color:var(--orange);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1{margin:0;color:var(--ink);font-size:28px;font-weight:500;line-height:1.16;letter-spacing:0}.subtitle{max-width:66ch;margin:var(--space-2) 0 0;color:var(--body)}.muted{color:var(--muted)}.readiness{border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-panel)}.readiness[data-state=setup-required]{border-color:color-mix(in srgb,var(--amber) 45%,var(--hairline));background:color-mix(in srgb,var(--peach) 35%,var(--surface))}.readiness[data-state=ready]{border-color:color-mix(in srgb,var(--green) 40%,var(--hairline));background:color-mix(in srgb,var(--mint) 40%,var(--surface))}.readiness[data-state=degraded]{border-color:color-mix(in srgb,var(--amber) 45%,var(--hairline));background:color-mix(in srgb,var(--peach) 28%,var(--surface))}.readiness-label{display:block;margin-bottom:var(--space-1);color:var(--muted);font-size:12px;font-weight:600}.readiness strong{color:var(--ink);font-size:16px;font-weight:650}.readiness[data-state=ready] strong{color:var(--green)}.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--amber)}.readiness .muted{margin:var(--space-2) 0 0}.console-main{display:grid;gap:var(--space-4)}.panel{min-width:0;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-4);color:var(--ink);font-size:16px;font-weight:700}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:14px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-5);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-weight:700}.checklist{margin:var(--space-3) 0 0;padding-left:1.2rem;color:var(--body)}.kv{display:grid;gap:var(--space-2);margin:0}.kv div{display:grid;grid-template-columns:10rem 1fr;gap:var(--space-2);padding:var(--space-2) 0;border-bottom:1px solid var(--hairline)}.kv div:last-child{border-bottom:none}.kv dt{color:var(--muted);font-size:12px;font-weight:600}.kv dd{margin:0;color:var(--ink);overflow-wrap:anywhere}code{font-size:.85rem;color:var(--ink)}.cmd{display:block;margin:0;padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f0ebe3;color:var(--ink);font-size:.85rem;overflow-x:auto;word-break:break-all}.external-link{color:var(--orange)}.error{color:var(--red)}.wizard-steps{display:flex;flex-wrap:wrap;gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-4);font-size:12px;color:var(--muted)}.wizard-steps li{min-height:28px;display:inline-flex;align-items:center;border:1px solid var(--hairline);border-radius:var(--radius-sm);padding:0 10px;background:var(--canvas-soft);font-weight:600}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink)}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4)}.field span{font-size:12px;font-weight:600}.field input{min-height:36px;padding:0 var(--space-3);border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink)}.field input:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 55%,white);outline-offset:2px;border-color:var(--orange)}.field input:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.cta-row,.wizard-body,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:flex-start}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:0 14px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:600;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast)}button:hover:not(:disabled),button:focus-visible:not(:disabled){border-color:var(--orange);background:var(--surface);color:var(--ink);outline:none}button:disabled{opacity:.5;cursor:not-allowed}.wizard-body button,.cta-row>button:first-child,button.primary{border-color:var(--orange);background:var(--orange);color:#fff}.wizard-body button:hover:not(:disabled),.cta-row>button:first-child:hover:not(:disabled),button.primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.wizard-body{display:block}.wizard-body>div{display:grid;gap:var(--space-2);justify-items:start}.wizard-body button+button{margin-left:0}.recovery-cta-row{margin-top:var(--space-3)}.recovery-cta-row button[disabled]{opacity:.4}@media(max-width:800px){.console-shell{width:min(100% - 24px,1080px);padding-top:var(--space-5)}.console-header,.kv div{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Loop Operator Console</title>
7
+ <script type="module" crossorigin src="/assets/index-BEIdBogJ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-Rt0TqimP.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+ const consoleDir = path.dirname(fileURLToPath(import.meta.url));
5
+ const repoRoot = path.resolve(consoleDir, "../../..");
6
+ /**
7
+ * Vite SPA build for Loop Operator Console.
8
+ * Output: dist/worker/console/static (no source maps in publish tarball).
9
+ */
10
+ export default defineConfig({
11
+ root: path.join(consoleDir, "static-src"),
12
+ base: "/",
13
+ build: {
14
+ outDir: path.join(repoRoot, "dist/worker/console/static"),
15
+ emptyOutDir: true,
16
+ sourcemap: false,
17
+ // Keep Phase 1 shell small; no code-split complexity required.
18
+ rollupOptions: {
19
+ input: path.join(consoleDir, "static-src/index.html"),
20
+ },
21
+ },
22
+ // React is a devDependency; esbuild jsx transform avoids needing @vitejs/plugin-react.
23
+ esbuild: {
24
+ jsx: "automatic",
25
+ jsxImportSource: "react",
26
+ },
27
+ });
@@ -4,6 +4,9 @@ import path from "node:path";
4
4
  import YAML from "yaml";
5
5
  import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/harness/task-store.js";
6
6
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
7
+ import { adoptTaskContract } from "../../task/contract/adopt.js";
8
+ import { observeTaskContractState } from "../../task/contract/observe.js";
9
+ import { sha256Utf8 } from "../../task/contract/hash.js";
7
10
  import { resolveLoopAgentProfile } from "../profile-mapping.js";
8
11
  import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
9
12
  import { resolveWorkflow } from "../task-spec/workflow-routing.js";
@@ -89,6 +92,11 @@ export async function materializeTaskSpec(options) {
89
92
  }), "utf-8");
90
93
  await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(options.taskSpec), "utf-8");
91
94
  await writeFile(sourcePaths.taskYamlPath, YAML.stringify(options.taskSpec), "utf-8");
95
+ await ensureMaterializedTaskContractManaged({
96
+ repoRoot: options.repoRoot,
97
+ taskId: harnessTaskId,
98
+ taskSpecPath: options.taskSpecPath,
99
+ });
92
100
  const profileMapping = resolveLoopAgentProfile(options.taskSpec);
93
101
  const manifest = {
94
102
  schemaVersion: 1,
@@ -138,6 +146,32 @@ function slugify(value) {
138
146
  .replace(/-{2,}/g, "-");
139
147
  }
140
148
  /** Exported for unit tests: derived contract must point back to immutable references. */
149
+ async function ensureMaterializedTaskContractManaged(input) {
150
+ const state = await observeTaskContractState(input.repoRoot, input.taskId);
151
+ if (state.effectiveStatus === "managed")
152
+ return;
153
+ if (state.effectiveStatus === "transaction-incomplete") {
154
+ throw new Error(`task contract transaction incomplete for ${input.taskId}; recover before materializing`);
155
+ }
156
+ if (!state.observedCanonicalHash) {
157
+ throw new Error(`unable to observe task contract hash for ${input.taskId}: ${state.observationErrors.map((error) => error.message).join("; ")}`);
158
+ }
159
+ const expectedRevision = state.ref?.revision ?? 0;
160
+ const requestPayload = JSON.stringify({
161
+ taskId: input.taskId,
162
+ taskSpecPath: input.taskSpecPath,
163
+ expectedRevision,
164
+ observedCanonicalHash: state.observedCanonicalHash,
165
+ });
166
+ await adoptTaskContract({
167
+ repoRoot: input.repoRoot,
168
+ taskId: input.taskId,
169
+ expectedRevision,
170
+ expectedObservedHash: state.observedCanonicalHash,
171
+ requestId: `worker-materialize-${sha256Utf8(requestPayload).slice(0, 24)}`,
172
+ requestPayloadSha256: sha256Utf8(requestPayload),
173
+ });
174
+ }
141
175
  export function renderRequirementMarkdown(taskSpec, options = {}) {
142
176
  const lines = [
143
177
  `# ${taskSpec.title}`,
@@ -0,0 +1,57 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+ import { findPackageRoot, readPackageName, readPackageVersion, } from "../../shared/package-metadata.js";
4
+ import { repoFingerprintV1 } from "../console/repo-fingerprint.js";
5
+ /** Observe `/api/health` schema version (independent of Console product health). */
6
+ export const OBSERVE_HEALTH_SCHEMA_VERSION = 1;
7
+ function routeExists(routes, method, pathname) {
8
+ return routes.some((route) => route.method === method && route.pattern.test(pathname));
9
+ }
10
+ /**
11
+ * Derive capability flags from live ROUTES matchers (single source of truth).
12
+ * Synthetic paths exercise patterns; do not maintain a second handwritten table.
13
+ */
14
+ export function deriveObserveRouteCapabilities(routes) {
15
+ return {
16
+ dagRun: routeExists(routes, "GET", "/api/dag-runs/example-run"),
17
+ task: routeExists(routes, "GET", "/api/tasks/example-task") ||
18
+ routeExists(routes, "GET", "/api/features/example-feature/tasks/example-task"),
19
+ feature: routeExists(routes, "GET", "/api/features/example-feature/tasks/example-task"),
20
+ workerRun: routeExists(routes, "GET", "/api/runs/example-run"),
21
+ batch: routeExists(routes, "GET", "/api/batches/example-batch"),
22
+ };
23
+ }
24
+ export function resolveObservePackageIdentity(fromFileUrl = import.meta.url) {
25
+ const startDir = path.dirname(fileURLToPath(fromFileUrl));
26
+ const packageRoot = findPackageRoot(startDir);
27
+ const packageName = packageRoot
28
+ ? (readPackageName(packageRoot) ?? "unknown")
29
+ : "unknown";
30
+ const packageVersion = packageRoot
31
+ ? (readPackageVersion(packageRoot) ?? "0.0.0")
32
+ : "0.0.0";
33
+ return { packageName, packageVersion };
34
+ }
35
+ /**
36
+ * Build versioned Observe health DTO for GET /api/health.
37
+ */
38
+ export function buildObserveHealthV1(input) {
39
+ const { packageName, packageVersion } = resolveObservePackageIdentity(input.fromFileUrl);
40
+ let repoFingerprint;
41
+ try {
42
+ repoFingerprint = repoFingerprintV1(input.repoRoot);
43
+ }
44
+ catch (error) {
45
+ repoFingerprint = `error:${error instanceof Error ? error.message : String(error)}`;
46
+ }
47
+ return {
48
+ ok: true,
49
+ schemaVersion: OBSERVE_HEALTH_SCHEMA_VERSION,
50
+ repoRoot: input.repoRoot,
51
+ repoFingerprint,
52
+ packageName,
53
+ packageVersion,
54
+ routeCapabilities: deriveObserveRouteCapabilities(input.routes),
55
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
56
+ };
57
+ }
@@ -11,6 +11,7 @@ import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
11
11
  import { getTaskPoolRoot } from "../pool/run-store.js";
12
12
  import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview, toRepoRelativeArtifactPath, } from "./paths.js";
13
13
  import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
14
+ import { buildObserveHealthV1 } from "./health.js";
14
15
  const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
15
16
  export function createObserveSnapshotCache() {
16
17
  return { expiresAt: 0 };
@@ -155,11 +156,10 @@ export async function serveStatic(req, res, staticDir) {
155
156
  res.end(content);
156
157
  }
157
158
  async function handleHealth(_req, res, _match, ctx) {
158
- sendJson(res, 200, {
159
- ok: true,
159
+ sendJson(res, 200, buildObserveHealthV1({
160
160
  repoRoot: ctx.repoRoot,
161
- generatedAt: new Date().toISOString(),
162
- });
161
+ routes: ROUTES,
162
+ }));
163
163
  }
164
164
  async function getSnapshot(ctx) {
165
165
  const now = Date.now();
@@ -739,7 +739,8 @@ async function loadDagRunSourceBinding(repoRoot, dagRunId) {
739
739
  async function handleDagNodeSpecEvidenceFile(_req, res, match, ctx) {
740
740
  const dagRunId = match.params.id;
741
741
  const nodeId = match.params.sub;
742
- if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
742
+ if (!isSafeObservabilityIdentifier(dagRunId) ||
743
+ !isSafeObservabilityIdentifier(nodeId)) {
743
744
  sendJson(res, 400, { error: "Invalid dag run or node identifier" });
744
745
  return;
745
746
  }
@@ -818,12 +819,16 @@ async function handleDagNodeSpecEvidenceFile(_req, res, match, ctx) {
818
819
  async function handleDagNodeSpecEvidence(_req, res, match, ctx) {
819
820
  const dagRunId = match.params.id;
820
821
  const nodeId = match.params.sub;
821
- if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
822
+ if (!isSafeObservabilityIdentifier(dagRunId) ||
823
+ !isSafeObservabilityIdentifier(nodeId)) {
822
824
  sendJson(res, 400, { error: "Invalid dag run or node identifier" });
823
825
  return;
824
826
  }
825
827
  // Extract skill injection info from the DAG run spec (run.json)
826
- const skillInjection = { skills: [], references: [] };
828
+ const skillInjection = {
829
+ skills: [],
830
+ references: [],
831
+ };
827
832
  const sourceBinding = await loadDagRunSourceBinding(ctx.repoRoot, dagRunId);
828
833
  {
829
834
  const dagRunsRoot = path.resolve(ctx.repoRoot, ".harness", "dag-runs");
@@ -864,7 +869,8 @@ async function handleDagNodeSpecEvidence(_req, res, match, ctx) {
864
869
  evidence.skillInjection = skillInjection;
865
870
  evidence.sourceBinding = sourceBinding;
866
871
  // Recompute status considering skill injection
867
- if (evidence.specReads.length === 0 && evidence.knowledgeBaseQueries.length === 0) {
872
+ if (evidence.specReads.length === 0 &&
873
+ evidence.knowledgeBaseQueries.length === 0) {
868
874
  if (evidence.specSearches.length > 0) {
869
875
  evidence.status = "search-only";
870
876
  }
@@ -874,7 +880,8 @@ async function handleDagNodeSpecEvidence(_req, res, match, ctx) {
874
880
  }
875
881
  else if (skillInjection.skills.length > 0) {
876
882
  evidence.status = "spec-injected";
877
- evidence.summary = "规范 skill 已注入但未观察到规范文件读取或知识库查询。";
883
+ evidence.summary =
884
+ "规范 skill 已注入但未观察到规范文件读取或知识库查询。";
878
885
  }
879
886
  }
880
887
  sendJson(res, 200, evidence);
@@ -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 [