@minonl/dia 0.0.7 → 0.0.9

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/dist/cli.js CHANGED
@@ -2098,6 +2098,9 @@ function install(from, opts = {}) {
2098
2098
  const kitRoot = resolve5(target, kitRootRel);
2099
2099
  if (!kitRoot.startsWith(target + sep4) && kitRoot !== target)
2100
2100
  throw new Error("E_MANIFEST: root escapes kit");
2101
+ const hasContent = ["files", "skills", "bin", "dirs", "knowledge", "commands", "config"].some((k) => (m[k]?.length ?? 0) > 0);
2102
+ if (!hasContent)
2103
+ throw new Error("E_MANIFEST: kit 未声明任何内容(files/skills/bin/dirs/knowledge/commands/config)");
2101
2104
  const planned = renderContent(src.dir, target, m, { dryRun: true }) ?? [];
2102
2105
  if (opts.dryRun)
2103
2106
  return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered: planned, hash: null, changed: false };
@@ -2110,6 +2113,10 @@ function install(from, opts = {}) {
2110
2113
  const rendered = renderContent(src.dir, target, m) ?? planned;
2111
2114
  const appliedConfig = apply(m.config ?? []);
2112
2115
  const hash = hashTree(target);
2116
+ if (hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") {
2117
+ rmSync3(target, { recursive: true, force: true });
2118
+ throw new Error(`E_KIT_EMPTY: kit ${m.id}@${m.version} 内容为空(检查 source 与清单路径)`);
2119
+ }
2113
2120
  writeFileSync5(join10(target, "kit.lock"), JSON.stringify({
2114
2121
  id: m.id,
2115
2122
  version: m.version,
@@ -4713,6 +4720,67 @@ function build(opts = {}) {
4713
4720
  return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
4714
4721
  }
4715
4722
 
4723
+ // src/graph-tui.ts
4724
+ var ORDER = ["doing", "blocked", "review", "todo", "awaiting_acceptance", "accepted", "done", "dropped", "rejected", "escalated"];
4725
+ var color = (s, code, on) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
4726
+ function render2(g, opts = {}) {
4727
+ const on = opts.color ?? false;
4728
+ const tasks = g.nodes.filter((n) => n.kind === "task");
4729
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
4730
+ const edgesFrom = (id, kind) => g.edges.filter((e) => e.from === id && e.kind === kind);
4731
+ const lines = [];
4732
+ const groups = new Map;
4733
+ for (const t of tasks) {
4734
+ const st = t.status || "todo";
4735
+ groups.set(st, [...groups.get(st) || [], t]);
4736
+ }
4737
+ for (const st of [...ORDER.filter((s) => groups.has(s)), ...[...groups.keys()].filter((s) => !ORDER.includes(s))]) {
4738
+ lines.push(color(`[${st}]`, "1;36", on));
4739
+ for (const t of groups.get(st).sort((a, b) => a.label.localeCompare(b.label))) {
4740
+ lines.push(` ${color(t.label, "1", on)} ${color(t.id.replace(/^task:/, ""), "2", on)}${t.owner ? ` owner=${t.owner}` : ""}`);
4741
+ for (const e of edgesFrom(t.id, "depends_on")) {
4742
+ const dep = taskById.get(e.to);
4743
+ lines.push(` ${color("↳ depends", "34", on)} ${dep?.label || e.to}${dep ? ` [${dep.status}]` : ""}`);
4744
+ }
4745
+ for (const e of edgesFrom(t.id, "touches"))
4746
+ lines.push(` ${color("↳ touches", "35", on)} ${e.to.replace(/^path:/, "")}`);
4747
+ if (t.blocked_by?.length)
4748
+ lines.push(` ${color(`⚠ blocked_by ${t.blocked_by.join(", ")}`, "31", on)}`);
4749
+ }
4750
+ lines.push("");
4751
+ }
4752
+ if (!tasks.length)
4753
+ lines.push("(no tasks)");
4754
+ const claimEdges = g.edges.filter((e) => e.kind === "claims");
4755
+ if (claimEdges.length) {
4756
+ lines.push(color("claims", "1;36", on));
4757
+ for (const e of claimEdges)
4758
+ lines.push(` ${e.from.replace(/^session:/, "")} → ${e.to.replace(/^path:/, "")}`);
4759
+ lines.push("");
4760
+ }
4761
+ for (const c of g.cycles)
4762
+ lines.push(color(`⚠ cycle: ${c.join(" -> ")}`, "31", on));
4763
+ return lines.join(`
4764
+ `);
4765
+ }
4766
+ function watch(build, opts = {}) {
4767
+ const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { color: opts.color ?? process.stdout.isTTY }) + `
4768
+ `);
4769
+ paint();
4770
+ const timer = setInterval(() => {
4771
+ try {
4772
+ paint();
4773
+ } catch {}
4774
+ }, opts.intervalMs ?? 2000);
4775
+ const stop = () => {
4776
+ clearInterval(timer);
4777
+ process.exit(0);
4778
+ };
4779
+ process.on("SIGINT", stop);
4780
+ process.on("SIGTERM", stop);
4781
+ return new Promise(() => {});
4782
+ }
4783
+
4716
4784
  // packages/cli/src/index.ts
4717
4785
  init_paths();
4718
4786
 
@@ -5137,7 +5205,7 @@ function scopeEnv(s, extra = {}) {
5137
5205
  }
5138
5206
 
5139
5207
  // src/doctor.ts
5140
- import { existsSync as existsSync22 } from "node:fs";
5208
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "node:fs";
5141
5209
  import { homedir as homedir12 } from "node:os";
5142
5210
  import { join as join25, resolve as resolve13 } from "node:path";
5143
5211
 
@@ -5484,6 +5552,25 @@ function jobRootResolved() {
5484
5552
  const legacy = join24(homedir11(), ".local", "share", "dia-job");
5485
5553
  return !existsSync21(next) && existsSync21(legacy) ? legacy : next;
5486
5554
  }
5555
+ function runJobCapture(argv) {
5556
+ const { bin, error } = jobBin();
5557
+ if (!bin) {
5558
+ process.stderr.write(`dia job: ${error || "未找到 dia-job;先跑 packages/job/install.sh 或设置 DIA_JOB_BIN"}
5559
+ `);
5560
+ return { status: 2, stdout: "", stderr: "" };
5561
+ }
5562
+ const r = spawnSync2(bin, argv, {
5563
+ encoding: "utf8",
5564
+ stdio: ["inherit", "pipe", "pipe"],
5565
+ env: { ...process.env, DIA_JOB_ROOT: jobRootResolved() }
5566
+ });
5567
+ if (r.error) {
5568
+ process.stderr.write(`dia job: ${r.error.message}
5569
+ `);
5570
+ return { status: 1, stdout: "", stderr: "" };
5571
+ }
5572
+ return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
5573
+ }
5487
5574
  function runJob(argv) {
5488
5575
  const { bin, error } = jobBin();
5489
5576
  if (!bin) {
@@ -5579,17 +5666,20 @@ async function personaChecks(id, prefix, shared) {
5579
5666
  } catch (e) {
5580
5667
  checks.push({ name: `${prefix}persona.render`, status: "ok", detail: `跳过(${e?.message || String(e)})` });
5581
5668
  }
5582
- const installed = list();
5583
- if (!installed.length)
5669
+ let refs = [];
5670
+ try {
5671
+ refs = JSON.parse(readFileSync19(join25(sc.dir, "dia.lock"), "utf8")).kits ?? [];
5672
+ } catch {
5673
+ refs = [];
5674
+ }
5675
+ if (!refs.length)
5584
5676
  checks.push({ name: `${prefix}kits`, status: "ok", detail: "无独立 kit(内容由 persona 自带)" });
5585
- for (const k of installed) {
5586
- for (const v of k.versions) {
5587
- try {
5588
- const r = verify2(k.id, v);
5589
- checks.push({ name: `${prefix}kit:${k.id}@${v}`, status: r.ok ? "ok" : "fail", detail: r.ok ? "一致" : r.drift.join("; ") });
5590
- } catch (e) {
5591
- checks.push({ name: `${prefix}kit:${k.id}@${v}`, status: "fail", detail: e?.message });
5592
- }
5677
+ for (const k of refs) {
5678
+ try {
5679
+ const r = verify2(k.id, k.version);
5680
+ checks.push({ name: `${prefix}kit:${k.id}@${k.version}`, status: r.ok ? "ok" : "fail", detail: r.ok ? "一致" : r.drift.join("; ") });
5681
+ } catch (e) {
5682
+ checks.push({ name: `${prefix}kit:${k.id}@${k.version}`, status: "fail", detail: e?.message });
5593
5683
  }
5594
5684
  }
5595
5685
  if (sc.knowledgeRoot) {
@@ -5672,7 +5762,7 @@ async function run(opts = {}) {
5672
5762
  }
5673
5763
 
5674
5764
  // src/env.ts
5675
- import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
5765
+ import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
5676
5766
  import { homedir as homedir13 } from "node:os";
5677
5767
  import { join as join26 } from "node:path";
5678
5768
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -5687,7 +5777,7 @@ function parseFile(path) {
5687
5777
  const out = {};
5688
5778
  let text = "";
5689
5779
  try {
5690
- text = readFileSync19(path, "utf8");
5780
+ text = readFileSync20(path, "utf8");
5691
5781
  } catch {
5692
5782
  return out;
5693
5783
  }
@@ -5757,7 +5847,7 @@ function run2(cfg, cmd, opts = {}) {
5757
5847
  }
5758
5848
 
5759
5849
  // src/bw.ts
5760
- import { chmodSync as chmodSync4, existsSync as existsSync24, mkdirSync as mkdirSync17, readFileSync as readFileSync20, rmSync as rmSync7, writeFileSync as writeFileSync14 } from "node:fs";
5850
+ import { chmodSync as chmodSync4, existsSync as existsSync24, mkdirSync as mkdirSync17, readFileSync as readFileSync21, rmSync as rmSync7, writeFileSync as writeFileSync14 } from "node:fs";
5761
5851
  init_paths();
5762
5852
  import { join as join27 } from "node:path";
5763
5853
  import { execFileSync as execFileSync10 } from "node:child_process";
@@ -5770,7 +5860,7 @@ function sessionFile2() {
5770
5860
  }
5771
5861
  function readSession2() {
5772
5862
  try {
5773
- const s = readFileSync20(sessionFile2(), "utf8").trim();
5863
+ const s = readFileSync21(sessionFile2(), "utf8").trim();
5774
5864
  return s || null;
5775
5865
  } catch {
5776
5866
  return null;
@@ -6013,17 +6103,17 @@ function askBroker(req, timeoutMs = 5000) {
6013
6103
  import { execFileSync as execFileSync12, spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
6014
6104
  import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
6015
6105
  import { basename as basename10, join as join32 } from "node:path";
6016
- import { existsSync as existsSync29, readFileSync as readFileSync25 } from "node:fs";
6106
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
6017
6107
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6018
6108
  import { resolve as resolvePath, sep as pathSep } from "node:path";
6019
6109
 
6020
6110
  // src/team.ts
6021
- import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as existsSync26, mkdirSync as mkdirSync18, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "node:fs";
6111
+ import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as existsSync26, mkdirSync as mkdirSync18, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "node:fs";
6022
6112
  import { homedir as homedir15, tmpdir as tmpdir3 } from "node:os";
6023
6113
  import { join as join29, resolve as resolve14 } from "node:path";
6024
6114
 
6025
6115
  // src/pricing.ts
6026
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
6116
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
6027
6117
  import { homedir as homedir14 } from "node:os";
6028
6118
  import { join as join28 } from "node:path";
6029
6119
  var DEFAULT = {
@@ -6038,7 +6128,7 @@ function loadPrices() {
6038
6128
  if (!existsSync25(pricesFile()))
6039
6129
  return { ...DEFAULT };
6040
6130
  try {
6041
- const user = JSON.parse(readFileSync21(pricesFile(), "utf8"));
6131
+ const user = JSON.parse(readFileSync22(pricesFile(), "utf8"));
6042
6132
  return { ...DEFAULT, ...user };
6043
6133
  } catch {
6044
6134
  return { ...DEFAULT };
@@ -6111,7 +6201,7 @@ function writeUsage(dir, id, acc) {
6111
6201
  }
6112
6202
  function judgeWorkspace(dir, fallback) {
6113
6203
  try {
6114
- const run = JSON.parse(readFileSync22(join29(dir, "run.json"), "utf8"));
6204
+ const run = JSON.parse(readFileSync23(join29(dir, "run.json"), "utf8"));
6115
6205
  const ws = String(run?.workspace || "");
6116
6206
  if (ws)
6117
6207
  return ws;
@@ -6126,17 +6216,17 @@ function gate(id) {
6126
6216
  return { ok: false, reason: "missing: " + missing.join(",") };
6127
6217
  let verify, redteam, run;
6128
6218
  try {
6129
- verify = JSON.parse(readFileSync22(join29(dir, "verify.json"), "utf8"));
6219
+ verify = JSON.parse(readFileSync23(join29(dir, "verify.json"), "utf8"));
6130
6220
  } catch {
6131
6221
  return { ok: false, reason: "bad verify.json" };
6132
6222
  }
6133
6223
  try {
6134
- redteam = JSON.parse(readFileSync22(join29(dir, "redteam.json"), "utf8"));
6224
+ redteam = JSON.parse(readFileSync23(join29(dir, "redteam.json"), "utf8"));
6135
6225
  } catch {
6136
6226
  return { ok: false, reason: "bad redteam.json" };
6137
6227
  }
6138
6228
  try {
6139
- run = JSON.parse(readFileSync22(join29(dir, "run.json"), "utf8"));
6229
+ run = JSON.parse(readFileSync23(join29(dir, "run.json"), "utf8"));
6140
6230
  } catch {
6141
6231
  return { ok: false, reason: "bad run.json" };
6142
6232
  }
@@ -6282,7 +6372,7 @@ function snapPath(id, dir, tag) {
6282
6372
  const f = join29(dir, "snapshots.json");
6283
6373
  let list = [];
6284
6374
  try {
6285
- list = JSON.parse(readFileSync22(f, "utf8"));
6375
+ list = JSON.parse(readFileSync23(f, "utf8"));
6286
6376
  } catch {
6287
6377
  list = [];
6288
6378
  }
@@ -6305,7 +6395,7 @@ function cleanupSnapshots(runDir) {
6305
6395
  if (process.env.DIA_KEEP_SNAPSHOTS === "1")
6306
6396
  return;
6307
6397
  try {
6308
- for (const p of JSON.parse(readFileSync22(join29(runDir, "snapshots.json"), "utf8"))) {
6398
+ for (const p of JSON.parse(readFileSync23(join29(runDir, "snapshots.json"), "utf8"))) {
6309
6399
  if (resolve14(p).startsWith(resolve14(tmpdir3()) + "/"))
6310
6400
  rmSync8(p, { recursive: true, force: true });
6311
6401
  }
@@ -6392,7 +6482,7 @@ function gitDiff(ws) {
6392
6482
  const untracked = execFileSync11("git", ["-C", ws, "ls-files", "--others", "--exclude-standard"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(`
6393
6483
  `).filter(Boolean);
6394
6484
  for (const f of untracked) {
6395
- const body = readFileSync22(join29(ws, f), "utf8");
6485
+ const body = readFileSync23(join29(ws, f), "utf8");
6396
6486
  out += `
6397
6487
  --- /dev/null
6398
6488
  +++ b/${f}
@@ -6585,7 +6675,7 @@ function dshSessionFile(sessionId) {
6585
6675
  return null;
6586
6676
  }
6587
6677
  function dshUsages(file) {
6588
- const raw = zstdDecompressSync2(readFileSync22(file)).toString("utf8");
6678
+ const raw = zstdDecompressSync2(readFileSync23(file)).toString("utf8");
6589
6679
  const out = [];
6590
6680
  for (const line of raw.split(`
6591
6681
  `)) {
@@ -6869,7 +6959,7 @@ function reject(id, reason) {
6869
6959
  var DEFAULT_ACP_TIMEOUT = 600000;
6870
6960
 
6871
6961
  // src/proposal.ts
6872
- import { existsSync as existsSync27, readFileSync as readFileSync23, readdirSync as readdirSync12 } from "node:fs";
6962
+ import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync12 } from "node:fs";
6873
6963
  import { join as join30 } from "node:path";
6874
6964
  import { createHash as createHash10 } from "node:crypto";
6875
6965
  init_paths();
@@ -6885,7 +6975,7 @@ function push(out, p) {
6885
6975
  }
6886
6976
  function readJson3(path) {
6887
6977
  try {
6888
- return JSON.parse(readFileSync23(path, "utf8"));
6978
+ return JSON.parse(readFileSync24(path, "utf8"));
6889
6979
  } catch {
6890
6980
  return null;
6891
6981
  }
@@ -6971,7 +7061,7 @@ function expandIds(cell) {
6971
7061
  function collectHypotheses(out, path) {
6972
7062
  if (!existsSync27(path))
6973
7063
  return;
6974
- const lines = readFileSync23(path, "utf8").split(`
7064
+ const lines = readFileSync24(path, "utf8").split(`
6975
7065
  `);
6976
7066
  const titles = new Map;
6977
7067
  for (const line of lines) {
@@ -7014,7 +7104,7 @@ import {
7014
7104
  existsSync as existsSync28,
7015
7105
  lstatSync as lstatSync3,
7016
7106
  mkdirSync as mkdirSync19,
7017
- readFileSync as readFileSync24,
7107
+ readFileSync as readFileSync25,
7018
7108
  readdirSync as readdirSync13,
7019
7109
  readlinkSync as readlinkSync2,
7020
7110
  rmSync as rmSync9,
@@ -7153,7 +7243,7 @@ function readServiceManifest(dir) {
7153
7243
  throw new Error(`E_NO_MANIFEST: dia.json not found in ${dir}(adopted 服务需先补 dia.json)`);
7154
7244
  let raw;
7155
7245
  try {
7156
- raw = JSON.parse(readFileSync24(p, "utf8"));
7246
+ raw = JSON.parse(readFileSync25(p, "utf8"));
7157
7247
  } catch {
7158
7248
  throw new Error("E_MANIFEST: invalid json");
7159
7249
  }
@@ -7194,7 +7284,7 @@ function isSymlink2(p) {
7194
7284
  return false;
7195
7285
  }
7196
7286
  }
7197
- var shaFile3 = (p) => createHash11("sha256").update(readFileSync24(p)).digest("hex");
7287
+ var shaFile3 = (p) => createHash11("sha256").update(readFileSync25(p)).digest("hex");
7198
7288
  var UNKNOWN = "unknown";
7199
7289
  var SIGNALS = { 6: "SIGABRT", 9: "SIGKILL", 11: "SIGSEGV", 13: "SIGPIPE", 15: "SIGTERM" };
7200
7290
  function judgeHealth(exit, opts = {}) {
@@ -7227,7 +7317,18 @@ var STALE_FACTOR = 1.5;
7227
7317
  var MIN_HEALTH_INTERVAL = 60;
7228
7318
  var healthInterval = (hc) => hc?.interval ?? DEFAULT_HEALTH_INTERVAL;
7229
7319
  var staleAfter = (hc) => Math.floor(healthInterval(hc) * STALE_FACTOR);
7230
- var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && typeof v.state === "string";
7320
+ var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && (v.state === "ok" || v.state === "degraded" || v.state === "fail");
7321
+ var TS_RE = /"ts"\s*:\s*(-?\d+)/;
7322
+ var KIND_RE = /"kind"\s*:\s*"([A-Za-z0-9_-]+)"/;
7323
+ function parseStateLine(line) {
7324
+ const head = line.slice(0, 256), tail = line.slice(-256);
7325
+ const ts = TS_RE.exec(head)?.[1] ?? TS_RE.exec(tail)?.[1];
7326
+ const kind = KIND_RE.exec(head)?.[1] ?? KIND_RE.exec(tail)?.[1];
7327
+ if (!ts || !kind)
7328
+ return null;
7329
+ const n = Number(ts);
7330
+ return Number.isFinite(n) ? { ts: n, kind } : null;
7331
+ }
7231
7332
  function findLastHealth(lines, service, gen) {
7232
7333
  for (let i = lines.length - 1;i >= 0; i--) {
7233
7334
  const r = lines[i];
@@ -7412,6 +7513,24 @@ function diaCmd() {
7412
7513
  const entry = entries.find((p) => existsSync28(p));
7413
7514
  return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
7414
7515
  }
7516
+ function decorateStatus(text, view) {
7517
+ const lines = String(text ?? "").split(`
7518
+ `);
7519
+ const outLines = [];
7520
+ for (const line of lines) {
7521
+ outLines.push(line);
7522
+ const m = line.match(/local\.dia-job-svc-([A-Za-z0-9._-]+)/) || line.match(/(?:^|\s)svc-([A-Za-z0-9._-]+)\s/);
7523
+ if (!m)
7524
+ continue;
7525
+ try {
7526
+ const v = view(m[1]);
7527
+ if (v?.label)
7528
+ outLines.push(` health=${v.label}`);
7529
+ } catch {}
7530
+ }
7531
+ return outLines.join(`
7532
+ `);
7533
+ }
7415
7534
  function servicesRoot() {
7416
7535
  return process.env.DIA_SERVICE_HOME || join31(diaHome(), "services");
7417
7536
  }
@@ -7425,7 +7544,7 @@ function readLockFile(id) {
7425
7544
  if (!existsSync28(p))
7426
7545
  throw new Error(`E_NOT_INSTALLED: service ${id}(缺 lock)`);
7427
7546
  try {
7428
- return JSON.parse(readFileSync24(p, "utf8"));
7547
+ return JSON.parse(readFileSync25(p, "utf8"));
7429
7548
  } catch {
7430
7549
  throw new Error(`E_DRIFT: service.lock.json 不可解析(${p})`);
7431
7550
  }
@@ -7446,15 +7565,34 @@ function readHealthLines(file) {
7446
7565
  if (!existsSync28(file))
7447
7566
  return [];
7448
7567
  const rows = [];
7449
- for (const line of readFileSync24(file, "utf8").split(`
7450
- `)) {
7568
+ const lines = readFileSync25(file, "utf8").split(`
7569
+ `);
7570
+ for (const line of lines) {
7451
7571
  if (!line.trim())
7452
7572
  continue;
7453
7573
  try {
7454
7574
  const d = JSON.parse(line);
7455
7575
  if (isRecord(d))
7456
7576
  rows.push(d);
7577
+ continue;
7457
7578
  } catch {}
7579
+ const head = parseStateLine(line);
7580
+ const state = head ? /"state"\s*:\s*"(ok|degraded|fail)"/.exec(line)?.[1] : undefined;
7581
+ if (head && state) {
7582
+ rows.push({
7583
+ ...head,
7584
+ state,
7585
+ service: "",
7586
+ gen: "",
7587
+ source: "schedule",
7588
+ candidate: state,
7589
+ streak: 0,
7590
+ raw: state,
7591
+ exit: null,
7592
+ timeout: false,
7593
+ detail: ""
7594
+ });
7595
+ }
7458
7596
  }
7459
7597
  return rows;
7460
7598
  }
@@ -7463,6 +7601,41 @@ function appendHealthRecord(file, rec) {
7463
7601
  appendFileSync9(file, JSON.stringify(rec) + `
7464
7602
  `);
7465
7603
  }
7604
+ function readServiceHealth(id, opts = {}) {
7605
+ const { healthFile } = opts;
7606
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
7607
+ const jr = opts.jobRoot ?? jobRoot2();
7608
+ const dir = opts.servicesRoot ?? (process.env.DIA_SERVICE_HOME || join31(jr, "services"));
7609
+ const installDir = join31(dir, id);
7610
+ let lock = null;
7611
+ try {
7612
+ lock = readLockFile(id);
7613
+ } catch {}
7614
+ let svc = null;
7615
+ try {
7616
+ svc = readServiceManifest(lock?.dir ?? installDir);
7617
+ } catch {}
7618
+ if (!svc)
7619
+ return healthView({ adopted: true, hasHealthcheck: false, now });
7620
+ const hc = svc?.healthcheck;
7621
+ if (!hc)
7622
+ return healthView({ hasHealthcheck: false, now });
7623
+ const ledger = healthFile ?? healthStatePath(id);
7624
+ const recs = readHealthLines(ledger).filter(isRecord);
7625
+ let rec = lock ? findLastHealth(recs, id, lock.gen) : null;
7626
+ if (!rec) {
7627
+ for (let i = recs.length - 1;i >= 0; i--) {
7628
+ const r = recs[i];
7629
+ if (r.state !== "ok" && r.state !== "degraded" && r.state !== "fail")
7630
+ continue;
7631
+ if (r.service && r.service !== id)
7632
+ continue;
7633
+ if (!rec || r.ts > rec.ts)
7634
+ rec = r;
7635
+ }
7636
+ }
7637
+ return healthView({ record: rec, hasHealthcheck: true, hc, now });
7638
+ }
7466
7639
  function listServices(opts = {}) {
7467
7640
  const root = opts.servicesRoot ?? servicesRoot();
7468
7641
  const jr = opts.jobRoot ?? jobRoot2();
@@ -7643,7 +7816,7 @@ function readRegistryTasks(file = registryPath2()) {
7643
7816
  if (!existsSync28(file))
7644
7817
  return null;
7645
7818
  try {
7646
- const d = JSON.parse(readFileSync24(file, "utf8"));
7819
+ const d = JSON.parse(readFileSync25(file, "utf8"));
7647
7820
  return new Set(Object.keys(d?.tasks ?? {}));
7648
7821
  } catch {
7649
7822
  return null;
@@ -8009,7 +8182,7 @@ function cliVersion() {
8009
8182
  ];
8010
8183
  for (const u of candidates) {
8011
8184
  try {
8012
- return String(JSON.parse(readFileSync25(u, "utf8")).version || "0.0.0");
8185
+ return String(JSON.parse(readFileSync26(u, "utf8")).version || "0.0.0");
8013
8186
  } catch {}
8014
8187
  }
8015
8188
  return "0.0.0";
@@ -8155,7 +8328,7 @@ usage:
8155
8328
  dia agents list|register|unregister|discover
8156
8329
  dia msg deliver <agent> <id> [--transport tmux|acp]
8157
8330
  dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
8158
- dia graph [--json] [--task t-...]
8331
+ dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
8159
8332
  dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] | dia unclaim <path|id> [--force]
8160
8333
  dia team run <id> --mock | --acp|--dsh --workspace DIR
8161
8334
  dia team gate|accept|reject|next
@@ -8548,6 +8721,19 @@ async function taskCli(argv) {
8548
8721
  }
8549
8722
  async function graphCli(argv) {
8550
8723
  const asJson = has(argv, "--json");
8724
+ if (has(argv, "--tui") || has(argv, "--watch")) {
8725
+ const paint = (g) => out(render2(g, { color: !!process.stdout.isTTY && !has(argv, "--no-color") }));
8726
+ try {
8727
+ if (has(argv, "--watch"))
8728
+ return await watch(() => build({ task: flag(argv, "--task") }), { color: !!process.stdout.isTTY && !has(argv, "--no-color") });
8729
+ const g = build({ task: flag(argv, "--task") });
8730
+ paint(g);
8731
+ return g.cycles.length ? 3 : 0;
8732
+ } catch (e) {
8733
+ err(e?.message || String(e));
8734
+ return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
8735
+ }
8736
+ }
8551
8737
  try {
8552
8738
  const g = build({ task: flag(argv, "--task") });
8553
8739
  if (asJson)
@@ -9049,7 +9235,7 @@ async function collabCli(argv) {
9049
9235
  }
9050
9236
  if (sub === "reply") {
9051
9237
  const id = positional(args)[0];
9052
- const body = flag(args, "--body") ?? (flag(args, "--body-file") ? readFileSync25(flag(args, "--body-file"), "utf8") : undefined);
9238
+ const body = flag(args, "--body") ?? (flag(args, "--body-file") ? readFileSync26(flag(args, "--body-file"), "utf8") : undefined);
9053
9239
  if (!id || body === undefined) {
9054
9240
  err("usage: dia reply <id> --body B|--body-file F [--session S]");
9055
9241
  return 2;
@@ -9766,7 +9952,7 @@ async function main(rawArgv) {
9766
9952
  if (!npmManaged) {
9767
9953
  let source = "";
9768
9954
  try {
9769
- source = String(JSON.parse(readFileSync25(join32(install, "install.json"), "utf8")).source || "");
9955
+ source = String(JSON.parse(readFileSync26(join32(install, "install.json"), "utf8")).source || "");
9770
9956
  } catch {}
9771
9957
  out(`这是脚本安装,不走 npm 升级。升级方式:`);
9772
9958
  out(` cd ${source || "<dia 仓库>"} && git pull && bun run build && bash install.sh`);
@@ -10076,8 +10262,19 @@ async function main(rawArgv) {
10076
10262
  return agentsCli(argv.slice(1));
10077
10263
  if (cmd === "team")
10078
10264
  return teamCli(argv.slice(1));
10079
- if (cmd === "job")
10080
- return runJob(argv.slice(1));
10265
+ if (cmd === "job") {
10266
+ const rest = argv.slice(1);
10267
+ if (rest[0] === "status" || rest[0] === "list") {
10268
+ const r = runJobCapture(rest);
10269
+ if (r.stderr)
10270
+ process.stderr.write(r.stderr);
10271
+ const text = decorateStatus(r.stdout, (id) => readServiceHealth(id));
10272
+ if (text)
10273
+ out(text.replace(/\n$/, ""));
10274
+ return r.status;
10275
+ }
10276
+ return runJob(rest);
10277
+ }
10081
10278
  if (cmd === "price") {
10082
10279
  const table = loadPrices();
10083
10280
  if (has(argv.slice(1), "--json"))
package/docs/cli.md CHANGED
@@ -95,11 +95,11 @@ dia persona remove dabo
95
95
  | `persona update <id>` | **已实现** | 从 lock 记录的 source+ref 重装,并把 active 指向新版本;本地源亦可;`--force` 覆盖宿主/内联内容冲突(先备份) |
96
96
  | `persona default <id>` | **已实现** | 改宿主 `default_agent`:备份原文件 + 记原值,`--unset` 精确还原;非严格 JSON 直接拒绝 |
97
97
  | `persona create / render` | **已实现** | `create` 出骨架;`render --check` 只比对不写盘(跳过宿主自写文件,如 dsh `cordis.yml` / `package.json` / 锁文件);`render` 只覆盖 persona 产物,不整树重建宿主 profile |
98
- | `kit add / list / verify / remove` | **已实现** | 复制/软链/记账;冲突写盘前全量检查(`E_CONFLICT`);`src/kit.ts` + `tests/kit.test.ts` |
98
+ | `kit add / list / verify / remove` | **已实现** | 复制/软链/记账;冲突写盘前全量检查(`E_CONFLICT`);**空清单/空内容拒绝**(`E_MANIFEST`/`E_KIT_EMPTY`);`src/kit.ts` + `tests/kit.test.ts` |
99
99
  | `env list / check / run` | **已实现** | 白名单注入、不回显值、`--redact`、密钥不进 argv;清单由 persona 声明(`src/env.ts` + `tests/env.test.ts`) |
100
100
  | `update [--check]` | **已实现** | 查 npm `dist-tags`;`--check` 有新版 exit 1、领先/最新 exit 0;**按安装方式分流**:npm 全局包 → `npm i -g`;脚本安装 → 提示 `git pull && bun run build && bash install.sh`(避免装到不在 PATH 的 npm prefix) |
101
101
  | `deps list / record` | **已实现** | `list` = 登记 + 实测派生视图(required 缺失 exit 2);`record` = 只追加一行(`--id/--kind/--version` 必填);`src/deps.ts` + `tests/deps*.test.ts` |
102
- | `doctor` | **已实现** | 组合自检:persona verify / **persona.render(宿主产物只读漂移,warn + 修复命令)** / default_agent / kit / knowledge(**索引新鲜度三态**,过期给重建命令)/ dsh preset / **deps 登记校验** / **platform 版本(读 `docs/platforms/registry.json`)** / wt;exit 0/1/2。不指定 persona 且装了多个 → **逐个自检**(条目加 `<id>.` 前缀),不再整体 `E_SCOPE` |
102
+ | `doctor` | **已实现** | 组合自检:persona verify / **persona.render(宿主产物只读漂移,warn + 修复命令)** / default_agent / kit(**只查该 persona 引用的**,孤儿 kit 不拖红)/ knowledge(**索引新鲜度三态**,过期给重建命令)/ dsh preset / **deps 登记校验** / **platform 版本(读 `docs/platforms/registry.json`)** / wt;exit 0/1/2。不指定 persona 且装了多个 → **逐个自检**(条目加 `<id>.` 前缀),不再整体 `E_SCOPE` |
103
103
  | `wt …` | **已实现(需安装包)** | 转发 `packages/wt/bin/dia-wt`(create/adopt/doctor/gate/list/status/check/clean/prune-merged;详见 [worktree.md](worktree.md));未装时 `E_WT`(exit 33),`install.sh` 会分发 |
104
104
  | `kb index/search/query/get/lint/path/review` | **已实现** | persona 作用域下索引过期自动重建;显式 `--root` 保持「缺索引即报错」 |
105
105
  | `kb <域命令>` | **已实现(委派)** | 未命中内置 → persona `commands` → 委派;argv 透传、exit code 原样 |
@@ -64,3 +64,17 @@ dia graph --json --task t-xxx
64
64
  - `blocked_by` 只在 task 节点里做派生提示,不是边;
65
65
  - 任务依赖成环:`cycles` 非空,退出码 **3**(参数错仍是 2);
66
66
  - `--task` 只输出该任务的直接依赖/touches/claim 邻居,不做全图闭包。
67
+
68
+ ---
69
+
70
+ # 只读 TUI 图(M2 第一步)
71
+
72
+ ```bash
73
+ dia graph --tui # 打印一次(有环 exit 3)
74
+ dia graph --watch # 每 2s 刷新,Ctrl-C 退出
75
+ dia graph --tui --no-color # 关 ANSI 颜色
76
+ ```
77
+
78
+ - 按状态分组显示 task → deps / touches / blocked_by;claims 单列;
79
+ - 不启 HTTP/daemon,遵守框架“无网络模块”约束;
80
+ - `--task t-...` 只显示该任务的直接邻居。
@@ -352,3 +352,79 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
352
352
 
353
353
  1. 判据写成**可判定的检查**:`node scripts/docs-index.mjs --check && node scripts/docs-link-check.mjs`(+ 可选 checklist 计数);
354
354
  2. 或者把它当**评审工具**用:接受 `escalated` 是预期结果,只取 `verify.json` 的 findings 作为 grill 产出。
355
+
356
+ ## 14. P2:agent 型服务(entry = persona 会话)
357
+
358
+ > 状态:**草案(待实现)**。§0 已定「本版只做脚本型」;本节把 P2 的形状先定下来,避免实现时二次发明语义。
359
+
360
+ ### 14.1 与脚本型的关系
361
+
362
+ ```mermaid
363
+ flowchart LR
364
+ S["service"] --> A["脚本型(已实现)<br/>entry = bin/*"]
365
+ S --> B["agent 型(P2)<br/>entry = ACP 会话"]
366
+ B --> J["job:把 ACP 端点拉起并保持<br/>(dsh --profile dia-acp / opencode acp)"]
367
+ B --> H["healthcheck:端点可应答 + 最近一轮成功<br/>(不判断“想得对不对”)"]
368
+ ```
369
+
370
+ **不变的**:`lifecycle`(job 托管)、`healthcheck` 契约(专用码域)、台账(`kind:"health"`)、`gen` 代际。
371
+ **不同的**:没有 `stdin` 事件契约 —— 输入是 ACP 的 `prompt`,输出是会话事件;所以 `contract` 换成 ACP 语义。
372
+
373
+ ### 14.2 清单
374
+
375
+ ```jsonc
376
+ {
377
+ "kind": "service",
378
+ "id": "example-agent",
379
+ "entry": {
380
+ "agent": {
381
+ "host": "dsh", // dsh | opencode
382
+ "profile": "dia-acp", // 由 persona/宿主提供;缺省用宿主默认
383
+ "persona": "yan" // 可选:仅做归属校验,不注入内容
384
+ }
385
+ },
386
+ "lifecycle": { "type": "service", "target": "local" },
387
+ "healthcheck": { "kind": "acp", "interval": 1800, "timeout": 60 },
388
+ "kits": []
389
+ }
390
+ ```
391
+
392
+ | 字段 | 语义 |
393
+ |---|---|
394
+ | `entry.agent.host` | 用哪套 ACP 实现(`dsh --profile <p>` 或 `opencode acp`) |
395
+ | `entry.agent.profile` | 宿主的 profile 名(不复制 persona 内容,只引用) |
396
+ | `entry.agent.persona` | 可选;`verify` 时校验「该 profile 是否属于此 persona」,防止挂错人 |
397
+ | `healthcheck.kind: "acp"` | 走 ACP 健康检查(见 §14.3),**不再要求 `cmd`** |
398
+
399
+ ### 14.3 健康检查(ACP 版)
400
+
401
+ | 检查 | 判据 | 失败归类 |
402
+ |---|---|---|
403
+ | 端点存活 | `initialize` 能返回 `protocolVersion`(JSON-RPC 1.0) | 连不上/超时 → `degraded` |
404
+ | 会话可用 | `session/new` 成功返回 `sessionId` | 返回错误 → `fail` |
405
+ | 最近一轮 | 台账里最近一次 `prompt` 的成功/失败(由调用方写事件) | 连续失败 `debounce` 次 → `degraded` |
406
+
407
+ **明确不做**:不判断「回答质量」、不重放 prompt、不在 healthcheck 里跑真实业务(成本 + 副作用)。
408
+
409
+ ### 14.4 与脚本型的复用
410
+
411
+ - `healthView` / `transition` / `gen` / 台账格式**完全复用**(`state.jsonl` + `kind:"health"`)
412
+ - 差异只在「怎么产生一次 judgement」:`runHealth()` 换成 `runAcpHealth()`(`initialize` + `session/new`)
413
+ - `dia service health <id>` 对 agent 型走 `runAcpHealth`,退出码沿用 §5
414
+
415
+ ### 14.5 边界
416
+
417
+ | 场景 | 行为 |
418
+ |---|---|
419
+ | 宿主未安装 / profile 不存在 | `add` 期 `E_BACKEND`(不装半套) |
420
+ | 端点活着但 `session/new` 失败 | `fail`(配置/鉴权问题,不是 infra 抖动) |
421
+ | 长任务(一轮 prompt 很久) | **不算失败**;只有「端点不可应答」才算 |
422
+ | 多实例 | `lifecycle.target` 声明归属;ACP 端点同机只允许一个 |
423
+ | 权限/凭据 | 由宿主 profile 自己管;`dia` 不读取、不注入 |
424
+
425
+ ### 14.6 验收(P2)
426
+
427
+ - [ ] 单测:清单校验(`entry.agent` 与 `entry.cmd` **互斥**,同时给 → `E_MANIFEST`)
428
+ - [ ] 单测:`runAcpHealth` 用 `tests/fixtures/fake-acp.mjs` 覆盖 `initialize` 失败 / `session/new` 失败 / 正常三态
429
+ - [ ] 单测:`healthcheck.kind` 缺省时按脚本型(向后兼容)
430
+ - [ ] e2e:装一个 agent 型 demo(fake ACP)→ `dia job status` 显示 `health=ok…`,杀掉端点后变 `degraded(stale)`