conductor-board 2.1.0 → 2.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/bin/cli.js +64 -14
- package/cli/complete.js +12 -1
- package/cli/knowledge.js +109 -0
- package/cli/setup.js +19 -10
- package/cli/validate.js +2 -0
- package/cli/writer.js +257 -18
- package/dist/assets/index-B-Kwn4YB.js +34 -0
- package/dist/assets/index-a4RLv680.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/server.js +193 -55
- package/dist/assets/index-D0azgMCk.css +0 -1
- package/dist/assets/index-DV9PNUB3.js +0 -34
package/cli/writer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
|
+
import { discoverConductor, loadConductor, mergeKnowledge, saveConductor, SCOPES } from "./knowledge.js";
|
|
4
5
|
|
|
5
6
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
6
7
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
@@ -96,7 +97,13 @@ export async function runGate(args) {
|
|
|
96
97
|
return ok(`${id} gate → ${gate}`);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
// conductor-board heartbeat <id> "note" [--iteration X --
|
|
100
|
+
// conductor-board heartbeat <id> "note" [--iteration X --sub Y --insight-type T
|
|
101
|
+
// --insight-seed S --insight-scope SC --final --to STEP]
|
|
102
|
+
//
|
|
103
|
+
// For a loop sub-step (--iteration AND --sub), the beat is written to the
|
|
104
|
+
// sub-step cell AND bubbled up to the loop parent's heartbeat array (tagged with
|
|
105
|
+
// iteration + sub) so the monitor and freeball banner — which read top-level
|
|
106
|
+
// arrays — see every level of activity without the agent beating twice.
|
|
100
107
|
export async function runHeartbeat(args) {
|
|
101
108
|
const sp = statusPathOf(args);
|
|
102
109
|
const [id, note] = positionals(args);
|
|
@@ -104,15 +111,19 @@ export async function runHeartbeat(args) {
|
|
|
104
111
|
const s = load(sp);
|
|
105
112
|
if (!s) return fail("no status.json — run status-init first");
|
|
106
113
|
const step = (s.steps[id] = s.steps[id] || { attempt: 1 });
|
|
114
|
+
|
|
107
115
|
const entry = { at: now(), note };
|
|
108
116
|
const it = flag(args, ["--iteration"]);
|
|
109
117
|
if (typeof it === "string") entry.iteration = it;
|
|
118
|
+
const sub = flag(args, ["--sub"]);
|
|
119
|
+
if (typeof sub === "string") entry.sub = sub;
|
|
110
120
|
const itype = flag(args, ["--insight-type"]);
|
|
111
121
|
if (typeof itype === "string") {
|
|
112
122
|
entry.insight = {
|
|
113
123
|
type: itype,
|
|
114
124
|
seed: typeof flag(args, ["--insight-seed"]) === "string" ? flag(args, ["--insight-seed"]) : note,
|
|
115
125
|
step: id,
|
|
126
|
+
scope: typeof flag(args, ["--insight-scope"]) === "string" ? flag(args, ["--insight-scope"]) : "this-conductor",
|
|
116
127
|
confidence: typeof flag(args, ["--insight-confidence"]) === "string" ? flag(args, ["--insight-confidence"]) : "medium",
|
|
117
128
|
};
|
|
118
129
|
}
|
|
@@ -121,9 +132,55 @@ export async function runHeartbeat(args) {
|
|
|
121
132
|
const to = flag(args, ["--to"]);
|
|
122
133
|
if (typeof to === "string") entry.handoff = { to };
|
|
123
134
|
}
|
|
124
|
-
|
|
135
|
+
|
|
136
|
+
if (typeof it === "string" && typeof sub === "string") {
|
|
137
|
+
// Sub-step beat bubbled to the loop parent's array (tagged iteration + sub).
|
|
138
|
+
// The board reads top-level arrays for the monitor and the freeball banner,
|
|
139
|
+
// and the iteration cards filter this array by iteration/sub — so one write
|
|
140
|
+
// lights up every level. (We write to the parent only, never also the cell,
|
|
141
|
+
// to avoid double-counting since the readers aggregate the whole tree.)
|
|
142
|
+
step.type = "loop";
|
|
143
|
+
step.iterations = step.iterations || {};
|
|
144
|
+
const iter = (step.iterations[it] = step.iterations[it] || {});
|
|
145
|
+
iter[sub] = iter[sub] || { attempt: 1 };
|
|
146
|
+
(step.heartbeat = step.heartbeat || []).push(entry);
|
|
147
|
+
} else {
|
|
148
|
+
(step.heartbeat = step.heartbeat || []).push(entry);
|
|
149
|
+
}
|
|
150
|
+
save(sp, s);
|
|
151
|
+
return ok(`${id}${typeof sub === "string" ? `/${it}/${sub}` : ""} ♥ ${note.length > 50 ? note.slice(0, 50) + "…" : note}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// conductor-board loop-scope <loopId> <item...> [--note "..."]
|
|
155
|
+
//
|
|
156
|
+
// Frontload a loop's whole iteration list as pending the moment it's determined
|
|
157
|
+
// (§6.2): writes every item into the iterations map and sets total, so the board
|
|
158
|
+
// shows the full plan before any card moves. Also appends a "scope beat" naming
|
|
159
|
+
// the items, unless --note is given.
|
|
160
|
+
export async function runLoopScope(args) {
|
|
161
|
+
const sp = statusPathOf(args);
|
|
162
|
+
const [loopId, ...items] = positionals(args);
|
|
163
|
+
if (!loopId || items.length === 0)
|
|
164
|
+
return fail("usage: conductor-board loop-scope <loopId> <item1> <item2> … [--note \"...\"]");
|
|
165
|
+
const s = load(sp);
|
|
166
|
+
if (!s) return fail("no status.json — run status-init first");
|
|
167
|
+
const lp = (s.steps[loopId] = s.steps[loopId] || { type: "loop", iterations: {} });
|
|
168
|
+
lp.type = "loop";
|
|
169
|
+
lp.iterations = lp.iterations || {};
|
|
170
|
+
for (const item of items) {
|
|
171
|
+
lp.iterations[item] = lp.iterations[item] || {}; // sub-steps materialize as work begins
|
|
172
|
+
}
|
|
173
|
+
lp.total = items.length;
|
|
174
|
+
lp.completed = lp.completed || 0;
|
|
175
|
+
if (lp.status !== "running") lp.status = lp.status || "pending";
|
|
176
|
+
const noteFlag = flag(args, ["--note"]);
|
|
177
|
+
const note =
|
|
178
|
+
typeof noteFlag === "string"
|
|
179
|
+
? noteFlag
|
|
180
|
+
: `${items.length} scoped: ${items.join(", ")}. All pending.`;
|
|
181
|
+
(lp.heartbeat = lp.heartbeat || []).push({ at: now(), note });
|
|
125
182
|
save(sp, s);
|
|
126
|
-
return ok(`${
|
|
183
|
+
return ok(`${loopId} scoped — ${items.length} iterations frontloaded`);
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
// conductor-board loop <loopId> <item> <subId> <status>
|
|
@@ -155,31 +212,116 @@ export async function runLoop(args) {
|
|
|
155
212
|
return ok(`${loopId}/${item}/${subId} → ${status}`);
|
|
156
213
|
}
|
|
157
214
|
|
|
158
|
-
// conductor-board suggest "title" --type T --step S --
|
|
215
|
+
// conductor-board suggest "title" --scope SC [--type T --step S --current X
|
|
216
|
+
// --proposed Y --note Z --conductor <file>]
|
|
217
|
+
//
|
|
218
|
+
// Writes the learning straight into the conductor file's knowledge: section —
|
|
219
|
+
// the conductor IS the knowledge base (§10.5). --scope is REQUIRED and routes
|
|
220
|
+
// the insight: this-conductor (auto-appliable in Phase 0) | upstream | template |
|
|
221
|
+
// tooling | corpus. A repeat sighting bumps `observed` and escalates the status
|
|
222
|
+
// (emerging → proven at 3×). Structural types (new_step/remove_step/reorder)
|
|
223
|
+
// need human approval, so they never auto-apply.
|
|
159
224
|
export async function runSuggest(args) {
|
|
225
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
226
|
+
console.log(
|
|
227
|
+
'usage: conductor-board suggest "title" --scope <scope> [--type <kind>] [--step <id>]\n' +
|
|
228
|
+
" [--current X] [--proposed Y] [--note Z] [--conductor <file>]\n" +
|
|
229
|
+
` --scope (required): ${SCOPES.join(" | ")}\n` +
|
|
230
|
+
" Appends to the conductor's knowledge: section. this-conductor insights with\n" +
|
|
231
|
+
" current/proposed auto-apply once proven; structural types need approval.",
|
|
232
|
+
);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
160
235
|
const sp = statusPathOf(args);
|
|
161
236
|
const [title] = positionals(args);
|
|
162
|
-
if (!title) return fail('usage: conductor-board suggest "title" --
|
|
163
|
-
const s = load(sp);
|
|
164
|
-
if (!s) return fail("no status.json — run status-init first");
|
|
237
|
+
if (!title) return fail('usage: conductor-board suggest "title" --scope this-conductor');
|
|
165
238
|
const str = (names, def) => {
|
|
166
239
|
const v = flag(args, names);
|
|
167
240
|
return typeof v === "string" ? v : def;
|
|
168
241
|
};
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
const scope = str(["--scope"], undefined);
|
|
243
|
+
if (!scope) return fail("--scope is required (this-conductor | upstream | template | tooling | corpus)");
|
|
244
|
+
if (!SCOPES.includes(scope)) return fail(`--scope must be one of: ${SCOPES.join(", ")}`);
|
|
245
|
+
|
|
246
|
+
const conductorPath = discoverConductor(sp, str(["--conductor", "-c"], undefined));
|
|
247
|
+
if (!conductorPath) return fail("no conductor file found next to status.json or in cwd");
|
|
248
|
+
let doc;
|
|
249
|
+
try {
|
|
250
|
+
doc = loadConductor(conductorPath);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
return fail(`could not parse conductor: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
const merged = mergeKnowledge(doc, {
|
|
172
255
|
title,
|
|
173
|
-
|
|
256
|
+
scope,
|
|
174
257
|
step: str(["--step"], undefined),
|
|
175
|
-
|
|
176
|
-
rationale: str(["--rationale"], undefined),
|
|
258
|
+
type: str(["--type"], undefined),
|
|
177
259
|
current: str(["--current"], undefined),
|
|
178
260
|
proposed: str(["--proposed"], undefined),
|
|
179
|
-
|
|
261
|
+
note: str(["--note"], undefined),
|
|
180
262
|
});
|
|
181
|
-
|
|
182
|
-
|
|
263
|
+
const res = saveConductor(conductorPath, doc);
|
|
264
|
+
if (!res.ok) return fail(`knowledge not written — ${res.error}`);
|
|
265
|
+
return ok(
|
|
266
|
+
`knowledge [${scope}] "${title.length > 46 ? title.slice(0, 46) + "…" : title}" — ${merged.status} (observed ${merged.observed}×)`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// conductor-board knowledge [list] [--min N] [--scope SC] [--status ST] [--conductor file]
|
|
271
|
+
//
|
|
272
|
+
// With --min, exits 0 when the conductor holds at least N knowledge entries
|
|
273
|
+
// (use as the final-step "captured learnings" gate). With `list`, prints them.
|
|
274
|
+
export async function runKnowledge(args) {
|
|
275
|
+
const sp = statusPathOf(args);
|
|
276
|
+
const str = (names) => {
|
|
277
|
+
const v = flag(args, names);
|
|
278
|
+
return typeof v === "string" ? v : undefined;
|
|
279
|
+
};
|
|
280
|
+
const conductorPath = discoverConductor(sp, str(["--conductor", "-c"]));
|
|
281
|
+
if (!conductorPath) return fail("no conductor file found");
|
|
282
|
+
let doc;
|
|
283
|
+
try {
|
|
284
|
+
doc = loadConductor(conductorPath);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return fail(`could not parse conductor: ${e.message}`);
|
|
287
|
+
}
|
|
288
|
+
const all = (Array.isArray(doc.knowledge) ? doc.knowledge : []).filter(
|
|
289
|
+
(k) => k && typeof k === "object" && k.title,
|
|
290
|
+
);
|
|
291
|
+
const scope = str(["--scope"]);
|
|
292
|
+
const st = str(["--status"]);
|
|
293
|
+
const filtered = all.filter(
|
|
294
|
+
(k) =>
|
|
295
|
+
(!scope || (k.scope || "this-conductor") === scope) &&
|
|
296
|
+
(!st || (k.status || "emerging") === st),
|
|
297
|
+
);
|
|
298
|
+
// Quality gate (§3.5): enforce captured learnings by VALUE, not count.
|
|
299
|
+
// --min N at least N knowledge entries
|
|
300
|
+
// --min-scopes M entries span at least M distinct scopes (forces the
|
|
301
|
+
// cross-cutting reflection — the highest-leverage insights)
|
|
302
|
+
const min = str(["--min"]);
|
|
303
|
+
const minScopes = str(["--min-scopes"]);
|
|
304
|
+
if (min !== undefined || minScopes !== undefined) {
|
|
305
|
+
const n = min !== undefined ? Number(min) || 0 : 0;
|
|
306
|
+
const distinctScopes = new Set(filtered.map((k) => k.scope || "this-conductor")).size;
|
|
307
|
+
const m = minScopes !== undefined ? Number(minScopes) || 0 : 0;
|
|
308
|
+
const countOk = filtered.length >= n;
|
|
309
|
+
const scopesOk = distinctScopes >= m;
|
|
310
|
+
if (countOk && scopesOk)
|
|
311
|
+
return ok(`knowledge: ${filtered.length} entr${filtered.length === 1 ? "y" : "ies"}, ${distinctScopes} scope${distinctScopes === 1 ? "" : "s"} (ok)`);
|
|
312
|
+
const why = [];
|
|
313
|
+
if (!countOk) why.push(`need ≥ ${n} entries (have ${filtered.length})`);
|
|
314
|
+
if (!scopesOk) why.push(`need ≥ ${m} scopes (have ${distinctScopes})`);
|
|
315
|
+
return fail(
|
|
316
|
+
`knowledge gate: ${why.join(", ")} — capture what you learned, including cross-cutting:\n` +
|
|
317
|
+
' conductor-board suggest "…" --scope upstream|template|tooling|corpus',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (filtered.length === 0) console.log(dim(" (no knowledge yet)"));
|
|
321
|
+
for (const k of filtered) {
|
|
322
|
+
console.log(` ${k.status || "emerging"} · ${k.scope || "this-conductor"} · ${k.observed || 1}× — ${k.title}`);
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
183
325
|
}
|
|
184
326
|
|
|
185
327
|
// conductor-board status-init <conductor.yaml> [--run-id ID]
|
|
@@ -196,7 +338,97 @@ export async function runStatusInit(args) {
|
|
|
196
338
|
const runId =
|
|
197
339
|
(typeof flag(args, ["--run-id"]) === "string" && flag(args, ["--run-id"])) ||
|
|
198
340
|
now().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
341
|
+
const wfName = doc.name || "workflow";
|
|
342
|
+
|
|
343
|
+
// §6.2 — every run gets a human name: {workflow}-run-{N}-{timestamp}. N is the
|
|
344
|
+
// count of archived runs + 1; the timestamp is the run id trimmed to minutes.
|
|
345
|
+
const nameSlug = String(wfName).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
346
|
+
const historyDir = path.join(path.dirname(sp), "history");
|
|
347
|
+
let priorRuns = 0;
|
|
348
|
+
try {
|
|
349
|
+
priorRuns = fs.readdirSync(historyDir).filter((f) => f.endsWith(".json")).length;
|
|
350
|
+
} catch {
|
|
351
|
+
/* no history yet */
|
|
352
|
+
}
|
|
353
|
+
const tsShort = runId.replace(/-\d{2}$/, ""); // 2026-06-04T12-30-00 → 2026-06-04T12-30
|
|
354
|
+
const runName =
|
|
355
|
+
(typeof flag(args, ["--run-name"]) === "string" && flag(args, ["--run-name"])) ||
|
|
356
|
+
`${nameSlug}-run-${priorRuns + 1}-${tsShort}`;
|
|
357
|
+
|
|
358
|
+
// §6.1 — auto_improve (default on). When off, the Phase 0 self-improvement
|
|
359
|
+
// pass is fully disabled: no improvement cards injected at all.
|
|
360
|
+
const autoImprove = doc.auto_improve !== false;
|
|
361
|
+
|
|
199
362
|
const steps = {};
|
|
363
|
+
let improvements = 0;
|
|
364
|
+
|
|
365
|
+
if (autoImprove) {
|
|
366
|
+
// Phase 0 (§10.2): auto-inject improvement cards from PROVEN this-conductor
|
|
367
|
+
// knowledge BEFORE the workflow steps. Entries with current/proposed apply
|
|
368
|
+
// automatically; structural ones (new_step/remove_step/reorder) are flagged
|
|
369
|
+
// for human approval. A _validate card closes the phase.
|
|
370
|
+
const STRUCTURAL = new Set(["new_step", "remove_step", "reorder"]);
|
|
371
|
+
const knowledge = (Array.isArray(doc.knowledge) ? doc.knowledge : []).filter(
|
|
372
|
+
(k) => k && typeof k === "object" && k.title,
|
|
373
|
+
);
|
|
374
|
+
const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
|
|
377
|
+
// _improve::read-knowledge leads the phase: read + categorize the knowledge.
|
|
378
|
+
if (knowledge.length > 0) {
|
|
379
|
+
const cat = (s) => knowledge.filter((k) => (k.status || "emerging") === s).length;
|
|
380
|
+
const cross = knowledge.filter((k) => (k.scope || "this-conductor") !== "this-conductor").length;
|
|
381
|
+
steps["_improve::read-knowledge"] = {
|
|
382
|
+
status: "pending",
|
|
383
|
+
gate: "pending",
|
|
384
|
+
attempt: 1,
|
|
385
|
+
improve: {
|
|
386
|
+
title: "Read knowledge",
|
|
387
|
+
kind: "read-knowledge",
|
|
388
|
+
note:
|
|
389
|
+
`${cat("proven")} proven · ${cat("emerging")} emerging · ${cat("applied")} applied · ` +
|
|
390
|
+
`${cross} cross-cutting`,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const k of knowledge) {
|
|
396
|
+
if ((k.status || "emerging") !== "proven") continue;
|
|
397
|
+
if ((k.scope || "this-conductor") !== "this-conductor") continue;
|
|
398
|
+
const structural = STRUCTURAL.has(k.type);
|
|
399
|
+
const textChange = k.current && k.proposed;
|
|
400
|
+
if (!structural && !textChange) continue; // proven but nothing actionable
|
|
401
|
+
let id = `_improve::${slug(k.title)}`;
|
|
402
|
+
while (seen.has(id)) id += "-x";
|
|
403
|
+
seen.add(id);
|
|
404
|
+
steps[id] = {
|
|
405
|
+
status: "pending",
|
|
406
|
+
gate: "pending",
|
|
407
|
+
attempt: 1,
|
|
408
|
+
improve: {
|
|
409
|
+
step: k.step,
|
|
410
|
+
title: k.title,
|
|
411
|
+
current: k.current,
|
|
412
|
+
proposed: k.proposed,
|
|
413
|
+
note: k.note,
|
|
414
|
+
observed: k.observed || 1,
|
|
415
|
+
scope: k.scope || "this-conductor",
|
|
416
|
+
structural,
|
|
417
|
+
kind: k.type || "instruction",
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
improvements += 1;
|
|
421
|
+
}
|
|
422
|
+
if (improvements > 0) {
|
|
423
|
+
steps["_improve::validate"] = {
|
|
424
|
+
status: "pending",
|
|
425
|
+
gate: "pending",
|
|
426
|
+
attempt: 1,
|
|
427
|
+
improve: { title: "Validate conductor", kind: "validate" },
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
200
432
|
for (const st of doc.steps || []) {
|
|
201
433
|
if (!st || !st.id) continue;
|
|
202
434
|
steps[st.id] =
|
|
@@ -205,8 +437,10 @@ export async function runStatusInit(args) {
|
|
|
205
437
|
: { status: "pending", gate: "pending", attempt: 1 };
|
|
206
438
|
}
|
|
207
439
|
const status = {
|
|
208
|
-
workflow:
|
|
440
|
+
workflow: wfName,
|
|
209
441
|
run_id: runId,
|
|
442
|
+
run_name: runName,
|
|
443
|
+
auto_improve: autoImprove,
|
|
210
444
|
status: "running",
|
|
211
445
|
goal: (doc.description || "").trim().replace(/\s+/g, " "),
|
|
212
446
|
current_step: null,
|
|
@@ -214,5 +448,10 @@ export async function runStatusInit(args) {
|
|
|
214
448
|
steps,
|
|
215
449
|
};
|
|
216
450
|
save(sp, status);
|
|
217
|
-
|
|
451
|
+
const workflowCount = (doc.steps || []).filter((s) => s && s.id).length;
|
|
452
|
+
return ok(
|
|
453
|
+
`status.json initialized (${workflowCount} steps` +
|
|
454
|
+
(improvements ? `, ${improvements} Phase 0 improvement${improvements === 1 ? "" : "s"}` : "") +
|
|
455
|
+
`)`,
|
|
456
|
+
);
|
|
218
457
|
}
|