@tea-agent/loop-agent 0.18.0 → 0.19.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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +29 -73
- package/README.md +4 -6
- package/dist/commands/docs-audit.js +1 -0
- package/dist/commands/init.js +3 -3
- package/dist/governance/exec-plans.js +4 -0
- package/dist/worker/cli.js +13 -12
- package/dist/worker/console/doctor.js +55 -2
- package/dist/worker/console/index.js +1 -0
- package/dist/worker/console/loopback.js +2 -2
- package/dist/worker/console/observe-link.js +7 -2
- package/dist/worker/console/operator-actions.js +30 -2
- package/dist/worker/console/operator-selection.js +92 -0
- package/dist/worker/console/operator-surface-health.js +23 -0
- package/dist/worker/console/recovery-cta.js +2 -2
- package/dist/worker/console/routes.js +45 -19
- package/dist/worker/console/security.js +29 -4
- package/dist/worker/console/server.js +106 -8
- package/dist/worker/console/static/assets/index-3vsjZJHq.js +16 -0
- package/dist/worker/console/static/assets/index-i1wV4LrY.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +63 -21
- package/dist/worker/observe/server.js +3 -10
- package/dist/worker/observe/static/index.html +6 -3
- package/dist/worker/observe/static/styles.css +53 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +58 -33
- package/dist/workflows/dag/init-hybrid.js +3 -3
- package/docs/README.md +3 -2
- package/docs/architecture/evolution.md +6 -6
- package/docs/architecture/worker-and-feature.md +9 -9
- package/docs/templates/backend-test-dag.json +4 -4
- package/package.json +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +2 -2
- package/skills/loop-agent/references/command-reference.md +4 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
- package/dist/worker/console/static/assets/index-KUSib7aM.js +0 -16
- package/dist/worker/console/static/assets/index-ucIzpaGJ.css +0 -1
|
@@ -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;--orange-soft: color-mix(in srgb, var(--orange) 12%, white);--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: 6px;--radius-md: 8px;--radius-lg: 12px;--radius-pill: 999px;--transition-fast: .16s ease;--transition-base: .22s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%), 0 8px 24px rgb(38 37 30 / 3%);--shadow-soft: 0 10px 30px rgb(38 37 30 / 6%);color-scheme:light;font-family:Segoe UI,PingFang SC,Hiragino Sans GB,Noto Sans SC,system-ui,-apple-system,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:radial-gradient(1200px 480px at 8% -10%,rgb(241 216 203 / 45%),transparent 55%),radial-gradient(900px 420px at 100% 0%,rgb(216 234 213 / 35%),transparent 50%),var(--canvas);min-height:100%}body{margin:0;min-height:100vh;min-width:320px;background:transparent;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}.embedded-operator-nav{position:sticky;top:0;z-index:40;display:flex;gap:6px;align-items:center;margin:0;padding:8px max(24px,calc((100vw - 1120px)/2));border-bottom:1px solid var(--hairline);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-size:13px;font-weight:700}.embedded-operator-nav a,.embedded-operator-nav span{padding:6px 12px;border-radius:var(--radius-pill);text-decoration:none;color:var(--ink)}.embedded-operator-nav a:hover,.embedded-operator-nav a:focus-visible{background:var(--surface);color:var(--orange-active);outline:none}.embedded-operator-nav span[aria-current=page]{background:var(--ink);color:#fff}.console-shell{width:min(100% - 48px,1120px);margin:0 auto;padding:var(--space-5) 0 var(--space-12, 48px)}.console-header{display:grid;gap:var(--space-4);grid-template-columns:1fr auto;align-items:center;margin-bottom:var(--space-4);padding-bottom:var(--space-4);border-bottom:1px solid var(--hairline)}.brand-block{display:grid;gap:2px;align-content:start;min-width:0}h1{margin:0;color:var(--ink);font-size:clamp(20px,2.2vw,24px);font-weight:650;line-height:1.2;letter-spacing:-.02em}.subtitle{max-width:58ch;margin:0;color:var(--body);font-size:13px;line-height:1.4}.top-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:0 0 var(--space-5);padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:color-mix(in srgb,var(--surface) 88%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:var(--shadow-panel)}.nav-link{appearance:none;display:inline-flex;align-items:center;min-height:34px;padding:0 14px;border:1px solid transparent;border-radius:var(--radius-pill);background:transparent;color:var(--body);font-size:13px;font-weight:600;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.nav-link:hover:not(:disabled),.nav-link:focus-visible:not(:disabled){background:var(--surface);color:var(--ink);outline:none}.nav-link-active{background:var(--ink);color:#fff}.nav-link-active:hover:not(:disabled),.nav-link-active:focus-visible:not(:disabled){background:var(--ink);color:#fff}.nav-link-external{margin-left:auto;border-color:var(--hairline);background:var(--surface);color:var(--orange-active)}.nav-link-external:hover:not(:disabled){border-color:var(--orange);background:var(--orange-soft);color:var(--orange-active)}.readiness{position:relative;display:grid;gap:2px;min-width:140px;max-width:280px;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-md);padding:8px 32px 8px 12px;box-shadow:var(--shadow-panel)}.readiness:before{content:"";position:absolute;top:50%;right:12px;width:8px;height:8px;border-radius:50%;transform:translateY(-50%);background:var(--muted-soft)}.readiness[data-state=setup-required]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=setup-required]:before{background:var(--amber)}.readiness[data-state=ready]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=ready]:before{background:color-mix(in srgb,var(--green) 72%,white);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 12%,transparent)}.readiness[data-state=degraded]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=degraded]:before{background:var(--amber);box-shadow:0 0 0 3px color-mix(in srgb,var(--amber) 12%,transparent)}.readiness-label{display:block;margin:0;color:var(--muted);font-size:11px;font-weight:600;line-height:1.2}.readiness strong{color:var(--ink);font-size:14px;font-weight:700;line-height:1.2}.readiness[data-state=ready] strong,.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--ink)}.readiness .muted{margin:2px 0 0;font-size:12px;line-height:1.35}.flash{display:flex;gap:var(--space-3);align-items:flex-start;margin:0 0 var(--space-4);padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);animation:flash-in .22s ease}.flash-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 6%,var(--surface));color:var(--red)}.flash-info{border-color:color-mix(in srgb,var(--blue) 30%,var(--hairline));background:color-mix(in srgb,var(--blue) 6%,var(--surface));color:var(--body)}.flash-label{flex:0 0 auto;min-width:3.5rem;font-size:12px;font-weight:700;letter-spacing:.02em}.flash-body{margin:0;overflow-wrap:anywhere}.muted{color:var(--muted)}.console-main{display:grid;gap:var(--space-4)}.inspect-split-panel{position:fixed;z-index:40;top:16px;right:16px;bottom:16px;width:min(420px,calc(100vw - 32px));display:grid;align-content:start;gap:var(--space-4);padding:var(--space-5);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-soft)}.inspect-split-header{display:flex;align-items:start;justify-content:space-between;gap:var(--space-3)}.inspect-split-header h2,.inspect-split-panel h3{margin:0}.inspect-split-kicker,.inspect-split-id{margin:0;color:var(--muted);font-size:12px}.inspect-split-facts{display:grid;gap:var(--space-2);margin:0}.inspect-split-facts div{display:grid;grid-template-columns:7rem 1fr;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--hairline)}.inspect-split-facts dt{color:var(--muted)}.inspect-split-facts dd{margin:0;overflow-wrap:anywhere}.inspect-split-events{display:grid;gap:8px;margin:0;padding-left:18px;color:var(--body);font-size:12px}.panel{min-width:0;border:1px solid var(--hairline);background:color-mix(in srgb,var(--surface) 96%,transparent);border-radius:var(--radius-lg);padding:var(--space-6);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-2);color:var(--ink);font-size:18px;font-weight:700;letter-spacing:-.01em}.panel-lead{margin:0 0 var(--space-5);color:var(--muted);font-size:13px}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:15px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-6);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-size:16px;font-weight:700}.checklist{margin:0 0 var(--space-4);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);list-style:none;color:var(--body)}.checklist li{position:relative;padding:6px 0 6px 22px}.checklist li:before{content:"";position:absolute;left:0;top:12px;width:8px;height:8px;border-radius:50%;background:var(--orange);opacity:.75}.kv{display:grid;gap:var(--space-1);margin:0}.kv div{display:grid;grid-template-columns:9rem 1fr;gap:var(--space-3);padding:10px 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;width:100%;margin:0;padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f3efe8;color:var(--ink);font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:280px}.external-link{color:var(--orange);text-decoration:none;font-weight:600}.external-link:hover{text-decoration:underline}.error{color:var(--red)}.wizard-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(108px,1fr));gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-5)}.wizard-steps li{position:relative;min-height:52px;display:grid;align-content:center;gap:2px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:8px 10px;background:var(--canvas-soft);color:var(--muted);font-size:12px;font-weight:650;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast)}.wizard-steps li[data-done=true]{border-color:color-mix(in srgb,var(--green) 35%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,var(--surface));color:var(--green)}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink);box-shadow:0 0 0 3px var(--orange-soft)}.wizard-steps .step-index{font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted-soft)}.wizard-steps li[data-active=true] .step-index,.wizard-steps li[data-done=true] .step-index{color:inherit;opacity:.8}.wizard-steps .step-label{line-height:1.25}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4);width:min(100%,520px)}.field span{font-size:12px;font-weight:600;color:var(--muted)}.field input,.field textarea,.field select{width:100%;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.field input{min-height:40px;padding:0 var(--space-3)}.field select{min-height:40px;padding:0 var(--space-3);cursor:pointer}.field textarea{min-height:120px;padding:var(--space-3);resize:vertical}.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{outline:none;border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.field input:disabled,.field textarea:disabled,.field select:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.workflow-hint{margin:calc(var(--space-1) * -1) 0 var(--space-3);color:var(--muted);font-size:12px}.meta-row{display:grid;gap:6px;margin:0 0 var(--space-4);padding:var(--space-3) var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);font-size:13px}.meta-row code{font-size:12px}.cta-row,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}.wizard-body{display:grid;gap:var(--space-3);justify-items:stretch;animation:panel-in .24s ease}.wizard-body>div{display:grid;gap:var(--space-3);justify-items:stretch}.wizard-body .field{width:100%;max-width:640px}.wizard-body .cta-row{margin-top:var(--space-1)}.interview-card{width:100%;padding:var(--space-5);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:linear-gradient(180deg,var(--surface),var(--canvas-soft));box-shadow:var(--shadow-panel)}.interview-q{margin:0 0 var(--space-2);color:var(--ink)}.btn,button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:0 16px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:650;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast),transform var(--transition-fast)}.btn:hover:not(:disabled),button:hover:not(:disabled){border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline-strong));background:var(--canvas-soft);outline:none}.btn:focus-visible:not(:disabled),button:focus-visible:not(:disabled){outline:none;box-shadow:0 0 0 3px var(--orange-soft)}.btn:active:not(:disabled),button:active:not(:disabled){transform:translateY(1px)}.btn:disabled,button:disabled{opacity:.48;cursor:not-allowed;transform:none}.btn-primary,button.btn-primary{border-color:var(--orange);background:var(--orange);color:#fff;box-shadow:0 6px 16px #f54e002e}.btn-primary:hover:not(:disabled),button.btn-primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.btn-secondary,button.btn-secondary{border-color:var(--hairline-strong);background:var(--surface);color:var(--ink)}.btn-ghost,button.btn-ghost{border-color:transparent;background:transparent;color:var(--body)}.btn-ghost:hover:not(:disabled),button.btn-ghost:hover:not(:disabled){background:var(--surface-muted);color:var(--ink)}.recovery-cta-row{margin-top:var(--space-3)}.recovery-cta-row button[disabled]{opacity:.4}@keyframes flash-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@keyframes panel-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(max-width:800px){.console-shell{width:min(100% - 24px,1120px);padding-top:var(--space-5)}.console-header{grid-template-columns:1fr}.nav-link-external{margin-left:0}.inspect-split-panel{inset:auto 0 0;width:100%;max-height:min(76vh,680px);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.kv div{grid-template-columns:1fr}.wizard-steps{grid-template-columns:repeat(2,minmax(0,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}}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loop 操作台 · Operator Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-3vsjZJHq.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-i1wV4LrY.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { open as openFile, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
6
7
|
import { parseWorkerEventLine } from "../observability/events.js";
|
|
7
8
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
@@ -17,7 +18,18 @@ const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
|
17
18
|
export function createObserveSnapshotCache() {
|
|
18
19
|
return { expiresAt: 0 };
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
export function defaultObserveStaticDir(fromFileUrl = import.meta.url) {
|
|
22
|
+
return path.join(path.dirname(fileURLToPath(fromFileUrl)), "static");
|
|
23
|
+
}
|
|
24
|
+
export function createObserveRouteContext(repoRoot, options = {}) {
|
|
25
|
+
return {
|
|
26
|
+
repoRoot: path.resolve(repoRoot),
|
|
27
|
+
pollIntervalMs: options.pollIntervalMs ?? 1000,
|
|
28
|
+
staticDir: options.staticDir ?? defaultObserveStaticDir(),
|
|
29
|
+
snapshotCache: options.snapshotCache ?? createObserveSnapshotCache(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export const ROUTES = [
|
|
21
33
|
{ method: "GET", pattern: /^\/api\/health$/, handler: handleHealth },
|
|
22
34
|
{ method: "GET", pattern: /^\/api\/snapshot$/, handler: handleSnapshot },
|
|
23
35
|
{ method: "GET", pattern: /^\/api\/batches$/, handler: handleBatches },
|
|
@@ -96,16 +108,9 @@ const ROUTES = [
|
|
|
96
108
|
handler: handleEventStream,
|
|
97
109
|
},
|
|
98
110
|
];
|
|
99
|
-
export
|
|
100
|
-
const method = req.method ?? "GET";
|
|
101
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
102
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
103
|
-
if (method !== "GET") {
|
|
104
|
-
sendJson(res, 405, { error: "Method not allowed" });
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
111
|
+
export function matchObserveGetRoute(pathname) {
|
|
107
112
|
for (const route of ROUTES) {
|
|
108
|
-
if (route.method !==
|
|
113
|
+
if (route.method !== "GET")
|
|
109
114
|
continue;
|
|
110
115
|
const match = route.pattern.exec(pathname);
|
|
111
116
|
if (!match)
|
|
@@ -115,16 +120,34 @@ export async function handleRequest(req, res, ctx) {
|
|
|
115
120
|
params.id = match[1];
|
|
116
121
|
if (match[2])
|
|
117
122
|
params.sub = match[2];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
return { params, query: new URLSearchParams(), handler: route.handler };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
export async function handleMatchedObserveRoute(req, res, match, ctx) {
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
130
|
+
await match.handler(req, res, { ...match, query: url.searchParams }, ctx);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!res.headersSent) {
|
|
134
|
+
sendJson(res, 500, {
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
});
|
|
127
137
|
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function handleRequest(req, res, ctx) {
|
|
141
|
+
const method = req.method ?? "GET";
|
|
142
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
143
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
144
|
+
if (method !== "GET") {
|
|
145
|
+
sendJson(res, 405, { error: "Method not allowed" });
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
const match = matchObserveGetRoute(pathname);
|
|
149
|
+
if (match) {
|
|
150
|
+
await handleMatchedObserveRoute(req, res, match, ctx);
|
|
128
151
|
return true;
|
|
129
152
|
}
|
|
130
153
|
if (pathname.startsWith("/api/")) {
|
|
@@ -134,8 +157,17 @@ export async function handleRequest(req, res, ctx) {
|
|
|
134
157
|
return false;
|
|
135
158
|
}
|
|
136
159
|
export async function serveStatic(req, res, staticDir) {
|
|
160
|
+
await serveObserveStatic(req, res, staticDir);
|
|
161
|
+
}
|
|
162
|
+
export async function serveObserveStatic(req, res, staticDir, options = {}) {
|
|
137
163
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
138
164
|
let pathname = decodeURIComponent(url.pathname);
|
|
165
|
+
const urlPrefix = options.urlPrefix?.replace(/\/$/, "");
|
|
166
|
+
const embeddedShell = Boolean(urlPrefix) &&
|
|
167
|
+
(pathname === urlPrefix || pathname.startsWith(`${urlPrefix}/`));
|
|
168
|
+
if (urlPrefix && embeddedShell) {
|
|
169
|
+
pathname = pathname.slice(urlPrefix.length) || "/";
|
|
170
|
+
}
|
|
139
171
|
if (pathname === "/") {
|
|
140
172
|
pathname = "/index.html";
|
|
141
173
|
}
|
|
@@ -151,9 +183,19 @@ export async function serveStatic(req, res, staticDir) {
|
|
|
151
183
|
sendJson(res, 404, { error: "Not found" });
|
|
152
184
|
return;
|
|
153
185
|
}
|
|
154
|
-
|
|
186
|
+
let content = await readFile(resolvedFile);
|
|
155
187
|
const contentType = contentTypeFor(pathname);
|
|
156
|
-
|
|
188
|
+
if (embeddedShell && contentType.startsWith("text/html")) {
|
|
189
|
+
content = Buffer.from(content
|
|
190
|
+
.toString("utf8")
|
|
191
|
+
.replace(/<body\b/i, '<body data-embedded-shell="true"'));
|
|
192
|
+
}
|
|
193
|
+
res.writeHead(200, {
|
|
194
|
+
"Content-Type": contentType,
|
|
195
|
+
"Content-Length": content.byteLength,
|
|
196
|
+
"X-Content-Type-Options": "nosniff",
|
|
197
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
198
|
+
});
|
|
157
199
|
res.end(content);
|
|
158
200
|
}
|
|
159
201
|
async function handleHealth(_req, res, _match, ctx) {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { createObserveSnapshotCache, handleRequest, serveStatic, } from "./routes.js";
|
|
5
|
-
const STATIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
3
|
+
import { createObserveRouteContext, handleRequest, serveStatic, } from "./routes.js";
|
|
6
4
|
/** Align with common reverse-proxy keepalive (e.g. nginx 75s). */
|
|
7
5
|
const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 65_000;
|
|
8
6
|
export function createObserveServer(options) {
|
|
@@ -15,12 +13,7 @@ export function createObserveServer(options) {
|
|
|
15
13
|
const pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
16
14
|
const debug = options.debug === true;
|
|
17
15
|
const keepAliveTimeoutMs = options.keepAliveTimeoutMs ?? DEFAULT_KEEP_ALIVE_TIMEOUT_MS;
|
|
18
|
-
const routeContext = {
|
|
19
|
-
repoRoot,
|
|
20
|
-
pollIntervalMs,
|
|
21
|
-
staticDir: STATIC_DIR,
|
|
22
|
-
snapshotCache: createObserveSnapshotCache(),
|
|
23
|
-
};
|
|
16
|
+
const routeContext = createObserveRouteContext(repoRoot, { pollIntervalMs });
|
|
24
17
|
return new Promise((resolve, reject) => {
|
|
25
18
|
const server = createServer((req, res) => {
|
|
26
19
|
const startedAt = Date.now();
|
|
@@ -73,7 +66,7 @@ export function createObserveServer(options) {
|
|
|
73
66
|
async function dispatch(req, res) {
|
|
74
67
|
const handled = await handleRequest(req, res, routeContext);
|
|
75
68
|
if (!handled) {
|
|
76
|
-
await serveStatic(req, res,
|
|
69
|
+
await serveStatic(req, res, routeContext.staticDir);
|
|
77
70
|
}
|
|
78
71
|
}
|
|
79
72
|
server.on("error", reject);
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
rel="stylesheet"
|
|
11
11
|
href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css"
|
|
12
12
|
/>
|
|
13
|
-
<link rel="stylesheet" href="
|
|
13
|
+
<link rel="stylesheet" href="styles.css" />
|
|
14
14
|
</head>
|
|
15
15
|
<body data-shell="observe-console">
|
|
16
16
|
<a class="skip-link" href="#main-content">跳到主内容</a>
|
|
17
|
+
<nav class="embedded-operator-nav" aria-label="Operator 导航">
|
|
18
|
+
<a href="/">操作</a>
|
|
19
|
+
<span aria-current="page">观测</span>
|
|
20
|
+
</nav>
|
|
17
21
|
<div id="app">
|
|
18
22
|
<header class="site-header">
|
|
19
23
|
<a href="#/" class="logo" aria-label="Loop Agent 运行看板首页">
|
|
@@ -53,7 +57,6 @@
|
|
|
53
57
|
>
|
|
54
58
|
<header class="page-header dashboard-page-header">
|
|
55
59
|
<div class="view-heading page-header-heading">
|
|
56
|
-
<p class="view-kicker">Observe</p>
|
|
57
60
|
<h2>运行总览</h2>
|
|
58
61
|
</div>
|
|
59
62
|
<div
|
|
@@ -345,6 +348,6 @@
|
|
|
345
348
|
</section>
|
|
346
349
|
</main>
|
|
347
350
|
</div>
|
|
348
|
-
<script type="module" src="
|
|
351
|
+
<script type="module" src="app.js"></script>
|
|
349
352
|
</body>
|
|
350
353
|
</html>
|
|
@@ -74,6 +74,59 @@ body {
|
|
|
74
74
|
line-height: 1.5;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
.embedded-operator-nav {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
body[data-embedded-shell="true"] .site-header .logo {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body[data-embedded-shell="true"] .site-header {
|
|
86
|
+
position: sticky;
|
|
87
|
+
top: 49px;
|
|
88
|
+
z-index: 30;
|
|
89
|
+
grid-template-columns: 1fr auto;
|
|
90
|
+
grid-template-areas: "nav status" "crumb crumb";
|
|
91
|
+
min-height: 56px;
|
|
92
|
+
margin-bottom: var(--space-5);
|
|
93
|
+
padding: 10px 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
body[data-embedded-shell="true"] .embedded-operator-nav {
|
|
97
|
+
position: sticky;
|
|
98
|
+
top: 0;
|
|
99
|
+
z-index: 40;
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: 6px;
|
|
102
|
+
align-items: center;
|
|
103
|
+
padding: 8px max(24px, calc((100vw - 1680px) / 2));
|
|
104
|
+
border-bottom: 1px solid var(--hairline);
|
|
105
|
+
background: color-mix(in srgb, var(--canvas) 92%, transparent);
|
|
106
|
+
backdrop-filter: blur(10px);
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
font-weight: 700;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.embedded-operator-nav a,
|
|
112
|
+
.embedded-operator-nav span {
|
|
113
|
+
padding: 6px 12px;
|
|
114
|
+
border-radius: var(--radius-pill);
|
|
115
|
+
text-decoration: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.embedded-operator-nav a:hover,
|
|
119
|
+
.embedded-operator-nav a:focus-visible {
|
|
120
|
+
background: var(--surface);
|
|
121
|
+
color: var(--orange-active);
|
|
122
|
+
outline: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.embedded-operator-nav span {
|
|
126
|
+
background: var(--ink);
|
|
127
|
+
color: #fff;
|
|
128
|
+
}
|
|
129
|
+
|
|
77
130
|
button,
|
|
78
131
|
input,
|
|
79
132
|
select,
|
|
@@ -326,8 +326,6 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
const acIds = unique(testCase.body.match(AC_ID) ?? []);
|
|
329
|
-
if (acIds.length === 0)
|
|
330
|
-
throw new Error(`${testCase.id} has no AC-* reference`);
|
|
331
329
|
acIds.forEach((id) => coveredAc.add(id));
|
|
332
330
|
const sourceSection = sectionBody(testCase.body, "Source References");
|
|
333
331
|
const sourceRefs = extractSourceReferences({
|
|
@@ -378,12 +376,41 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
378
376
|
];
|
|
379
377
|
return `${report.join("\n")}\n`;
|
|
380
378
|
}
|
|
379
|
+
function collectMarkdownCaseIds(markdown) {
|
|
380
|
+
return splitCases(markdown).map((testCase) => testCase.id);
|
|
381
|
+
}
|
|
382
|
+
function testFunctionRegion(input) {
|
|
383
|
+
const functionLineStart = input.source.lastIndexOf("\n", input.functionIndex - 1) + 1;
|
|
384
|
+
const prefix = input.source.slice(0, functionLineStart);
|
|
385
|
+
const previousBlankLine = Math.max(prefix.lastIndexOf("\n\n"), prefix.lastIndexOf("\r\n\r\n"));
|
|
386
|
+
const decoratorCandidateStart = previousBlankLine >= 0
|
|
387
|
+
? previousBlankLine + (prefix.startsWith("\r\n", previousBlankLine) ? 4 : 2)
|
|
388
|
+
: 0;
|
|
389
|
+
const decoratorCandidate = input.source.slice(decoratorCandidateStart, functionLineStart);
|
|
390
|
+
const regionStart = /^\s*@/m.test(decoratorCandidate)
|
|
391
|
+
? decoratorCandidateStart
|
|
392
|
+
: functionLineStart;
|
|
393
|
+
const nextBoundary = /^(?:(?:async\s+)?def|class)\s+[A-Za-z_][A-Za-z0-9_]*\b/gm;
|
|
394
|
+
nextBoundary.lastIndex = input.functionHeaderEnd;
|
|
395
|
+
const regionEnd = nextBoundary.exec(input.source)?.index ?? input.source.length;
|
|
396
|
+
return input.source
|
|
397
|
+
.slice(regionStart, regionEnd)
|
|
398
|
+
.split(/\r?\n/)
|
|
399
|
+
.filter((line) => !line.trimStart().startsWith("#"))
|
|
400
|
+
.join("\n");
|
|
401
|
+
}
|
|
402
|
+
function symbolCaseId(symbol) {
|
|
403
|
+
const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{3})(?:_|$)/i);
|
|
404
|
+
return match ? match[1].replaceAll("_", "-").toUpperCase() : undefined;
|
|
405
|
+
}
|
|
381
406
|
export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
382
407
|
const files = await markdownFiles(workspaceRoot);
|
|
383
408
|
const markdownIds = new Set();
|
|
384
409
|
for (const file of files) {
|
|
410
|
+
if (path.basename(file).toLowerCase() === "readme.md")
|
|
411
|
+
continue;
|
|
385
412
|
const content = await readFile(file, "utf8");
|
|
386
|
-
for (const id of content
|
|
413
|
+
for (const id of collectMarkdownCaseIds(content))
|
|
387
414
|
markdownIds.add(id);
|
|
388
415
|
}
|
|
389
416
|
const testcaseRoot = path.join(workspaceRoot, "testcase");
|
|
@@ -398,7 +425,8 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
398
425
|
}
|
|
399
426
|
}
|
|
400
427
|
await walk(testcaseRoot);
|
|
401
|
-
const
|
|
428
|
+
const mappings = new Map();
|
|
429
|
+
const automationIds = new Set();
|
|
402
430
|
for (const file of pythonFiles) {
|
|
403
431
|
const content = await readFile(file, "utf8");
|
|
404
432
|
if (/pytest\.(?:skip|xfail)\s*\(|@pytest\.mark\.(?:skip|xfail)\b/.test(content)) {
|
|
@@ -410,37 +438,34 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
410
438
|
}
|
|
411
439
|
for (const match of content.matchAll(/^(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)) {
|
|
412
440
|
const symbol = match[1];
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
441
|
+
const region = testFunctionRegion({
|
|
442
|
+
source: content,
|
|
443
|
+
functionIndex: match.index,
|
|
444
|
+
functionHeaderEnd: match.index + match[0].length,
|
|
416
445
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const id
|
|
422
|
-
(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
bucket.push(`${path.relative(workspaceRoot, file).replaceAll(path.sep, "/")}#${symbol}`);
|
|
429
|
-
symbols.set(id, bucket);
|
|
430
|
-
const tail = content.slice(match.index + match[0].length, match.index + match[0].length + 500);
|
|
431
|
-
if (!new RegExp(`^[\\s\\r\\n]*(?:[rRuUfF]{0,2})?[\"']{3}[^\n]*${id}`, "m").test(tail)) {
|
|
432
|
-
throw new Error(`${id} pytest symbol is missing a matching first-line docstring: ${bucket.at(-1)}`);
|
|
446
|
+
const associatedIds = new Set(region.match(CASE_ID) ?? []);
|
|
447
|
+
const fromSymbol = symbolCaseId(symbol);
|
|
448
|
+
if (fromSymbol)
|
|
449
|
+
associatedIds.add(fromSymbol);
|
|
450
|
+
for (const id of associatedIds) {
|
|
451
|
+
automationIds.add(id);
|
|
452
|
+
if (!markdownIds.has(id))
|
|
453
|
+
continue;
|
|
454
|
+
const bucket = mappings.get(id) ?? [];
|
|
455
|
+
bucket.push(`${path.relative(workspaceRoot, file).replaceAll(path.sep, "/")}#${symbol}`);
|
|
456
|
+
mappings.set(id, bucket);
|
|
433
457
|
}
|
|
434
458
|
}
|
|
435
459
|
}
|
|
436
|
-
const missing = [...markdownIds].filter((id) => !
|
|
437
|
-
const
|
|
438
|
-
.filter(([, refs]) => refs.length
|
|
460
|
+
const missing = [...markdownIds].filter((id) => !mappings.has(id));
|
|
461
|
+
const multiple = [...mappings]
|
|
462
|
+
.filter(([, refs]) => refs.length > 1)
|
|
439
463
|
.map(([id]) => id);
|
|
440
|
-
const extra = [...
|
|
441
|
-
if (missing.length
|
|
442
|
-
throw new Error(`traceability mismatch: missing=${missing.join(",")
|
|
464
|
+
const extra = [...automationIds].filter((id) => !markdownIds.has(id));
|
|
465
|
+
if (missing.length > 0) {
|
|
466
|
+
throw new Error(`traceability mismatch: missing=${missing.join(",")}; multiple=${multiple.join(",") || "none"}; extra=${extra.join(",") || "none"}`);
|
|
443
467
|
}
|
|
468
|
+
const mappingCount = [...mappings.values()].reduce((total, refs) => total + refs.length, 0);
|
|
444
469
|
return ([
|
|
445
470
|
"# Backend Test Traceability",
|
|
446
471
|
"",
|
|
@@ -449,11 +474,11 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
449
474
|
"PASS",
|
|
450
475
|
"",
|
|
451
476
|
`- Markdown Case IDs: ${markdownIds.size}`,
|
|
452
|
-
`-
|
|
453
|
-
`-
|
|
477
|
+
`- Covered Markdown Case IDs: ${mappings.size}`,
|
|
478
|
+
`- Pytest mappings: ${mappingCount}`,
|
|
454
479
|
"- Missing mappings: 0",
|
|
455
|
-
"
|
|
456
|
-
"
|
|
480
|
+
`- Multiple mappings: ${multiple.length}${multiple.length ? ` (${multiple.join(", ")})` : ""}`,
|
|
481
|
+
`- Extra automation Case IDs: ${extra.length}${extra.length ? ` (${extra.join(", ")})` : ""}`,
|
|
457
482
|
"- skip/xfail findings: 0",
|
|
458
483
|
].join("\n") + "\n");
|
|
459
484
|
}
|
|
@@ -3333,14 +3333,14 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3333
3333
|
executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive",
|
|
3334
3334
|
writeSet: ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"],
|
|
3335
3335
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])), forbiddenPaths: forbidden,
|
|
3336
|
-
outputContract: "Convert every final automatable Markdown case
|
|
3336
|
+
outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring; no JSON and no pytest execution.",
|
|
3337
3337
|
subtask_prompt: [
|
|
3338
3338
|
"Convert validated testcase/md/** to pytest using upstream environment and validation evidence plus only bounded pytest config/conftest.",
|
|
3339
|
-
"
|
|
3339
|
+
"Ensure every final Markdown Case ID appears in at least one real top-level test_* function region, preferably as `test_BE_<MODULE>_<NNN>_<description>` or in that function's docstring. Multiple test functions may cover one Case ID; assertions come only from Expected Results and setup comes only from Preconditions/Test Data/Automation Notes.",
|
|
3340
3340
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON.",
|
|
3341
3341
|
].join("\n\n"),
|
|
3342
3342
|
};
|
|
3343
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "Fail closed
|
|
3343
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "Fail closed only when a real Markdown case heading has no associated top-level pytest test function. Accept exact Case IDs in the function name or its decorator/body/docstring region; report multiple mappings and extra automation Case IDs without blocking. Continue to reject skip/xfail or swallowed exceptions.", "Run-owned reports/backend-test-traceability.md proving every real Markdown Case ID is covered by at least one pytest test function.");
|
|
3344
3344
|
const pytestCommand = [
|
|
3345
3345
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3346
3346
|
'PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest testcase/ -v -p no:cacheprovider --junitxml="${HARNESS_DAG_RUN_DIR}/reports/backend-test.junit.xml"',
|
package/docs/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**索引职责**:本文件只索引**核心契约、方法论、产物目录入口与模板**。单篇 progress / report / completed plan 的全量列表分别由对应子目录 `README.md` 维护,避免三处精选榜漂移。
|
|
8
8
|
|
|
9
|
-
**维护日:2026-07-22** — 索引对齐仓库头 `@tea-agent/loop-agent@0.
|
|
9
|
+
**维护日:2026-07-22** — 索引对齐仓库头 `@tea-agent/loop-agent@0.18.0`、reports 按类型分子目录,以及 progress / completed 导读制;版本与能力细节以 `CHANGELOG.md`、`reports/current-capability-summary.md` 与 active plans 为准。
|
|
10
10
|
|
|
11
11
|
站上用法文档在 `../website/docs/`;双树收敛见 `../skills/loop-agent/references/docs-converge.md`。
|
|
12
12
|
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
- `design/dag-source-binding-and-recovery.md` — 新生成 DAG 的任务源绑定与中断恢复
|
|
58
58
|
- `design/agent-worker-fullstack-workflow-integration.md` — workflow routing、Task Outcome、artifact-aware Ready、`fullstack-v1` 与 Verification Bundle
|
|
59
59
|
- `design/fullstack-end-to-end-delivery-optimization-roadmap.md` — 全栈端到端优化收敛路线图(release train / Delivery / Final Verification)
|
|
60
|
-
- `design/
|
|
60
|
+
- `design/2026-07-22-console-observe-unified-operator-surface.md` — Observe 深度融合进 Console(Wave 1–3 已实现,目标 0.18.0)
|
|
61
|
+
- `design/local-operator-console-from-pi-web.md` — Operator Console 设计输入;MVP 已随 `0.17.0`–`0.17.2` 发布;统一 surface 见上条
|
|
61
62
|
- `design/taskspec-to-loop-agent-mapping.md` — TaskSpec → loop-agent task 兼容契约(文档镜像;runtime 真源在代码)
|
|
62
63
|
|
|
63
64
|
已实现且仅作历史说明的设计见 `design/archive/`(例如 `design/archive/2026-07-14-loop-agent-self-update-notifier.md`)。
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
本页区分 loop-agent **当前已实现**的架构能力与**未来规划**。当前事实以代码、发布 CLI、已完成计划为准;未来能力一律标「规划 / 未实现 / 前瞻」。权威源:`CHANGELOG.md`、`docs/reports/current-capability-summary.md`、ADR 0001–0005、`docs/exec-plans/completed/` 与 `docs/exec-plans/active/`。
|
|
4
4
|
|
|
5
|
-
**对照版本:`@tea-agent/loop-agent@0.
|
|
5
|
+
**对照版本:`@tea-agent/loop-agent@0.18.0`(2026-07-22)**。细节版本条目见根 `CHANGELOG.md`;本表只保留架构层可读摘要。
|
|
6
6
|
|
|
7
|
-
## 当前已实现(累计至 0.
|
|
7
|
+
## 当前已实现(累计至 0.18.0)
|
|
8
8
|
|
|
9
9
|
| 域 | 现状 | 权威入口 |
|
|
10
10
|
| --- | --- | --- |
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
| Backend-test | **Markdown-first 8 节点**(env → MD cases/review → 单次 pytest+HTML → report+L-5);历史 JSON 读兼容 | completed `2026-07-22-backend-test-markdown-first-8-node.md` |
|
|
21
21
|
| Frontend-test | 默认 short-chain RAG;结果链 / Outcome 投影;formal smoke 仍 active | `CHANGELOG.md [0.16.24]`;active success-rate plan |
|
|
22
22
|
| Frontend-implementation | Contract / trace / repair / Mock assess;条件 design/repair 分支 | `CHANGELOG.md [0.13.0]` / `[0.17.x]` |
|
|
23
|
-
| Observe |
|
|
24
|
-
| Local Operator Console | `agent-worker console serve\|doctor`(loopback);Happy Path / Interview /
|
|
23
|
+
| Inspect(原 Observe) | 统一 Console 的 `/inspect/` 只读运营面与富时间线;兼容期仍保留独立 `observe serve` | `website/docs/guides/observe-ui.md`、`CHANGELOG.md [Unreleased]` |
|
|
24
|
+
| Local Operator Console | `agent-worker console serve\|doctor`(loopback);Operate + Inspect、Happy Path / Interview / split view / recovery CTA;**非**远端多用户 Console | unify design;ADR 0005 |
|
|
25
25
|
| Task Contract / operator surface | journaled `task contract *`;machine envelope;**DagSpec v4 `taskContractBinding`**(exclusive writer 强制) | `CHANGELOG.md [0.17.0]`–`[0.17.2]`;ADR 0005 |
|
|
26
26
|
| DagSpec / repair | v3 `runtimeContract` + 显式 `repairNodeId`;新 writer 生成默认 v4 binding | `dag-execution.md`、`CHANGELOG.md [0.11.0]` / `[0.17.0]` |
|
|
27
27
|
| Eval Lab | corpus / campaign / promote(人工门禁);`autoPromote=false`;**非** RSI Level 1 | `CHANGELOG.md [0.15.0]`;active plan 可归档 |
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
| fullstack Delivery / Final Verification live 闭环 | 进行中(active train) | `docs/exec-plans/active/2026-07-19-fullstack-dogfood-016x-release-train.md` |
|
|
51
51
|
| Dynamic Workflow runtime limits 强执法、更广 profile | 设计输入 | `docs/design/dynamic-workflow-dag-engine-roadmap.md` |
|
|
52
52
|
| Loop 与 Dynamic Workflow 更深双向集成 / 自动恢复 | 设计输入 | 同上;当前已有基础 `workflow` action |
|
|
53
|
-
| Console Phase 4 General Operator Chat
|
|
53
|
+
| Console Phase 4 General Operator Chat | 规划 / 未实现 | Console design residual |
|
|
54
54
|
| Eval Lab auto-promote / RSI Level 1 产品声称 | **禁止**写成已交付 | Eval Lab design + active plan |
|
|
55
55
|
|
|
56
|
-
> 本地 Operator Console
|
|
56
|
+
> 本地 Operator Console 统一面(0.18.0)已发布:单仓库、loopback、随 `@tea-agent/loop-agent` 同包、canonical mutation 只经 sibling CLI,Inspect 复用 GET-only Observe read model。它不是远端 Web Console,也不能把多租户/云编排需求偷渡进本地 Console。
|
|
57
57
|
|
|
58
58
|
> 注意:`docs/design/dynamic-workflow-dag-engine-roadmap.md` 含 2026-07-04 历史叙述;文中凡把 Cursor 写成受治理 executor 或 `loop` 的 `cursor-fix` 动作,均为**历史叙述**,现状以 Pi-only + 显式 `cursor-prompt` sidecar 为准。
|
|
59
59
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Worker 与 Feature 架构
|
|
2
2
|
|
|
3
|
-
本页说明 `agent-worker` 如何通过冻结的已发布 `loop-agent` 子进程执行 DAG(不 in-process import runtime kernel),以及其上的产品线 read model:TaskSpec、Task Pool、Feature
|
|
3
|
+
本页说明 `agent-worker` 如何通过冻结的已发布 `loop-agent` 子进程执行 DAG(不 in-process import runtime kernel),以及其上的产品线 read model:TaskSpec、Task Pool、Feature 与统一 Operator Console(Operate + Inspect)。边界契约权威是 `runtime-boundaries.md` §Worker adapter。
|
|
4
4
|
|
|
5
|
-
**维护校准:2026-07-22 / `@tea-agent/loop-agent@0.
|
|
5
|
+
**维护校准:2026-07-22 / `@tea-agent/loop-agent@0.18.0`** — 统一 Operator Console(Operate + Inspect)已落地;细节以 `CHANGELOG.md` 与 active unify plan 为准。
|
|
6
6
|
|
|
7
7
|
## 核心事实:子进程,非 in-process
|
|
8
8
|
|
|
@@ -73,19 +73,19 @@ controller identity 与 DAG skill snapshot 是两个不同冻结层(前者跨
|
|
|
73
73
|
- Delivery / Closeout:clean Delivery HEAD 上生成 canonical QA/最终验证证据、Delivery Package、Acceptance Coverage、PR 草稿;Closeout 默认预览,显式 `--apply --owner` 才原子写回。
|
|
74
74
|
- 权威证据:`CHANGELOG.md [0.10.0]`、`docs/reports/feature/2026-07-12-m2-completion-audit.md`。
|
|
75
75
|
|
|
76
|
-
### Observe
|
|
76
|
+
### Inspect(Observe 只读 read model)
|
|
77
77
|
|
|
78
78
|
- 模块:`src/worker/observe/`、`src/worker/observability/{read-model,event-store}.ts`。
|
|
79
79
|
- 全局快照:`buildGlobalSnapshot({ repoRoot })`(`src/worker/observability/read-model.ts`),是 **derived** 视图,消费 `.harness/` 与 Task Pool 事实,**不**改变执行成败。
|
|
80
|
-
- Observe
|
|
81
|
-
-
|
|
80
|
+
- Observe read model 是本地只读 Inspect 能力;snapshot 投影失败返回安全错误摘要而非全零健康状态。
|
|
81
|
+
- `agent-worker console serve` 在同一进程挂载 `/inspect/` 与现有 GET `/api/**`;`/api/health` 返回 `OperatorSurfaceHealthV1`。兼容期独立 Observe `:8787` 保持旧 health DTO 与 GET-only 行为。
|
|
82
82
|
|
|
83
|
-
### Local Operator Console(Official,0.
|
|
83
|
+
### Local Operator Console(Official,0.18.0)
|
|
84
84
|
|
|
85
85
|
- 模块:`src/worker/console/`(loopback `serve|doctor`、operator API、Vite SPA)。
|
|
86
|
-
- **已发布能力**:Happy Path / Interview
|
|
87
|
-
-
|
|
88
|
-
- 写入路径仍只经 `LoopAgentClient` → 已发布 `loop-agent`(含 `task contract *` / DAG confirm-run
|
|
86
|
+
- **已发布能力**:Happy Path / Interview、Task Contract、SSE、同站 Inspect、Run/Recovery 紧凑检视与 recovery CTA 矩阵(无 Cancel / 无主 CTA「直接改代码」)。
|
|
87
|
+
- Inspect 仍复用唯一 Observe read model,GET-only;不复制 snapshot projector。
|
|
88
|
+
- 写入路径仍只经 `LoopAgentClient` → 已发布 `loop-agent`(含 `task contract *` / DAG confirm-run)。
|
|
89
89
|
- 证据:completed Console Phase 0.5 / 1–3;handoff `docs/reports/feature/2026-07-22-console-mvp-handoff.md`;ADR 0005。
|
|
90
90
|
|
|
91
91
|
## 版本化自举的 deterministic canary
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
".harness/dag-runs/**",
|
|
203
203
|
"artifacts/**"
|
|
204
204
|
],
|
|
205
|
-
"outputContract": "Convert every final automatable Markdown case
|
|
206
|
-
"subtask_prompt": "Convert validated testcase/md/** to pytest using upstream environment and validation evidence plus only bounded pytest config/conftest.\n\
|
|
205
|
+
"outputContract": "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring; no JSON and no pytest execution.",
|
|
206
|
+
"subtask_prompt": "Convert validated testcase/md/** to pytest using upstream environment and validation evidence plus only bounded pytest config/conftest.\n\nEnsure every final Markdown Case ID appears in at least one real top-level test_* function region, preferably as `test_BE_<MODULE>_<NNN>_<description>` or in that function's docstring. Multiple test functions may cover one Case ID; assertions come only from Expected Results and setup comes only from Preconditions/Test Data/Automation Notes.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON."
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"id": "backend-test-traceability-gate-shell",
|
|
@@ -223,8 +223,8 @@
|
|
|
223
223
|
".harness/dag-runs/**",
|
|
224
224
|
"artifacts/**"
|
|
225
225
|
],
|
|
226
|
-
"outputContract": "Run-owned reports/backend-test-traceability.md proving Markdown
|
|
227
|
-
"subtask_prompt": "Fail closed
|
|
226
|
+
"outputContract": "Run-owned reports/backend-test-traceability.md proving every real Markdown Case ID is covered by at least one pytest test function.",
|
|
227
|
+
"subtask_prompt": "Fail closed only when a real Markdown case heading has no associated top-level pytest test function. Accept exact Case IDs in the function name or its decorator/body/docstring region; report multiple mappings and extra automation Case IDs without blocking. Continue to reject skip/xfail or swallowed exceptions.",
|
|
228
228
|
"shell": {
|
|
229
229
|
"commands": [],
|
|
230
230
|
"backendTestPipeline": "markdown-traceability",
|
package/package.json
CHANGED
|
@@ -52,8 +52,8 @@ Leaf DAG nodes 不得递归启动 `agent-worker`。Worker 负责 DAG 之外的 s
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
doctor 只读;migrate 默认零写入,apply 失败全回滚且不改 JSONL。
|
|
55
|
-
8. Observe
|
|
56
|
-
9. Official Console:`agent-worker console serve|doctor
|
|
55
|
+
8. Inspect(原 Observe)只读:推荐 `agent-worker console serve` 的 `/inspect/#/...`;兼容 `observe serve|snapshot`。canonical Task 路由为 `/api/features/:featureId/tasks/:taskId` 与 `#/feature/:featureId/task/:taskId`。8790 `/api/health` 为 `OperatorSurfaceHealthV1`;8787 仍为 `ObserveHealthV1`。
|
|
56
|
+
9. Official Console:`agent-worker console serve|doctor`(默认 loopback;可 `--host 0.0.0.0` / `--debug`)。Recovery CTA 为 report/doctor/decision/resume/reconcile/regenerate/打开检视;无 Cancel、无主 CTA「直接改代码」。主会话 Compatibility Assist 不得替代 Console/CLI 执法。
|
|
57
57
|
|
|
58
58
|
## Versioned Self-Hosting
|
|
59
59
|
|