@tea-agent/loop-agent 0.18.1 → 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.
Files changed (33) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +4 -6
  4. package/dist/commands/init.js +3 -3
  5. package/dist/governance/exec-plans.js +4 -0
  6. package/dist/worker/cli.js +13 -12
  7. package/dist/worker/console/doctor.js +55 -2
  8. package/dist/worker/console/index.js +1 -0
  9. package/dist/worker/console/loopback.js +2 -2
  10. package/dist/worker/console/observe-link.js +7 -2
  11. package/dist/worker/console/operator-actions.js +30 -2
  12. package/dist/worker/console/operator-selection.js +92 -0
  13. package/dist/worker/console/operator-surface-health.js +23 -0
  14. package/dist/worker/console/recovery-cta.js +2 -2
  15. package/dist/worker/console/routes.js +45 -19
  16. package/dist/worker/console/security.js +29 -4
  17. package/dist/worker/console/server.js +106 -8
  18. package/dist/worker/console/static/assets/index-3vsjZJHq.js +16 -0
  19. package/dist/worker/console/static/assets/index-i1wV4LrY.css +1 -0
  20. package/dist/worker/console/static/index.html +2 -2
  21. package/dist/worker/observe/routes.js +63 -21
  22. package/dist/worker/observe/server.js +3 -10
  23. package/dist/worker/observe/static/index.html +6 -3
  24. package/dist/worker/observe/static/styles.css +53 -0
  25. package/docs/README.md +3 -2
  26. package/docs/architecture/evolution.md +6 -6
  27. package/docs/architecture/worker-and-feature.md +9 -9
  28. package/package.json +1 -1
  29. package/skills/agent-worker/references/agent-worker-operator.md +2 -2
  30. package/skills/loop-agent/references/command-reference.md +4 -2
  31. package/skills/loop-agent/references/harness-policy.md +1 -1
  32. package/dist/worker/console/static/assets/index-KUSib7aM.js +0 -16
  33. 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-KUSib7aM.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-ucIzpaGJ.css">
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
- const ROUTES = [
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 async function handleRequest(req, res, ctx) {
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 !== 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
- try {
119
- await route.handler(req, res, { params, query: url.searchParams }, ctx);
120
- }
121
- catch (error) {
122
- if (!res.headersSent) {
123
- sendJson(res, 500, {
124
- error: error instanceof Error ? error.message : String(error),
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
- const content = await readFile(resolvedFile);
186
+ let content = await readFile(resolvedFile);
155
187
  const contentType = contentTypeFor(pathname);
156
- res.writeHead(200, { "Content-Type": contentType });
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 { fileURLToPath } from "node:url";
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, STATIC_DIR);
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="/styles.css" />
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="/app.js"></script>
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,
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.17.2`、reports 按类型分子目录,以及 progress / completed 导读制;版本与能力细节以 `CHANGELOG.md`、`reports/current-capability-summary.md` 与 active plans 为准。
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/local-operator-console-from-pi-web.md` — Operator Console 设计输入;MVP 已随 `0.17.0`–`0.17.2` 发布
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.17.2`(2026-07-22)**。细节版本条目见根 `CHANGELOG.md`;本表只保留架构层可读摘要。
5
+ **对照版本:`@tea-agent/loop-agent@0.18.0`(2026-07-22)**。细节版本条目见根 `CHANGELOG.md`;本表只保留架构层可读摘要。
6
6
 
7
- ## 当前已实现(累计至 0.17.2
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 | 本地只读暖白运营控制台 R1–R5 + 执行面富时间线;versioned `/api/health` | `website/docs/guides/observe-ui.md`、`CHANGELOG.md [0.11.0]` / `[0.14.0]` / `[0.17.x]` |
24
- | Local Operator Console | `agent-worker console serve\|doctor`(loopback);Happy Path / Interview / Observe 深链 / recovery CTA;**非**远端多用户 Console | completed Console plans;`docs/design/local-operator-console-from-pi-web.md`;ADR 0005 |
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 designADR 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 / 与 Observe 合服 | 规划 / 未实现 | Console design residual |
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 MVP(0.17.0–0.17.2)已发布:单仓库、loopback、随 `@tea-agent/loop-agent` 同包、canonical mutation 只经 sibling CLI、与 Observe 分进程。它不是远端 Web Console,也不能把多租户/云编排需求偷渡进本地 MVP
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、Observe Local Operator Console。边界契约权威是 `runtime-boundaries.md` §Worker adapter。
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.17.2`** — Console MVP、Task Contract TaskSpec workflow 路由已落地;细节以 `CHANGELOG.md` 与 completed plans 为准。
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(只读 read model)
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 是本地只读暖白控制台;snapshot 投影失败返回安全错误摘要而非全零健康状态(见 `CHANGELOG.md [0.9.0]`)。
81
- - Phase 3:`GET /api/health` versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package 元数据、由 ROUTES 派生的 `routeCapabilities`)。Console 深链 fail-closed 依赖该合同;Observe 仍独立进程、不经 Console proxy。
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.17.x MVP
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 策略面、Task Contract 落地、SSE 运行跟随、Observe 深链 fail-closed、recovery CTA 矩阵(无 Cancel / 无主 CTA「直接改代码」)。
87
- - Observe 协作:versioned Observe health + `repoFingerprint`;**不** mount / proxy / 复制第二套 read model
88
- - 写入路径仍只经 `LoopAgentClient` → 已发布 `loop-agent`(含 `task contract *` / DAG confirm-run);不与 Observe 合服。
86
+ - **已发布能力**:Happy Path / InterviewTask ContractSSE、同站 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -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(`observe serve|snapshot`)只读;canonical Task 路由为 `/api/features/:featureId/tasks/:taskId` 与 `#/feature/:featureId/task/:taskId`。`GET /api/health` 为 versioned DTO(fingerprint + routeCapabilities)。Console `observeLink` match 后深链;offline 展示 `agent-worker observe serve --repo . --port 8787`。
56
- 9. Official Console:`agent-worker console serve|doctor`(loopback)。Recovery CTA 为 report/doctor/decision/resume/reconcile/regenerate;无 Cancel、无主 CTA「直接改代码」。主会话 Compatibility Assist 不得替代 Console/CLI 执法。
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
 
@@ -531,7 +531,9 @@ agent-worker batch run-ready \
531
531
  [--quiet] \
532
532
  [--pi-model <model>] # smoke 覆盖:所有 pi 节点强制用该模型
533
533
  agent-worker report morning --repo <repo-root> [--batch-run-id <id>] [--output <path>]
534
- agent-worker observe serve --repo <repo-root> [--port 8787] [--host 127.0.0.1]
534
+ agent-worker console serve --repo <repo-root> [--port 8790] [--host 127.0.0.1]
535
+ agent-worker console doctor --repo <repo-root> [--json] [--console-url <url>]
536
+ agent-worker observe serve --repo <repo-root> [--port 8787] [--host 127.0.0.1] # 兼容入口;stderr 会提示 OBSERVE_SERVE_DEPRECATED
535
537
  agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON 到 stdout
536
538
  ```
537
539
 
@@ -552,7 +554,7 @@ agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON
552
554
  - `report metrics` 按 UTC 月去重投影 Feature/Failure/Follow-up/Delivery/AC/decision/recovery/boundary 指标,同时写 JSON 与 Markdown;每项保留 numerator、denominator、sampleSize 和 missingData。
553
555
  - `task draft-followup` 会按全部 failure category 生成 TaskDraft 或人工行动卡:ProductBug/TestBug/FlakyTest/DependencyFailure 可批准;EnvFailure 连续两次后才生成 ENV-CHECK;Spec/Contract/Risk/Human/Unknown 只给行动卡。人工以 `feature approve-followup --dry-run` 预览,再带非空 `--owner` 批准 TaskDraft;行动卡不能批准。批准在 staging validation 后写 TaskSpec、graph、Ready/approval/event,原失败事实不改写,并有 rename/state/approval/index/event 回滚门禁。
554
556
  - `task retry` 是失败 Task 的唯一重试入口。它会保留原有运行记录和 failure handoff,并让下一次 `batch run-ready` 使用新的 `workerRunId`;不要删除运行态文件或手动修改状态来重试。
555
- - `observe serve` 是独立于执行过程的本地只读看板,默认监听 `127.0.0.1:8787`;`observe snapshot` 只输出同一份运行态快照 JSON。两者都不会启动、暂停或重试 Task / Worker / DAG。
557
+ - **推荐** `console serve`(默认 `127.0.0.1:8790`)提供 Operate + Inspect;Inspect 路径为 `/inspect/#/...`,API 仍为根 `/api/**`。`observe serve` 为兼容入口(默认 `8787`,启动时 stderr 输出 `OBSERVE_SERVE_DEPRECATED`);`observe snapshot` 保留。两者都不会启动、暂停或重试 Task / Worker / DAG。
556
558
  - 当前 Worker 仍是 v0(库 + CLI + dogfood),未接入定时/CI 驱动;`report morning` 只能从已有 Task Pool runs 汇总。
557
559
 
558
560
  ### 查看 duration statistics
@@ -17,7 +17,7 @@
17
17
  | Tier | Default purpose | Commands |
18
18
  |---|---|---|
19
19
  | Primary | Normal autonomous implementation(经 CLI) | `new-task` -> `dag run-task --profile auto` -> `dag validate --strict-models --strict-governance` -> `run-dag` |
20
- | Operator | Diagnose, recover, close out, inspect facts | `status`, `instructions`, `dag status`, `dag doctor`, `dag report`, `dag reconcile-run`, `dag closeout-draft`, `dag reconcile-tasks`, `dag final-verification`, `inspect`, `doctor`, `spine audit`, `knowledge curate`, `docs audit`, `handoff check`, `loop-benchmark`;Observe:`agent-worker observe serve\|snapshot` |
20
+ | Operator | Diagnose, recover, close out, inspect facts | `status`, `instructions`, `dag status`, `dag doctor`, `dag report`, `dag reconcile-run`, `dag closeout-draft`, `dag reconcile-tasks`, `dag final-verification`, `inspect`, `doctor`, `spine audit`, `knowledge curate`, `docs audit`, `handoff check`, `loop-benchmark`;Inspect:`agent-worker console serve`(`/inspect/`)与兼容 `observe serve\|snapshot` |
21
21
  | Compatibility (CLI helpers) | Legacy task metadata and feature-study helpers | `goal`, `reference`, `study` |
22
22
  | Escape hatch | Isolated delegation, one-shot diagnosis or **显式** bounded repair | `delegate`, `worktree`, `harvest`, `pi-prompt`, `cursor-prompt`(不得作失败默认恢复) |
23
23
  | Experimental | Long-running outer task state | `loop init\|status\|run\|record-round\|add-signal\|closeout` |