conductor-board 1.2.0 → 2.0.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.
package/cli/stop.js ADDED
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { scanBoards } from "./discover.js";
4
+
5
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
6
+
7
+ const flag = (args, names, def) => {
8
+ for (const n of names) {
9
+ const i = args.indexOf(n);
10
+ if (i !== -1) return args[i + 1] && !args[i + 1].startsWith("-") ? args[i + 1] : true;
11
+ }
12
+ return def;
13
+ };
14
+
15
+ const kill = (pid) => {
16
+ try {
17
+ process.kill(pid, "SIGTERM");
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+
24
+ /** Stop the board for this project, or every board with --all. */
25
+ export async function runStop(args) {
26
+ console.log("");
27
+ if (args.includes("--all")) {
28
+ const boards = await scanBoards();
29
+ if (boards.length === 0) {
30
+ console.log(dim(" no boards running."));
31
+ console.log("");
32
+ return true;
33
+ }
34
+ let n = 0;
35
+ for (const b of boards) {
36
+ if (b.pid && kill(b.pid)) {
37
+ n += 1;
38
+ console.log(dim(` stopped pid ${b.pid} (port ${b.port})`));
39
+ }
40
+ }
41
+ console.log("");
42
+ console.log(dim(` stopped ${n} board${n === 1 ? "" : "s"}.`));
43
+ console.log("");
44
+ return true;
45
+ }
46
+
47
+ // this project
48
+ const dir = String(flag(args, ["--dir"], ".conductor"));
49
+ const serverJson = path.resolve(process.cwd(), dir, "server.json");
50
+ let info;
51
+ try {
52
+ info = JSON.parse(fs.readFileSync(serverJson, "utf8"));
53
+ } catch {
54
+ console.log(dim(` no board for this project (${path.relative(process.cwd(), serverJson)} not found).`));
55
+ console.log(dim(" use conductor-board ps to see boards, or stop --all to stop them."));
56
+ console.log("");
57
+ return true;
58
+ }
59
+ if (info.pid && kill(info.pid)) {
60
+ console.log(dim(` stopped the board for this project (pid ${info.pid}, port ${info.port}).`));
61
+ } else {
62
+ try {
63
+ fs.unlinkSync(serverJson);
64
+ } catch {
65
+ /* ignore */
66
+ }
67
+ console.log(dim(` board pid ${info.pid} was not running — cleaned up server.json.`));
68
+ }
69
+ console.log("");
70
+ return true;
71
+ }
package/cli/validate.js CHANGED
@@ -46,6 +46,8 @@ function findOrphans(steps, ids) {
46
46
  const indexById = new Map(steps.map((s, i) => [s.id, i]));
47
47
  const successors = (s, i) => {
48
48
  if (s.type === "condition") return [s.if_true, s.if_false].filter(Boolean);
49
+ if (s.type === "approval")
50
+ return [s.approval?.actions?.approve, s.approval?.actions?.reject].filter(Boolean);
49
51
  if (s.then) return [s.then];
50
52
  const next = steps[i + 1];
51
53
  return next ? [next.id] : [];
@@ -108,8 +110,23 @@ export function validateConductor(doc) {
108
110
  if (!s || !s.id) continue;
109
111
  const isCond = s.type === "condition";
110
112
  const isLoop = s.type === "loop";
111
-
112
- if (!isLoop && !s.instruction) errors.push(`Step "${s.id}" has no instruction`);
113
+ const isApproval = s.type === "approval";
114
+
115
+ if (!isLoop && !isApproval && !s.instruction)
116
+ errors.push(`Step "${s.id}" has no instruction`);
117
+
118
+ if (isApproval) {
119
+ if (!s.approval || typeof s.approval !== "object") {
120
+ errors.push(`Approval step "${s.id}" is missing the "approval" block`);
121
+ } else {
122
+ const approve = s.approval.actions?.approve;
123
+ const reject = s.approval.actions?.reject;
124
+ if (approve && !ids.has(approve))
125
+ errors.push(`Approval "${s.id}" references unknown step "${approve}" in actions.approve`);
126
+ if (reject && !ids.has(reject))
127
+ errors.push(`Approval "${s.id}" references unknown step "${reject}" in actions.reject`);
128
+ }
129
+ }
113
130
 
114
131
  for (const g of s.gate ?? []) {
115
132
  if (!gateOk(g)) errors.push(`Step "${s.id}" has a malformed gate criterion`);
@@ -178,6 +195,7 @@ export async function runValidate(args) {
178
195
  countGates(steps, acc);
179
196
  const conditions = steps.filter((s) => s.type === "condition").length;
180
197
  const loops = steps.filter((s) => s.type === "loop").length;
198
+ const approvals = steps.filter((s) => s.type === "approval").length;
181
199
  const part = (n, one, many) => `${n} ${n === 1 ? one : many}`;
182
200
  console.log(green(`✓ ${path.basename(file)} is valid`));
183
201
  console.log(
@@ -186,7 +204,8 @@ export async function runValidate(args) {
186
204
  `${part(acc.soft, "soft gate", "soft gates")}, ` +
187
205
  `${part(acc.hard, "hard gate", "hard gates")}, ` +
188
206
  `${part(conditions, "condition", "conditions")}, ` +
189
- `${part(loops, "loop", "loops")}`,
207
+ `${part(loops, "loop", "loops")}, ` +
208
+ `${part(approvals, "approval", "approvals")}`,
190
209
  ),
191
210
  );
192
211
  console.log("");
package/cli/writer.js ADDED
@@ -0,0 +1,191 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+
5
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
6
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
7
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
8
+ const now = () => new Date().toISOString();
9
+
10
+ function flag(args, names) {
11
+ for (const n of names) {
12
+ const i = args.indexOf(n);
13
+ if (i !== -1) {
14
+ const v = args[i + 1];
15
+ return v && !v.startsWith("-") ? v : true;
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ function statusPathOf(args) {
22
+ const p = flag(args, ["--path", "-p"]);
23
+ if (typeof p === "string") return path.resolve(process.cwd(), p);
24
+ const dir = flag(args, ["--dir"]);
25
+ return path.resolve(process.cwd(), typeof dir === "string" ? dir : ".conductor", "status.json");
26
+ }
27
+
28
+ function positionals(args) {
29
+ const out = [];
30
+ for (let i = 0; i < args.length; i++) {
31
+ if (args[i].startsWith("-")) {
32
+ const v = args[i + 1];
33
+ if (v && !v.startsWith("-")) i++; // skip flag value
34
+ continue;
35
+ }
36
+ out.push(args[i]);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function load(sp) {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(sp, "utf8"));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function save(sp, s) {
49
+ fs.mkdirSync(path.dirname(sp), { recursive: true });
50
+ fs.writeFileSync(sp, JSON.stringify(s, null, 2));
51
+ }
52
+ const fail = (msg) => {
53
+ console.error(red(`✗ ${msg}`));
54
+ return false;
55
+ };
56
+ const ok = (msg) => {
57
+ console.log(green(`✓ ${msg}`));
58
+ return true;
59
+ };
60
+
61
+ // conductor-board step <id> <pending|running|done|failed> [--goal "..."]
62
+ export async function runStep(args) {
63
+ const sp = statusPathOf(args);
64
+ const [id, status] = positionals(args);
65
+ if (!id || !status) return fail("usage: conductor-board step <id> <running|done|failed>");
66
+ const s = load(sp);
67
+ if (!s) return fail(`no status.json at ${path.relative(process.cwd(), sp)} — run status-init first`);
68
+ const step = (s.steps[id] = s.steps[id] || { attempt: 1 });
69
+ step.status = status;
70
+ if (status === "running") {
71
+ step.started_at = step.started_at || now();
72
+ step.gate = step.gate && step.gate !== "passed" ? step.gate : "pending";
73
+ s.current_step = id;
74
+ const g = flag(args, ["--goal"]);
75
+ if (typeof g === "string") s.current_step_goal = g;
76
+ } else if (status === "done") {
77
+ step.completed_at = now();
78
+ if (!step.gate || step.gate === "pending" || step.gate === "checking") step.gate = "passed";
79
+ } else if (status === "failed") {
80
+ step.completed_at = now();
81
+ step.gate = "failed";
82
+ }
83
+ save(sp, s);
84
+ return ok(`${id} → ${status}`);
85
+ }
86
+
87
+ // conductor-board gate <id> <checking|passed|failed>
88
+ export async function runGate(args) {
89
+ const sp = statusPathOf(args);
90
+ const [id, gate] = positionals(args);
91
+ if (!id || !gate) return fail("usage: conductor-board gate <id> <checking|passed|failed>");
92
+ const s = load(sp);
93
+ if (!s || !s.steps[id]) return fail(`status.json has no step "${id}"`);
94
+ s.steps[id].gate = gate;
95
+ save(sp, s);
96
+ return ok(`${id} gate → ${gate}`);
97
+ }
98
+
99
+ // conductor-board heartbeat <id> "note" [--iteration X --insight-type T --insight-seed S --final --to STEP]
100
+ export async function runHeartbeat(args) {
101
+ const sp = statusPathOf(args);
102
+ const [id, note] = positionals(args);
103
+ if (!id || !note) return fail('usage: conductor-board heartbeat <id> "note" [flags]');
104
+ const s = load(sp);
105
+ if (!s) return fail("no status.json — run status-init first");
106
+ const step = (s.steps[id] = s.steps[id] || { attempt: 1 });
107
+ const entry = { at: now(), note };
108
+ const it = flag(args, ["--iteration"]);
109
+ if (typeof it === "string") entry.iteration = it;
110
+ const itype = flag(args, ["--insight-type"]);
111
+ if (typeof itype === "string") {
112
+ entry.insight = {
113
+ type: itype,
114
+ seed: typeof flag(args, ["--insight-seed"]) === "string" ? flag(args, ["--insight-seed"]) : note,
115
+ step: id,
116
+ confidence: typeof flag(args, ["--insight-confidence"]) === "string" ? flag(args, ["--insight-confidence"]) : "medium",
117
+ };
118
+ }
119
+ if (args.includes("--final")) {
120
+ entry.finalBeat = true;
121
+ const to = flag(args, ["--to"]);
122
+ if (typeof to === "string") entry.handoff = { to };
123
+ }
124
+ (step.heartbeat = step.heartbeat || []).push(entry);
125
+ save(sp, s);
126
+ return ok(`${id} ♥ ${note.length > 50 ? note.slice(0, 50) + "…" : note}`);
127
+ }
128
+
129
+ // conductor-board loop <loopId> <item> <subId> <status>
130
+ export async function runLoop(args) {
131
+ const sp = statusPathOf(args);
132
+ const [loopId, item, subId, status] = positionals(args);
133
+ if (!loopId || !item || !subId || !status)
134
+ return fail("usage: conductor-board loop <loopId> <item> <subId> <pending|running|done|failed>");
135
+ const s = load(sp);
136
+ if (!s) return fail("no status.json");
137
+ const lp = (s.steps[loopId] = s.steps[loopId] || { type: "loop", iterations: {} });
138
+ lp.type = "loop";
139
+ lp.iterations = lp.iterations || {};
140
+ const iter = (lp.iterations[item] = lp.iterations[item] || {});
141
+ const cell = (iter[subId] = iter[subId] || { attempt: 1 });
142
+ cell.status = status;
143
+ if (status === "done") cell.gate = "passed";
144
+ if (status === "running") {
145
+ lp.current_item = item;
146
+ lp.status = "running";
147
+ s.current_step = loopId;
148
+ }
149
+ // recompute completed
150
+ lp.completed = Object.values(lp.iterations).filter((sub) =>
151
+ Object.values(sub).every((c) => c.status === "done"),
152
+ ).length;
153
+ if (lp.total && lp.completed >= lp.total) lp.status = "done";
154
+ save(sp, s);
155
+ return ok(`${loopId}/${item}/${subId} → ${status}`);
156
+ }
157
+
158
+ // conductor-board status-init <conductor.yaml> [--run-id ID]
159
+ export async function runStatusInit(args) {
160
+ const [conductorPath] = positionals(args);
161
+ if (!conductorPath) return fail("usage: conductor-board status-init <conductor.yaml>");
162
+ let doc;
163
+ try {
164
+ doc = yaml.load(fs.readFileSync(path.resolve(process.cwd(), conductorPath), "utf8"));
165
+ } catch (e) {
166
+ return fail(`could not read conductor: ${e.message}`);
167
+ }
168
+ const sp = statusPathOf(args);
169
+ const runId =
170
+ (typeof flag(args, ["--run-id"]) === "string" && flag(args, ["--run-id"])) ||
171
+ now().replace(/\.\d+Z$/, "").replace(/:/g, "-");
172
+ const steps = {};
173
+ for (const st of doc.steps || []) {
174
+ if (!st || !st.id) continue;
175
+ steps[st.id] =
176
+ st.type === "loop"
177
+ ? { status: "pending", type: "loop", total: 0, completed: 0, iterations: {} }
178
+ : { status: "pending", gate: "pending", attempt: 1 };
179
+ }
180
+ const status = {
181
+ workflow: doc.name || "workflow",
182
+ run_id: runId,
183
+ status: "running",
184
+ goal: (doc.description || "").trim().replace(/\s+/g, " "),
185
+ current_step: null,
186
+ started_at: now(),
187
+ steps,
188
+ };
189
+ save(sp, status);
190
+ return ok(`status.json initialized at ${path.relative(process.cwd(), sp)} (${Object.keys(steps).length} steps)`);
191
+ }