@zhushanwen/pi-subagent-workflow 0.1.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/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Workflow Config Loader — 统一资源发现版(ADR-031)
3
+ *
4
+ * 扫描逻辑委托给 shared/resource-discovery(与 agent 发现共享同一套扫描源)。
5
+ * 本文件只保留 workflow 专属的 meta 提取(regex)+ 60s TTL 缓存。
6
+ *
7
+ * Failed imports are marked available=false — the loader never throws.
8
+ */
9
+
10
+ import { readFile } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+
13
+ // WorkflowMeta / WorkflowSource 的规范来源是 engine/models/workflow-script.ts
14
+ import type { WorkflowMeta, WorkflowSource } from "./models/workflow-script.ts";
15
+ export type { WorkflowMeta, WorkflowSource };
16
+
17
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
18
+
19
+ import {
20
+ discoverResources,
21
+ findWorkspaceRoot,
22
+ type ResourceSource,
23
+ type ScanConfig,
24
+ } from "../shared/resource-discovery.ts";
25
+
26
+ // ── Public types ──────────────────────────────────────────────
27
+
28
+ export interface CachedWorkflowMeta extends WorkflowMeta {
29
+ /** Absolute path to the script file */
30
+ path: string;
31
+ /** false when the script failed to load or has no valid meta export */
32
+ available: boolean;
33
+ /** Whether this is a saved (fixed) or temporary (ad-hoc) workflow */
34
+ source: WorkflowSource;
35
+ }
36
+
37
+ // ── Internal types ────────────────────────────────────────────
38
+
39
+ interface WorkerResult {
40
+ success: boolean;
41
+ meta?: WorkflowMeta;
42
+ error?: string;
43
+ }
44
+
45
+ interface CacheEntry {
46
+ meta: CachedWorkflowMeta;
47
+ cachedAt: number;
48
+ }
49
+
50
+ // ── Constants ─────────────────────────────────────────────────
51
+
52
+ const CACHE_TTL_MS = 60_000;
53
+
54
+ // ── Cache ─────────────────────────────────────────────────────
55
+
56
+ // Keyed by workspace root so that switching projects does not serve stale entries.
57
+ const cache = new Map<string, Map<string, CacheEntry>>();
58
+
59
+ function getCacheBucket(workspaceRoot: string): Map<string, CacheEntry> {
60
+ let bucket = cache.get(workspaceRoot);
61
+ if (!bucket) {
62
+ bucket = new Map<string, CacheEntry>();
63
+ cache.set(workspaceRoot, bucket);
64
+ }
65
+ return bucket;
66
+ }
67
+
68
+ function isCacheValid(entry: CacheEntry): boolean {
69
+ return Date.now() - entry.cachedAt < CACHE_TTL_MS;
70
+ }
71
+
72
+ // ── Helpers ───────────────────────────────────────────────────
73
+
74
+ /** Extract filename stem (no directory, no extension). */
75
+ function stem(filePath: string): string {
76
+ const base = filePath.split("/").pop() ?? filePath;
77
+ const dot = base.lastIndexOf(".");
78
+ return dot > 0 ? base.slice(0, dot) : base;
79
+ }
80
+
81
+ // ── Regex-based meta extraction ─────────────────────────────
82
+
83
+ /**
84
+ * Extract the `meta` object from a workflow script using regex.
85
+ *
86
+ * This avoids executing user code (no Worker/import/require), so it works
87
+ * regardless of whether the script uses CJS, ESM, top-level await, or
88
+ * references runtime globals like `agent` or `$ARGS`.
89
+ *
90
+ * Supports both `const meta = { ... }` and `export const meta = { ... }`.
91
+ */
92
+ async function extractMetaViaRegex(scriptPath: string): Promise<WorkerResult> {
93
+ try {
94
+ const content = await readFile(scriptPath, "utf-8");
95
+
96
+ const metaPattern = /(?:export\s+)?const\s+meta\s*=\s*(\{[^]*?\});?\s*$/m;
97
+ const match = metaPattern.exec(content);
98
+ if (!match) {
99
+ return { success: false, error: "No 'const meta = { ... }' declaration found" };
100
+ }
101
+
102
+ const metaObj = safeEvalObject(match[1]);
103
+ if (!metaObj || typeof metaObj !== "object") {
104
+ return { success: false, error: "Failed to parse meta object" };
105
+ }
106
+
107
+ if (typeof metaObj.name !== "string") {
108
+ return { success: false, error: "meta.name must be a string" };
109
+ }
110
+
111
+ return {
112
+ success: true,
113
+ meta: {
114
+ name: metaObj.name,
115
+ description: typeof metaObj.description === "string" ? metaObj.description : "",
116
+ phases: Array.isArray(metaObj.phases)
117
+ ? metaObj.phases.filter(
118
+ (p: unknown) => typeof p === "string" || (typeof p === "object" && p !== null && "title" in p),
119
+ ) as (string | { title: string; detail?: string })[]
120
+ : [],
121
+ },
122
+ };
123
+ } catch (err) {
124
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Safely evaluate a simple object literal string.
130
+ * Uses `new Function` to avoid eval while still supporting basic JS
131
+ * literal syntax (strings, numbers, arrays, nested objects).
132
+ */
133
+ function safeEvalObject(literal: string): Record<string, unknown> | undefined {
134
+ try {
135
+ const fn = new Function(`return (${literal});`);
136
+ const result = fn();
137
+ if (typeof result === "object" && result !== null && !Array.isArray(result)) {
138
+ return result as Record<string, unknown>;
139
+ }
140
+ return undefined;
141
+ } catch {
142
+ return undefined;
143
+ }
144
+ }
145
+
146
+ // ── ResourceSource → WorkflowSource 映射 ─────────────────────
147
+
148
+ /** 统一模块的 ResourceSource 映射为 workflow 的 saved/tmp 语义 */
149
+ function toWorkflowSource(source: ResourceSource): WorkflowSource {
150
+ return source === "project-pi-tmp" ? "tmp" : "saved";
151
+ }
152
+
153
+ // ── 单文件 → CachedWorkflowMeta ───────────────────────────────
154
+
155
+ /** 提取单个文件的 meta,失败时标 available=false(与原行为一致) */
156
+ async function toCachedMeta(
157
+ filePath: string,
158
+ source: ResourceSource,
159
+ ): Promise<CachedWorkflowMeta> {
160
+ const fallbackName = stem(filePath);
161
+ const result = await extractMetaViaRegex(filePath);
162
+ const wfSource = toWorkflowSource(source);
163
+
164
+ if (result.success && result.meta) {
165
+ return {
166
+ name: result.meta.name,
167
+ description: result.meta.description,
168
+ phases: result.meta.phases,
169
+ path: filePath,
170
+ available: true,
171
+ source: wfSource,
172
+ };
173
+ }
174
+
175
+ return {
176
+ name: fallbackName,
177
+ description: "",
178
+ phases: [],
179
+ path: filePath,
180
+ available: false,
181
+ source: wfSource,
182
+ };
183
+ }
184
+
185
+ // ── Public API ────────────────────────────────────────────────
186
+
187
+ /**
188
+ * workflow 发现的扫描配置。每个字段显式声明一个扫描源目录。
189
+ *
190
+ * 生产环境用 defaultScanConfig() 构造默认值(全局 ~/.pi/agent/* 目录)。
191
+ * 测试/隔离环境构造完整 config 指向 tmp 目录,完全不碰全局文件系统。
192
+ */
193
+ export interface WorkflowScanConfig {
194
+ /** 项目级脚本目录(workspaceRoot/.pi/workflows) */
195
+ projectDir: string;
196
+ /** user 级脚本目录(~/.pi/agent/workflows) */
197
+ userDir: string;
198
+ /** 临时脚本目录(workspaceRoot/.pi/workflows/.tmp) */
199
+ tmpDir: string;
200
+ /** npm 包扫描目录(~/.pi/agent/npm/node_modules 等) */
201
+ npmDirs: string[];
202
+ }
203
+
204
+ /**
205
+ * 把 WorkflowScanConfig 转为统一模块的 ScanConfig。
206
+ *
207
+ * 测试隔离场景下传入完整 config——此时按声明的 projectDir/tmpDir 反推
208
+ * workspaceRoot(与原行为一致:resolve(config.projectDir, "../.."))。
209
+ * 生产场景(省略或部分 config)走 findWorkspaceRoot(cwd)。
210
+ */
211
+ function toScanConfig(
212
+ configOrCwd: Partial<WorkflowScanConfig> & { cwd?: string } | undefined,
213
+ ): ScanConfig {
214
+ // 测试隔离:传入了 projectDir,直接反推 workspaceRoot
215
+ if (configOrCwd?.projectDir) {
216
+ const workspaceRoot = resolve(configOrCwd.projectDir, "../..");
217
+ return {
218
+ kind: "workflows",
219
+ workspaceRoot,
220
+ agentDir: "test-no-agent-dir",
221
+ includeTmp: true,
222
+ };
223
+ }
224
+
225
+ // 生产默认
226
+ const cwd = configOrCwd?.cwd;
227
+ const workspaceRoot = findWorkspaceRoot(cwd);
228
+ return {
229
+ kind: "workflows",
230
+ workspaceRoot,
231
+ agentDir: getAgentDir(),
232
+ includeTmp: true,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * 从指定 config 扫描所有 workflow 脚本,按 tmp>project>npm>user 优先级
238
+ * 去重,60s TTL 缓存(按 workspaceRoot 分桶)。
239
+ *
240
+ * 扫描逻辑委托给 shared/resource-discovery(与 agent 发现共享同一套扫描源)。
241
+ *
242
+ * Never throws. 解析失败的脚本以 available=false 返回。
243
+ *
244
+ * @param configOrCwd 完整 WorkflowScanConfig(隔离用)、部分字段(覆盖默认)、
245
+ * 或省略(纯生产默认)。可选 cwd 用于推导 workspaceRoot。
246
+ */
247
+ export async function discoverWorkflows(
248
+ configOrCwd?: Partial<WorkflowScanConfig> & { cwd?: string },
249
+ ): Promise<CachedWorkflowMeta[]> {
250
+ const scanConfig = toScanConfig(configOrCwd);
251
+ const workspaceRoot = scanConfig.workspaceRoot;
252
+
253
+ // 统一发现:返回已去重的资源列表(按优先级合并)
254
+ const resources = await discoverResources(scanConfig);
255
+
256
+ // 提取 meta(逐文件)
257
+ const mergedMap = new Map<string, CachedWorkflowMeta>();
258
+ for (const resource of resources) {
259
+ const cachedMeta = await toCachedMeta(resource.path, resource.source);
260
+ // available=false 的不覆盖已有的 available=true(与统一模块逻辑一致)
261
+ if (!cachedMeta.available && mergedMap.has(cachedMeta.name)) {
262
+ continue;
263
+ }
264
+ mergedMap.set(cachedMeta.name, cachedMeta);
265
+ }
266
+
267
+ const merged = Array.from(mergedMap.values());
268
+
269
+ // Update cache (scoped to current workspace root)
270
+ const bucket = getCacheBucket(workspaceRoot);
271
+ const now = Date.now();
272
+ for (const wf of merged) {
273
+ bucket.set(wf.name, { meta: wf, cachedAt: now });
274
+ }
275
+
276
+ return merged;
277
+ }
278
+
279
+ /**
280
+ * Load and cache all available workflow scripts from project-level
281
+ * (.pi/workflows/) and user-level (~/.pi/agent/workflows/) directories.
282
+ *
283
+ * discoverWorkflows() 的生产 preset——用全局默认目录。
284
+ *
285
+ * Never throws. Failed imports are returned with available=false.
286
+ */
287
+ export async function loadWorkflows(): Promise<CachedWorkflowMeta[]> {
288
+ return discoverWorkflows();
289
+ }
290
+
291
+ /**
292
+ * Get a specific workflow by name.
293
+ * Returns cached result if still valid, otherwise triggers a fresh load.
294
+ */
295
+ export async function getWorkflow(name: string): Promise<CachedWorkflowMeta | undefined> {
296
+ const workspaceRoot = findWorkspaceRoot();
297
+ const bucket = getCacheBucket(workspaceRoot);
298
+ const cached = bucket.get(name);
299
+ if (cached && isCacheValid(cached)) {
300
+ return cached.meta;
301
+ }
302
+
303
+ const workflows = await loadWorkflows();
304
+ return workflows.find((wf) => wf.name === name);
305
+ }
306
+
307
+ /**
308
+ * Invalidate the internal meta cache.
309
+ * The next call to loadWorkflows or getWorkflow will re-scan directories.
310
+ */
311
+ export function invalidateCache(): void {
312
+ cache.clear();
313
+ }