@loomhelm/multi-task-flow 0.2.8 → 0.2.9

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 (59) hide show
  1. package/README.md +14 -14
  2. package/package.json +2 -2
  3. package/src/cli/service.ts +26 -0
  4. package/src/cli.ts +3 -1
  5. package/toolkit/artifact-manifest.json +67 -71
  6. package/toolkit/package.json +1 -1
  7. package/toolkit/skills/bin/check_source_policy.ts +13 -7
  8. package/toolkit/skills/bin/multi_task_platform_gate.ts +2 -2
  9. package/toolkit/skills/bin/tests/core/package_scripts.test.ts +1 -1
  10. package/toolkit/skills/bin/tests/plan/multi-task-flow.test.ts +3 -3
  11. package/toolkit/skills/catalog.json +0 -1
  12. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/data-and-scripts.md +8 -6
  13. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/execution.md +1 -1
  14. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/plan-revision-and-change-audit.md +1 -1
  15. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/web-template.md +1 -1
  16. package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/IMPLEMENTATION.md +0 -2
  17. package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/SPEC.md +0 -1
  18. package/toolkit/skills/docs/vendor/dot-codex/specs/dev-plan-standalone-typescript-finalizer/IMPLEMENTATION.md +1 -1
  19. package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/IMPLEMENTATION.md +3 -3
  20. package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/SCHEMA.md +4 -4
  21. package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/IMPLEMENTATION.md +0 -2
  22. package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/SPEC.md +0 -1
  23. package/toolkit/skills/multi-task-flow/SKILL.md +65 -47
  24. package/toolkit/skills/multi-task-flow/dist/app.css +1 -1
  25. package/toolkit/skills/multi-task-flow/dist/app.js +74 -74
  26. package/toolkit/skills/multi-task-flow/dist/index.html +3 -3
  27. package/toolkit/skills/multi-task-flow/lib/atomic-orchestration.ts +1 -0
  28. package/toolkit/skills/multi-task-flow/lib/engine.ts +65 -7
  29. package/toolkit/skills/multi-task-flow/lib/package-source.ts +5 -5
  30. package/toolkit/skills/multi-task-flow/lib/schema.ts +82 -2
  31. package/toolkit/skills/multi-task-flow/lib/service-pages.ts +1 -26
  32. package/toolkit/skills/multi-task-flow/lib/view.ts +16 -0
  33. package/toolkit/skills/multi-task-flow/references/shared-viewer.md +1 -1
  34. package/toolkit/skills/multi-task-flow/scripts/check-client-skills.ts +7 -4
  35. package/toolkit/skills/multi-task-flow/scripts/check-skills-installation.ts +14 -8
  36. package/toolkit/skills/multi-task-flow/scripts/import-client-skills.ts +1 -1
  37. package/toolkit/skills/multi-task-flow/scripts/package-client-skills.ts +4 -4
  38. package/toolkit/skills/multi-task-flow/scripts/test-viewer-source.ts +1 -1
  39. package/toolkit/skills/multi-task-flow/tests/atomic-orchestration-schema.test.ts +5 -0
  40. package/toolkit/skills/multi-task-flow/tests/browser/README.md +1 -1
  41. package/toolkit/skills/multi-task-flow/tests/check-skills-installation.test.ts +12 -38
  42. package/toolkit/skills/multi-task-flow/tests/delivery-topology.test.ts +33 -0
  43. package/toolkit/skills/multi-task-flow/tests/orchestrate.test.ts +3 -5
  44. package/toolkit/skills/multi-task-flow/tests/ui/details-content.test.ts +9 -0
  45. package/toolkit/skills/multi-task-flow/tests/ui/responsive-width-contract.test.ts +25 -1
  46. package/toolkit/skills/multi-task-flow/tests/ui/task-context-menu.test.ts +2 -1
  47. package/toolkit/skills/multi-task-flow/web/app.css +64 -9
  48. package/toolkit/skills/multi-task-flow/web/app.ts +33 -14
  49. package/toolkit/skills/multi-task-flow/web/atomic-graph-view.tsx +1 -1
  50. package/toolkit/skills/multi-task-flow/web/details-content.ts +49 -38
  51. package/toolkit/skills/multi-task-flow/web/graph-view.tsx +1 -1
  52. package/toolkit/skills/multi-task-flow/web/index.html +3 -3
  53. package/toolkit/skills/multi-task-flow/web/model.ts +1 -1
  54. package/toolkit/skills/multi-task-flow/web/timeline.ts +11 -8
  55. package/toolkit/skills/package.json +2 -2
  56. package/toolkit/skills/source-manifest.json +691 -398
  57. package/toolkit/skills/support-manifest.json +1 -0
  58. package/toolkit/skills/multi-task-flow/scripts/serve.ts +0 -218
  59. package/toolkit/skills/multi-task-flow/tests/serve.test.ts +0 -194
@@ -28,6 +28,7 @@
28
28
  "skills/multi-task-flow/tests/pr-details.test.ts",
29
29
  "skills/multi-task-flow/tests/service-lifecycle.test.ts",
30
30
  "skills/multi-task-flow/tests/single-delivery-unit.test.ts",
31
+ "skills/multi-task-flow/tests/delivery-topology.test.ts",
31
32
  "skills/multi-task-flow/tests/ui/graph-controller.test.tsx",
32
33
  "skills/multi-task-flow/tests/ui/pr-status.test.ts",
33
34
  "skills/multi-task-flow/tests/ui/resource-popover.test.ts",
@@ -1,218 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import { Runtime } from "../../bin/modules/core/runtime";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { ContractError, requireCondition } from "../lib/errors";
6
- import { defaultRoot, safeId } from "../lib/store";
7
- import { startDashboard } from "../lib/server";
8
- import {
9
- defaultRegistryDir,
10
- PortRegistry,
11
- RegistryError,
12
- } from "../../bin/modules/workspace/port_registry";
13
-
14
- const args = process.argv.slice(2);
15
- function flag(name: string): string {
16
- const index = args.indexOf(name);
17
- const value = index >= 0 ? args[index + 1] : undefined;
18
- requireCondition(
19
- !!value && !value.startsWith("--"),
20
- "INVALID_ARGUMENTS",
21
- `Missing ${name}`,
22
- );
23
- return value;
24
- }
25
-
26
- function optionalFlag(name: string): string | undefined {
27
- const index = args.indexOf(name);
28
- if (index < 0) return undefined;
29
- const value = args[index + 1];
30
- requireCondition(
31
- !!value && !value.startsWith("--"),
32
- "INVALID_ARGUMENTS",
33
- `Missing ${name}`,
34
- );
35
- return value;
36
- }
37
-
38
- function registryJson(
39
- registry: string,
40
- command: string,
41
- options: string[],
42
- failureCode = "LEASE_OPERATION_FAILED",
43
- ): Record<string, unknown> {
44
- const result = Runtime.spawnSync(
45
- [process.execPath, "--no-install", registry, "--json", command, ...options],
46
- { stdout: "pipe", stderr: "pipe" },
47
- );
48
- const stderr = result.stderr.toString().trim();
49
- requireCondition(
50
- result.exitCode === 0,
51
- failureCode,
52
- stderr || `Port lease command failed: ${command}`,
53
- 5,
54
- );
55
- try {
56
- return JSON.parse(result.stdout.toString());
57
- } catch {
58
- throw new ContractError(
59
- failureCode,
60
- `Port lease command returned invalid JSON: ${command}`,
61
- 5,
62
- );
63
- }
64
- }
65
-
66
- let scope = "";
67
- let service = "";
68
- let port = 0;
69
- let dashboard: Awaited<ReturnType<typeof startDashboard>> | undefined;
70
- let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
71
- let leaseRegistered = false;
72
- let shutdownPromise: Promise<void> | undefined;
73
-
74
- const registry = fileURLToPath(new URL('../../global-port-manager/scripts/port_registry.' + (import.meta.url.endsWith('.js') ? 'js' : 'ts'), import.meta.url));
75
-
76
- async function shutdown(exitCode: number): Promise<void> {
77
- if (shutdownPromise) return shutdownPromise;
78
- shutdownPromise = (async () => {
79
- let finalCode = exitCode;
80
- if (heartbeatTimer) clearInterval(heartbeatTimer);
81
- if (leaseRegistered) {
82
- try {
83
- registryJson(registry, "release-service", [
84
- "--scope-id",
85
- scope,
86
- "--service",
87
- service,
88
- "--port",
89
- String(port),
90
- "--pid",
91
- String(process.pid),
92
- ]);
93
- } catch {
94
- finalCode = 1;
95
- }
96
- leaseRegistered = false;
97
- }
98
- try {
99
- await dashboard?.stop();
100
- } catch {
101
- finalCode = 1;
102
- }
103
- dashboard = undefined;
104
- process.exitCode = finalCode;
105
- })();
106
- return shutdownPromise;
107
- }
108
-
109
- try {
110
- const runId = safeId(flag("--run"));
111
- scope = flag("--scope-id");
112
- service = flag("--service");
113
- port = Number(flag("--port"));
114
- const agentSession = optionalFlag("--agent-session");
115
- requireCondition(
116
- Number.isInteger(port) && port > 0 && port <= 65535,
117
- "INVALID_PORT",
118
- "An allocated port is required",
119
- );
120
- const root = process.env.TASK_ORCHESTRATION_DATA_ROOT ?? defaultRoot();
121
- const assetsDir = args.includes("--custom-view")
122
- ? join(root, "runs", runId, "view")
123
- : join(import.meta.dir, "..", "dist");
124
- const portRegistry = new PortRegistry(defaultRegistryDir());
125
- const activeDashboard = await portRegistry
126
- .claimAndRun(
127
- {
128
- scopeId: scope,
129
- service,
130
- port,
131
- pid: process.pid,
132
- ...(agentSession === undefined ? {} : { agentSession }),
133
- },
134
- async () => {
135
- const startedDashboard = await startDashboard({
136
- root,
137
- runId,
138
- port,
139
- assetsDir,
140
- });
141
- // Publish the cleanup handle before claim verification or persistence.
142
- dashboard = startedDashboard;
143
- return startedDashboard;
144
- },
145
- )
146
- .catch((error) => {
147
- if (error instanceof RegistryError)
148
- throw new ContractError("LEASE_UNAVAILABLE", error.message, 5);
149
- throw error;
150
- });
151
- dashboard = activeDashboard;
152
- leaseRegistered = true;
153
- const heartbeatLease = () => {
154
- const heartbeatOptions = [
155
- "--scope-id",
156
- scope,
157
- "--service",
158
- service,
159
- "--port",
160
- String(port),
161
- "--pid",
162
- String(process.pid),
163
- "--expected-pid",
164
- String(process.pid),
165
- ];
166
- if (agentSession) heartbeatOptions.push("--agent-session", agentSession);
167
- registryJson(registry, "heartbeat", heartbeatOptions);
168
- };
169
- try {
170
- heartbeatLease();
171
- } catch {
172
- throw new ContractError(
173
- "LEASE_HEARTBEAT_FAILED",
174
- "Port lease heartbeat failed",
175
- 5,
176
- );
177
- }
178
- heartbeatTimer = setInterval(() => {
179
- if (shutdownPromise) return;
180
- try {
181
- heartbeatLease();
182
- } catch {
183
- console.error(
184
- JSON.stringify({
185
- ok: false,
186
- code: "LEASE_HEARTBEAT_FAILED",
187
- message: "Port lease heartbeat failed",
188
- }),
189
- );
190
- void shutdown(5);
191
- }
192
- }, 30_000);
193
- console.log(
194
- JSON.stringify({
195
- ok: true,
196
- url: `http://127.0.0.1:${activeDashboard.port}/s/${activeDashboard.token}/`,
197
- port: activeDashboard.port,
198
- scope_id: scope,
199
- service,
200
- agent_session_recorded: Boolean(agentSession),
201
- }),
202
- );
203
- const close = () => {
204
- void shutdown(0);
205
- };
206
- process.once("SIGTERM", close);
207
- process.once("SIGINT", close);
208
- } catch (error) {
209
- const code = error instanceof ContractError ? error.code : "SERVER_ERROR";
210
- console.error(
211
- JSON.stringify({
212
- ok: false,
213
- code,
214
- message: error instanceof Error ? error.message : "Server failed",
215
- }),
216
- );
217
- await shutdown(error instanceof ContractError ? error.exitCode : 1);
218
- }
@@ -1,194 +0,0 @@
1
- import { createServer } from "node:net";
2
- import { mkdtemp, mkdir, realpath, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join, resolve } from "node:path";
5
- import { describe, expect, test } from "bun:test";
6
- import { PortRegistry } from "../../bin/modules/workspace/port_registry";
7
- import { createDemoLedger } from "../examples/orchestration-viewer/ledger";
8
- import { LedgerStore } from "../lib/store";
9
-
10
- async function freePort(): Promise<number> {
11
- const listener = createServer();
12
- await new Promise<void>((resolvePromise, reject) => {
13
- listener.once("error", reject);
14
- listener.listen(0, "127.0.0.1", () => resolvePromise());
15
- });
16
- const address = listener.address();
17
- const port =
18
- typeof address === "object" && address !== null ? address.port : 0;
19
- await new Promise<void>((resolvePromise, reject) => {
20
- listener.close((error) => (error ? reject(error) : resolvePromise()));
21
- });
22
- return port;
23
- }
24
-
25
- async function readLine(stream: ReadableStream<Uint8Array>): Promise<string> {
26
- const reader = stream.getReader();
27
- const decoder = new TextDecoder();
28
- let output = "";
29
- try {
30
- while (true) {
31
- const chunk = await reader.read();
32
- if (chunk.done)
33
- throw new Error("serve.ts exited before announcing its URL");
34
- output += decoder.decode(chunk.value, { stream: true });
35
- const newline = output.indexOf("\n");
36
- if (newline >= 0) return output.slice(0, newline);
37
- }
38
- } finally {
39
- reader.releaseLock();
40
- }
41
- }
42
-
43
- describe("multi-task dashboard service wrapper", () => {
44
- test("records its pid/session and releases its lease on SIGTERM", async () => {
45
- const root = await mkdtemp(
46
- join(await realpath(tmpdir()), "multi-task-serve-"),
47
- );
48
- const dataRoot = join(root, "data");
49
- const registryRoot = join(root, "registry");
50
- const repoRoot = resolve(import.meta.dir, "../../..");
51
- const run = createDemoLedger();
52
- const runStore = new LedgerStore(dataRoot, run.orchestration_id);
53
- const viewRoot = join(runStore.runPath, "view");
54
- const port = await freePort();
55
- let child: ReturnType<typeof Bun.spawn> | undefined;
56
- let duplicate: ReturnType<typeof Bun.spawn> | undefined;
57
- try {
58
- runStore.create(run);
59
- await mkdir(viewRoot, { recursive: true });
60
- await writeFile(
61
- join(viewRoot, "index.html"),
62
- "<!doctype html><title>test</title>",
63
- );
64
- await writeFile(join(viewRoot, "app.js"), "");
65
- await writeFile(join(viewRoot, "app.css"), "");
66
-
67
- const registry = new PortRegistry(registryRoot);
68
- await registry.register({
69
- scopeId: "scope-test",
70
- service: "dashboard",
71
- port,
72
- project: "test",
73
- repoRoot,
74
- branch: "test",
75
- worktreePath: repoRoot,
76
- });
77
-
78
- child = Bun.spawn(
79
- [
80
- process.execPath,
81
- join(import.meta.dir, "../scripts/serve.ts"),
82
- "--run",
83
- run.orchestration_id,
84
- "--scope-id",
85
- "scope-test",
86
- "--service",
87
- "dashboard",
88
- "--port",
89
- String(port),
90
- "--agent-session",
91
- "native-session-test",
92
- "--custom-view",
93
- ],
94
- {
95
- cwd: repoRoot,
96
- env: {
97
- ...process.env,
98
- CODEX_HOME: repoRoot,
99
- CODEX_PORT_REGISTRY_DIR: registryRoot,
100
- TASK_ORCHESTRATION_DATA_ROOT: dataRoot,
101
- },
102
- stdout: "pipe",
103
- stderr: "pipe",
104
- },
105
- );
106
-
107
- const stdout = child.stdout;
108
- if (stdout === undefined || typeof stdout === "number")
109
- throw new Error("serve.ts stdout is not piped");
110
- const announcement = JSON.parse(await readLine(stdout));
111
- expect(announcement).toMatchObject({
112
- ok: true,
113
- port,
114
- scope_id: "scope-test",
115
- service: "dashboard",
116
- agent_session_recorded: true,
117
- });
118
-
119
- const runningRows = await registry.readRows();
120
- expect(runningRows).toHaveLength(1);
121
- expect(runningRows[0]).toMatchObject({
122
- scope_id: "scope-test",
123
- service: "dashboard",
124
- port: String(port),
125
- agent_session: "native-session-test",
126
- pid: String(child.pid),
127
- });
128
-
129
- duplicate = Bun.spawn(
130
- [
131
- process.execPath,
132
- join(import.meta.dir, "../scripts/serve.ts"),
133
- "--run",
134
- run.orchestration_id,
135
- "--scope-id",
136
- "scope-test",
137
- "--service",
138
- "dashboard",
139
- "--port",
140
- String(port),
141
- "--agent-session",
142
- "native-session-test-duplicate",
143
- "--custom-view",
144
- ],
145
- {
146
- cwd: repoRoot,
147
- env: {
148
- ...process.env,
149
- CODEX_HOME: repoRoot,
150
- CODEX_PORT_REGISTRY_DIR: registryRoot,
151
- TASK_ORCHESTRATION_DATA_ROOT: dataRoot,
152
- },
153
- stdout: "pipe",
154
- stderr: "pipe",
155
- },
156
- );
157
- const duplicateExit = await duplicate.exited;
158
- const duplicateStderr = duplicate.stderr;
159
- const duplicateOutput =
160
- duplicateStderr === undefined || typeof duplicateStderr === "number"
161
- ? ""
162
- : await new Response(duplicateStderr).text();
163
- duplicate = undefined;
164
- expect(duplicateExit).toBe(5);
165
- expect(duplicateOutput).toContain('"code":"LEASE_UNAVAILABLE"');
166
- expect(await registry.readRows()).toEqual(runningRows);
167
-
168
- process.kill(child.pid, "SIGTERM");
169
- const stderr = child.stderr;
170
- const exitCode = await child.exited;
171
- child = undefined;
172
- if (exitCode !== 0 && stderr !== undefined && typeof stderr !== "number")
173
- throw new Error(
174
- `serve.ts exited with ${exitCode}: ${await new Response(stderr).text()}`,
175
- );
176
- expect(exitCode).toBe(0);
177
- expect(await registry.readRows()).toHaveLength(0);
178
- } finally {
179
- if (duplicate) {
180
- try {
181
- process.kill(duplicate.pid, "SIGKILL");
182
- } catch {}
183
- await duplicate.exited;
184
- }
185
- if (child) {
186
- try {
187
- process.kill(child.pid, "SIGKILL");
188
- } catch {}
189
- await child.exited;
190
- }
191
- await rm(root, { recursive: true, force: true });
192
- }
193
- }, 10_000);
194
- });