@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4

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 (206) hide show
  1. package/docs/http-api.md +80 -4
  2. package/package.json +1 -4
  3. package/src/api/__tests__/index.test.ts +443 -32
  4. package/src/api/index.ts +108 -127
  5. package/src/cli/__tests__/analyze-task.test.ts +40 -7
  6. package/src/cli/__tests__/index.test.ts +337 -17
  7. package/src/cli/__tests__/types.test.ts +0 -18
  8. package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
  9. package/src/cli/analyze-task.ts +3 -14
  10. package/src/cli/index.ts +73 -61
  11. package/src/cli/types.ts +0 -13
  12. package/src/cli/update-pipeline-json.ts +3 -14
  13. package/src/config/__tests__/paths.test.ts +46 -1
  14. package/src/config/__tests__/pipeline-registry.test.ts +767 -0
  15. package/src/config/__tests__/sse-events.test.ts +470 -0
  16. package/src/config/__tests__/statuses.test.ts +41 -0
  17. package/src/config/paths.ts +11 -2
  18. package/src/config/pipeline-registry.ts +258 -0
  19. package/src/config/sse-events.ts +122 -0
  20. package/src/config/statuses.ts +20 -1
  21. package/src/core/__tests__/agent-step.test.ts +115 -0
  22. package/src/core/__tests__/config.test.ts +545 -105
  23. package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
  24. package/src/core/__tests__/job-concurrency.test.ts +260 -0
  25. package/src/core/__tests__/job-submission.test.ts +396 -0
  26. package/src/core/__tests__/job-view.test.ts +1341 -0
  27. package/src/core/__tests__/json-file.test.ts +111 -0
  28. package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
  29. package/src/core/__tests__/logger.test.ts +22 -0
  30. package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
  31. package/src/core/__tests__/orchestrator.test.ts +615 -2
  32. package/src/core/__tests__/pipeline-runner.test.ts +946 -9
  33. package/src/core/__tests__/redact.test.ts +153 -0
  34. package/src/core/__tests__/runner-liveness.test.ts +910 -0
  35. package/src/core/__tests__/seed-naming.test.ts +57 -0
  36. package/src/core/__tests__/single-derivation.test.ts +159 -0
  37. package/src/core/__tests__/task-runner.test.ts +594 -3
  38. package/src/core/__tests__/task-telemetry.test.ts +241 -0
  39. package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
  40. package/src/core/agent-step.ts +4 -1
  41. package/src/core/agent-types.ts +5 -4
  42. package/src/core/config.ts +134 -222
  43. package/src/core/control.ts +3 -0
  44. package/src/core/job-concurrency.ts +56 -20
  45. package/src/core/job-submission.ts +133 -0
  46. package/src/core/job-view.ts +473 -0
  47. package/src/core/json-file.ts +45 -0
  48. package/src/core/lifecycle-policy.ts +23 -8
  49. package/src/core/logger.ts +0 -29
  50. package/src/core/orchestrator.ts +200 -74
  51. package/src/core/pipeline-runner.ts +85 -53
  52. package/src/core/redact.ts +40 -0
  53. package/src/core/runner-liveness.ts +280 -0
  54. package/src/core/seed-naming.ts +9 -0
  55. package/src/core/status-writer.ts +27 -33
  56. package/src/core/task-runner.ts +356 -319
  57. package/src/core/task-telemetry.ts +107 -0
  58. package/src/harness/__tests__/subprocess.test.ts +112 -1
  59. package/src/harness/subprocess.ts +55 -16
  60. package/src/llm/__tests__/index.test.ts +684 -33
  61. package/src/llm/index.ts +310 -67
  62. package/src/providers/__tests__/alibaba.test.ts +67 -6
  63. package/src/providers/__tests__/anthropic.test.ts +35 -14
  64. package/src/providers/__tests__/base.test.ts +62 -0
  65. package/src/providers/__tests__/claude-code.test.ts +99 -14
  66. package/src/providers/__tests__/deepseek.test.ts +16 -6
  67. package/src/providers/__tests__/gemini.test.ts +47 -25
  68. package/src/providers/__tests__/moonshot.test.ts +27 -0
  69. package/src/providers/__tests__/openai.test.ts +262 -74
  70. package/src/providers/__tests__/opencode.test.ts +77 -0
  71. package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
  72. package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
  73. package/src/providers/__tests__/types.test.ts +85 -0
  74. package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
  75. package/src/providers/__tests__/zhipu.test.ts +27 -14
  76. package/src/providers/alibaba.ts +20 -39
  77. package/src/providers/anthropic.ts +23 -11
  78. package/src/providers/base.ts +19 -0
  79. package/src/providers/claude-code.ts +27 -18
  80. package/src/providers/deepseek.ts +9 -28
  81. package/src/providers/gemini.ts +20 -58
  82. package/src/providers/moonshot.ts +15 -11
  83. package/src/providers/openai.ts +79 -61
  84. package/src/providers/opencode.ts +16 -33
  85. package/src/providers/stream-accumulator.ts +27 -21
  86. package/src/providers/types.ts +29 -4
  87. package/src/providers/zhipu.ts +15 -44
  88. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  89. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
  90. package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
  91. package/src/task-analysis/__tests__/repository.test.ts +65 -0
  92. package/src/task-analysis/__tests__/types.test.ts +63 -130
  93. package/src/task-analysis/analyzer.ts +91 -0
  94. package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
  95. package/src/task-analysis/index.ts +2 -36
  96. package/src/task-analysis/repository.ts +45 -0
  97. package/src/task-analysis/types.ts +42 -58
  98. package/src/ui/client/__tests__/api.test.ts +143 -1
  99. package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
  100. package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
  101. package/src/ui/client/__tests__/load-state.test.ts +70 -0
  102. package/src/ui/client/__tests__/types.test.ts +66 -3
  103. package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
  104. package/src/ui/client/__tests__/useJobList.test.ts +198 -23
  105. package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
  106. package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
  107. package/src/ui/client/adapters/job-adapter.ts +41 -16
  108. package/src/ui/client/api.ts +38 -15
  109. package/src/ui/client/bootstrap.ts +19 -14
  110. package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
  111. package/src/ui/client/hooks/useJobList.ts +26 -31
  112. package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
  113. package/src/ui/client/load-state.ts +20 -0
  114. package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
  115. package/src/ui/client/reducers/job-events.ts +137 -0
  116. package/src/ui/client/types.ts +16 -20
  117. package/src/ui/components/DAGGrid.tsx +6 -1
  118. package/src/ui/components/JobDetail.tsx +12 -4
  119. package/src/ui/components/JobTable.tsx +41 -13
  120. package/src/ui/components/StageTimeline.tsx +8 -20
  121. package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
  122. package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
  123. package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
  124. package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
  125. package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
  126. package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
  127. package/src/ui/components/types.ts +35 -15
  128. package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
  129. package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
  130. package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/pages/PipelineDetail.tsx +60 -4
  133. package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
  134. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
  135. package/src/ui/pages/__tests__/pages.test.tsx +236 -42
  136. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
  137. package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
  138. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  139. package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
  140. package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
  141. package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
  142. package/src/ui/server/__tests__/index.test.ts +63 -0
  143. package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
  144. package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
  145. package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
  146. package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
  147. package/src/ui/server/__tests__/router-threading.test.ts +148 -0
  148. package/src/ui/server/__tests__/router.test.ts +104 -0
  149. package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
  150. package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
  151. package/src/ui/server/config-bridge-node.ts +8 -26
  152. package/src/ui/server/config-bridge.ts +3 -4
  153. package/src/ui/server/embedded-assets-imports.d.ts +44 -0
  154. package/src/ui/server/embedded-assets.ts +13 -2
  155. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  156. package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
  157. package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
  158. package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
  159. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
  160. package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
  161. package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
  162. package/src/ui/server/endpoints/file-endpoints.ts +15 -10
  163. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  164. package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
  165. package/src/ui/server/endpoints/job-endpoints.ts +7 -0
  166. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  167. package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
  168. package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
  169. package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
  170. package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
  171. package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
  172. package/src/ui/server/index.ts +19 -14
  173. package/src/ui/server/job-reader.ts +14 -2
  174. package/src/ui/server/router.ts +33 -10
  175. package/src/ui/server/sse-broadcast.ts +14 -3
  176. package/src/ui/server/sse-enhancer.ts +12 -2
  177. package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
  178. package/src/ui/state/__tests__/snapshot.test.ts +120 -14
  179. package/src/ui/state/__tests__/types.test.ts +104 -5
  180. package/src/ui/state/snapshot.ts +2 -2
  181. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
  182. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
  183. package/src/ui/state/transformers/list-transformer.ts +13 -3
  184. package/src/ui/state/transformers/status-transformer.ts +36 -170
  185. package/src/ui/state/types.ts +15 -48
  186. package/src/utils/__tests__/path-containment.test.ts +160 -0
  187. package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
  188. package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
  189. package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
  190. package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
  191. package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
  192. package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
  193. package/src/task-analysis/__tests__/index.test.ts +0 -143
  194. package/src/task-analysis/__tests__/parser.test.ts +0 -41
  195. package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
  196. package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
  197. package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
  198. package/src/task-analysis/extractors/artifacts.ts +0 -143
  199. package/src/task-analysis/extractors/llm-calls.ts +0 -117
  200. package/src/task-analysis/extractors/stages.ts +0 -45
  201. package/src/task-analysis/parser.ts +0 -20
  202. package/src/task-analysis/utils/ast.ts +0 -45
  203. package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
  204. package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
  205. package/src/ui/embedded-assets.js +0 -12
  206. package/src/ui/server/__tests__/path-containment.test.ts +0 -54
@@ -0,0 +1,241 @@
1
+ import { afterEach, describe, test, expect } from "bun:test";
2
+ import { EventEmitter } from "node:events";
3
+ import { rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { withConsoleCapture, withTaskTelemetry } from "../task-telemetry";
7
+
8
+ const COMPLETE = "llm:request:complete";
9
+ const ERROR = "llm:request:error";
10
+
11
+ function makeHandlers() {
12
+ return {
13
+ onComplete: (_m: Record<string, unknown>) => {},
14
+ onError: (_m: Record<string, unknown>) => {},
15
+ };
16
+ }
17
+
18
+ describe("withTaskTelemetry", () => {
19
+ test("normal return removes both listeners and awaits flush", async () => {
20
+ const events = new EventEmitter();
21
+ const handlers = makeHandlers();
22
+ let flushed = 0;
23
+ const flush = async () => {
24
+ flushed += 1;
25
+ };
26
+
27
+ const result = await withTaskTelemetry(events, handlers, flush, async () => {
28
+ expect(events.listenerCount(COMPLETE)).toBe(1);
29
+ expect(events.listenerCount(ERROR)).toBe(1);
30
+ return "value";
31
+ });
32
+
33
+ expect(result).toBe("value");
34
+ expect(events.listenerCount(COMPLETE)).toBe(0);
35
+ expect(events.listenerCount(ERROR)).toBe(0);
36
+ expect(flushed).toBe(1);
37
+ });
38
+
39
+ test("throwing body removes both listeners, flushes, then re-throws the original error", async () => {
40
+ const events = new EventEmitter();
41
+ const handlers = makeHandlers();
42
+ const original = new Error("boom");
43
+ let flushed = 0;
44
+ const flush = async () => {
45
+ flushed += 1;
46
+ };
47
+
48
+ await expect(
49
+ withTaskTelemetry(events, handlers, flush, async () => {
50
+ throw original;
51
+ }),
52
+ ).rejects.toBe(original);
53
+
54
+ expect(events.listenerCount(COMPLETE)).toBe(0);
55
+ expect(events.listenerCount(ERROR)).toBe(0);
56
+ expect(flushed).toBe(1);
57
+ });
58
+
59
+ test("a rejecting flush does not replace the body error", async () => {
60
+ const events = new EventEmitter();
61
+ const handlers = makeHandlers();
62
+ const original = new Error("body error");
63
+ const flush = async () => {
64
+ throw new Error("flush error");
65
+ };
66
+
67
+ await expect(
68
+ withTaskTelemetry(events, handlers, flush, async () => {
69
+ throw original;
70
+ }),
71
+ ).rejects.toBe(original);
72
+
73
+ expect(events.listenerCount(COMPLETE)).toBe(0);
74
+ expect(events.listenerCount(ERROR)).toBe(0);
75
+ });
76
+
77
+ test("a rejecting flush does not turn a normal return into a rejection", async () => {
78
+ const events = new EventEmitter();
79
+ const handlers = makeHandlers();
80
+ const flush = async () => {
81
+ throw new Error("flush error");
82
+ };
83
+
84
+ const result = await withTaskTelemetry(events, handlers, flush, async () => "ok");
85
+
86
+ expect(result).toBe("ok");
87
+ expect(events.listenerCount(COMPLETE)).toBe(0);
88
+ expect(events.listenerCount(ERROR)).toBe(0);
89
+ });
90
+
91
+ test("cleanup is exactly-once: listener count returns to baseline with no double-remove error", async () => {
92
+ const events = new EventEmitter();
93
+ const handlers = makeHandlers();
94
+
95
+ // Establish a baseline of pre-existing, unrelated listeners.
96
+ const otherComplete = () => {};
97
+ const otherError = () => {};
98
+ events.on(COMPLETE, otherComplete);
99
+ events.on(ERROR, otherError);
100
+ const baselineComplete = events.listenerCount(COMPLETE);
101
+ const baselineError = events.listenerCount(ERROR);
102
+
103
+ await withTaskTelemetry(events, handlers, async () => {}, async () => {
104
+ // Remove our own listeners mid-body so finally's removeListener is a no-op
105
+ // on an absent listener — must not throw, must not touch the baseline.
106
+ events.removeListener(COMPLETE, handlers.onComplete);
107
+ events.removeListener(ERROR, handlers.onError);
108
+ return undefined;
109
+ });
110
+
111
+ expect(events.listenerCount(COMPLETE)).toBe(baselineComplete);
112
+ expect(events.listenerCount(ERROR)).toBe(baselineError);
113
+ });
114
+
115
+ test("flush is thunked: it awaits the current queue, not a value captured at attach", async () => {
116
+ const events = new EventEmitter();
117
+ const handlers = makeHandlers();
118
+ const order: string[] = [];
119
+
120
+ // Simulates task-runner's reassigned tokenWriteQueue. Distinct promises so
121
+ // we can prove which one the thunk awaits.
122
+ const staleQueue = Promise.resolve().then(() => {
123
+ order.push("stale-queue");
124
+ });
125
+ let queue: Promise<void> = staleQueue;
126
+
127
+ const result = await withTaskTelemetry(
128
+ events,
129
+ handlers,
130
+ () => queue,
131
+ async () => {
132
+ // Reassign the queue after attach but before finally.
133
+ queue = staleQueue.then(() => {
134
+ order.push("latest-queue");
135
+ });
136
+ order.push("body");
137
+ return 42;
138
+ },
139
+ );
140
+
141
+ expect(result).toBe(42);
142
+ // The thunk awaited the reassigned queue: "latest-queue" (chained after the
143
+ // stale one) is the last recorded entry, proving finally-time evaluation.
144
+ expect(order).toEqual(["body", "stale-queue", "latest-queue"]);
145
+ });
146
+ });
147
+
148
+ describe("withConsoleCapture", () => {
149
+ // Console is process-global state: snapshot the five methods and restore the
150
+ // exact originals after every test so a leak in one test can't taint another.
151
+ const originals = {
152
+ log: console.log,
153
+ error: console.error,
154
+ warn: console.warn,
155
+ info: console.info,
156
+ debug: console.debug,
157
+ };
158
+
159
+ const tmpFiles: string[] = [];
160
+ const logPathFor = (name: string) => {
161
+ const p = join(tmpdir(), `task-telemetry-${name}-${Date.now()}-${Math.random()}.log`);
162
+ tmpFiles.push(p);
163
+ return p;
164
+ };
165
+
166
+ afterEach(async () => {
167
+ console.log = originals.log;
168
+ console.error = originals.error;
169
+ console.warn = originals.warn;
170
+ console.info = originals.info;
171
+ console.debug = originals.debug;
172
+ await Promise.all(tmpFiles.splice(0).map((p) => rm(p, { force: true })));
173
+ });
174
+
175
+ test("restores all five console methods to the exact originals on normal return", async () => {
176
+ await withConsoleCapture(logPathFor("ok"), async () => {
177
+ // Inside the body the methods are the capture functions, not the originals.
178
+ expect(console.log).not.toBe(originals.log);
179
+ return undefined;
180
+ });
181
+
182
+ expect(console.log).toBe(originals.log);
183
+ expect(console.error).toBe(originals.error);
184
+ expect(console.warn).toBe(originals.warn);
185
+ expect(console.info).toBe(originals.info);
186
+ expect(console.debug).toBe(originals.debug);
187
+ });
188
+
189
+ test("restores all five console methods to the exact originals on body throw", async () => {
190
+ const boom = new Error("boom");
191
+
192
+ await expect(
193
+ withConsoleCapture(logPathFor("throw"), async () => {
194
+ throw boom;
195
+ }),
196
+ ).rejects.toBe(boom);
197
+
198
+ expect(console.log).toBe(originals.log);
199
+ expect(console.error).toBe(originals.error);
200
+ expect(console.warn).toBe(originals.warn);
201
+ expect(console.info).toBe(originals.info);
202
+ expect(console.debug).toBe(originals.debug);
203
+ });
204
+
205
+ test("a direct double restore is a no-op and cannot reinstate stale originals", async () => {
206
+ const { captureConsoleOutput } = await import("../task-telemetry");
207
+ const logPath = logPathFor("double");
208
+
209
+ const restore = captureConsoleOutput(logPath);
210
+ await restore();
211
+
212
+ // After the first restore, console.log is the original again.
213
+ expect(console.log).toBe(originals.log);
214
+
215
+ // Simulate a later, unrelated reassignment of console.log by other code.
216
+ const replacement = () => {};
217
+ console.log = replacement;
218
+
219
+ // A second restore must be a no-op: it must not overwrite the replacement
220
+ // with the stale captured original.
221
+ await restore();
222
+ expect(console.log).toBe(replacement);
223
+ });
224
+
225
+ test("the stage log file receives the captured output", async () => {
226
+ const logPath = logPathFor("output");
227
+
228
+ await withConsoleCapture(logPath, async () => {
229
+ console.log("plain line");
230
+ console.error("err line");
231
+ console.warn("warn line");
232
+ console.info("info line");
233
+ console.debug("debug line");
234
+ });
235
+
236
+ const contents = await Bun.file(logPath).text();
237
+ expect(contents).toBe(
238
+ "plain line\n[ERROR] err line\n[WARN] warn line\n[INFO] info line\n[DEBUG] debug line\n",
239
+ );
240
+ });
241
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+
5
+ // Source scan modeled on the #327 deleted-config-symbols scan
6
+ // (src/core/__tests__/config.test.ts "source deletion scan" block).
7
+ // Matches the wrong-pattern root-resolution expression:
8
+ // process.env["PO_ROOT"] ?? process.cwd() (or single-quote variant)
9
+ // Does NOT match bare process.env["PO_ROOT"] or bare process.cwd() — see AC-8.
10
+ const WRONG_PATTERN = /process\.env\[["']PO_ROOT["']\]\s*\?\?\s*process\.cwd\(\)/;
11
+
12
+ // AC-8 allowlist: legitimate entry-point consumers of PO_ROOT, relative to src/.
13
+ const ALLOWLIST: ReadonlySet<string> = new Set([
14
+ "config/paths.ts",
15
+ "core/config.ts",
16
+ "cli/index.ts",
17
+ "core/orchestrator.ts",
18
+ "core/pipeline-runner.ts",
19
+ "ui/server/index.ts",
20
+ ]);
21
+
22
+ const SRC_ROOT = join(import.meta.dir, "..", "..");
23
+
24
+ function walkTs(dir: string): string[] {
25
+ const out: string[] = [];
26
+ for (const entry of readdirSync(dir)) {
27
+ const full = join(dir, entry);
28
+ if (statSync(full).isDirectory()) {
29
+ out.push(...walkTs(full));
30
+ continue;
31
+ }
32
+ if (/\.tsx?$/.test(entry)) out.push(full);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ describe("workspace-root resolution guard scan (AC-8)", () => {
38
+ test("no non-allowlisted src/ file contains the PO_ROOT ?? process.cwd() fallback pattern", () => {
39
+ const files = walkTs(SRC_ROOT).filter((f) => !f.includes("/__tests__/"));
40
+ const offending: Array<{ file: string; relPath: string }> = [];
41
+ for (const file of files) {
42
+ const content = readFileSync(file, "utf8");
43
+ if (!WRONG_PATTERN.test(content)) continue;
44
+ const relPath = relative(SRC_ROOT, file).split("\\").join("/");
45
+ if (ALLOWLIST.has(relPath)) continue;
46
+ offending.push({ file, relPath });
47
+ }
48
+ if (offending.length > 0) {
49
+ const lines = offending.map((o) => ` ${o.relPath}: ${WRONG_PATTERN.source}`).join("\n");
50
+ throw new Error(`Workspace-root fallback expression outside allowlist under src/:\n${lines}`);
51
+ }
52
+ expect(offending).toHaveLength(0);
53
+ });
54
+
55
+ test("router.ts distDir cwd use does not trip the pattern", () => {
56
+ // router.ts uses process.cwd() only for `distDir = options.distDir ?? path.join(process.cwd(), "dist")`
57
+ // (no process.env["PO_ROOT"]), so it must not match the wrong-pattern expression.
58
+ const routerTs = join(SRC_ROOT, "ui", "server", "router.ts");
59
+ const content = readFileSync(routerTs, "utf8");
60
+ expect(WRONG_PATTERN.test(content)).toBe(false);
61
+ });
62
+ });
@@ -4,6 +4,7 @@ import { cp, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { createTaskFileIO, generateLogName } from "./file-io.ts";
7
+ import { redactSecrets } from "./redact.ts";
7
8
  import { LogEvent, LogFileExtension } from "../config/log-events.ts";
8
9
  import { run } from "../harness/index.ts";
9
10
  import { startMcpIoServer } from "../harness/mcp-io-server.ts";
@@ -341,7 +342,9 @@ export async function executeAgent(
341
342
  permissionMode: "bypass",
342
343
  onEvent: (event: HarnessEvent) => {
343
344
  observedEvents.push(event);
344
- void io.writeLog(logName, JSON.stringify(event.raw) + "\n", {
345
+ const token = mcpHandle?.connection?.token;
346
+ const redacted = redactSecrets(event.raw, { secrets: token ? [token] : [] });
347
+ void io.writeLog(logName, JSON.stringify(redacted) + "\n", {
345
348
  mode: "append",
346
349
  });
347
350
  },
@@ -1,4 +1,4 @@
1
- import type { HarnessName, Usage } from "../harness/index.ts";
1
+ import type { HarnessName, ModelPayload, Usage } from "../harness/index.ts";
2
2
 
3
3
  export interface McpServerConnection {
4
4
  url: string;
@@ -7,7 +7,7 @@ export interface McpServerConnection {
7
7
 
8
8
  export interface AgentEntryConfig {
9
9
  harness: HarnessName;
10
- model?: string;
10
+ model?: ModelPayload | string;
11
11
  prompt?: string;
12
12
  promptFrom?: string;
13
13
  cwd?: string;
@@ -38,8 +38,9 @@ export interface TaskAgentOptions {
38
38
  harness: HarnessName;
39
39
  /** The instruction handed to the agent. */
40
40
  prompt: string;
41
- /** Optional model id passed through to the harness verbatim. */
42
- model?: string;
41
+ /** Optional model selection passed through to the harness verbatim — a bare
42
+ * model id or a resolved {@link ModelPayload} carrying reasoning effort. */
43
+ model?: ModelPayload | string;
43
44
  /** Working directory for the agent. Defaults to the task directory. */
44
45
  cwd?: string;
45
46
  /**