@minonl/dia 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,18 @@ bash tests/smoke.sh # 用 Node 跑一遍构建产物
90
90
  bun src/cli.ts version # 直接跑源码
91
91
  ```
92
92
 
93
+ ### CI 矩阵(`.github/workflows/test.yml`)
94
+
95
+ | job | 触发 | 是否 required | 说明 |
96
+ |---|---|---|---|
97
+ | `test (ubuntu-latest)` | PR | ✅ | 全量:单测 / kb / job / build / install / smoke / e2e |
98
+ | `test (macos-latest)` | PR | ✅ | 同上(**macOS 特有差异只有这里能抓到**,例如没有 `timeout(1)`) |
99
+ | `windows-smoke` | PR(代码改动) | ❌ 非阻塞 | 装依赖 + `dia --version` + 纯逻辑测试;`dia job` 的 Windows 后端尚未实现(`install` 返回 rc=3),落地后再升 required |
100
+ | `changes` | PR | ❌ | 判定改动是否只碰 `docs/**` / `llms*.txt`:是则跳过测试矩阵(docs-only 快通道) |
101
+
102
+ > 只在 `pull_request` 上跑(不再对特性分支的 push 重复跑)。
103
+ > 改这里时**保持 `test (…)` 的 job 名不变**——分支保护按名字要求这两个 check。
104
+
93
105
  ## 命令
94
106
 
95
107
  ```bash
package/dist/cli.js CHANGED
@@ -4720,6 +4720,67 @@ function build(opts = {}) {
4720
4720
  return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
4721
4721
  }
4722
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
+
4723
4784
  // packages/cli/src/index.ts
4724
4785
  init_paths();
4725
4786
 
@@ -5491,6 +5552,25 @@ function jobRootResolved() {
5491
5552
  const legacy = join24(homedir11(), ".local", "share", "dia-job");
5492
5553
  return !existsSync21(next) && existsSync21(legacy) ? legacy : next;
5493
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
+ }
5494
5574
  function runJob(argv) {
5495
5575
  const { bin, error } = jobBin();
5496
5576
  if (!bin) {
@@ -6020,7 +6100,7 @@ function askBroker(req, timeoutMs = 5000) {
6020
6100
  }
6021
6101
 
6022
6102
  // packages/cli/src/index.ts
6023
- import { execFileSync as execFileSync12, spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
6103
+ import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
6024
6104
  import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
6025
6105
  import { basename as basename10, join as join32 } from "node:path";
6026
6106
  import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
@@ -7016,7 +7096,7 @@ function propose(opts = {}) {
7016
7096
  }
7017
7097
 
7018
7098
  // src/service.ts
7019
- import { spawnSync as spawnSync4 } from "node:child_process";
7099
+ import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
7020
7100
  import { createHash as createHash11, randomBytes as randomBytes4 } from "node:crypto";
7021
7101
  import {
7022
7102
  appendFileSync as appendFileSync9,
@@ -7041,6 +7121,52 @@ var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
7041
7121
  var VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
7042
7122
  var LIFECYCLE_TYPES = new Set(["periodic", "bounded", "service"]);
7043
7123
  var STDIN_TYPES = new Set(["json", "text", "none"]);
7124
+ var ACP_HOSTS = ["dsh", "opencode"];
7125
+ var HEALTHCHECK_KINDS = new Set(["script", "acp"]);
7126
+ var isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
7127
+ var isNonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
7128
+ function acpAgentCommand(agent) {
7129
+ if (!ACP_HOSTS.includes(agent?.host))
7130
+ throw new Error(`E_MANIFEST: entry.agent.host must be ${ACP_HOSTS.join("|")}`);
7131
+ const profile = agent.profile?.trim();
7132
+ if (agent.host === "dsh")
7133
+ return ["dsh", ...profile ? ["--profile", profile] : [], "acp"];
7134
+ return ["opencode", "acp"];
7135
+ }
7136
+ var isAgentEntry = (e) => !!e && e.agent !== undefined && e.agent !== null;
7137
+ var healthcheckKind = (hc) => hc?.kind ?? "script";
7138
+ var entryCmdOf = (svc) => svc.entry.cmd ?? "";
7139
+ function checkAgentEntry(a) {
7140
+ if (!isPlainObject(a))
7141
+ throw new Error("E_MANIFEST: entry.agent must be an object");
7142
+ if (!ACP_HOSTS.includes(a.host))
7143
+ throw new Error(`E_MANIFEST: entry.agent.host must be ${ACP_HOSTS.join("|")}`);
7144
+ for (const k of ["profile", "persona"]) {
7145
+ if (a[k] !== undefined && !isNonEmptyString(a[k]))
7146
+ throw new Error(`E_MANIFEST: entry.agent.${k} must be a non-empty string`);
7147
+ }
7148
+ if (a.host !== "dsh" && a.profile !== undefined) {
7149
+ throw new Error(`E_MANIFEST: entry.agent.profile 只适用于 host=dsh(${a.host} 没有 profile)`);
7150
+ }
7151
+ return a;
7152
+ }
7153
+ function checkEntry(entry) {
7154
+ if (!isPlainObject(entry))
7155
+ throw new Error("E_MANIFEST: entry required");
7156
+ const hasCmd = entry.cmd !== undefined && entry.cmd !== null;
7157
+ const hasAgent = entry.agent !== undefined && entry.agent !== null;
7158
+ if (hasCmd && hasAgent)
7159
+ throw new Error("E_MANIFEST: entry.cmd 与 entry.agent 互斥,只能声明其一");
7160
+ if (!hasCmd && !hasAgent)
7161
+ throw new Error("E_MANIFEST: entry.cmd required(脚本型)或 entry.agent(agent 型)");
7162
+ if (hasCmd) {
7163
+ if (typeof entry.cmd !== "string" || !entry.cmd.trim())
7164
+ throw new Error("E_MANIFEST: entry.cmd required");
7165
+ } else {
7166
+ checkAgentEntry(entry.agent);
7167
+ }
7168
+ return entry;
7169
+ }
7044
7170
  function entryDir() {
7045
7171
  return process.env.DIA_JOB_ENTRY_DIR || join31(jobRoot2(), "bin");
7046
7172
  }
@@ -7083,8 +7209,14 @@ var LEVELS = ["ok", "degraded", "fail"];
7083
7209
  function checkHealthcheck(hc) {
7084
7210
  if (hc === null || typeof hc !== "object" || Array.isArray(hc))
7085
7211
  throw new Error("E_MANIFEST: healthcheck must be an object");
7086
- if (typeof hc.cmd !== "string" || !hc.cmd.trim())
7087
- throw new Error("E_MANIFEST: healthcheck.cmd required");
7212
+ if (hc.kind !== undefined && !HEALTHCHECK_KINDS.has(hc.kind))
7213
+ throw new Error("E_MANIFEST: healthcheck.kind must be script|acp");
7214
+ if (healthcheckKind(hc) === "script") {
7215
+ if (typeof hc.cmd !== "string" || !hc.cmd.trim())
7216
+ throw new Error("E_MANIFEST: healthcheck.cmd required");
7217
+ } else if (hc.cmd !== undefined) {
7218
+ throw new Error("E_MANIFEST: healthcheck.kind=acp 不得再声明 cmd(判据是 initialize + session/new,不是外部脚本)");
7219
+ }
7088
7220
  if (hc.interval !== undefined) {
7089
7221
  const why = checkCronInterval(hc.interval);
7090
7222
  if (why)
@@ -7119,10 +7251,14 @@ function validateServiceManifest(m) {
7119
7251
  throw new Error("E_MANIFEST: bad id");
7120
7252
  if (!m.version || !VERSION_RE.test(m.version))
7121
7253
  throw new Error("E_MANIFEST: bad version");
7122
- if (m.entry === null || typeof m.entry !== "object" || Array.isArray(m.entry))
7123
- throw new Error("E_MANIFEST: entry required");
7124
- if (typeof m.entry.cmd !== "string" || !m.entry.cmd.trim())
7125
- throw new Error("E_MANIFEST: entry.cmd required");
7254
+ checkEntry(m.entry);
7255
+ const agentEntry = isAgentEntry(m.entry);
7256
+ if (agentEntry && healthcheckKind(m.healthcheck) !== "acp") {
7257
+ throw new Error('E_MANIFEST: entry.agent 必须配 healthcheck.kind="acp"(脚本型 healthcheck 无 cmd 可执行)');
7258
+ }
7259
+ if (!agentEntry && m.healthcheck && healthcheckKind(m.healthcheck) === "acp") {
7260
+ throw new Error('E_MANIFEST: healthcheck.kind="acp" 只适用于 entry.agent 型服务');
7261
+ }
7126
7262
  if (m.lifecycle === null || typeof m.lifecycle !== "object" || Array.isArray(m.lifecycle))
7127
7263
  throw new Error("E_MANIFEST: lifecycle required");
7128
7264
  if (!LIFECYCLE_TYPES.has(m.lifecycle.type))
@@ -7237,7 +7373,18 @@ var STALE_FACTOR = 1.5;
7237
7373
  var MIN_HEALTH_INTERVAL = 60;
7238
7374
  var healthInterval = (hc) => hc?.interval ?? DEFAULT_HEALTH_INTERVAL;
7239
7375
  var staleAfter = (hc) => Math.floor(healthInterval(hc) * STALE_FACTOR);
7240
- var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && typeof v.state === "string";
7376
+ var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && (v.state === "ok" || v.state === "degraded" || v.state === "fail");
7377
+ var TS_RE = /"ts"\s*:\s*(-?\d+)/;
7378
+ var KIND_RE = /"kind"\s*:\s*"([A-Za-z0-9_-]+)"/;
7379
+ function parseStateLine(line) {
7380
+ const head = line.slice(0, 256), tail = line.slice(-256);
7381
+ const ts = TS_RE.exec(head)?.[1] ?? TS_RE.exec(tail)?.[1];
7382
+ const kind = KIND_RE.exec(head)?.[1] ?? KIND_RE.exec(tail)?.[1];
7383
+ if (!ts || !kind)
7384
+ return null;
7385
+ const n = Number(ts);
7386
+ return Number.isFinite(n) ? { ts: n, kind } : null;
7387
+ }
7241
7388
  function findLastHealth(lines, service, gen) {
7242
7389
  for (let i = lines.length - 1;i >= 0; i--) {
7243
7390
  const r = lines[i];
@@ -7422,6 +7569,24 @@ function diaCmd() {
7422
7569
  const entry = entries.find((p) => existsSync28(p));
7423
7570
  return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
7424
7571
  }
7572
+ function decorateStatus(text, view) {
7573
+ const lines = String(text ?? "").split(`
7574
+ `);
7575
+ const outLines = [];
7576
+ for (const line of lines) {
7577
+ outLines.push(line);
7578
+ const m = line.match(/local\.dia-job-svc-([A-Za-z0-9._-]+)/) || line.match(/(?:^|\s)svc-([A-Za-z0-9._-]+)\s/);
7579
+ if (!m)
7580
+ continue;
7581
+ try {
7582
+ const v = view(m[1]);
7583
+ if (v?.label)
7584
+ outLines.push(` health=${v.label}`);
7585
+ } catch {}
7586
+ }
7587
+ return outLines.join(`
7588
+ `);
7589
+ }
7425
7590
  function servicesRoot() {
7426
7591
  return process.env.DIA_SERVICE_HOME || join31(diaHome(), "services");
7427
7592
  }
@@ -7456,15 +7621,34 @@ function readHealthLines(file) {
7456
7621
  if (!existsSync28(file))
7457
7622
  return [];
7458
7623
  const rows = [];
7459
- for (const line of readFileSync25(file, "utf8").split(`
7460
- `)) {
7624
+ const lines = readFileSync25(file, "utf8").split(`
7625
+ `);
7626
+ for (const line of lines) {
7461
7627
  if (!line.trim())
7462
7628
  continue;
7463
7629
  try {
7464
7630
  const d = JSON.parse(line);
7465
7631
  if (isRecord(d))
7466
7632
  rows.push(d);
7633
+ continue;
7467
7634
  } catch {}
7635
+ const head = parseStateLine(line);
7636
+ const state = head ? /"state"\s*:\s*"(ok|degraded|fail)"/.exec(line)?.[1] : undefined;
7637
+ if (head && state) {
7638
+ rows.push({
7639
+ ...head,
7640
+ state,
7641
+ service: "",
7642
+ gen: "",
7643
+ source: "schedule",
7644
+ candidate: state,
7645
+ streak: 0,
7646
+ raw: state,
7647
+ exit: null,
7648
+ timeout: false,
7649
+ detail: ""
7650
+ });
7651
+ }
7468
7652
  }
7469
7653
  return rows;
7470
7654
  }
@@ -7473,6 +7657,41 @@ function appendHealthRecord(file, rec) {
7473
7657
  appendFileSync9(file, JSON.stringify(rec) + `
7474
7658
  `);
7475
7659
  }
7660
+ function readServiceHealth(id, opts = {}) {
7661
+ const { healthFile } = opts;
7662
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
7663
+ const jr = opts.jobRoot ?? jobRoot2();
7664
+ const dir = opts.servicesRoot ?? (process.env.DIA_SERVICE_HOME || join31(jr, "services"));
7665
+ const installDir = join31(dir, id);
7666
+ let lock = null;
7667
+ try {
7668
+ lock = readLockFile(id);
7669
+ } catch {}
7670
+ let svc = null;
7671
+ try {
7672
+ svc = readServiceManifest(lock?.dir ?? installDir);
7673
+ } catch {}
7674
+ if (!svc)
7675
+ return healthView({ adopted: true, hasHealthcheck: false, now });
7676
+ const hc = svc?.healthcheck;
7677
+ if (!hc)
7678
+ return healthView({ hasHealthcheck: false, now });
7679
+ const ledger = healthFile ?? healthStatePath(id);
7680
+ const recs = readHealthLines(ledger).filter(isRecord);
7681
+ let rec = lock ? findLastHealth(recs, id, lock.gen) : null;
7682
+ if (!rec) {
7683
+ for (let i = recs.length - 1;i >= 0; i--) {
7684
+ const r = recs[i];
7685
+ if (r.state !== "ok" && r.state !== "degraded" && r.state !== "fail")
7686
+ continue;
7687
+ if (r.service && r.service !== id)
7688
+ continue;
7689
+ if (!rec || r.ts > rec.ts)
7690
+ rec = r;
7691
+ }
7692
+ }
7693
+ return healthView({ record: rec, hasHealthcheck: true, hc, now });
7694
+ }
7476
7695
  function listServices(opts = {}) {
7477
7696
  const root = opts.servicesRoot ?? servicesRoot();
7478
7697
  const jr = opts.jobRoot ?? jobRoot2();
@@ -7641,6 +7860,134 @@ function runHealth(svc, opts) {
7641
7860
  }
7642
7861
  return { judgement, report, stdout, stderr, ms };
7643
7862
  }
7863
+ async function runAcpHealth(svc, opts) {
7864
+ const hc = svc.healthcheck;
7865
+ if (!hc || healthcheckKind(hc) !== "acp")
7866
+ throw new Error("E_MANIFEST: runAcpHealth 只处理 healthcheck.kind=acp");
7867
+ const agent = svc.entry.agent;
7868
+ if (!agent)
7869
+ throw new Error("E_MANIFEST: healthcheck.kind=acp 需要 entry.agent");
7870
+ const timeoutMs = (hc.timeout ?? DEFAULT_HEALTH_TIMEOUT) * 1000;
7871
+ const now = opts.now ?? Date.now;
7872
+ const t0 = now();
7873
+ const argv = acpAgentCommand(agent);
7874
+ const env = { ...process.env, ...opts.env ?? {} };
7875
+ const steps = [];
7876
+ const finish = (status, exit) => {
7877
+ const judgement = {
7878
+ ...judgeHealth(exit, {}),
7879
+ state: status,
7880
+ exit,
7881
+ timeout: false,
7882
+ detail: steps.filter((x) => x.state !== "ok").map((x) => `${x.step}: ${x.detail}`).join("; ") || `initialize + session/new ok(${agent.host}${agent.profile ? " --profile " + agent.profile : ""})`
7883
+ };
7884
+ return { judgement, report: { status, checks: steps.map((x) => ({ name: x.step, status: x.state, detail: x.detail })) }, ms: now() - t0, steps };
7885
+ };
7886
+ const child = spawn2(argv[0], argv.slice(1), { cwd: opts.dir, env, stdio: ["pipe", "pipe", "pipe"] });
7887
+ const pending = new Map;
7888
+ let buf = "";
7889
+ child.stdout.setEncoding("utf8");
7890
+ child.stdout.on("data", (chunk) => {
7891
+ buf += chunk;
7892
+ for (let i = buf.indexOf(`
7893
+ `);i >= 0; i = buf.indexOf(`
7894
+ `)) {
7895
+ const line = buf.slice(0, i).trim();
7896
+ buf = buf.slice(i + 1);
7897
+ if (!line)
7898
+ continue;
7899
+ let msg;
7900
+ try {
7901
+ msg = JSON.parse(line);
7902
+ } catch {
7903
+ continue;
7904
+ }
7905
+ const waiter = msg && msg.id !== undefined ? pending.get(msg.id) : undefined;
7906
+ if (waiter) {
7907
+ pending.delete(msg.id);
7908
+ waiter(msg);
7909
+ }
7910
+ }
7911
+ });
7912
+ const spawnError = await new Promise((resolve) => {
7913
+ let settled = false;
7914
+ const done = (v) => {
7915
+ if (!settled) {
7916
+ settled = true;
7917
+ resolve(v);
7918
+ }
7919
+ };
7920
+ child.once("error", (e) => done(String(e?.message || e)));
7921
+ child.once("spawn", () => done(null));
7922
+ setTimeout(() => done(null), 50);
7923
+ });
7924
+ if (spawnError)
7925
+ return steps.push({ step: "initialize", state: "degraded", detail: `端点起不来:${spawnError}` }), finish("degraded", 20);
7926
+ const call = (id, method, params) => new Promise((resolve) => {
7927
+ const timer = setTimeout(() => {
7928
+ pending.delete(id);
7929
+ resolve({ timeout: true });
7930
+ }, timeoutMs);
7931
+ pending.set(id, (m) => {
7932
+ clearTimeout(timer);
7933
+ resolve(m);
7934
+ });
7935
+ try {
7936
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + `
7937
+ `);
7938
+ } catch (e) {
7939
+ clearTimeout(timer);
7940
+ pending.delete(id);
7941
+ resolve({ died: String(e?.message || e) });
7942
+ }
7943
+ });
7944
+ try {
7945
+ const init = await call(1, "initialize", { protocolVersion: 1, clientCapabilities: {} });
7946
+ if (init.timeout)
7947
+ steps.push({ step: "initialize", state: "degraded", detail: "initialize 超时" });
7948
+ else if (init.died)
7949
+ steps.push({ step: "initialize", state: "degraded", detail: `端点已退出:${init.died}` });
7950
+ else if (init.error)
7951
+ steps.push({ step: "initialize", state: "degraded", detail: `initialize 报错:${init.error?.message ?? JSON.stringify(init.error)}` });
7952
+ else if (!init.result)
7953
+ steps.push({ step: "initialize", state: "degraded", detail: "initialize 无 result" });
7954
+ else
7955
+ steps.push({ step: "initialize", state: "ok", detail: "initialize ok" });
7956
+ if (steps[0].state !== "ok")
7957
+ return finish("degraded", 20);
7958
+ const ns = await call(2, "session/new", { cwd: opts.dir, mcpServers: [] });
7959
+ if (ns.timeout)
7960
+ steps.push({ step: "session/new", state: "fail", detail: "session/new 超时" });
7961
+ else if (ns.error)
7962
+ steps.push({ step: "session/new", state: "fail", detail: `session/new 报错:${ns.error?.message ?? JSON.stringify(ns.error)}` });
7963
+ else if (!ns.result?.sessionId)
7964
+ steps.push({ step: "session/new", state: "fail", detail: "session/new 未返回 sessionId" });
7965
+ else
7966
+ steps.push({ step: "session/new", state: "ok", detail: `session ${ns.result.sessionId}` });
7967
+ return steps[1].state === "ok" ? finish("ok", 0) : finish("fail", 21);
7968
+ } finally {
7969
+ try {
7970
+ child.kill("SIGKILL");
7971
+ } catch {}
7972
+ }
7973
+ }
7974
+ function recordHealth(svc, judgement, opts = {}) {
7975
+ const id = svc.id;
7976
+ const gen = opts.gen || generationOf(id);
7977
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
7978
+ const file = opts.stateFile || process.env.DIA_SERVICE_HEALTH_FILE || healthStatePath(id);
7979
+ const record = transition(findLastHealth(readHealthLines(file), id, gen), {
7980
+ service: id,
7981
+ gen,
7982
+ source: opts.source ?? "manual",
7983
+ ts: now,
7984
+ judgement,
7985
+ debounce: svc.healthcheck?.debounce,
7986
+ checks: opts.checks
7987
+ });
7988
+ appendHealthRecord(file, record);
7989
+ return { record, statePath: file };
7990
+ }
7644
7991
  function whichTimeout1() {
7645
7992
  const probe = spawnSync4("bash", ["-c", "command -v timeout || command -v gtimeout"], { encoding: "utf8" });
7646
7993
  const p = String(probe.stdout ?? "").trim().split(`
@@ -7699,7 +8046,7 @@ function collectManagedFiles(base, hc) {
7699
8046
  }
7700
8047
  function verify4(dir, opts = {}) {
7701
8048
  const svc = readServiceManifest(dir);
7702
- const entry = resolveEntry(svc.entry.cmd, dir);
8049
+ const entry = resolveEntry(entryCmdOf(svc), dir);
7703
8050
  const drift = [];
7704
8051
  if (!existsSync28(entry.path))
7705
8052
  drift.push({ code: "entry_missing", detail: `entry 不存在:${entry.path}` });
@@ -7802,7 +8149,7 @@ function add2(opts) {
7802
8149
  }
7803
8150
  if (svc.healthcheck)
7804
8151
  healthJobInterval(svc);
7805
- const entry = resolveEntry(svc.entry.cmd, dir);
8152
+ const entry = resolveEntry(entryCmdOf(svc), dir);
7806
8153
  if (!existsSync28(entry.path))
7807
8154
  throw new Error(`E_UNSTABLE_ENTRY: entry 不存在:${entry.path}`);
7808
8155
  if (!isExecutableFile(entry.path))
@@ -7816,7 +8163,7 @@ function add2(opts) {
7816
8163
  gen,
7817
8164
  dryRun: !!opts.dryRun,
7818
8165
  backend,
7819
- entry: { declared: svc.entry.cmd, kind: entry.kind, path: entry.path, link: null },
8166
+ entry: { declared: entryCmdOf(svc), kind: entry.kind, path: entry.path, link: null },
7820
8167
  jobs: { service: serviceJobName(svc.id), health: svc.healthcheck ? healthJobName(svc.id) : null },
7821
8168
  target_mismatch: mismatch
7822
8169
  };
@@ -7825,7 +8172,7 @@ function add2(opts) {
7825
8172
  rmSync9(dir, { recursive: true, force: true });
7826
8173
  return result;
7827
8174
  }
7828
- const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(svc.entry.cmd, dir)}`];
8175
+ const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(entryCmdOf(svc), dir)}`];
7829
8176
  const runner = opts.runner ?? defaultRunner({ backfill });
7830
8177
  const installed = [];
7831
8178
  const fail = (code, msg, rc) => {
@@ -7842,7 +8189,7 @@ function add2(opts) {
7842
8189
  entryPath = linkEntry(entry.path);
7843
8190
  result.entry.link = entryPath;
7844
8191
  result.entry.path = entryPath;
7845
- } else if (svc.entry.cmd.startsWith(entryDir())) {
8192
+ } else if (entryCmdOf(svc).startsWith(entryDir())) {
7846
8193
  result.entry.link = entry.path;
7847
8194
  }
7848
8195
  const jobArgs = (name, cmd) => {
@@ -8159,13 +8506,13 @@ usage:
8159
8506
  dia collab status|enable|disable [--persona ID] # dia-msg 默认能力开关(默认开)
8160
8507
  dia whoami [--json] [--name NAME] # 当前会话身份(session id)
8161
8508
  dia in [--unread] [--all] [--legacy] [--json] # 会话收件箱(正文内联)
8162
- dia out --to <session_id> --subject S (--body B|--body-file F) [--kind K] [--reply-to ID]
8509
+ dia out --to <session_id> --subject S (--body B|--body-file F) [--kind K] [--touch p1,p2] [--reply-to ID]
8163
8510
  dia ack <id> | dia reply <id> --body B | dia close <id> --reason R | dia show <id>
8164
8511
  dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
8165
8512
  dia agents list|register|unregister|discover
8166
8513
  dia msg deliver <agent> <id> [--transport tmux|acp]
8167
8514
  dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
8168
- dia graph [--json] [--task t-...]
8515
+ dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
8169
8516
  dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] | dia unclaim <path|id> [--force]
8170
8517
  dia team run <id> --mock | --acp|--dsh --workspace DIR
8171
8518
  dia team gate|accept|reject|next
@@ -8558,6 +8905,19 @@ async function taskCli(argv) {
8558
8905
  }
8559
8906
  async function graphCli(argv) {
8560
8907
  const asJson = has(argv, "--json");
8908
+ if (has(argv, "--tui") || has(argv, "--watch")) {
8909
+ const paint = (g) => out(render2(g, { color: !!process.stdout.isTTY && !has(argv, "--no-color") }));
8910
+ try {
8911
+ if (has(argv, "--watch"))
8912
+ return await watch(() => build({ task: flag(argv, "--task") }), { color: !!process.stdout.isTTY && !has(argv, "--no-color") });
8913
+ const g = build({ task: flag(argv, "--task") });
8914
+ paint(g);
8915
+ return g.cycles.length ? 3 : 0;
8916
+ } catch (e) {
8917
+ err(e?.message || String(e));
8918
+ return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
8919
+ }
8920
+ }
8561
8921
  try {
8562
8922
  const g = build({ task: flag(argv, "--task") });
8563
8923
  if (asJson)
@@ -8644,7 +9004,7 @@ function spawnDetachedDelivery(args) {
8644
9004
  const log = join32(dir, `deliver-${id}.log`);
8645
9005
  const fd = openSync3(log, "a");
8646
9006
  const bin = process.argv[1];
8647
- const child = spawn2(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
9007
+ const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
8648
9008
  detached: true,
8649
9009
  stdio: ["ignore", fd, fd],
8650
9010
  env: process.env
@@ -8934,6 +9294,13 @@ async function collabCli(argv) {
8934
9294
  i++;
8935
9295
  }
8936
9296
  }
9297
+ const touchPaths = [...flags(args, "--touch"), ...(flag(args, "--touches") || "").split(",")].map((x) => x.trim()).filter(Boolean);
9298
+ if (touchPaths.length)
9299
+ refs.touches = touchPaths.join(",");
9300
+ const refPaths = (m) => [
9301
+ ...(m.refs?.touches || "").split(","),
9302
+ ...m.refs?.path ? [m.refs.path] : []
9303
+ ].map((x) => x.trim()).filter(Boolean);
8937
9304
  const view = (m) => ({ ...m, body: bodyOf(m) });
8938
9305
  const print = (m) => {
8939
9306
  out(`${m.id} [${m.state}] ${m.subject}`);
@@ -8942,6 +9309,12 @@ async function collabCli(argv) {
8942
9309
  out(` kind: ${m.kind}${m.requires_reply ? " 需回复" : ""}${m.deadline ? ` deadline=${m.deadline}` : ""} thread=${m.thread_id}#${m.seq}`);
8943
9310
  if (m.refs && Object.keys(m.refs).length)
8944
9311
  out(` refs: ${Object.entries(m.refs).map(([k, v]) => `${k}=${v}`).join(" ")}`);
9312
+ const touches = refPaths(m);
9313
+ if (touches.length) {
9314
+ const held = list3({}).filter((c) => touches.includes(c.path) && c.holder !== m.to.session_id);
9315
+ for (const c of held)
9316
+ out(` ⚠ claim: ${c.path} 由 ${c.holder} 持有(${c.reason || "no reason"})`);
9317
+ }
8945
9318
  const body = bodyOf(m);
8946
9319
  if (body) {
8947
9320
  const max = 4000;
@@ -9044,7 +9417,14 @@ async function collabCli(argv) {
9044
9417
  idempotencyKey: flag(args, "--idempotency-key"),
9045
9418
  hops: flag(args, "--hops") ? Number(flag(args, "--hops")) : undefined
9046
9419
  });
9047
- out(asJson ? JSON.stringify(m, null, 1) : `${m.id} ${m.thread_id}#${m.seq} ${m.from.session_id} → ${m.to.session_id} ${m.kind} ${m.subject}`);
9420
+ const overlaps = list3({}).filter((c) => touchPaths.includes(c.path) && c.holder !== m.from.session_id);
9421
+ if (asJson)
9422
+ out(JSON.stringify(overlaps.length ? { ...m, claim_overlaps: overlaps } : m, null, 1));
9423
+ else {
9424
+ out(`${m.id} ${m.thread_id}#${m.seq} ${m.from.session_id} → ${m.to.session_id} ${m.kind} ${m.subject}`);
9425
+ for (const c of overlaps)
9426
+ out(` ⚠ claim overlap: ${c.path} 由 ${c.holder} 持有(${c.reason || "no reason"})`);
9427
+ }
9048
9428
  return 0;
9049
9429
  }
9050
9430
  if (sub === "ack") {
@@ -9638,6 +10018,18 @@ async function serviceCli(argv) {
9638
10018
  }
9639
10019
  const fromWrapper = has(args, "--from-wrapper");
9640
10020
  const ex = flag(args, "--exit");
10021
+ const dir = serviceDir(id);
10022
+ const svc = readServiceManifest(dir);
10023
+ if (healthcheckKind(svc.healthcheck) === "acp" && !fromWrapper) {
10024
+ const run = await runAcpHealth(svc, { dir });
10025
+ const rec = recordHealth(svc, run.judgement, { gen: flag(args, "--gen"), stateFile: flag(args, "--state-file") });
10026
+ const code = run.judgement.state === "ok" ? 0 : run.judgement.state === "degraded" ? 30 : 1;
10027
+ if (asJson)
10028
+ out(JSON.stringify({ id, verdict: run.judgement.state, shown: rec.record.state, exit: run.judgement.exit, detail: run.judgement.detail, steps: run.steps }, null, 1));
10029
+ else
10030
+ out(`${id}: ${run.judgement.state}(shown=${rec.record.state}${run.judgement.detail ? ", " + run.judgement.detail : ""})`);
10031
+ return code;
10032
+ }
9641
10033
  const r = runServiceHealth(id, {
9642
10034
  wrapperExit: fromWrapper ? Number(ex ?? "1") : undefined,
9643
10035
  gen: flag(args, "--gen"),
@@ -10086,8 +10478,19 @@ async function main(rawArgv) {
10086
10478
  return agentsCli(argv.slice(1));
10087
10479
  if (cmd === "team")
10088
10480
  return teamCli(argv.slice(1));
10089
- if (cmd === "job")
10090
- return runJob(argv.slice(1));
10481
+ if (cmd === "job") {
10482
+ const rest = argv.slice(1);
10483
+ if (rest[0] === "status" || rest[0] === "list") {
10484
+ const r = runJobCapture(rest);
10485
+ if (r.stderr)
10486
+ process.stderr.write(r.stderr);
10487
+ const text = decorateStatus(r.stdout, (id) => readServiceHealth(id));
10488
+ if (text)
10489
+ out(text.replace(/\n$/, ""));
10490
+ return r.status;
10491
+ }
10492
+ return runJob(rest);
10493
+ }
10091
10494
  if (cmd === "price") {
10092
10495
  const table = loadPrices();
10093
10496
  if (has(argv.slice(1), "--json"))
package/docs/index.md CHANGED
@@ -31,7 +31,7 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
31
31
  | [价目表(tokens → 金额)](pricing.md) | 原则:没有价格就不编造;unpriced 明确记录,预算按 tokens 兜底。 |
32
32
  | [目标提案(team next)](proposals.md) | 定位:团队可以提候选,不能定目标。 |
33
33
  | [Sandbox 细化设计(dia)](security-sandbox.md) | 依据:dsh 本机源码实测、Hermes/Pi 取证、[/tmp/dia-sbx 实验]。 |
34
- | [服务(service):可托管的执行体](service-noun.md) | 状态:提案(设计,未实现)。只做脚本型服务;healthcheck 按专用退出码域判定,旧结果超新鲜度窗口即报 stale。 |
34
+ | [服务(service):可托管的执行体](service-noun.md) | 状态:已实现(v0.0.10,脚本型 + agent 型)。脚本型走 entry.cmd,agent 型走 entry.agent + healthcheck.kind="acp"; |
35
35
  | [dia 状态页(单一事实源)](status.md) | 任何"现在做到哪了"的问题,以本页为准;其他文档是细节。 |
36
36
  | [全自动团队:人只在验收介入](team-automation.md) | 目标:计划→实现→验证→对抗→修复全自动循环;人只在验收阶段拍板。 |
37
37
  | [团队章程:谁来做 dia(开发团队)](team.md) | 2026-10-28 升级:开发团队执行模式改为全自动循环(计划→实现→验证→对抗→修复),人只在验收阶段介入; |