@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.
- package/CHANGELOG.md +45 -1
- package/README.md +14 -3
- package/dist/cli/command-definitions.js +43 -0
- package/dist/cli/program.js +26 -0
- package/dist/commands/dag-approve.js +4 -0
- package/dist/commands/dag-resume.js +1 -0
- package/dist/commands/dag-validate.js +6 -0
- package/dist/commands/operator.js +44 -0
- package/dist/commands/task-contract.js +271 -0
- package/dist/executors/dag-pi-executor.js +55 -7
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +55 -8
- package/dist/executors/shell-presets.js +16 -3
- package/dist/shared/operator/capabilities.js +255 -0
- package/dist/shared/operator/envelope.js +59 -0
- package/dist/shared/operator/index.js +4 -0
- package/dist/shared/operator/registry.js +38 -0
- package/dist/shared/operator/types.js +5 -0
- package/dist/task/contract/adopt.js +166 -0
- package/dist/task/contract/apply.js +326 -0
- package/dist/task/contract/canonicalize.js +60 -0
- package/dist/task/contract/constants.js +29 -0
- package/dist/task/contract/diff.js +177 -0
- package/dist/task/contract/hash.js +42 -0
- package/dist/task/contract/import-revision.js +96 -0
- package/dist/task/contract/index.js +17 -0
- package/dist/task/contract/journal.js +155 -0
- package/dist/task/contract/lock.js +153 -0
- package/dist/task/contract/observe.js +296 -0
- package/dist/task/contract/paths.js +19 -0
- package/dist/task/contract/project.js +170 -0
- package/dist/task/contract/recover.js +312 -0
- package/dist/task/contract/request-ledger.js +37 -0
- package/dist/task/contract/schema.js +151 -0
- package/dist/task/contract/transaction.js +160 -0
- package/dist/task/contract/types.js +1 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/index.js +3 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/runtime.js +5 -1
- package/dist/task/source-references.js +7 -0
- package/dist/worker/cli.js +150 -32
- package/dist/worker/console/app-data.js +185 -0
- package/dist/worker/console/dag-confirmation.js +313 -0
- package/dist/worker/console/doctor.js +169 -0
- package/dist/worker/console/draft-store.js +80 -0
- package/dist/worker/console/index.js +15 -0
- package/dist/worker/console/interview/assessment.js +67 -0
- package/dist/worker/console/interview/session.js +100 -0
- package/dist/worker/console/interview/tools.js +109 -0
- package/dist/worker/console/loopback.js +16 -0
- package/dist/worker/console/observe-health-match.js +174 -0
- package/dist/worker/console/observe-link.js +33 -0
- package/dist/worker/console/operation-runner.js +166 -0
- package/dist/worker/console/operation-sse.js +158 -0
- package/dist/worker/console/operation-store.js +147 -0
- package/dist/worker/console/operator-actions.js +769 -0
- package/dist/worker/console/pi-readiness.js +266 -0
- package/dist/worker/console/recovery-cta.js +133 -0
- package/dist/worker/console/repo-fingerprint.js +29 -0
- package/dist/worker/console/resource-loader.js +95 -0
- package/dist/worker/console/routes.js +368 -0
- package/dist/worker/console/security.js +126 -0
- package/dist/worker/console/server.js +149 -0
- package/dist/worker/console/sibling-controller.js +28 -0
- package/dist/worker/console/static/assets/index-BEIdBogJ.js +9 -0
- package/dist/worker/console/static/assets/index-Rt0TqimP.css +1 -0
- package/dist/worker/console/static/index.html +13 -0
- package/dist/worker/console/vite.config.js +27 -0
- package/dist/worker/materialize/harness-task-materializer.js +34 -0
- package/dist/worker/observe/health.js +57 -0
- package/dist/worker/observe/routes.js +16 -9
- package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
- package/dist/workflows/dag/frontend-repair.js +1 -10
- package/dist/workflows/dag/init-hybrid.js +269 -116
- package/dist/workflows/dag/node-execution.js +25 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +74 -10
- package/dist/workflows/dag/validate.js +25 -7
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +2 -0
- package/docs/architecture/system-overview.md +6 -0
- package/docs/architecture/worker-and-feature.md +7 -0
- package/docs/templates/agent-dag.schema.json +49 -2
- package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
- package/docs/templates/backend-test-dag.json +26 -154
- package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
- package/package.json +8 -2
- package/skills/agent-worker/SKILL.md +1 -0
- package/skills/agent-worker/references/agent-worker-operator.md +3 -2
- package/skills/frontend-design-review/SKILL.md +25 -16
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/loop-agent/references/command-reference.md +48 -1
- 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
|
-
|
|
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) ||
|
|
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) ||
|
|
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 = {
|
|
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 &&
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
392
|
+
throw new Error("evidence gap description must be non-empty");
|
|
368
393
|
const out = { description };
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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) {
|
|
@@ -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 [
|