conductor-board 2.0.0 → 2.2.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 +73 -13
- package/cli/complete.js +47 -6
- package/cli/knowledge.js +109 -0
- package/cli/setup.js +19 -8
- package/cli/validate.js +2 -0
- package/cli/writer.js +245 -4
- package/dist/assets/index-DvTrz5lj.js +34 -0
- package/dist/assets/index-ilfk-igS.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/server.js +183 -54
- package/dist/assets/index-BIA1HyJ3.js +0 -34
- package/dist/assets/index-Di1Pl_pH.css +0 -1
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,6 +212,118 @@ export async function runLoop(args) {
|
|
|
155
212
|
return ok(`${loopId}/${item}/${subId} → ${status}`);
|
|
156
213
|
}
|
|
157
214
|
|
|
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.
|
|
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
|
+
}
|
|
235
|
+
const sp = statusPathOf(args);
|
|
236
|
+
const [title] = positionals(args);
|
|
237
|
+
if (!title) return fail('usage: conductor-board suggest "title" --scope this-conductor');
|
|
238
|
+
const str = (names, def) => {
|
|
239
|
+
const v = flag(args, names);
|
|
240
|
+
return typeof v === "string" ? v : def;
|
|
241
|
+
};
|
|
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, {
|
|
255
|
+
title,
|
|
256
|
+
scope,
|
|
257
|
+
step: str(["--step"], undefined),
|
|
258
|
+
type: str(["--type"], undefined),
|
|
259
|
+
current: str(["--current"], undefined),
|
|
260
|
+
proposed: str(["--proposed"], undefined),
|
|
261
|
+
note: str(["--note"], undefined),
|
|
262
|
+
});
|
|
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;
|
|
325
|
+
}
|
|
326
|
+
|
|
158
327
|
// conductor-board status-init <conductor.yaml> [--run-id ID]
|
|
159
328
|
export async function runStatusInit(args) {
|
|
160
329
|
const [conductorPath] = positionals(args);
|
|
@@ -170,6 +339,73 @@ export async function runStatusInit(args) {
|
|
|
170
339
|
(typeof flag(args, ["--run-id"]) === "string" && flag(args, ["--run-id"])) ||
|
|
171
340
|
now().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
172
341
|
const steps = {};
|
|
342
|
+
|
|
343
|
+
// Phase 0 (§10.2): auto-inject improvement cards from PROVEN this-conductor
|
|
344
|
+
// knowledge BEFORE the workflow steps. Entries with current/proposed apply
|
|
345
|
+
// automatically; structural ones (new_step/remove_step/reorder) are flagged
|
|
346
|
+
// for human approval. A _validate card closes the phase.
|
|
347
|
+
const STRUCTURAL = new Set(["new_step", "remove_step", "reorder"]);
|
|
348
|
+
const knowledge = (Array.isArray(doc.knowledge) ? doc.knowledge : []).filter(
|
|
349
|
+
(k) => k && typeof k === "object" && k.title,
|
|
350
|
+
);
|
|
351
|
+
const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
352
|
+
const seen = new Set();
|
|
353
|
+
|
|
354
|
+
// _improve::read-knowledge leads the phase: read + categorize the knowledge.
|
|
355
|
+
if (knowledge.length > 0) {
|
|
356
|
+
const cat = (s) => knowledge.filter((k) => (k.status || "emerging") === s).length;
|
|
357
|
+
const cross = knowledge.filter((k) => (k.scope || "this-conductor") !== "this-conductor").length;
|
|
358
|
+
steps["_improve::read-knowledge"] = {
|
|
359
|
+
status: "pending",
|
|
360
|
+
gate: "pending",
|
|
361
|
+
attempt: 1,
|
|
362
|
+
improve: {
|
|
363
|
+
title: "Read knowledge",
|
|
364
|
+
kind: "read-knowledge",
|
|
365
|
+
note:
|
|
366
|
+
`${cat("proven")} proven · ${cat("emerging")} emerging · ${cat("applied")} applied · ` +
|
|
367
|
+
`${cross} cross-cutting`,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let improvements = 0;
|
|
373
|
+
for (const k of knowledge) {
|
|
374
|
+
if ((k.status || "emerging") !== "proven") continue;
|
|
375
|
+
if ((k.scope || "this-conductor") !== "this-conductor") continue;
|
|
376
|
+
const structural = STRUCTURAL.has(k.type);
|
|
377
|
+
const textChange = k.current && k.proposed;
|
|
378
|
+
if (!structural && !textChange) continue; // proven but nothing actionable
|
|
379
|
+
let id = `_improve::${slug(k.title)}`;
|
|
380
|
+
while (seen.has(id)) id += "-x";
|
|
381
|
+
seen.add(id);
|
|
382
|
+
steps[id] = {
|
|
383
|
+
status: "pending",
|
|
384
|
+
gate: "pending",
|
|
385
|
+
attempt: 1,
|
|
386
|
+
improve: {
|
|
387
|
+
step: k.step,
|
|
388
|
+
title: k.title,
|
|
389
|
+
current: k.current,
|
|
390
|
+
proposed: k.proposed,
|
|
391
|
+
note: k.note,
|
|
392
|
+
observed: k.observed || 1,
|
|
393
|
+
scope: k.scope || "this-conductor",
|
|
394
|
+
structural,
|
|
395
|
+
kind: k.type || "instruction",
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
improvements += 1;
|
|
399
|
+
}
|
|
400
|
+
if (improvements > 0) {
|
|
401
|
+
steps["_improve::validate"] = {
|
|
402
|
+
status: "pending",
|
|
403
|
+
gate: "pending",
|
|
404
|
+
attempt: 1,
|
|
405
|
+
improve: { title: "Validate conductor", kind: "validate" },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
173
409
|
for (const st of doc.steps || []) {
|
|
174
410
|
if (!st || !st.id) continue;
|
|
175
411
|
steps[st.id] =
|
|
@@ -187,5 +423,10 @@ export async function runStatusInit(args) {
|
|
|
187
423
|
steps,
|
|
188
424
|
};
|
|
189
425
|
save(sp, status);
|
|
190
|
-
|
|
426
|
+
const workflowCount = (doc.steps || []).filter((s) => s && s.id).length;
|
|
427
|
+
return ok(
|
|
428
|
+
`status.json initialized (${workflowCount} steps` +
|
|
429
|
+
(improvements ? `, ${improvements} Phase 0 improvement${improvements === 1 ? "" : "s"}` : "") +
|
|
430
|
+
`)`,
|
|
431
|
+
);
|
|
191
432
|
}
|