@tea-agent/loop-agent 0.40.0-next.9 → 0.40.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 (143) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -21
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/pi-sdk-executor.js +33 -5
  14. package/dist/executors/shell-executor.js +94 -20
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/shared/operator/capabilities.js +9 -1
  17. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  18. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  19. package/dist/shared/pi-context-pressure/env.js +58 -0
  20. package/dist/shared/pi-context-pressure/extension.js +100 -0
  21. package/dist/shared/pi-context-pressure/index.js +7 -0
  22. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  23. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  24. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  25. package/dist/worker/console/chat/assistant-content.js +7 -0
  26. package/dist/worker/console/chat/chat-event-store.js +82 -11
  27. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  28. package/dist/worker/console/chat/goals.js +300 -0
  29. package/dist/worker/console/chat/pi-runtime.js +285 -8
  30. package/dist/worker/console/chat/provider-error.js +2 -1
  31. package/dist/worker/console/chat/repo-browser.js +94 -12
  32. package/dist/worker/console/chat/routes.js +495 -30
  33. package/dist/worker/console/chat/session-catalog.js +48 -0
  34. package/dist/worker/console/chat/session-stats.js +96 -0
  35. package/dist/worker/console/chat/session-store.js +27 -18
  36. package/dist/worker/console/chat/shortcuts.js +7 -0
  37. package/dist/worker/console/chat/sift-bridge.js +1 -0
  38. package/dist/worker/console/chat/turn-process.js +42 -5
  39. package/dist/worker/console/dag-execution-receipt.js +20 -2
  40. package/dist/worker/console/operator-actions.js +20 -5
  41. package/dist/worker/console/operator-selection.js +6 -2
  42. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  43. package/dist/worker/console/pi-plugins.js +179 -0
  44. package/dist/worker/console/routes.js +246 -6
  45. package/dist/worker/console/server.js +173 -42
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-M25fNMTE.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
  78. package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
  79. package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-CG8D9fuL.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-BuB6P2rX.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  105. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  106. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  107. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  108. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  109. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  110. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  113. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  114. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  115. package/dist/worker/console/workspace-context.js +28 -3
  116. package/dist/worker/console/workspace-registry.js +333 -60
  117. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  118. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  119. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  120. package/dist/worker/observe/static/operator-chrome.css +23 -4
  121. package/dist/worker/observe/static/operator-chrome.js +7 -2
  122. package/dist/worker/observe/static/styles.css +87 -25
  123. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  124. package/dist/worker/observe/static/views/dags.js +7 -5
  125. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  127. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  128. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  129. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  130. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  131. package/dist/workflows/dag/init-hybrid.js +5 -4
  132. package/docs/architecture/runtime-boundaries.md +2 -1
  133. package/docs/templates/backend-test-dag.json +6 -5
  134. package/docs/templates/init-managed-agents.md +1 -1
  135. package/package.json +3 -2
  136. package/skills/loop-agent/references/command-reference.md +15 -1
  137. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  138. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  139. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  140. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  141. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  142. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  143. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -0,0 +1,179 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import catalogData from "./pi-plugins.catalog.json" with { type: "json" };
6
+ export function getPiAgentDir() {
7
+ const envDir = process.env.PI_CODING_AGENT_DIR;
8
+ return envDir ? envDir : path.join(os.homedir(), ".pi", "agent");
9
+ }
10
+ function npmRoot() {
11
+ return path.join(getPiAgentDir(), "npm");
12
+ }
13
+ function nodeModulesRoot() {
14
+ return path.join(npmRoot(), "node_modules");
15
+ }
16
+ function settingsPath() {
17
+ return path.join(getPiAgentDir(), "settings.json");
18
+ }
19
+ function readSettings() {
20
+ try {
21
+ if (!existsSync(settingsPath()))
22
+ return {};
23
+ return JSON.parse(readFileSync(settingsPath(), "utf8"));
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ function packageNameFromSpec(spec) {
30
+ const base = spec.startsWith("npm:") ? spec.slice(4) : spec;
31
+ // 剥掉尾部 @<version>(如 pi-lens@3.8.72 → pi-lens),但保留 scoped 包名开头的 @
32
+ // (如 @scope/pkg → @scope/pkg,@scope/pkg@1.0.0 → @scope/pkg)。
33
+ const at = base.lastIndexOf("@");
34
+ if (at > 0)
35
+ return base.slice(0, at);
36
+ return base;
37
+ }
38
+ function listInstalledPackageNames() {
39
+ const settings = readSettings();
40
+ return (settings.packages ?? []).map(packageNameFromSpec);
41
+ }
42
+ function isPackageInstalledOnDisk(pkg) {
43
+ return existsSync(path.join(nodeModulesRoot(), ...pkg.split("/")));
44
+ }
45
+ /** 读取已安装包的实际版本(node_modules 里 package.json 的 version)。 */
46
+ function readInstalledVersion(pkg) {
47
+ try {
48
+ const manifest = JSON.parse(readFileSync(path.join(nodeModulesRoot(), ...pkg.split("/"), "package.json"), "utf8"));
49
+ return typeof manifest.version === "string" ? manifest.version : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ /** 目录 + 每个插件的已装状态(settings.packages 声明 + node_modules 存在)+ 已装版本。 */
56
+ export function listPiPlugins() {
57
+ const installed = new Set(listInstalledPackageNames());
58
+ const plugins = catalogData.plugins.map((p) => {
59
+ const isInstalled = installed.has(p.name) && isPackageInstalledOnDisk(p.name);
60
+ return {
61
+ ...p,
62
+ installed: isInstalled,
63
+ installedVersion: isInstalled ? readInstalledVersion(p.name) : undefined,
64
+ };
65
+ });
66
+ return { summary: catalogData.summary, plugins };
67
+ }
68
+ let cachedRegistry;
69
+ /** 读取 npm 配置的 registry(`npm config get registry`),失败回退官方源。 */
70
+ function getNpmRegistry() {
71
+ if (cachedRegistry)
72
+ return Promise.resolve(cachedRegistry);
73
+ return new Promise((resolve) => {
74
+ const fallback = () => {
75
+ cachedRegistry = "https://registry.npmjs.org/";
76
+ resolve(cachedRegistry);
77
+ };
78
+ try {
79
+ const child = spawn("npm", ["config", "get", "registry"], {
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ cwd: getPiAgentDir(),
82
+ shell: process.platform === "win32",
83
+ });
84
+ let out = "";
85
+ child.stdout.on("data", (d) => (out += d.toString("utf8")));
86
+ child.on("close", () => {
87
+ const reg = out.trim();
88
+ cachedRegistry = reg || "https://registry.npmjs.org/";
89
+ resolve(cachedRegistry);
90
+ });
91
+ child.on("error", fallback);
92
+ }
93
+ catch {
94
+ fallback();
95
+ }
96
+ });
97
+ }
98
+ /** 从 npm 配置的仓库拉取某包的全部已发布版本 + latest。 */
99
+ export async function listPiPluginVersions(name) {
100
+ const registry = (await getNpmRegistry()).replace(/\/+$/, "");
101
+ const url = `${registry}/${encodeURIComponent(name).replace(/%2F/g, "/")}`;
102
+ try {
103
+ const res = await fetch(url, {
104
+ headers: { accept: "application/json", "user-agent": "loop-agent/1.0" },
105
+ });
106
+ if (!res.ok)
107
+ return { ok: false, error: `npm registry HTTP ${res.status}` };
108
+ const body = (await res.json());
109
+ const versions = body.versions && typeof body.versions === "object"
110
+ ? Object.keys(body.versions).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
111
+ : [];
112
+ const latest = body["dist-tags"]?.latest;
113
+ return {
114
+ ok: true,
115
+ versions,
116
+ latest: typeof latest === "string" ? latest : undefined,
117
+ };
118
+ }
119
+ catch (error) {
120
+ return {
121
+ ok: false,
122
+ error: error instanceof Error ? error.message : String(error),
123
+ };
124
+ }
125
+ }
126
+ function runPi(args) {
127
+ return new Promise((resolve) => {
128
+ mkdirSync(getPiAgentDir(), { recursive: true });
129
+ // Windows 上 pi 是 pi.cmd(无 .exe),.cmd/.bat 只能 shell:true 解析。
130
+ const child = spawn("pi", args, {
131
+ stdio: ["ignore", "pipe", "pipe"],
132
+ cwd: getPiAgentDir(),
133
+ shell: process.platform === "win32",
134
+ });
135
+ let stdout = "";
136
+ let stderr = "";
137
+ child.stdout.on("data", (d) => (stdout += d.toString("utf8")));
138
+ child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
139
+ child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
140
+ child.on("error", (err) => resolve({ code: -1, stdout, stderr: String(err?.message ?? err) }));
141
+ });
142
+ }
143
+ /** 校验安装目标是否在目录中,且该包确实声明了 pi.extensions(可安装为 Pi 扩展)。 */
144
+ export function findCatalogPlugin(name) {
145
+ return catalogData.plugins.find((p) => p.name === name);
146
+ }
147
+ /** 全局(用户级 ~/.pi/agent)安装:调用 `pi install npm:<name>`(可带版本,缺省为最新)。 */
148
+ export async function installPiPlugin(name, version) {
149
+ const entry = findCatalogPlugin(name);
150
+ if (!entry) {
151
+ return { ok: false, error: `包 ${name} 不在插件目录中` };
152
+ }
153
+ if (!entry.valid) {
154
+ return {
155
+ ok: false,
156
+ error: `包 ${name} 未声明 pi.extensions,无法作为 Pi 扩展使用`,
157
+ };
158
+ }
159
+ const spec = version && version.trim() ? `npm:${name}@${version.trim()}` : `npm:${name}`;
160
+ const result = await runPi(["install", spec]);
161
+ if (result.code !== 0) {
162
+ return {
163
+ ok: false,
164
+ error: `pi install 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
165
+ };
166
+ }
167
+ return { ok: true };
168
+ }
169
+ /** 全局卸载:调用 `pi remove npm:<name>`。 */
170
+ export async function uninstallPiPlugin(name) {
171
+ const result = await runPi(["remove", `npm:${name}`]);
172
+ if (result.code !== 0) {
173
+ return {
174
+ ok: false,
175
+ error: `pi remove 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
176
+ };
177
+ }
178
+ return { ok: true };
179
+ }
@@ -8,6 +8,7 @@ import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
8
8
  import { ROUTES } from "../observe/routes.js";
9
9
  import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
10
10
  import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
11
+ import { installPiPlugin, listPiPluginVersions, listPiPlugins, uninstallPiPlugin, } from "./pi-plugins.js";
11
12
  import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
12
13
  export function sendJson(res, status, body, extraHeaders) {
13
14
  const payload = JSON.stringify(body);
@@ -462,6 +463,95 @@ async function handleNightAuxTickerPost(req, res, ctx) {
462
463
  const status = ticker.setEnabled(body.enabled);
463
464
  sendJson(res, 200, { ok: true, ticker: status });
464
465
  }
466
+ async function handlePiPluginInstall(req, res) {
467
+ let body;
468
+ try {
469
+ body = (await readJsonBody(req));
470
+ }
471
+ catch (error) {
472
+ sendJson(res, 400, {
473
+ ok: false,
474
+ error: {
475
+ code: "INVALID_INPUT",
476
+ message: error instanceof Error ? error.message : String(error),
477
+ },
478
+ });
479
+ return;
480
+ }
481
+ const name = typeof body.name === "string" ? body.name.trim() : "";
482
+ if (!name) {
483
+ sendJson(res, 400, {
484
+ ok: false,
485
+ error: { code: "INVALID_INPUT", message: "missing package name" },
486
+ });
487
+ return;
488
+ }
489
+ const version = typeof body.version === "string" ? body.version.trim() : undefined;
490
+ try {
491
+ const result = await installPiPlugin(name, version);
492
+ if (!result.ok) {
493
+ sendJson(res, 400, {
494
+ ok: false,
495
+ error: { code: "INSTALL_FAILED", message: result.error },
496
+ });
497
+ return;
498
+ }
499
+ sendJson(res, 200, { ok: true });
500
+ }
501
+ catch (error) {
502
+ sendJson(res, 500, {
503
+ ok: false,
504
+ error: {
505
+ code: "INTERNAL_ERROR",
506
+ message: error instanceof Error ? error.message : String(error),
507
+ },
508
+ });
509
+ }
510
+ }
511
+ async function handlePiPluginUninstall(req, res) {
512
+ let body;
513
+ try {
514
+ body = (await readJsonBody(req));
515
+ }
516
+ catch (error) {
517
+ sendJson(res, 400, {
518
+ ok: false,
519
+ error: {
520
+ code: "INVALID_INPUT",
521
+ message: error instanceof Error ? error.message : String(error),
522
+ },
523
+ });
524
+ return;
525
+ }
526
+ const name = typeof body.name === "string" ? body.name.trim() : "";
527
+ if (!name) {
528
+ sendJson(res, 400, {
529
+ ok: false,
530
+ error: { code: "INVALID_INPUT", message: "missing package name" },
531
+ });
532
+ return;
533
+ }
534
+ try {
535
+ const result = await uninstallPiPlugin(name);
536
+ if (!result.ok) {
537
+ sendJson(res, 400, {
538
+ ok: false,
539
+ error: { code: "UNINSTALL_FAILED", message: result.error },
540
+ });
541
+ return;
542
+ }
543
+ sendJson(res, 200, { ok: true });
544
+ }
545
+ catch (error) {
546
+ sendJson(res, 500, {
547
+ ok: false,
548
+ error: {
549
+ code: "INTERNAL_ERROR",
550
+ message: error instanceof Error ? error.message : String(error),
551
+ },
552
+ });
553
+ }
554
+ }
465
555
  export async function handleConsoleOperatorRequest(req, res, ctx) {
466
556
  const method = (req.method ?? "GET").toUpperCase();
467
557
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -495,6 +585,34 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
495
585
  await handleNightAuxTickerGet(req, res, ctx);
496
586
  return true;
497
587
  }
588
+ if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
589
+ sendJson(res, 200, { ok: true, data: listPiPlugins() });
590
+ return true;
591
+ }
592
+ if (method === "GET" &&
593
+ pathname === "/api/operator/v1/pi-plugins/versions") {
594
+ const name = url.searchParams.get("name")?.trim() ?? "";
595
+ if (!name) {
596
+ sendJson(res, 400, {
597
+ ok: false,
598
+ error: { code: "INVALID_INPUT", message: "missing name" },
599
+ });
600
+ return true;
601
+ }
602
+ const result = await listPiPluginVersions(name);
603
+ if (!result.ok) {
604
+ sendJson(res, 400, {
605
+ ok: false,
606
+ error: { code: "FETCH_FAILED", message: result.error },
607
+ });
608
+ return true;
609
+ }
610
+ sendJson(res, 200, {
611
+ ok: true,
612
+ data: { versions: result.versions, latest: result.latest },
613
+ });
614
+ return true;
615
+ }
498
616
  const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
499
617
  if (opMatch) {
500
618
  const operationId = decodeURIComponent(opMatch[1]);
@@ -526,6 +644,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
526
644
  await handleNightAuxTickerPost(req, res, ctx);
527
645
  return true;
528
646
  }
647
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
648
+ await handlePiPluginInstall(req, res);
649
+ return true;
650
+ }
651
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
652
+ await handlePiPluginUninstall(req, res);
653
+ return true;
654
+ }
529
655
  // Allow other operator POSTs only under known routes; unknown → 404
530
656
  sendJson(res, 404, {
531
657
  ok: false,
@@ -587,9 +713,11 @@ function pendingReadinessSummary(inspectReady) {
587
713
  * POST /api/operator/v1/workspaces/pick — host native folder chooser
588
714
  * POST /api/operator/v1/workspaces — body { "name": "<label>",
589
715
  * "directoryPath": "<dir>" }
716
+ * PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
717
+ * "<id>" | null }
590
718
  * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
591
719
  *
592
- * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
720
+ * POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
593
721
  * workspace selection parameter.
594
722
  *
595
723
  * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
@@ -598,8 +726,53 @@ function pendingReadinessSummary(inspectReady) {
598
726
  * requests. A hanging probe can never delay the 200 list.
599
727
  */
600
728
  export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
729
+ if (method === "GET" &&
730
+ pathname === "/api/operator/v1/workspaces/events") {
731
+ res.writeHead(200, {
732
+ "Content-Type": "text/event-stream; charset=utf-8",
733
+ "Cache-Control": "no-cache, no-transform",
734
+ Connection: "keep-alive",
735
+ "X-Accel-Buffering": "no",
736
+ });
737
+ const sendRevision = (event, revision) => {
738
+ if (res.destroyed || res.writableEnded)
739
+ return;
740
+ res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
741
+ };
742
+ let baselineSent = false;
743
+ let pendingRevision = 0;
744
+ const unsubscribe = registry.subscribe((revision) => {
745
+ if (!baselineSent)
746
+ pendingRevision = Math.max(pendingRevision, revision);
747
+ else
748
+ sendRevision("workspace_registry_changed", revision);
749
+ });
750
+ const initial = await registry.snapshotVisible();
751
+ sendRevision("workspace_registry_ready", initial.revision);
752
+ baselineSent = true;
753
+ if (pendingRevision > initial.revision) {
754
+ sendRevision("workspace_registry_changed", pendingRevision);
755
+ }
756
+ const heartbeat = setInterval(() => {
757
+ if (!res.destroyed && !res.writableEnded)
758
+ res.write(": keepalive\n\n");
759
+ }, 15_000);
760
+ heartbeat.unref();
761
+ let closed = false;
762
+ const close = () => {
763
+ if (closed)
764
+ return;
765
+ closed = true;
766
+ clearInterval(heartbeat);
767
+ unsubscribe();
768
+ };
769
+ req.once("aborted", close);
770
+ res.once("close", close);
771
+ return;
772
+ }
601
773
  if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
602
- const records = await registry.listVisible();
774
+ const snapshot = await registry.snapshotVisible();
775
+ const records = snapshot.workspaces;
603
776
  const inspectReady = readinessDeps.inspectReady;
604
777
  const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
605
778
  const workspaces = records.map((record) => {
@@ -611,6 +784,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
611
784
  repoFingerprint: record.repoFingerprint,
612
785
  createdAt: record.createdAt,
613
786
  lastAccessedAt: record.lastAccessedAt,
787
+ sessionIds: record.sessionIds,
614
788
  readiness: cached
615
789
  ? {
616
790
  state: cached.state,
@@ -622,7 +796,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
622
796
  // Respond FIRST, then refresh readiness in the background (AC-006).
623
797
  sendJson(res, 200, {
624
798
  ok: true,
625
- schemaVersion: 1,
799
+ schemaVersion: 2,
800
+ revision: snapshot.revision,
626
801
  workspaces,
627
802
  });
628
803
  void Promise.all(records.map(async (record) => {
@@ -789,8 +964,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
789
964
  });
790
965
  return;
791
966
  }
792
- const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
793
- if (method === "DELETE" && removeMatch) {
967
+ const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
968
+ if (method === "PATCH" && workspaceMatch) {
969
+ const failure = evaluateMutationGate(req, {
970
+ cookieName: deps.bootToken.cookieName,
971
+ bootToken: deps.bootToken.value,
972
+ confirmationToken: deps.bootToken.confirmationToken,
973
+ consoleOrigin: deps.getConsoleOrigin(),
974
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
975
+ });
976
+ if (failure) {
977
+ sendJson(res, failure.status, {
978
+ ok: false,
979
+ error: { code: failure.code, message: failure.message },
980
+ });
981
+ return;
982
+ }
983
+ let body;
984
+ try {
985
+ body = (await readJsonBody(req));
986
+ }
987
+ catch (error) {
988
+ sendJson(res, 400, {
989
+ ok: false,
990
+ error: {
991
+ code: "INVALID_INPUT",
992
+ message: error instanceof Error ? error.message : String(error),
993
+ },
994
+ });
995
+ return;
996
+ }
997
+ if (body.beforeWorkspaceId !== null &&
998
+ typeof body.beforeWorkspaceId !== "string") {
999
+ sendJson(res, 400, {
1000
+ ok: false,
1001
+ error: {
1002
+ code: "INVALID_INPUT",
1003
+ message: "beforeWorkspaceId must be a workspace id or null",
1004
+ },
1005
+ });
1006
+ return;
1007
+ }
1008
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
1009
+ const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
1010
+ ? body.beforeWorkspaceId
1011
+ : null;
1012
+ const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
1013
+ if (!reordered.ok) {
1014
+ sendJson(res, 404, {
1015
+ ok: false,
1016
+ error: {
1017
+ code: reordered.code,
1018
+ message: reordered.code === "ANCHOR_NOT_FOUND"
1019
+ ? `unknown workspace anchor: ${beforeWorkspaceId}`
1020
+ : `unknown workspace: ${workspaceId}`,
1021
+ },
1022
+ });
1023
+ return;
1024
+ }
1025
+ sendJson(res, 200, {
1026
+ ok: true,
1027
+ workspaceId,
1028
+ beforeWorkspaceId,
1029
+ changed: reordered.changed,
1030
+ });
1031
+ return;
1032
+ }
1033
+ if (method === "DELETE" && workspaceMatch) {
794
1034
  const failure = evaluateMutationGate(req, {
795
1035
  cookieName: deps.bootToken.cookieName,
796
1036
  bootToken: deps.bootToken.value,
@@ -805,7 +1045,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
805
1045
  });
806
1046
  return;
807
1047
  }
808
- const workspaceId = decodeURIComponent(removeMatch[1]);
1048
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
809
1049
  const removed = await registry.remove(workspaceId);
810
1050
  if (!removed) {
811
1051
  sendJson(res, 404, {