conductor-board 1.0.0 → 1.3.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/README.md CHANGED
@@ -42,7 +42,7 @@ npx conductor-board --help # all commands + options
42
42
  | `--path`, `-p` | `.conductor/status.json` | Path to the status file |
43
43
  | `--conductor`, `-c` | auto-discovered | Path to the conductor `.yaml` |
44
44
  | `--port` | `3042` | Port to serve on (walks forward if taken) |
45
- | `--no-open` | — | Don't open the browser |
45
+ | `--no-open` | — | Don't open the browser — CI / headless only (it opens by default) |
46
46
  | `--help`, `-h` | — | Show help |
47
47
 
48
48
  ```bash
package/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import readline from "node:readline";
5
6
  import { startServer } from "../server/server.js";
6
7
 
7
8
  const argv = process.argv.slice(2);
@@ -35,7 +36,8 @@ const HELP = `
35
36
  --path, -p <file> Path to status.json (default: .conductor/status.json)
36
37
  --conductor, -c <file> Path to the conductor (default: auto-discovered)
37
38
  --port <n> Port to serve on (default: 3042)
38
- --no-open Don't open the browser
39
+ --no-open Don't open the browser (CI / headless only;
40
+ the board opens your browser by default)
39
41
 
40
42
  init options
41
43
  --name, -n <name> Workflow name (skips the prompts)
@@ -116,6 +118,91 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
116
118
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
117
119
  const iris = (s) => `\x1b[38;5;141m${s}\x1b[0m`;
118
120
  const mint = (s) => `\x1b[38;5;78m${s}\x1b[0m`;
121
+ const amber = (s) => `\x1b[38;5;214m${s}\x1b[0m`;
122
+
123
+ function ago(iso) {
124
+ const t = new Date(iso).getTime();
125
+ if (Number.isNaN(t)) return "";
126
+ const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
127
+ if (s < 60) return `${s}s ago`;
128
+ const m = Math.floor(s / 60);
129
+ if (m < 60) return `${m}m ago`;
130
+ const h = Math.floor(m / 60);
131
+ if (h < 24) return `${h}h ago`;
132
+ return `${Math.floor(h / 24)}d ago`;
133
+ }
134
+
135
+ function ask(question) {
136
+ return new Promise((resolve) => {
137
+ if (!process.stdin.isTTY) return resolve(false); // non-interactive — never block
138
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
139
+ rl.question(question, (answer) => {
140
+ rl.close();
141
+ resolve(/^y(es)?$/i.test(answer.trim()));
142
+ });
143
+ });
144
+ }
145
+
146
+ // A pid is "alive" if signalling it doesn't throw ESRCH (EPERM = alive, not ours).
147
+ function pidAlive(pid) {
148
+ try {
149
+ process.kill(pid, 0);
150
+ return true;
151
+ } catch (e) {
152
+ return e.code === "EPERM";
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Before binding a port, look for a board that's already running here. A live
158
+ * one we offer to reclaim; a dead pid leaves a stale server.json we just clear.
159
+ */
160
+ async function preflightStaleBoard(serverJsonPath) {
161
+ let info;
162
+ try {
163
+ info = JSON.parse(fs.readFileSync(serverJsonPath, "utf8"));
164
+ } catch {
165
+ return; // no (readable) server.json — nothing to do
166
+ }
167
+ const pid = info && info.pid;
168
+ const clear = () => {
169
+ try {
170
+ fs.unlinkSync(serverJsonPath);
171
+ } catch {
172
+ /* already gone */
173
+ }
174
+ };
175
+
176
+ if (!pid || !pidAlive(pid)) {
177
+ if (pid) {
178
+ clear();
179
+ console.log(dim(`\n cleared a stale server.json (pid ${pid} is no longer running).`));
180
+ }
181
+ return;
182
+ }
183
+
184
+ const port = info.port ?? "?";
185
+ const when = info.started_at ? `, started ${ago(info.started_at)}` : "";
186
+ console.log("");
187
+ console.log(` ${amber("⚠")} A board is already running on port ${bold(port)} (pid ${pid}${when}).`);
188
+ const yes = await ask(` Kill it and start fresh? ${dim("[y/N]")} `);
189
+ if (yes) {
190
+ try {
191
+ process.kill(pid, "SIGTERM");
192
+ } catch {
193
+ /* it may have just exited */
194
+ }
195
+ await new Promise((r) => setTimeout(r, 600)); // let it release the port
196
+ clear();
197
+ console.log(dim(` stopped pid ${pid}.`));
198
+ } else {
199
+ console.log(dim(` leaving it running — starting on the next free port.`));
200
+ }
201
+ }
202
+
203
+ await preflightStaleBoard(
204
+ path.join(path.dirname(path.resolve(process.cwd(), statusPath)), "server.json"),
205
+ );
119
206
 
120
207
  const { conductorPath: resolvedConductor, absStatus, server, serverJsonPath } =
121
208
  await listenWithFallback(wantedPort);
package/cli/setup.js CHANGED
@@ -27,8 +27,9 @@ steps:
27
27
 
28
28
  - id: start-board
29
29
  instruction: |
30
- Start the board server in the background:
31
- npx conductor-board &
30
+ Start the board server in the background. It opens the browser
31
+ automatically — do NOT pass --no-open here, that defeats the live view.
32
+ npx conductor-board >/tmp/conductor-board.log 2>&1 &
32
33
  Wait ~3 seconds for it to initialize. It auto-detects a free port if 3042
33
34
  is taken and records the chosen port in .conductor/server.json.
34
35
  requires: [preflight]
@@ -74,9 +75,22 @@ steps:
74
75
  instruction: |
75
76
  Execute the generated conductor workflow.
76
77
  Create .conductor/status.json with all steps pending and a timestamp run_id.
78
+ Set the top-level "goal" from the conductor's description, and refresh
79
+ "current_step_goal" each time current_step changes.
77
80
  Walk each step in order, updating status.json after every step and gate
78
- change. Retry on gate failure — never skip. Set the top-level status to
79
- "done" when the last step completes.
81
+ change. Retry on gate failure — never skip.
82
+ At least once per minute, append a heartbeat {at, note} to the current
83
+ step's heartbeat array (read prior entries first; orient against the gate
84
+ AND the goal; use [text](url) links for any PRs or pages you produce).
85
+ Before marking each step done, append a finalBeat — {at, note, finalBeat:
86
+ true, handoff: {to, context, produced}} — summarizing the step and handing
87
+ off to the next; read the previous step's finalBeat before you start one.
88
+ For loop steps, update "completed" and the "iterations" object as EACH
89
+ iteration finishes — don't wait until the loop ends.
90
+ Tag heartbeats with an "insight" object when you spot a way to improve the
91
+ workflow. Before setting status to "done", review your insight-tagged
92
+ heartbeats and write 3-5 optimization "suggestions" to status.json.
93
+ Set the top-level status to "done" when the last step completes.
80
94
  requires: [convert-to-conductor]
81
95
  gate:
82
96
  - name: "Status file exists"