ygl-trees 0.77.3 → 0.79.1
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/CHANGELOG.md +31 -0
- package/README.md +25 -3
- package/dist/apps/cli/src/bin.js +34 -7
- package/dist/bundle/bin.cjs +794 -91
- package/dist/packages/core/database/src/db.js +73 -5
- package/dist/packages/knowledge/skills/src/cmds.js +26 -1
- package/dist/packages/ops/tools/src/agent-cmds.js +35 -0
- package/dist/packages/ops/tools/src/db-cmds.js +70 -0
- package/dist/packages/ops/tools/src/doctor.js +79 -24
- package/dist/packages/ops/tools/src/webui.js +9 -0
- package/dist/packages/research/pipeline/src/cmds.js +128 -4
- package/dist/packages/research/pipeline/src/predict.js +193 -0
- package/dist/packages/research/produce/src/produce-cmds.js +6 -0
- package/dist/packages/research/produce/src/project-cmds.js +27 -2
- package/dist/packages/techtree/model/src/cmds.js +236 -72
- package/dist/packages/techtree/model/src/reads.js +8 -0
- package/package.json +4 -3
- package/skills/ygl-trees-quickstart/SKILL.md +18 -7
- package/skills/ygl-trees-research/SKILL.md +2 -0
- package/skills/ygl-trees-techtree/SKILL.md +2 -0
|
@@ -74,10 +74,44 @@ function run(env, action, rest, args) {
|
|
|
74
74
|
.then(fn)
|
|
75
75
|
.then(print)
|
|
76
76
|
.catch((e) => {
|
|
77
|
-
(0, fmt_1.bad)(ctx, e?.message || String(e));
|
|
77
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
78
78
|
return 1;
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
+
/** Report ③-3: selection changes are now first-class events — a parallel
|
|
82
|
+
* session flipping current_tree/current_project becomes observable
|
|
83
|
+
* (agent brief surfaces it; db timeline shows the history). Session
|
|
84
|
+
* identity comes from YGL_SESSION (agents that care can set it). */
|
|
85
|
+
function logSelectionEvent(database, kind, name) {
|
|
86
|
+
try {
|
|
87
|
+
database.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
88
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
89
|
+
new Date().toISOString(),
|
|
90
|
+
process.env.YGL_SESSION || "cli",
|
|
91
|
+
kind,
|
|
92
|
+
kind === "tree.use" ? "tree" : "project",
|
|
93
|
+
name,
|
|
94
|
+
JSON.stringify({ name, session: process.env.YGL_SESSION || "cli" }),
|
|
95
|
+
"info",
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
catch { /* telemetry only — never block the selection */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Report ③ enrichment: "node not found: X" earned a cross-tree lookup —
|
|
102
|
+
* if a parallel session flipped current_tree, the key usually exists in
|
|
103
|
+
* another tree; name it instead of making the user blind-list. */
|
|
104
|
+
function enrichNodeNotFound(msg) {
|
|
105
|
+
const m = /^node not found: (.+)$/.exec(msg);
|
|
106
|
+
if (!m)
|
|
107
|
+
return msg;
|
|
108
|
+
const others = trees.nodeInOtherTrees(m[1], tid);
|
|
109
|
+
if (others.length) {
|
|
110
|
+
const names = others.map((o) => `'${o.tree}'`).join(", ");
|
|
111
|
+
return `${msg}\n key exists in other tree(s): ${names} — pin it: ygl-trees ${env.group} ... -t ${others[0].tree}`;
|
|
112
|
+
}
|
|
113
|
+
return `${msg} (current tree: '${tree?.name ?? "?"}' — node list to inspect, -t <tree> to switch)`;
|
|
114
|
+
}
|
|
81
115
|
// ---------------------------------------------------------------- tree group
|
|
82
116
|
if (env.group === "tree") {
|
|
83
117
|
const sub = action; // new | list | use
|
|
@@ -91,6 +125,7 @@ function run(env, action, rest, args) {
|
|
|
91
125
|
const proj = pid ? db.get("SELECT id FROM projects WHERE name=?", [pid]) : null;
|
|
92
126
|
return runTreeWrite((ds) => treeWrites.treeCreate(ds, name, proj?.id || ""), (t) => {
|
|
93
127
|
db.metaSet("current_tree", t.name);
|
|
128
|
+
logSelectionEvent(db, "tree.use", t.name);
|
|
94
129
|
(0, fmt_1.ack)(ctx, t, t?.duplicate === true
|
|
95
130
|
? `tree exists, selected: ${t.name} (duplicate, not re-created)`
|
|
96
131
|
: `tree created and selected: ${t.name}`);
|
|
@@ -114,6 +149,7 @@ function run(env, action, rest, args) {
|
|
|
114
149
|
return 1;
|
|
115
150
|
}
|
|
116
151
|
db.metaSet("current_tree", name);
|
|
152
|
+
logSelectionEvent(db, "tree.use", name);
|
|
117
153
|
(0, fmt_1.ack)(ctx, { tree: name }, `now using tree: ${name}`);
|
|
118
154
|
return 0;
|
|
119
155
|
}
|
|
@@ -300,56 +336,52 @@ function run(env, action, rest, args) {
|
|
|
300
336
|
}
|
|
301
337
|
if (sub === "can") {
|
|
302
338
|
if (!key) {
|
|
303
|
-
(0, fmt_1.bad)(ctx, "usage: node can <key
|
|
339
|
+
(0, fmt_1.bad)(ctx, "usage: node can <key...> [--context ctx] [--state k=v] [--state-file file] [--eval]");
|
|
304
340
|
return 2;
|
|
305
341
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.log(JSON.stringify(r, null, 2));
|
|
315
|
-
return 0;
|
|
316
|
-
}
|
|
317
|
-
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
318
|
-
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
319
|
-
if (r.missing_required.length) {
|
|
320
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
321
|
-
for (const m of r.missing_required)
|
|
322
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
323
|
-
}
|
|
324
|
-
if (r.blocked_by.length) {
|
|
325
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
326
|
-
for (const b of r.blocked_by)
|
|
327
|
-
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
328
|
-
}
|
|
329
|
-
if (r.missing_optional.length) {
|
|
330
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
331
|
-
for (const m of r.missing_optional)
|
|
332
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
333
|
-
}
|
|
334
|
-
if (r.cost_multiplier !== 1)
|
|
335
|
-
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
336
|
-
return 0;
|
|
342
|
+
// 0.79.1 (R297 user report): batch mode — multiple keys judged in ONE
|
|
343
|
+
// process (probes used to pay ~1s cold start per node), plus
|
|
344
|
+
// --state-file for JSON state bags (shared or per-node).
|
|
345
|
+
const keys = [...new Set(rest.filter(Boolean))];
|
|
346
|
+
const sharedState = parseState(args);
|
|
347
|
+
let perKeyBags = null;
|
|
348
|
+
try {
|
|
349
|
+
perKeyBags = loadStateFile(args, sharedState);
|
|
337
350
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
catch (e) {
|
|
352
|
+
const msg = e?.message || String(e);
|
|
353
|
+
(0, fmt_1.bad)(ctx, msg);
|
|
354
|
+
return /^usage:/.test(msg) ? 2 : /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
355
|
+
}
|
|
356
|
+
const stateFor = (k) => perKeyBags ? (perKeyBags[k] && typeof perKeyBags[k] === "object" && !Array.isArray(perKeyBags[k]) ? perKeyBags[k] : {}) : sharedState;
|
|
357
|
+
const ctxName = flagStr("context", "default");
|
|
358
|
+
// eval-family config read once per run (R8 credential priority:
|
|
359
|
+
// config decision_model_llm_* first, then env AI_GATEWAY_API_KEY)
|
|
345
360
|
const offline = !!process.env.YGL_EVAL_OFFLINE || (0, config_1.configGetBool)(env.db, "eval.offline");
|
|
346
361
|
const cfgToken = (0, config_1.configGet)(env.db, "decision_model_llm_token").trim();
|
|
347
362
|
const token = offline ? "" : cfgToken || (0, eval_1.evalToken)();
|
|
348
363
|
const endpoint = (0, config_1.configGet)(env.db, "decision_model_llm_api_base").trim() || eval_1.DEFAULT_ENDPOINT;
|
|
349
364
|
const passT = (0, config_1.configGetNumber)(env.db, "eval.pass_threshold");
|
|
350
365
|
const failT = (0, config_1.configGetNumber)(env.db, "eval.fail_threshold");
|
|
351
|
-
|
|
352
|
-
|
|
366
|
+
/** Judge one node: canUnlock (+ eval gates when --eval). Never throws —
|
|
367
|
+
* hard failures come back as {key, error}. */
|
|
368
|
+
const judgeOne = async (k) => {
|
|
369
|
+
let r;
|
|
370
|
+
try {
|
|
371
|
+
r = trees.canUnlock(tid, k, ctxName, stateFor(k));
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
return { key: k, error: enrichNodeNotFound(e?.message || String(e)) };
|
|
375
|
+
}
|
|
376
|
+
const state = stateFor(k);
|
|
377
|
+
const evalConds = flagBool("eval")
|
|
378
|
+
? trees.condList(tid, k).filter((c) => /^eval(-choice|-score)?$/.test(String(c.op)))
|
|
379
|
+
: [];
|
|
380
|
+
if (!evalConds.length)
|
|
381
|
+
return r;
|
|
382
|
+
// R8: decision-model evaluation for op=eval conditions.
|
|
383
|
+
// Offline-safe: no token / network failure → pending (rc=0).
|
|
384
|
+
const contextPkg = trees.evalContext(tid, k, state, ctxName);
|
|
353
385
|
const evaluated = [];
|
|
354
386
|
let usageIn = 0, usageOut = 0; // R10.4: token spend per run
|
|
355
387
|
for (const c of evalConds) {
|
|
@@ -398,7 +430,7 @@ function run(env, action, rest, args) {
|
|
|
398
430
|
r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
|
|
399
431
|
try {
|
|
400
432
|
const run = process.env.YGL_RUN_ID || "";
|
|
401
|
-
db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,'info')", [(0, text_1.shortId)(["eval.gate", "node",
|
|
433
|
+
db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,'info')", [(0, text_1.shortId)(["eval.gate", "node", k], 12), (0, text_1.nowIso)(), "cli", "eval.gate", "node", k,
|
|
402
434
|
JSON.stringify({
|
|
403
435
|
conds: evaluated.map((e) => ({ name: e.name, type: e.type, outcome: e.outcome, p: e.p ?? null, score: e.score ?? null })),
|
|
404
436
|
usage: { inputTokens: usageIn, outputTokens: usageOut },
|
|
@@ -406,37 +438,90 @@ function run(env, action, rest, args) {
|
|
|
406
438
|
})]);
|
|
407
439
|
}
|
|
408
440
|
catch { /* audit best-effort */ }
|
|
441
|
+
return r;
|
|
442
|
+
};
|
|
443
|
+
const batch = keys.length > 1;
|
|
444
|
+
return (async () => {
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const k of keys)
|
|
447
|
+
results.push(await judgeOne(k));
|
|
409
448
|
if (ctx.json) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
414
|
-
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : pendingList.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "… 待判定(决策模型 pending)", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
415
|
-
if (r.missing_required.length) {
|
|
416
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
417
|
-
for (const m of r.missing_required)
|
|
418
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
449
|
+
// single key → object (frozen shape); batch → array
|
|
450
|
+
console.log(JSON.stringify(batch ? results : results[0], null, 2));
|
|
451
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
419
452
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
(0, fmt_1.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
(0, fmt_1.out)(ctx, `
|
|
453
|
+
if (!batch) {
|
|
454
|
+
const r = results[0];
|
|
455
|
+
if (r.error) {
|
|
456
|
+
(0, fmt_1.bad)(ctx, r.error);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
if (!r.evaluated) {
|
|
460
|
+
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
461
|
+
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
462
|
+
if (r.missing_required.length) {
|
|
463
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
464
|
+
for (const m of r.missing_required)
|
|
465
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
466
|
+
}
|
|
467
|
+
if (r.blocked_by.length) {
|
|
468
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
469
|
+
for (const b of r.blocked_by)
|
|
470
|
+
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
471
|
+
}
|
|
472
|
+
if (r.missing_optional.length) {
|
|
473
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
474
|
+
for (const m of r.missing_optional)
|
|
475
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
476
|
+
}
|
|
477
|
+
if (r.cost_multiplier !== 1)
|
|
478
|
+
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
482
|
+
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : r.pending.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "… 待判定(决策模型 pending)", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
483
|
+
if (r.missing_required.length) {
|
|
484
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
485
|
+
for (const m of r.missing_required)
|
|
486
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
487
|
+
}
|
|
488
|
+
if (r.pending.length) {
|
|
489
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 决策模型 pending(灰带/未配置,非终态)", ctx.color));
|
|
490
|
+
for (const p of r.pending)
|
|
491
|
+
(0, fmt_1.out)(ctx, ` - ${p}`);
|
|
492
|
+
}
|
|
493
|
+
if (r.blocked_by.length) {
|
|
494
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
495
|
+
for (const b of r.blocked_by)
|
|
496
|
+
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
497
|
+
}
|
|
498
|
+
if (r.missing_optional.length) {
|
|
499
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
500
|
+
for (const m of r.missing_optional)
|
|
501
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
502
|
+
}
|
|
503
|
+
for (const e of r.evaluated)
|
|
504
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
|
|
505
|
+
if (r.cost_multiplier !== 1)
|
|
506
|
+
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
507
|
+
return 0;
|
|
429
508
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
509
|
+
// batch human view: one compact line per node (probe-friendly)
|
|
510
|
+
const via = perKeyBags ? "per-node --state-file" : Object.keys(sharedState).length ? "shared state" : "";
|
|
511
|
+
(0, fmt_1.head)(ctx, `can unlock × ${results.length}${via ? ` (${via})` : ""}`);
|
|
512
|
+
for (const r of results) {
|
|
513
|
+
if (r.error) {
|
|
514
|
+
(0, fmt_1.out)(ctx, ` ${(0, fmt_1.paint)(fmt_1.C.red, "✗", ctx.color)} ${r.key} ${r.error}`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const mark = r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓", ctx.color) : r.pending?.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "…", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗", ctx.color);
|
|
518
|
+
const blockers = [
|
|
519
|
+
...(r.missing_required || []).map((m) => m),
|
|
520
|
+
...(r.blocked_by || []).map((b) => `excluded:${b}`),
|
|
521
|
+
].join(", ");
|
|
522
|
+
(0, fmt_1.out)(ctx, ` ${mark} ${r.key}${r.pending?.length ? " (eval pending)" : ""}${blockers ? ` — ${blockers}` : ""}`);
|
|
434
523
|
}
|
|
435
|
-
|
|
436
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
|
|
437
|
-
if (r.cost_multiplier !== 1)
|
|
438
|
-
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
439
|
-
return 0;
|
|
524
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
440
525
|
})().catch((e) => {
|
|
441
526
|
(0, fmt_1.bad)(ctx, e?.message || String(e));
|
|
442
527
|
return 1;
|
|
@@ -444,6 +529,20 @@ function run(env, action, rest, args) {
|
|
|
444
529
|
}
|
|
445
530
|
if (sub === "ready") {
|
|
446
531
|
const state = parseState(args);
|
|
532
|
+
try {
|
|
533
|
+
// 0.79.1: --state-file flat bag works here too (per-node shape makes
|
|
534
|
+
// no sense for ready — it judges the whole tree, so flat-only).
|
|
535
|
+
const bags = loadStateFile(args, state);
|
|
536
|
+
if (bags) {
|
|
537
|
+
(0, fmt_1.bad)(ctx, "--state-file per-node shape (key→object) only applies to batch `node can k1 k2 …`; ready wants a flat counter object");
|
|
538
|
+
return 2;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
const msg = e?.message || String(e);
|
|
543
|
+
(0, fmt_1.bad)(ctx, msg);
|
|
544
|
+
return /^usage:/.test(msg) || /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
545
|
+
}
|
|
447
546
|
const ctxName = flagStr("context", "default");
|
|
448
547
|
if (!flagBool("eval")) {
|
|
449
548
|
const rows = trees.ready(tid, ctxName, state);
|
|
@@ -549,7 +648,14 @@ function run(env, action, rest, args) {
|
|
|
549
648
|
(0, fmt_1.bad)(ctx, "usage: node path <key>");
|
|
550
649
|
return 2;
|
|
551
650
|
}
|
|
552
|
-
|
|
651
|
+
let p;
|
|
652
|
+
try {
|
|
653
|
+
p = trees.pathTo(tid, key, flagStr("context", "default"));
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
553
659
|
if (ctx.json) {
|
|
554
660
|
console.log(JSON.stringify(p, null, 2));
|
|
555
661
|
return 0;
|
|
@@ -610,7 +716,7 @@ function run(env, action, rest, args) {
|
|
|
610
716
|
if (sub === "show") {
|
|
611
717
|
const n = trees.nodeGet(tid, key);
|
|
612
718
|
if (!n) {
|
|
613
|
-
(0, fmt_1.bad)(ctx, `node not found: ${key}`);
|
|
719
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(`node not found: ${key}`));
|
|
614
720
|
return 1;
|
|
615
721
|
}
|
|
616
722
|
const st = trees.nodeStateOf(tid, key);
|
|
@@ -704,11 +810,39 @@ function run(env, action, rest, args) {
|
|
|
704
810
|
return runTreeWrite((ds) => treeWrites.importTree(ds, spec, { name: flagStr("tree") || undefined, merge: flagBool("merge") }), (r) => {
|
|
705
811
|
(0, fmt_1.ack)(ctx, { tree: r.tree.name, nodes: r.nodes, edges: r.edges, conditions: r.conditions }, `imported tree ${r.tree.name}: ${r.nodes} nodes, ${r.edges} edges, ${r.conditions} conditions`);
|
|
706
812
|
db.metaSet("current_tree", r.tree.name);
|
|
813
|
+
logSelectionEvent(db, "tree.use", r.tree.name);
|
|
707
814
|
if (!ctx.json)
|
|
708
815
|
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` index rebuilt; now using tree: ${r.tree.name}`, ctx.color));
|
|
709
816
|
return 0;
|
|
710
817
|
});
|
|
711
818
|
}
|
|
819
|
+
// Report ⑤: tree-scoped growth timeline — workspace-level `db timeline`
|
|
820
|
+
// already existed; this projects a single tree's unlock/add milestones.
|
|
821
|
+
if (sub === "timeline") {
|
|
822
|
+
const rows = db.all(`SELECT e.ts, e.kind, e.payload FROM events e
|
|
823
|
+
WHERE e.entity = 'node'
|
|
824
|
+
AND e.entity_id IN (SELECT id FROM nodes WHERE tree_id = ?)
|
|
825
|
+
ORDER BY e.ts DESC LIMIT 30`, [tid]);
|
|
826
|
+
if (ctx.json) {
|
|
827
|
+
console.log(JSON.stringify(rows.map((r) => ({
|
|
828
|
+
ts: r.ts, kind: r.kind, ...JSON.parse(r.payload || "{}"),
|
|
829
|
+
})), null, 2));
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
832
|
+
(0, fmt_1.head)(ctx, `Timeline — tree '${tree.name}' (last ${rows.length})`);
|
|
833
|
+
for (const r of rows) {
|
|
834
|
+
let detail = "";
|
|
835
|
+
try {
|
|
836
|
+
const p = JSON.parse(r.payload || "{}");
|
|
837
|
+
detail = [p.key, p.name ? String(p.name).slice(0, 30) : ""].filter(Boolean).join(" ");
|
|
838
|
+
}
|
|
839
|
+
catch { /* */ }
|
|
840
|
+
(0, fmt_1.out)(ctx, ` ${String(r.ts).slice(0, 16)} ${r.kind.padEnd(14)} ${detail}`);
|
|
841
|
+
}
|
|
842
|
+
if (!rows.length)
|
|
843
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " (no node events yet — unlock/add to start the history)", ctx.color));
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
712
846
|
if (sub === "rm") {
|
|
713
847
|
if (!key) {
|
|
714
848
|
(0, fmt_1.bad)(ctx, "usage: node rm <key>");
|
|
@@ -737,3 +871,33 @@ function parseState(args) {
|
|
|
737
871
|
}
|
|
738
872
|
return out;
|
|
739
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* 0.79.1 (R297 user report): `--state-file <path.json>` loads the state bag
|
|
876
|
+
* from JSON instead of inline `--state k=v`. Two accepted shapes:
|
|
877
|
+
* flat shared {"y_imported": 12} → merged over --state, all keys share it
|
|
878
|
+
* per-node map {"node-a": {"y": 1}, ...} → each judged node gets its own bag
|
|
879
|
+
* (disambiguated by "every value is a plain object"). Returns the per-node map
|
|
880
|
+
* when shape B, null when merged into `shared`. Throws on unreadable/invalid.
|
|
881
|
+
*/
|
|
882
|
+
function loadStateFile(args, shared) {
|
|
883
|
+
const sf = args.flags["state-file"];
|
|
884
|
+
if (sf === undefined)
|
|
885
|
+
return null; // flag absent → no bags (plain can/ready)
|
|
886
|
+
if (sf === true || sf === "")
|
|
887
|
+
throw new Error("usage: --state-file <path.json>");
|
|
888
|
+
let parsed;
|
|
889
|
+
try {
|
|
890
|
+
parsed = JSON.parse(require("node:fs").readFileSync(String(sf), "utf8"));
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
throw new Error(`cannot read --state-file ${sf}: ${e?.message || e}`);
|
|
894
|
+
}
|
|
895
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
896
|
+
throw new Error("--state-file must contain a JSON object (flat counters, or per-node key→object map)");
|
|
897
|
+
}
|
|
898
|
+
const vals = Object.values(parsed);
|
|
899
|
+
if (vals.length && vals.every((v) => v && typeof v === "object" && !Array.isArray(v)))
|
|
900
|
+
return parsed;
|
|
901
|
+
Object.assign(shared, parsed);
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
@@ -44,6 +44,14 @@ class TreeStore {
|
|
|
44
44
|
nodeGet(treeId, keyOrId) {
|
|
45
45
|
return this.db.get("SELECT * FROM nodes WHERE tree_id=? AND (key=? OR id=?)", [treeId, keyOrId, keyOrId]);
|
|
46
46
|
}
|
|
47
|
+
/** Which OTHER trees contain this key? (Report ③: a parallel session
|
|
48
|
+
* flipped current_tree; `node can p_dates` then said just "node not
|
|
49
|
+
* found" — the key existed the whole time, in another tree. This lookup
|
|
50
|
+
* lets the error name the tree instead of forcing a blind node list.) */
|
|
51
|
+
nodeInOtherTrees(keyOrId, excludeTreeId) {
|
|
52
|
+
return this.db.all(`SELECT t.name AS tree, n.key FROM nodes n JOIN trees t ON t.id=n.tree_id
|
|
53
|
+
WHERE n.tree_id<>? AND (n.key=? OR n.id=?) LIMIT 5`, [excludeTreeId, keyOrId, keyOrId]);
|
|
54
|
+
}
|
|
47
55
|
/** Runtime state for one node (default context). Separate from nodeGet on
|
|
48
56
|
* purpose: nodeGet feeds WRITE paths, and a context join there could return
|
|
49
57
|
* one row per context — silently wrong for link/unlink/rm. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ygl-trees",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.1",
|
|
4
4
|
"description": "猿光 · 科技树 — database-centric CLI for research → production → knowledge reuse, built for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/apps/cli/index.js",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"reflect-metadata": "^0.2.2",
|
|
27
27
|
"typeorm": "^1.1.2",
|
|
28
28
|
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
29
|
-
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
|
|
30
29
|
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
|
|
30
|
+
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
|
|
31
31
|
"@deepseek-ai/schemastery": "~3.18.5-alpha.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
],
|
|
50
50
|
"exclude": [
|
|
51
51
|
"node_modules",
|
|
52
|
-
"dist"
|
|
52
|
+
"dist",
|
|
53
|
+
"tmp"
|
|
53
54
|
],
|
|
54
55
|
"env": {
|
|
55
56
|
"YGL_TEST": "1"
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
触发词:ygl-trees 入门、初始化、quickstart、工作区、退出码、exit code、备份、export、
|
|
10
10
|
PostgreSQL、sync、migrate、镜像、job、tick、定时、doctor、健康检查、任务、task、
|
|
11
11
|
关联、rel、插件、plugin、WebUI、webui、仪表盘、dashboard、bench、基准、bundle。
|
|
12
|
-
version: 0.
|
|
12
|
+
version: 0.79.0
|
|
13
13
|
tags: ygl-trees,quickstart,backup,postgres,schedule,doctor,plugin,webui,bench
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -56,25 +56,35 @@ ygl-trees llms --json # 结构化 JSON
|
|
|
56
56
|
## 2b. 运维命令速查
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
ygl-trees doctor #
|
|
60
|
-
ygl-trees doctor --fix #
|
|
59
|
+
ygl-trees doctor # 10 项健康检查(含锁 pid 存活、幻影写入对账)
|
|
60
|
+
ygl-trees doctor --fix # 一键安全修复(脏索引 + 死 pid 陈旧锁;活 holder 不动)
|
|
61
|
+
ygl-trees db unlock # 清残留 .ygllock(死 pid 直清;活 holder 回显其命令行并拒绝,--force 强清)
|
|
61
62
|
ygl-trees kb rm <id> # 删除笔记(与 skill rm 对称)
|
|
62
63
|
ygl-trees webui serve --host 0.0.0.0 --port 8642 # LAN/WSL 可访问
|
|
64
|
+
ygl-trees webui serve --help # 只打印用法不启服务(0.79.1 短路)
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
PG 迁移预检:`db migrate pg` 前自动逐表扫描 INTEGER 列类型——非整数值会**点名 table.column rowid=N** 而非迁移时才爆。
|
|
66
68
|
|
|
67
|
-
##
|
|
69
|
+
## 2b2. 多会话安全(0.79)
|
|
68
70
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
+
- `tree use` / `project use` 落 `tree.use` / `project.use` 事件(`YGL_SESSION` 会话标识)
|
|
72
|
+
- `agent brief` 的 `selection_changed`:最近 10 分钟选择变更;跨会话变更置顶提示带 `-t`/`-p`
|
|
73
|
+
- `kb add` 回执自检(`verified:true`)+ 工作区回显(防写错 home 的"丢笔记"事故)
|
|
74
|
+
- `research claim link <cid> <sid> --by-source`:来源下全部证据一次挂齐;误传来源 id 会列出其证据 id
|
|
75
|
+
|
|
76
|
+
## 2c. DB 锁行为(0.68 → 0.79.1)
|
|
77
|
+
|
|
78
|
+
- CLI 短命令:进程级锁(保护写窗口,退出自动释放);锁文件记录**持有者命令行**,撞锁/`db unlock`/`doctor` 都回显真实持有者身份
|
|
79
|
+
- webui serve:无锁(WAL 并发安全,与 CLI 共存;从不取写锁)
|
|
71
80
|
- 死锁自愈:下一进程探测持有者 PID,死则即时清(无需手动 rmdir)
|
|
81
|
+
- 无锁进程(serve / `db unlock` 自身)退出绝不误清并行会话的活锁(0.79.1 所有权守卫)
|
|
72
82
|
- YGL_TEST=1:全旁路(测试并行)
|
|
73
83
|
|
|
74
84
|
## 3. 健康检查与任务
|
|
75
85
|
|
|
76
86
|
```bash
|
|
77
|
-
ygl-trees doctor #
|
|
87
|
+
ygl-trees doctor # 10 项健康检查(schema/索引/FTS/重复/孤儿/config/锁存活/eval信封/插件/幻影写入)
|
|
78
88
|
ygl-trees task add "<标题>" # 建任务(todo 起步)
|
|
79
89
|
ygl-trees task list # 按状态分组
|
|
80
90
|
ygl-trees task done <id> # 勾销(全程留痕)
|
|
@@ -120,6 +130,7 @@ ygl-trees db export <file.sql> # 全部数据表 + 行数摘要
|
|
|
120
130
|
ygl-trees db sync pg <url> # 可刷新 PK-only 镜像
|
|
121
131
|
ygl-trees db migrate pg <url> [--fresh] # 一次性完整保真迁移
|
|
122
132
|
ygl-trees job add <title> -- "<cmd>" --every 60 # cron 每分钟跑 job tick
|
|
133
|
+
ygl-trees job add "闸门探针" -- "node probe.mjs" --every 30 # 树节奏化监控:任意命令皆可(0.79.1 实证)
|
|
123
134
|
```
|
|
124
135
|
|
|
125
136
|
禁止双后端同写:ygl-trees 永远只写 SQLite,PG 是单向出口。
|
|
@@ -57,5 +57,7 @@ ygl-trees produce export <reportId> report.md
|
|
|
57
57
|
|
|
58
58
|
- 先 `agent brief --json` 看 next_actions(它会告诉你缺来源/缺证据/缺断言)
|
|
59
59
|
- 每条断言至少一条证据再进报告;contested 的要么补证据要么 resolve
|
|
60
|
+
- 预测断言(`[预测·N天]`)定期 `research predict audit` 回测:本地判据自动测;`--state <file|url>`
|
|
61
|
+
桥接外部计数(裸键名后缀匹配 `counts.y_imported`);已 resolve 的默认隐藏(`--all` 查看终态表)
|
|
60
62
|
- `--json` 输出可直接 parse 拿 id;列表命令字段:source(id,title,url,relevance,…)、
|
|
61
63
|
claim(id,text,status,confidence,sup,ref)、evidence(id,source,locator,quote,used_by)
|
|
@@ -38,6 +38,7 @@ ygl-trees node index && ygl-trees node validate
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
ygl-trees node can steel --state "lab_level=3,has_eng=1" # 三类失败分开列+成本系数
|
|
41
|
+
ygl-trees node can a b c --state-file bags.json # 批量:一进程测 N 节点(探针 1s×N → 1s)
|
|
41
42
|
ygl-trees node ready # 当前可解锁集合
|
|
42
43
|
ygl-trees node path steel # 解锁顺序+累计成本(目标恒最后)
|
|
43
44
|
ygl-trees node ancestors bronze # 闭包索引直查,depth=1 是直接父
|
|
@@ -46,6 +47,7 @@ ygl-trees node unlock mining && ygl-trees node lock mining
|
|
|
46
47
|
|
|
47
48
|
- 写入时环守卫:门控边(prereq/unlock)成环当场拒绝
|
|
48
49
|
- `node can` 输出:missing_required(阻塞)/ blocked_by(被排斥)/ missing_optional(不阻塞)
|
|
50
|
+
- `--state-file` 两种形状:扁平 `{"y_imported":40}` 全节点共享;按节点 `{"a":{"y_imported":40}}` 各取各袋;多键 `--json` 返回数组(0.79.1)
|
|
49
51
|
|
|
50
52
|
## 3. 迁移与保真
|
|
51
53
|
|