@ryanfw/prompt-orchestration-pipeline 1.3.1 → 1.3.2

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 (31) hide show
  1. package/docs/pop-task-guide.md +1 -0
  2. package/package.json +1 -2
  3. package/src/core/__tests__/agent-step.test.ts +322 -3
  4. package/src/core/__tests__/task-runner.test.ts +57 -1
  5. package/src/core/agent-step.ts +231 -14
  6. package/src/core/agent-types.ts +3 -0
  7. package/src/core/file-io.ts +8 -74
  8. package/src/core/pipeline-runner.ts +54 -3
  9. package/src/core/status-writer.ts +44 -26
  10. package/src/core/task-runner.ts +27 -3
  11. package/src/harness/__tests__/discovery.test.ts +60 -8
  12. package/src/harness/discovery.ts +16 -6
  13. package/src/ui/client/hooks/useJobListWithUpdates.ts +16 -1
  14. package/src/ui/dist/assets/{index-CbS3OsW7.js → index--RH3sAt3.js} +14 -1
  15. package/src/ui/dist/assets/{index-CbS3OsW7.js.map → index--RH3sAt3.js.map} +1 -1
  16. package/src/ui/dist/assets/style-CSSKuMOe.css +2 -0
  17. package/src/ui/dist/index.html +2 -2
  18. package/src/ui/embedded-assets.js +6 -6
  19. package/src/ui/server/__tests__/gate-endpoints.test.ts +90 -0
  20. package/src/ui/server/__tests__/job-control-endpoints.test.ts +188 -0
  21. package/src/ui/server/__tests__/path-containment.test.ts +54 -0
  22. package/src/ui/server/__tests__/status-corruption.test.ts +55 -0
  23. package/src/ui/server/config-bridge.ts +1 -0
  24. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +77 -0
  25. package/src/ui/server/endpoints/gate-endpoints.ts +17 -1
  26. package/src/ui/server/endpoints/job-control-endpoints.ts +36 -2
  27. package/src/ui/server/endpoints/upload-endpoints.ts +13 -3
  28. package/src/ui/server/utils/http-utils.ts +6 -1
  29. package/src/ui/server/utils/path-containment.ts +18 -0
  30. package/src/ui/server/utils/status-corruption.ts +27 -0
  31. package/src/ui/dist/assets/style-BUFg3Sth.css +0 -2
@@ -25,14 +25,8 @@ describe("healedPath", () => {
25
25
  });
26
26
 
27
27
  it("does not duplicate dirs already on the base PATH", () => {
28
- // Add an existing standard dir to the base PATH, then check it's not duplicated
29
- const home = process.env.HOME ?? "";
30
- const testDir = join(home, ".local/bin");
31
- // Only test if that dir actually exists on this machine
32
- const result = healedPath(`${testDir}:/usr/bin`);
33
- const count = result.split(":").filter((d) => d === testDir).length;
34
- // Should be 0 or 1 — if dir exists it appears once, if not it appears zero times
35
- expect(count).toBeLessThanOrEqual(1);
28
+ const result = healedPath(`${existingDir}:/usr/bin`, [existingDir]);
29
+ expect(result.split(":").filter((d) => d === existingDir)).toHaveLength(1);
36
30
  });
37
31
 
38
32
  it("defaults to process.env.PATH when basePath is undefined", () => {
@@ -180,4 +174,62 @@ describe("applyHarnessDiscovery", () => {
180
174
  expect(env.POP_CODEX_BIN).toBeUndefined();
181
175
  expect(env.LLM_ADAPTER_CODEX_BIN).toBeUndefined();
182
176
  });
177
+
178
+ it("bridges existing POP_* binary overrides to adapter env vars when discovery misses", () => {
179
+ const env: Record<string, string | undefined> = {
180
+ PATH: "/usr/bin",
181
+ POP_OPENCODE_BIN: "/custom/opencode",
182
+ };
183
+ const statuses: Record<HarnessName, HarnessStatus> = {
184
+ opencode: makeStatus({ name: "opencode", installed: false }),
185
+ claude: makeStatus({ name: "claude", installed: false }),
186
+ codex: makeStatus({ name: "codex", installed: false }),
187
+ };
188
+
189
+ const messages = applyHarnessDiscovery(statuses, env);
190
+
191
+ expect(env.POP_OPENCODE_BIN).toBe("/custom/opencode");
192
+ expect(env.LLM_ADAPTER_OPENCODE_BIN).toBe("/custom/opencode");
193
+ expect(messages.find((m) => m.message.includes('"opencode"'))).toMatchObject({
194
+ level: "info",
195
+ });
196
+ });
197
+
198
+ it("bridges existing adapter binary overrides back to POP env vars when discovery misses", () => {
199
+ const env: Record<string, string | undefined> = {
200
+ PATH: "/usr/bin",
201
+ LLM_ADAPTER_CODEX_BIN: "/custom/codex",
202
+ };
203
+ const statuses: Record<HarnessName, HarnessStatus> = {
204
+ opencode: makeStatus({ name: "opencode", installed: false }),
205
+ claude: makeStatus({ name: "claude", installed: false }),
206
+ codex: makeStatus({ name: "codex", installed: false }),
207
+ };
208
+
209
+ applyHarnessDiscovery(statuses, env);
210
+
211
+ expect(env.LLM_ADAPTER_CODEX_BIN).toBe("/custom/codex");
212
+ expect(env.POP_CODEX_BIN).toBe("/custom/codex");
213
+ });
214
+
215
+ it("keeps POP_* binary overrides ahead of discovered PATH binaries", () => {
216
+ const env: Record<string, string | undefined> = {
217
+ PATH: "/usr/bin",
218
+ POP_CODEX_BIN: "/custom/codex",
219
+ };
220
+ const statuses: Record<HarnessName, HarnessStatus> = {
221
+ opencode: makeStatus({ name: "opencode", installed: false }),
222
+ claude: makeStatus({ name: "claude", installed: false }),
223
+ codex: makeStatus({ name: "codex", installed: true, binPath: "/path/codex", authenticated: true }),
224
+ };
225
+
226
+ const messages = applyHarnessDiscovery(statuses, env);
227
+
228
+ expect(env.POP_CODEX_BIN).toBe("/custom/codex");
229
+ expect(env.LLM_ADAPTER_CODEX_BIN).toBe("/custom/codex");
230
+ expect(messages.find((m) => m.message.includes('"codex"'))).toMatchObject({
231
+ level: "info",
232
+ message: 'harness "codex": using configured binary override /custom/codex',
233
+ });
234
+ });
183
235
  });
@@ -32,11 +32,11 @@ function standardBinDirs(): string[] {
32
32
  * Build a PATH with known bin dirs prepended — keeping only dirs that exist
33
33
  * and are not already present.
34
34
  */
35
- export function healedPath(basePath?: string): string {
35
+ export function healedPath(basePath?: string, candidateDirs = standardBinDirs()): string {
36
36
  const current = (basePath ?? process.env.PATH ?? "").split(":").filter(Boolean);
37
37
  const seen = new Set(current);
38
38
  const prepend: string[] = [];
39
- for (const dir of standardBinDirs()) {
39
+ for (const dir of candidateDirs) {
40
40
  if (dir && !seen.has(dir) && existsSync(dir)) {
41
41
  prepend.push(dir);
42
42
  seen.add(dir);
@@ -68,12 +68,22 @@ export function applyHarnessDiscovery(
68
68
 
69
69
  const messages: PreflightMessage[] = [];
70
70
  for (const status of Object.values(statuses) as HarnessStatus[]) {
71
- if (status.binPath) {
72
- env[adapterBinEnvVar(status.name)] = status.binPath;
73
- env[popBinEnvVar(status.name)] = status.binPath;
71
+ const adapterEnv = adapterBinEnvVar(status.name);
72
+ const popEnv = popBinEnvVar(status.name);
73
+ const explicitBin = env[popEnv] ?? env[adapterEnv];
74
+ const configuredBin = explicitBin ?? status.binPath;
75
+
76
+ if (configuredBin) {
77
+ env[adapterEnv] = configuredBin;
78
+ env[popEnv] = configuredBin;
74
79
  }
75
80
 
76
- if (!status.installed) {
81
+ if (explicitBin) {
82
+ messages.push({
83
+ level: "info",
84
+ message: `harness "${status.name}": using configured binary override ${configuredBin}`,
85
+ });
86
+ } else if (!status.installed && !configuredBin) {
77
87
  messages.push({
78
88
  level: "warn",
79
89
  message: `harness "${status.name}": CLI not found on PATH or known install dirs; agent steps using it will fail until it is installed`,
@@ -33,8 +33,23 @@ function sortNormalizedJobs(jobs: NormalizedJobSummary[]): NormalizedJobSummary[
33
33
  });
34
34
  }
35
35
 
36
+ function completedCountFromTasks(job: NormalizedJobSummary): number {
37
+ return Object.values(job.tasks).filter((task) => task.state === "done" || task.state === "skipped").length;
38
+ }
39
+
40
+ function comparableJob(job: NormalizedJobSummary): Record<string, unknown> {
41
+ const comparable: Record<string, unknown> = { ...job };
42
+ for (const [key, value] of Object.entries(comparable)) {
43
+ if (value === undefined) delete comparable[key];
44
+ }
45
+ if (comparable["completedCount"] === completedCountFromTasks(job)) {
46
+ delete comparable["completedCount"];
47
+ }
48
+ return comparable;
49
+ }
50
+
36
51
  function jobsEqual(left: NormalizedJobSummary[], right: NormalizedJobSummary[]): boolean {
37
- return JSON.stringify(left) === JSON.stringify(right);
52
+ return JSON.stringify(left.map(comparableJob)) === JSON.stringify(right.map(comparableJob));
38
53
  }
39
54
 
40
55
  function mergeJob(current: NormalizedJobSummary, incoming: NormalizedJobSummary): NormalizedJobSummary {
@@ -78431,8 +78431,21 @@ function sortNormalizedJobs(jobs) {
78431
78431
  return left.jobId.localeCompare(right.jobId);
78432
78432
  });
78433
78433
  }
78434
+ function completedCountFromTasks(job) {
78435
+ return Object.values(job.tasks).filter((task) => task.state === "done" || task.state === "skipped").length;
78436
+ }
78437
+ function comparableJob(job) {
78438
+ const comparable = { ...job };
78439
+ for (const [key, value] of Object.entries(comparable)) {
78440
+ if (value === void 0) delete comparable[key];
78441
+ }
78442
+ if (comparable["completedCount"] === completedCountFromTasks(job)) {
78443
+ delete comparable["completedCount"];
78444
+ }
78445
+ return comparable;
78446
+ }
78434
78447
  function jobsEqual(left, right) {
78435
- return JSON.stringify(left) === JSON.stringify(right);
78448
+ return JSON.stringify(left.map(comparableJob)) === JSON.stringify(right.map(comparableJob));
78436
78449
  }
78437
78450
  function mergeJob(current, incoming) {
78438
78451
  return {