@tea-agent/loop-agent 0.39.0-next.27 → 0.39.0-next.29

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 (117) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +150 -96
  3. package/README.md +8 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +16 -5
  6. package/dist/cli/program.js +16 -2
  7. package/dist/cli/update/notifier.js +2 -2
  8. package/dist/cli.js +2 -1
  9. package/dist/commands/init-model-catalog.js +464 -0
  10. package/dist/commands/init-upgrade.js +265 -97
  11. package/dist/commands/init.js +428 -27
  12. package/dist/commands/inspect-next.js +8 -0
  13. package/dist/executors/dag-pi-executor.js +18 -9
  14. package/dist/executors/pi-playwright-cli-tool.js +8 -1
  15. package/dist/executors/shell-executor.js +209 -13
  16. package/dist/shared/operator/capabilities.js +7 -0
  17. package/dist/shared/update/console-notifier.js +100 -0
  18. package/dist/{cli → shared}/update/npm-client.js +41 -8
  19. package/dist/{cli → shared}/update/state.js +41 -13
  20. package/dist/worker/cli.js +31 -1
  21. package/dist/worker/console/chat/routes.js +1 -0
  22. package/dist/worker/console/operator-user-error.js +1 -1
  23. package/dist/worker/console/routes.js +3 -0
  24. package/dist/worker/console/security.js +53 -5
  25. package/dist/worker/console/server.js +10 -4
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-DGn9iNrd.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-Dc0C2Nyv.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-COnz_Bhe.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-VhIP6vgf.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-D74lpmlb.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-DWcZjWB3.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BxmiogQE.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-C00Oi-2I.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DTrT1Vze.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DdKW7-fL.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-D0q1WN6w.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DBa14MSI.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-DkP-pEox.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-lgPogpcX.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D-apZer0.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-B1T2yAIe.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-IjoI3dqU.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-IjoI3dqU.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-0P6hi3jF.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CntzMSR8.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-C9zmXJtG.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-DfBCZqZw.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DLp-bhGL.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BksDcwZm.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-Bdag7DYP.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CoY5uNbk.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-KcgrDXHt.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CikErwJb.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DqmwwzvZ.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-35krh9WG.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-CvTF1gJ4.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BiOWWClJ.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-CqWlWE4l.js} +3 -3
  59. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Crha5dcA.js} +1 -1
  60. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DEtbscOZ.js} +1 -1
  61. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-D2YYzx3z.js} +1 -1
  62. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-C7x18ttQ.js} +1 -1
  63. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CCLZkz45.js} +1 -1
  64. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-Dmy4lSGv.js} +5 -5
  65. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CmvIol59.js} +1 -1
  66. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-DVpEdirN.js} +1 -1
  67. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-cKQy456v.js} +1 -1
  68. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-bx06I38m.js} +1 -1
  69. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-BjfIVIJr.js} +1 -1
  70. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-DcuPGbKM.js} +1 -1
  71. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DvgVeDoV.js} +1 -1
  72. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-DLBG9nm-.js} +1 -1
  73. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D33L7FBF.js} +1 -1
  74. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-hvqD9TpS.js} +1 -1
  75. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C6MII0D1.js +1 -0
  76. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-BtjH81Mz.js} +2 -2
  77. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D_tk0V8D.js +8 -0
  78. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-CJ47Kraf.js} +1 -1
  79. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-COVPl-wh.js} +1 -1
  80. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-CCoQJLUY.js} +1 -1
  81. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DTWJvfAd.js} +1 -1
  82. package/dist/worker/console/static/index.html +1 -1
  83. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  84. package/dist/worker/observe/node-input.js +2 -0
  85. package/dist/worker/observe/node-process.js +377 -0
  86. package/dist/worker/observe/routes.js +24 -0
  87. package/dist/worker/observe/shell-handler-keys.js +34 -0
  88. package/dist/worker/observe/static/api.js +8 -0
  89. package/dist/worker/observe/static/constants.js +8 -1
  90. package/dist/worker/observe/static/format.js +23 -0
  91. package/dist/worker/observe/static/markdown-render.js +20 -1
  92. package/dist/worker/observe/static/operator-chrome.js +4 -0
  93. package/dist/worker/observe/static/state.js +11 -0
  94. package/dist/worker/observe/static/styles.css +3 -0
  95. package/dist/worker/observe/static/views/dag-inspector.js +247 -7
  96. package/dist/worker/observe/static/views/dag.js +8 -1
  97. package/dist/worker/observe/static/views/session-timeline.js +60 -4
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  99. package/dist/workflows/dag/failure-category.js +8 -0
  100. package/dist/workflows/dag/frontend-implementation-contract.js +5 -1
  101. package/dist/workflows/dag/init-hybrid.js +35 -10
  102. package/dist/workflows/dag/node-execution.js +20 -2
  103. package/dist/workflows/dag/retry-policy.js +91 -0
  104. package/dist/workflows/dag/types.js +9 -1
  105. package/docs/init-surface.manifest.json +5 -0
  106. package/docs/operations/README.md +1 -1
  107. package/docs/templates/init-managed-agents.md +13 -3
  108. package/package.json +5 -1
  109. package/scripts/next-info.mjs +356 -0
  110. package/scripts/next-publish-gate.mjs +238 -0
  111. package/scripts/release-source-binding.mjs +251 -0
  112. package/skills/loop-agent/references/command-reference.md +11 -4
  113. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  114. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  115. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  116. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -0,0 +1,377 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
3
+ import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
4
+ import { parseExecutionOutputKey, } from "./dag-node-execution-output.js";
5
+ import { resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
6
+ import { namedShellHandlers } from "./shell-handler-keys.js";
7
+ export const NODE_PROCESS_SCHEMA_VERSION = 1;
8
+ export const NODE_PROCESS_FIELD_MAX_BYTES = 16 * 1024;
9
+ function boundField(value) {
10
+ if (value == null || value === "")
11
+ return { text: null, truncated: false };
12
+ const redacted = redactSecrets(value);
13
+ const truncated = truncateUtf8Preview(redacted, NODE_PROCESS_FIELD_MAX_BYTES);
14
+ return {
15
+ text: truncated,
16
+ truncated: truncated !== redacted,
17
+ };
18
+ }
19
+ function asRecord(value) {
20
+ if (!value || typeof value !== "object" || Array.isArray(value))
21
+ return null;
22
+ return value;
23
+ }
24
+ function asString(value) {
25
+ return typeof value === "string" && value.trim() ? value : null;
26
+ }
27
+ function asNumber(value) {
28
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
29
+ }
30
+ function asBoolean(value) {
31
+ return value === true;
32
+ }
33
+ async function readJsonArtifact(repoRoot, dagRunId, segments) {
34
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, segments);
35
+ if (!resolved.ok)
36
+ return null;
37
+ try {
38
+ const raw = await readFile(resolved.result.absolutePath, "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ return asRecord(parsed);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ async function readTextArtifact(repoRoot, dagRunId, segments) {
47
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, segments);
48
+ if (!resolved.ok)
49
+ return { text: null, truncated: false };
50
+ try {
51
+ const raw = await readFile(resolved.result.absolutePath, "utf8");
52
+ return boundField(raw);
53
+ }
54
+ catch {
55
+ return { text: null, truncated: false };
56
+ }
57
+ }
58
+ function emptyBody(dagRunId, nodeId, kind, emptyReason, extras = {}) {
59
+ return {
60
+ schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
61
+ dagRunId,
62
+ nodeId,
63
+ kind,
64
+ executor: extras.executor ?? null,
65
+ handler: extras.handler ?? null,
66
+ emptyReason,
67
+ key: extras.key ?? null,
68
+ commands: extras.commands ?? [],
69
+ steps: extras.steps ?? [],
70
+ };
71
+ }
72
+ function classifyKind(task) {
73
+ const executor = asString(task.executor);
74
+ const shell = asRecord(task.shell);
75
+ const handlers = namedShellHandlers(shell);
76
+ const handler = handlers[0] ?? null;
77
+ const commands = shell?.commands;
78
+ const commandsDeclared = Array.isArray(commands) &&
79
+ commands.some((item) => typeof item === "string" && item.trim());
80
+ if (executor === "static" || task.static) {
81
+ return { kind: "static", executor: executor ?? "static", handler, commandsDeclared };
82
+ }
83
+ if (executor === "shell" || shell) {
84
+ if (commandsDeclared) {
85
+ return { kind: "shell-commands", executor: executor ?? "shell", handler, commandsDeclared };
86
+ }
87
+ if (handler) {
88
+ return { kind: "shell-preset", executor: executor ?? "shell", handler, commandsDeclared };
89
+ }
90
+ return { kind: "shell-commands", executor: executor ?? "shell", handler, commandsDeclared };
91
+ }
92
+ return {
93
+ kind: "pi-session",
94
+ executor: executor ?? "pi",
95
+ handler,
96
+ commandsDeclared,
97
+ };
98
+ }
99
+ function receiptAttemptSegments(nodeId, parsed) {
100
+ if (parsed?.kind === "attempt") {
101
+ return [nodeId, `attempt-${parsed.index}`, "shell-command-receipt.json"];
102
+ }
103
+ return [nodeId, "shell-command-receipt.json"];
104
+ }
105
+ async function loadShellCommands(repoRoot, dagRunId, nodeId, parsed) {
106
+ const preferred = await readJsonArtifact(repoRoot, dagRunId, receiptAttemptSegments(nodeId, parsed));
107
+ const receipt = preferred ??
108
+ (parsed
109
+ ? await readJsonArtifact(repoRoot, dagRunId, [
110
+ nodeId,
111
+ "shell-command-receipt.json",
112
+ ])
113
+ : null);
114
+ if (!receipt)
115
+ return null;
116
+ const rawCommands = Array.isArray(receipt.commands) ? receipt.commands : [];
117
+ const commands = [];
118
+ for (let i = 0; i < rawCommands.length; i += 1) {
119
+ const entry = asRecord(rawCommands[i]) ?? {};
120
+ const stdout = await readTextArtifact(repoRoot, dagRunId, [
121
+ nodeId,
122
+ "commands",
123
+ `${i + 1}.stdout.txt`,
124
+ ]);
125
+ const stderr = await readTextArtifact(repoRoot, dagRunId, [
126
+ nodeId,
127
+ "commands",
128
+ `${i + 1}.stderr.txt`,
129
+ ]);
130
+ const commandBound = boundField(asString(entry.command) ?? "");
131
+ commands.push({
132
+ index: i + 1,
133
+ command: commandBound.text ?? "(command)",
134
+ ok: asBoolean(entry.ok),
135
+ exitCode: asNumber(entry.exitCode),
136
+ failureCategory: asString(entry.failureCategory),
137
+ startedAt: asString(entry.startedAt),
138
+ finishedAt: asString(entry.finishedAt),
139
+ durationMs: asNumber(entry.wallDurationMs) ??
140
+ asNumber(entry.executionDurationMs) ??
141
+ null,
142
+ reused: asBoolean(entry.reused),
143
+ stdout: stdout.text,
144
+ stderr: stderr.text,
145
+ truncated: commandBound.truncated || stdout.truncated || stderr.truncated,
146
+ });
147
+ }
148
+ return commands;
149
+ }
150
+ function handlerLabel(handler) {
151
+ if (handler === "frontendBrowserToolPreflight")
152
+ return "浏览器工具能力探测";
153
+ if (handler === "frontendTestStandardScenarios")
154
+ return "标准场景物化";
155
+ if (handler === "frontendTestEnvironmentProbe")
156
+ return "前端环境探测";
157
+ if (handler === "frontendTestCaseChecklist")
158
+ return "用例清单校验";
159
+ if (handler === "frontendTestCaseManifest")
160
+ return "用例清单物化";
161
+ if (handler === "frontendTestEvidenceValidation")
162
+ return "证据校验";
163
+ if (handler === "frontendTestResultFinalize")
164
+ return "结果定稿";
165
+ if (handler === "frontendTestReports" || handler === "frontendTestL5Report" || handler === "frontendTestHtmlReport") {
166
+ return "测试报告";
167
+ }
168
+ if (handler === "frontendPrewriteGate")
169
+ return "前端预写门禁";
170
+ if (handler === "finalWriteSetApprovalGate")
171
+ return "writeSet 终态批准";
172
+ return handler;
173
+ }
174
+ async function loadPresetSteps(repoRoot, dagRunId, nodeId, handler, parsed) {
175
+ const written = await readJsonArtifact(repoRoot, dagRunId, [
176
+ nodeId,
177
+ "process-receipt.json",
178
+ ]);
179
+ if (written && Array.isArray(written.steps)) {
180
+ return written.steps.flatMap((item, index) => {
181
+ const step = asRecord(item);
182
+ if (!step)
183
+ return [];
184
+ const detail = boundField(asString(step.detail));
185
+ return [
186
+ {
187
+ id: asString(step.id) ?? `step-${index + 1}`,
188
+ label: asString(step.label) ?? handlerLabel(handler ?? "preset"),
189
+ ok: asBoolean(step.ok),
190
+ detail: detail.text,
191
+ durationMs: asNumber(step.durationMs),
192
+ truncated: detail.truncated,
193
+ },
194
+ ];
195
+ });
196
+ }
197
+ const capability = await readJsonArtifact(repoRoot, dagRunId, [
198
+ nodeId,
199
+ "frontend-browser-capability.json",
200
+ ]);
201
+ if (capability && handler === "frontendBrowserToolPreflight") {
202
+ const detail = boundField(JSON.stringify(capability, null, 2));
203
+ return [
204
+ {
205
+ id: "capability",
206
+ label: handlerLabel(handler),
207
+ ok: asString(capability.status) !== "failed",
208
+ detail: detail.text,
209
+ durationMs: null,
210
+ truncated: detail.truncated,
211
+ },
212
+ ];
213
+ }
214
+ const attemptIndex = parsed?.kind === "attempt" ? parsed.index : 1;
215
+ const attempt = await readJsonArtifact(repoRoot, dagRunId, [
216
+ nodeId,
217
+ `attempt-${attemptIndex}.json`,
218
+ ]);
219
+ if (attempt) {
220
+ const ok = asBoolean(attempt.ok);
221
+ const detail = boundField(ok
222
+ ? (asString(attempt.stdout) ?? asString(attempt.assistantText))
223
+ : (asString(attempt.stderr) ?? asString(attempt.stdout)));
224
+ return [
225
+ {
226
+ id: "run",
227
+ label: handlerLabel(handler ?? "preset"),
228
+ ok,
229
+ detail: detail.text,
230
+ durationMs: asNumber(attempt.durationMs),
231
+ truncated: detail.truncated,
232
+ },
233
+ ];
234
+ }
235
+ return [];
236
+ }
237
+ export async function loadDagNodeProcess(repoRoot, dagRunId, nodeId, rawKey) {
238
+ if (!isSafeObservabilityIdentifier(dagRunId) ||
239
+ !isSafeObservabilityIdentifier(nodeId)) {
240
+ return {
241
+ status: 400,
242
+ body: { error: "Invalid dag run or node identifier", reason: "invalid-id" },
243
+ };
244
+ }
245
+ let parsedKey = null;
246
+ if (rawKey != null && String(rawKey).trim() !== "") {
247
+ const parsed = parseExecutionOutputKey(rawKey);
248
+ if (!parsed.ok) {
249
+ return {
250
+ status: 400,
251
+ body: {
252
+ error: "Invalid execution key (expected attempt-N or pass-N)",
253
+ reason: "invalid-key",
254
+ },
255
+ };
256
+ }
257
+ parsedKey = parsed;
258
+ }
259
+ const runResolved = resolveDagRunJson(repoRoot, dagRunId);
260
+ if (!runResolved.ok) {
261
+ if (runResolved.reason === "invalid-id") {
262
+ return {
263
+ status: 400,
264
+ body: { error: "Invalid dag run or node identifier", reason: "invalid-id" },
265
+ };
266
+ }
267
+ if (runResolved.reason === "ambiguous") {
268
+ return {
269
+ status: 409,
270
+ body: { error: "Ambiguous dag run lifecycle", reason: "ambiguous" },
271
+ };
272
+ }
273
+ return {
274
+ status: 404,
275
+ body: { error: "DAG run not found", reason: "not-found" },
276
+ };
277
+ }
278
+ let spec = {};
279
+ try {
280
+ const raw = JSON.parse(await readFile(runResolved.result.absolutePath, "utf8"));
281
+ spec = asRecord(raw) ?? {};
282
+ }
283
+ catch {
284
+ return {
285
+ status: 404,
286
+ body: { error: "run.json is unreadable", reason: "not-found" },
287
+ };
288
+ }
289
+ const tasks = Array.isArray(spec.tasks) ? spec.tasks : [];
290
+ const task = tasks
291
+ .map((item) => asRecord(item))
292
+ .find((item) => item && asString(item.id) === nodeId);
293
+ if (!task) {
294
+ return {
295
+ status: 200,
296
+ body: emptyBody(dagRunId, nodeId, "none", "not-executed"),
297
+ };
298
+ }
299
+ const classified = classifyKind(task);
300
+ const key = parsedKey?.key ?? null;
301
+ if (classified.kind === "pi-session") {
302
+ return {
303
+ status: 200,
304
+ body: emptyBody(dagRunId, nodeId, "pi-session", "pi-session", {
305
+ executor: classified.executor,
306
+ key,
307
+ }),
308
+ };
309
+ }
310
+ if (classified.kind === "static") {
311
+ return {
312
+ status: 200,
313
+ body: emptyBody(dagRunId, nodeId, "static", "static-no-process", {
314
+ executor: classified.executor,
315
+ key,
316
+ }),
317
+ };
318
+ }
319
+ if (classified.kind === "shell-commands" || classified.kind === "shell-preset") {
320
+ const commands = await loadShellCommands(repoRoot, dagRunId, nodeId, parsedKey);
321
+ if (commands && commands.length > 0) {
322
+ return {
323
+ status: 200,
324
+ body: {
325
+ schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
326
+ dagRunId,
327
+ nodeId,
328
+ kind: "shell-commands",
329
+ executor: classified.executor,
330
+ handler: classified.handler,
331
+ emptyReason: null,
332
+ key,
333
+ commands,
334
+ steps: [],
335
+ },
336
+ };
337
+ }
338
+ if (classified.handler || classified.kind === "shell-preset") {
339
+ const steps = await loadPresetSteps(repoRoot, dagRunId, nodeId, classified.handler, parsedKey);
340
+ if (steps.length > 0) {
341
+ return {
342
+ status: 200,
343
+ body: {
344
+ schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
345
+ dagRunId,
346
+ nodeId,
347
+ kind: "shell-preset",
348
+ executor: classified.executor,
349
+ handler: classified.handler,
350
+ emptyReason: null,
351
+ key,
352
+ commands: [],
353
+ steps,
354
+ },
355
+ };
356
+ }
357
+ }
358
+ return {
359
+ status: 200,
360
+ body: emptyBody(dagRunId, nodeId, classified.kind, classified.kind === "shell-preset"
361
+ ? "shell-preset-pending"
362
+ : "shell-receipt-missing", {
363
+ executor: classified.executor,
364
+ handler: classified.handler,
365
+ key,
366
+ }),
367
+ };
368
+ }
369
+ return {
370
+ status: 200,
371
+ body: emptyBody(dagRunId, nodeId, "none", "not-executed", {
372
+ executor: classified.executor,
373
+ handler: classified.handler,
374
+ key,
375
+ }),
376
+ };
377
+ }
@@ -16,6 +16,7 @@ import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview,
16
16
  import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
17
17
  import { buildDagNodeInput } from "./node-input.js";
18
18
  import { loadDagNodeExecutionOutput } from "./dag-node-execution-output.js";
19
+ import { loadDagNodeProcess } from "./node-process.js";
19
20
  import { buildObserveHealthV1 } from "./health.js";
20
21
  import { buildNightJobDetail, buildNightJobsDoctor, buildNightJobsList, buildNightJobsMorning, } from "./night-jobs.js";
21
22
  const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
@@ -100,6 +101,11 @@ export const ROUTES = [
100
101
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/execution-output$/,
101
102
  handler: handleDagNodeExecutionOutput,
102
103
  },
104
+ {
105
+ method: "GET",
106
+ pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/process$/,
107
+ handler: handleDagNodeProcess,
108
+ },
103
109
  {
104
110
  method: "GET",
105
111
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/spec-evidence$/,
@@ -570,6 +576,24 @@ async function handleDagNodeExecutionOutput(_req, res, match, ctx) {
570
576
  const result = await loadDagNodeExecutionOutput(ctx.repoRoot, dagRunId, nodeId, key);
571
577
  sendJson(res, result.status, result.body);
572
578
  }
579
+ async function handleDagNodeProcess(_req, res, match, ctx) {
580
+ const dagRunId = match.params.id;
581
+ const nodeId = match.params.sub;
582
+ if (!isSafeObservabilityIdentifier(dagRunId) ||
583
+ !isSafeObservabilityIdentifier(nodeId)) {
584
+ sendJson(res, 400, { error: "Invalid dag run or node identifier" });
585
+ return;
586
+ }
587
+ if (match.query.has("path") || match.query.has("artifactPath")) {
588
+ sendJson(res, 400, {
589
+ error: "Artifact path is not accepted; use optional key=attempt-N|pass-N",
590
+ reason: "path-not-allowed",
591
+ });
592
+ return;
593
+ }
594
+ const result = await loadDagNodeProcess(ctx.repoRoot, dagRunId, nodeId, match.query.get("key"));
595
+ sendJson(res, result.status, result.body);
596
+ }
573
597
  async function handleDagNodeSessionEvents(_req, res, match, ctx) {
574
598
  const dagRunId = match.params.id;
575
599
  const nodeId = match.params.sub;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Allowlist of in-process shell handlers (preset/gate flags on task.shell).
3
+ * Observe projects these names; the executor writes process receipts for them.
4
+ */
5
+ export const SHELL_NAMED_HANDLER_KEYS = [
6
+ "verdictGate",
7
+ "projectGovernanceGate",
8
+ "requirementCoverageGate",
9
+ "jsonArtifactGate",
10
+ "frontendPrewriteGate",
11
+ "frontendLintBaseline",
12
+ "frontendVerificationBundle",
13
+ "frontendReviewContext",
14
+ "frontendTestCaseChecklist",
15
+ "frontendBrowserToolPreflight",
16
+ "frontendTestStandardScenarios",
17
+ "frontendTestEnvironmentProbe",
18
+ "frontendTestEvidenceValidation",
19
+ "finalWriteSetApprovalGate",
20
+ "frontendTestL5Report",
21
+ "frontendTestHtmlReport",
22
+ "frontendTestCaseManifest",
23
+ "frontendTestResultFinalize",
24
+ "frontendTestReports",
25
+ "backendTestPipeline",
26
+ "jacocoCoverage",
27
+ "verifyEvidence",
28
+ "repairArtifactGate",
29
+ ];
30
+ export function namedShellHandlers(shell) {
31
+ if (!shell)
32
+ return [];
33
+ return SHELL_NAMED_HANDLER_KEYS.filter((key) => shell[key] != null);
34
+ }
@@ -77,6 +77,14 @@ export async function fetchDagNodeInput(runId, nodeId) {
77
77
  return fetchJsonResult(url);
78
78
  }
79
79
 
80
+ export async function fetchDagNodeProcess(runId, nodeId, key) {
81
+ const params = new URLSearchParams();
82
+ if (key) params.set("key", String(key));
83
+ const qs = params.toString();
84
+ const url = `/api/dag-runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}/process${qs ? `?${qs}` : ""}`;
85
+ return fetchJsonResult(url);
86
+ }
87
+
80
88
  /**
81
89
  * Fetch a spec-evidence file preview (binding source or successful read).
82
90
  * Resolves to the parsed JSON body, or throws an Error carrying the HTTP
@@ -53,7 +53,14 @@ export const UI_TEXT = {
53
53
  inputError: "加载节点输入失败。",
54
54
  inputFrozenLabel: "冻结 DAG 节点定义",
55
55
  inputFingerprintNote: "仅保存 assembled prompt 指纹,全文未持久化",
56
- noSessionEvents: "该节点暂无过程日志",
56
+ noSessionEvents: "该 Pi 节点暂无过程日志(可能尚未执行、走 CLI 路径,或为历史 run)",
57
+ noShellProcess: "该 shell 节点尚未留下命令回执。",
58
+ noPresetProcess: "该内部处理器尚未留下过程回执。",
59
+ staticNoProcess: "静态节点没有逐步执行过程,请查看节点输出。",
60
+ processLoading: "正在加载执行过程…",
61
+ processError: "加载执行过程失败。",
62
+ shellCommandsHeading: "Shell 命令",
63
+ shellPresetHeading: "内部处理器",
57
64
  noArtifacts: "暂无产物记录。",
58
65
  noActiveCommand: "当前没有正在执行的命令。",
59
66
  noFailures: "当前没有异常 Task。",
@@ -345,3 +345,26 @@ export function formatRepoPathDisplay(repoRoot) {
345
345
  .replace(/^\/Users\/[^/]+/, "~")
346
346
  .replace(/^\/home\/[^/]+/, "~");
347
347
  }
348
+
349
+ /**
350
+ * If `text` is a complete JSON object or array, return pretty-printed JSON.
351
+ * Primitives, JSONL, truncated/invalid JSON, and markdown return null so
352
+ * callers can fall back to Markdown rendering.
353
+ */
354
+ export function prettyJsonBlock(text) {
355
+ if (typeof text !== "string") return null;
356
+ const trimmed = text.trim();
357
+ if (trimmed.length < 2) return null;
358
+ const start = trimmed[0];
359
+ const end = trimmed[trimmed.length - 1];
360
+ if (!((start === "{" && end === "}") || (start === "[" && end === "]"))) {
361
+ return null;
362
+ }
363
+ try {
364
+ const value = JSON.parse(trimmed);
365
+ if (value === null || typeof value !== "object") return null;
366
+ return JSON.stringify(value, null, 2);
367
+ } catch {
368
+ return null;
369
+ }
370
+ }
@@ -1,6 +1,10 @@
1
1
  /** Shared safe Markdown → DOM rendering for Observe UI. */
2
2
  import { el } from "./dom.js";
3
- import { parseMarkdownBlocks, markdownLinkHref } from "./format.js";
3
+ import {
4
+ parseMarkdownBlocks,
5
+ markdownLinkHref,
6
+ prettyJsonBlock,
7
+ } from "./format.js";
4
8
 
5
9
  export function appendInlineMarkdown(target, text) {
6
10
  const tokenPattern = /(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
@@ -49,6 +53,21 @@ export function appendParagraphMarkdown(target, text) {
49
53
  }
50
54
  }
51
55
 
56
+ export function renderInspectorDocument(content) {
57
+ const pretty = prettyJsonBlock(content);
58
+ if (pretty != null) {
59
+ const article = el("article", "markdown-output json-output");
60
+ const pre = document.createElement("pre");
61
+ const code = document.createElement("code");
62
+ code.dataset.language = "json";
63
+ code.textContent = pretty;
64
+ pre.appendChild(code);
65
+ article.appendChild(pre);
66
+ return article;
67
+ }
68
+ return renderMarkdown(content);
69
+ }
70
+
52
71
  export function renderMarkdown(content) {
53
72
  const article = el("article", "markdown-output");
54
73
  for (const block of parseMarkdownBlocks(content)) {
@@ -436,6 +436,10 @@ export function mountOperatorChrome(root, config = {}) {
436
436
  const a = el("a", `och-mode-btn${btn.active ? " och-on" : ""}`);
437
437
  a.href = btn.href;
438
438
  a.dataset.mode = btn.id;
439
+ // 2026-08-22: inline-flex 布局下裸文本节点不参与 name-from-content,
440
+ // a11y snapshot 序列化为无名 link;显式 aria-label 保证「操作/观测」
441
+ // 对 find/读屏稳定可断言(frontend-test passed 权威依赖可见 find)。
442
+ a.setAttribute("aria-label", btn.label);
439
443
  if (btn.active) a.setAttribute("aria-current", "page");
440
444
  a.appendChild(makeIcon(btn.id === "operate" ? "zap" : "eye"));
441
445
  a.appendChild(document.createTextNode(btn.label));
@@ -61,6 +61,12 @@ export const uiState = {
61
61
  dagNodeInputData: null,
62
62
  dagNodeInputError: null,
63
63
  dagNodeInputLoading: false,
64
+ /** Node-process request identity ("<dagRunId>:<nodeId>:<key>#<seq>"). */
65
+ dagNodeProcessIdentity: null,
66
+ dagNodeProcessRequestSeq: 0,
67
+ dagNodeProcessData: null,
68
+ dagNodeProcessError: null,
69
+ dagNodeProcessLoading: false,
64
70
  /**
65
71
  * Execution-output request identity ("<dagRunId>:<nodeId>:<key>#<seq>").
66
72
  * Stale async responses are rejected when this no longer matches.
@@ -222,6 +228,11 @@ export function makeDagNodeInputIdentity(dagRunId, nodeId) {
222
228
  return `${dagRunId}:${nodeId}#${uiState.dagNodeInputRequestSeq}`;
223
229
  }
224
230
 
231
+ export function makeDagNodeProcessIdentity(dagRunId, nodeId, key) {
232
+ uiState.dagNodeProcessRequestSeq += 1;
233
+ return `${dagRunId}:${nodeId}:${key ?? "latest"}#${uiState.dagNodeProcessRequestSeq}`;
234
+ }
235
+
225
236
  /** Build a fresh execution-output identity token (includes key). */
226
237
  export function makeDagNodeExecutionOutputIdentity(dagRunId, nodeId, key) {
227
238
  uiState.dagNodeExecutionOutputRequestSeq += 1;
@@ -2111,6 +2111,9 @@ body.is-resizing-dag-graph {
2111
2111
  background: transparent;
2112
2112
  color: inherit;
2113
2113
  }
2114
+ .markdown-output.json-output pre {
2115
+ white-space: pre;
2116
+ }
2114
2117
  .markdown-output a {
2115
2118
  color: var(--orange-active);
2116
2119
  text-decoration-color: var(--orange);