@minonl/dia 0.0.9 → 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 +12 -0
- package/dist/cli.js +232 -16
- package/docs/index.md +1 -1
- package/docs/msg-collab-m1.md +15 -0
- package/docs/overview.md +6 -0
- package/docs/service-noun.md +38 -4
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +153 -67
- package/packages/wt/tests/adopt.test.sh +46 -44
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
|
@@ -6100,7 +6100,7 @@ function askBroker(req, timeoutMs = 5000) {
|
|
|
6100
6100
|
}
|
|
6101
6101
|
|
|
6102
6102
|
// packages/cli/src/index.ts
|
|
6103
|
-
import { execFileSync as execFileSync12, spawn as
|
|
6103
|
+
import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
6104
6104
|
import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
|
|
6105
6105
|
import { basename as basename10, join as join32 } from "node:path";
|
|
6106
6106
|
import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
|
|
@@ -7096,7 +7096,7 @@ function propose(opts = {}) {
|
|
|
7096
7096
|
}
|
|
7097
7097
|
|
|
7098
7098
|
// src/service.ts
|
|
7099
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
7099
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
|
|
7100
7100
|
import { createHash as createHash11, randomBytes as randomBytes4 } from "node:crypto";
|
|
7101
7101
|
import {
|
|
7102
7102
|
appendFileSync as appendFileSync9,
|
|
@@ -7121,6 +7121,52 @@ var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
7121
7121
|
var VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
7122
7122
|
var LIFECYCLE_TYPES = new Set(["periodic", "bounded", "service"]);
|
|
7123
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
|
+
}
|
|
7124
7170
|
function entryDir() {
|
|
7125
7171
|
return process.env.DIA_JOB_ENTRY_DIR || join31(jobRoot2(), "bin");
|
|
7126
7172
|
}
|
|
@@ -7163,8 +7209,14 @@ var LEVELS = ["ok", "degraded", "fail"];
|
|
|
7163
7209
|
function checkHealthcheck(hc) {
|
|
7164
7210
|
if (hc === null || typeof hc !== "object" || Array.isArray(hc))
|
|
7165
7211
|
throw new Error("E_MANIFEST: healthcheck must be an object");
|
|
7166
|
-
if (
|
|
7167
|
-
throw new Error("E_MANIFEST: healthcheck.
|
|
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
|
+
}
|
|
7168
7220
|
if (hc.interval !== undefined) {
|
|
7169
7221
|
const why = checkCronInterval(hc.interval);
|
|
7170
7222
|
if (why)
|
|
@@ -7199,10 +7251,14 @@ function validateServiceManifest(m) {
|
|
|
7199
7251
|
throw new Error("E_MANIFEST: bad id");
|
|
7200
7252
|
if (!m.version || !VERSION_RE.test(m.version))
|
|
7201
7253
|
throw new Error("E_MANIFEST: bad version");
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
if (
|
|
7205
|
-
throw new Error(
|
|
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
|
+
}
|
|
7206
7262
|
if (m.lifecycle === null || typeof m.lifecycle !== "object" || Array.isArray(m.lifecycle))
|
|
7207
7263
|
throw new Error("E_MANIFEST: lifecycle required");
|
|
7208
7264
|
if (!LIFECYCLE_TYPES.has(m.lifecycle.type))
|
|
@@ -7804,6 +7860,134 @@ function runHealth(svc, opts) {
|
|
|
7804
7860
|
}
|
|
7805
7861
|
return { judgement, report, stdout, stderr, ms };
|
|
7806
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
|
+
}
|
|
7807
7991
|
function whichTimeout1() {
|
|
7808
7992
|
const probe = spawnSync4("bash", ["-c", "command -v timeout || command -v gtimeout"], { encoding: "utf8" });
|
|
7809
7993
|
const p = String(probe.stdout ?? "").trim().split(`
|
|
@@ -7862,7 +8046,7 @@ function collectManagedFiles(base, hc) {
|
|
|
7862
8046
|
}
|
|
7863
8047
|
function verify4(dir, opts = {}) {
|
|
7864
8048
|
const svc = readServiceManifest(dir);
|
|
7865
|
-
const entry = resolveEntry(svc
|
|
8049
|
+
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
7866
8050
|
const drift = [];
|
|
7867
8051
|
if (!existsSync28(entry.path))
|
|
7868
8052
|
drift.push({ code: "entry_missing", detail: `entry 不存在:${entry.path}` });
|
|
@@ -7965,7 +8149,7 @@ function add2(opts) {
|
|
|
7965
8149
|
}
|
|
7966
8150
|
if (svc.healthcheck)
|
|
7967
8151
|
healthJobInterval(svc);
|
|
7968
|
-
const entry = resolveEntry(svc
|
|
8152
|
+
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
7969
8153
|
if (!existsSync28(entry.path))
|
|
7970
8154
|
throw new Error(`E_UNSTABLE_ENTRY: entry 不存在:${entry.path}`);
|
|
7971
8155
|
if (!isExecutableFile(entry.path))
|
|
@@ -7979,7 +8163,7 @@ function add2(opts) {
|
|
|
7979
8163
|
gen,
|
|
7980
8164
|
dryRun: !!opts.dryRun,
|
|
7981
8165
|
backend,
|
|
7982
|
-
entry: { declared: svc
|
|
8166
|
+
entry: { declared: entryCmdOf(svc), kind: entry.kind, path: entry.path, link: null },
|
|
7983
8167
|
jobs: { service: serviceJobName(svc.id), health: svc.healthcheck ? healthJobName(svc.id) : null },
|
|
7984
8168
|
target_mismatch: mismatch
|
|
7985
8169
|
};
|
|
@@ -7988,7 +8172,7 @@ function add2(opts) {
|
|
|
7988
8172
|
rmSync9(dir, { recursive: true, force: true });
|
|
7989
8173
|
return result;
|
|
7990
8174
|
}
|
|
7991
|
-
const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(svc
|
|
8175
|
+
const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(entryCmdOf(svc), dir)}`];
|
|
7992
8176
|
const runner = opts.runner ?? defaultRunner({ backfill });
|
|
7993
8177
|
const installed = [];
|
|
7994
8178
|
const fail = (code, msg, rc) => {
|
|
@@ -8005,7 +8189,7 @@ function add2(opts) {
|
|
|
8005
8189
|
entryPath = linkEntry(entry.path);
|
|
8006
8190
|
result.entry.link = entryPath;
|
|
8007
8191
|
result.entry.path = entryPath;
|
|
8008
|
-
} else if (svc.
|
|
8192
|
+
} else if (entryCmdOf(svc).startsWith(entryDir())) {
|
|
8009
8193
|
result.entry.link = entry.path;
|
|
8010
8194
|
}
|
|
8011
8195
|
const jobArgs = (name, cmd) => {
|
|
@@ -8322,7 +8506,7 @@ usage:
|
|
|
8322
8506
|
dia collab status|enable|disable [--persona ID] # dia-msg 默认能力开关(默认开)
|
|
8323
8507
|
dia whoami [--json] [--name NAME] # 当前会话身份(session id)
|
|
8324
8508
|
dia in [--unread] [--all] [--legacy] [--json] # 会话收件箱(正文内联)
|
|
8325
|
-
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]
|
|
8326
8510
|
dia ack <id> | dia reply <id> --body B | dia close <id> --reason R | dia show <id>
|
|
8327
8511
|
dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
|
|
8328
8512
|
dia agents list|register|unregister|discover
|
|
@@ -8820,7 +9004,7 @@ function spawnDetachedDelivery(args) {
|
|
|
8820
9004
|
const log = join32(dir, `deliver-${id}.log`);
|
|
8821
9005
|
const fd = openSync3(log, "a");
|
|
8822
9006
|
const bin = process.argv[1];
|
|
8823
|
-
const child =
|
|
9007
|
+
const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
|
|
8824
9008
|
detached: true,
|
|
8825
9009
|
stdio: ["ignore", fd, fd],
|
|
8826
9010
|
env: process.env
|
|
@@ -9110,6 +9294,13 @@ async function collabCli(argv) {
|
|
|
9110
9294
|
i++;
|
|
9111
9295
|
}
|
|
9112
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);
|
|
9113
9304
|
const view = (m) => ({ ...m, body: bodyOf(m) });
|
|
9114
9305
|
const print = (m) => {
|
|
9115
9306
|
out(`${m.id} [${m.state}] ${m.subject}`);
|
|
@@ -9118,6 +9309,12 @@ async function collabCli(argv) {
|
|
|
9118
9309
|
out(` kind: ${m.kind}${m.requires_reply ? " 需回复" : ""}${m.deadline ? ` deadline=${m.deadline}` : ""} thread=${m.thread_id}#${m.seq}`);
|
|
9119
9310
|
if (m.refs && Object.keys(m.refs).length)
|
|
9120
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
|
+
}
|
|
9121
9318
|
const body = bodyOf(m);
|
|
9122
9319
|
if (body) {
|
|
9123
9320
|
const max = 4000;
|
|
@@ -9220,7 +9417,14 @@ async function collabCli(argv) {
|
|
|
9220
9417
|
idempotencyKey: flag(args, "--idempotency-key"),
|
|
9221
9418
|
hops: flag(args, "--hops") ? Number(flag(args, "--hops")) : undefined
|
|
9222
9419
|
});
|
|
9223
|
-
|
|
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
|
+
}
|
|
9224
9428
|
return 0;
|
|
9225
9429
|
}
|
|
9226
9430
|
if (sub === "ack") {
|
|
@@ -9814,6 +10018,18 @@ async function serviceCli(argv) {
|
|
|
9814
10018
|
}
|
|
9815
10019
|
const fromWrapper = has(args, "--from-wrapper");
|
|
9816
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
|
+
}
|
|
9817
10033
|
const r = runServiceHealth(id, {
|
|
9818
10034
|
wrapperExit: fromWrapper ? Number(ex ?? "1") : undefined,
|
|
9819
10035
|
gen: flag(args, "--gen"),
|
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) |
|
|
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 升级:开发团队执行模式改为全自动循环(计划→实现→验证→对抗→修复),人只在验收阶段介入; |
|
package/docs/msg-collab-m1.md
CHANGED
|
@@ -78,3 +78,18 @@ dia graph --tui --no-color # 关 ANSI 颜色
|
|
|
78
78
|
- 按状态分组显示 task → deps / touches / blocked_by;claims 单列;
|
|
79
79
|
- 不启 HTTP/daemon,遵守框架“无网络模块”约束;
|
|
80
80
|
- `--task t-...` 只显示该任务的直接邻居。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# handoff + claim 联动(M2)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dia claim src/collab.ts --reason "正在重构"
|
|
88
|
+
dia out --to ses_xxx --kind handoff --subject "交接" --body-file handoff.md \
|
|
89
|
+
--touches src/collab.ts,docs/msg-collab-m1.md
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `--touches a,b`(也接受重复 `--touch`)写入消息 `refs.touches`;
|
|
93
|
+
- 发送时如果有他人活跃 claim,输出 `⚠ claim overlap`(advisory,仍发送);
|
|
94
|
+
- 收件方 `dia in` 会在消息下显示 `⚠ claim: <path> 由 <holder> 持有`;
|
|
95
|
+
- 不自动排队/阻断;claim 仍是可见性工具。
|
package/docs/overview.md
CHANGED
|
@@ -85,6 +85,12 @@ $ dia --persona yan kb scene S1 → 正常出场景卡
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
+
## 3.1 CI 口径(见 README「CI 矩阵」)
|
|
89
|
+
|
|
90
|
+
- required = `test (ubuntu-latest)` + `test (macos-latest)`(分支保护,`enforce_admins`)
|
|
91
|
+
- `windows-smoke` 非阻塞:Windows 后端未实现(`dia job install` rc=3),落地后再升 required
|
|
92
|
+
- **docs-only 快通道**:只改 `docs/**` / 根 `*.md` / `llms*.txt` 时跳过测试矩阵
|
|
93
|
+
|
|
88
94
|
## 4. 已知缺口(按优先级)
|
|
89
95
|
|
|
90
96
|
1. **yan 安装落后来源仓 1 个提交**(`83fbaa9` vs `8d10cc5`,内容是 dsh profile 根 `cordis.yml` 修复)。
|
package/docs/service-noun.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# 服务(service):可托管的执行体
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 状态:**已实现(v0.0.10,脚本型 + agent 型)**。脚本型走 `entry.cmd`,agent 型走 `entry.agent` + `healthcheck.kind="acp"`;
|
|
4
|
+
> healthcheck 按专用码域 `0/20/21` 判定(外加 `124`=timeout、信号=crash),旧结果超新鲜度窗口即报 `unknown (stale …)`。
|
|
5
|
+
> **实现状态逐节对照见 §15。**
|
|
4
6
|
> 日期 2026-11-05(rev.2:按 [设计评审 B1–B5 / C1–C7 / D1–D4](review/service-noun-review-2026-11-05.md) 改稿;
|
|
5
|
-
> **rev.3**:按复核意见改「去抖单一写入方 / health 码域 `0/20/21` / timeout 映射 / 手动检查落台账 / adopted 口径」,见 §13
|
|
7
|
+
> **rev.3**:按复核意见改「去抖单一写入方 / health 码域 `0/20/21` / timeout 映射 / 手动检查落台账 / adopted 口径」,见 §13;
|
|
8
|
+
> **rev.4**:回填实现状态(PR #49 / #54 / #60)与 §15 对照表)。
|
|
6
9
|
> 关联:[`cli.md`](cli.md)(名词与动词矩阵 / 退出码)· [`persona-install.md`](persona-install.md)(`dia.json` schema)·
|
|
7
10
|
> [`kit-install.md`](kit-install.md) · [`acp-integration.md`](acp-integration.md)(agent 会话型服务)· `packages/job`(生命周期、状态台账)
|
|
8
11
|
|
|
@@ -243,9 +246,11 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
|
|
|
243
246
|
|
|
244
247
|
- `job` 仍是**唯一调度实现**(不新增第二套);健康检查任务本身就是一个 `job`(`periodic`)。
|
|
245
248
|
- `service` 只多一层「声明 + 校验 + 健康检查」。
|
|
246
|
-
- `dia job status` 的每行增加 `health=<ok|degraded|fail|unknown|n/a> (Ns ago)`(读 §4.5
|
|
249
|
+
- `dia job status` 的每行增加 `health=<ok|degraded|fail|unknown|n/a> (Ns ago)`(读 §4.5 缓存,不执行)。**✅ 已实现(PR #54)**:
|
|
250
|
+
仅对 `local.dia-job-svc-<id>` 行追加,其它行原样(向后兼容)。
|
|
247
251
|
- 对 `service` 型(常驻)任务:Linux 走 systemd `--user`,**常驻需 `loginctl enable-linger`**(`job doctor` 已提示,`dia service add` 一并转述);
|
|
248
252
|
对 `periodic` 任务,cron 后端**没有进程状态**,health 是唯一判据 → 新鲜度条款(§4.6)尤其关键。
|
|
253
|
+
- **性能**:只读台账、零执行;实测 N=50 时 `3.1ms`(阈值 200ms),基准见 `tests/service-health-status.perf.test.ts`。
|
|
249
254
|
|
|
250
255
|
## 7. 边界与异常
|
|
251
256
|
|
|
@@ -355,7 +360,8 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
|
|
|
355
360
|
|
|
356
361
|
## 14. P2:agent 型服务(entry = persona 会话)
|
|
357
362
|
|
|
358
|
-
>
|
|
363
|
+
> 状态:**已实现(PR #60)**。本节定义脚本型之外的 agent 型入口(`entry.agent` + `healthcheck.kind="acp"`);
|
|
364
|
+
> 实现与本节一致,差异见 §15。
|
|
359
365
|
|
|
360
366
|
### 14.1 与脚本型的关系
|
|
361
367
|
|
|
@@ -428,3 +434,31 @@ flowchart LR
|
|
|
428
434
|
- [ ] 单测:`runAcpHealth` 用 `tests/fixtures/fake-acp.mjs` 覆盖 `initialize` 失败 / `session/new` 失败 / 正常三态
|
|
429
435
|
- [ ] 单测:`healthcheck.kind` 缺省时按脚本型(向后兼容)
|
|
430
436
|
- [ ] e2e:装一个 agent 型 demo(fake ACP)→ `dia job status` 显示 `health=ok…`,杀掉端点后变 `degraded(stale)`
|
|
437
|
+
|
|
438
|
+
## 15. 实现状态(2026-11-06 回填)
|
|
439
|
+
|
|
440
|
+
> 规则:**设计先改、代码同批**(R35)。本节是「设计 ↔ 实现」的对照表;后续改动必须同批更新本表。
|
|
441
|
+
|
|
442
|
+
| 设计小节 | 状态 | 实现落点 |
|
|
443
|
+
|---|---|---|
|
|
444
|
+
| §2 构成(entry/lifecycle/contract/healthcheck) | ✅ | `src/service.ts`:`ServiceManifest` / `validateServiceManifest`(PR #49) |
|
|
445
|
+
| §3 清单与校验(entry 规则、`E_MANIFEST`) | ✅ | `checkEntry` / `checkAgentEntry` / `resolveEntry`(#49、#60) |
|
|
446
|
+
| §4.1 码域 `0/20/21` + `124` + 信号 | ✅ | `judgeHealth` / `judgeSpawnResult`;`124` 与 `137/143` 分流(#49;无 `timeout(1)` 的兜底见 #49 后续修复 `29a4341`) |
|
|
447
|
+
| §4.2 执行环境 / `E_BACKEND` | ✅ | `healthShell`、`checkCronInterval`(#49) |
|
|
448
|
+
| §4.3 去抖 + 单一写入方 + `gen` | ✅ | `transition`(`gen` 不同的行不计数)(#49) |
|
|
449
|
+
| §4.4 崩溃语义(保持上次状态 + `last_error`) | ✅ | `judgeSpawnResult` + `transition`(#49) |
|
|
450
|
+
| §4.5 台账(`state.jsonl` + `kind:"health"`) | ✅ | `appendHealthRecord` / `findLastHealth` / `readHealthLines`(#49、#54) |
|
|
451
|
+
| §4.6 新鲜度(`interval × 1.5` → stale) | ✅ | `healthView` / `staleAfter`(#54) |
|
|
452
|
+
| §5 CLI(add/remove/list/show/verify/health) | ✅ | `packages/cli/src/index.ts`(#49);`health` 的 agent 分支(#60) |
|
|
453
|
+
| §6 `dia job status` 追列 health | ✅ | `decorateStatus` + `runJobCapture`(#54) |
|
|
454
|
+
| §7 边界(`E_UNSTABLE_ENTRY` / adopted / three-state) | ✅ | `verify` / `healthView`(#49、#54) |
|
|
455
|
+
| §14 agent 型(`entry.agent` + `kind:"acp"`) | ✅ | `acpAgentCommand` / `runAcpHealth` / `recordHealth`(#60) |
|
|
456
|
+
| §6 性能基准(N=50 ≤200ms) | ✅ | 实测 **3.1ms**(单服务 0.1ms,macOS / bun 1.4.4);`tests/service-health-status.perf.test.ts` 断言 ≤200ms 且不随 N 线性增长 |
|
|
457
|
+
|
|
458
|
+
### 15.1 已知与设计的偏差
|
|
459
|
+
|
|
460
|
+
| 项 | 设计 | 实现 | 处置 |
|
|
461
|
+
|---|---|---|---|
|
|
462
|
+
| `healthcheck.kind` 缺省 | 未写 | 缺省 `script`(向后兼容) | 与 §14.6 一致,已写回 §14 |
|
|
463
|
+
| `dia service health` 的 agent 分支 | §5 未细化 | 先跑 ACP 探测再落 `source:"manual"` 行 | 与 §5/§14 语义一致,无需改设计 |
|
|
464
|
+
| `entry.agent` 与 `healthcheck.kind` 的强绑定 | §14.6 验收项 | 已实现为 `E_MANIFEST`(agent 型必须 acp;acp 只能配 agent 型) | 已写回 §14.2 校验说明 |
|
package/package.json
CHANGED
package/packages/wt/bin/dia-wt
CHANGED
|
@@ -1263,15 +1263,32 @@ exit 0
|
|
|
1263
1263
|
HOOKBODY
|
|
1264
1264
|
}
|
|
1265
1265
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
local
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1266
|
+
# simple-git-hooks RC 注入目录(跟随 dia 数据根)
|
|
1267
|
+
wt_hooks_rc_dir() { printf '%s/hooks' "${DIA_ROOT:-${DIA_DATA_DIR:-$HOME/.dia}}"; }
|
|
1268
|
+
# 在 ~/.zshenv 里确保 export KEY=VALUE(幂等;带 marker 便于移除)
|
|
1269
|
+
wt_ensure_env_export() {
|
|
1270
|
+
local key="$1" val="$2" rc="${DIA_WT_SHELL_RC:-$HOME/.zshenv}"
|
|
1271
|
+
local marker="# dia-wt: $key"
|
|
1272
|
+
[ -f "$rc" ] || : > "$rc"
|
|
1273
|
+
grep -q "^$marker" "$rc" 2>/dev/null && return 0
|
|
1274
|
+
printf '\n%s\nexport %s="%s"\n' "$marker" "$key" "$val" >> "$rc"
|
|
1275
|
+
}
|
|
1276
|
+
wt_write_hook() {
|
|
1277
|
+
local hooksdir="$1" name="$2" extra="${3:-}" chain="${4:-}"
|
|
1278
|
+
{
|
|
1279
|
+
printf '#!/usr/bin/env bash\n# dia-wt guard(managed by `dia wt adopt`;勿手改)\n'
|
|
1280
|
+
wt_guard_body
|
|
1281
|
+
} > "$hooksdir/$name"
|
|
1282
|
+
if [ -n "$chain" ]; then
|
|
1283
|
+
python3 - "$hooksdir/$name" "$chain" <<'PYCHAIN'
|
|
1284
|
+
import sys
|
|
1285
|
+
p, chain = sys.argv[1], sys.argv[2]
|
|
1286
|
+
s = open(p).read()
|
|
1287
|
+
s = s.replace('orig="$common/hooks/$name"', 'orig="%s/$name"' % chain.replace('\\', '\\\\').replace('"', '\\"'))
|
|
1288
|
+
open(p, "w").write(s)
|
|
1289
|
+
PYCHAIN
|
|
1290
|
+
fi
|
|
1291
|
+
chmod +x "$hooksdir/$name"
|
|
1275
1292
|
}
|
|
1276
1293
|
|
|
1277
1294
|
cmd_guard() {
|
|
@@ -1329,11 +1346,13 @@ cmd_adopt() {
|
|
|
1329
1346
|
|
|
1330
1347
|
if [ "$mode" = "revert" ]; then
|
|
1331
1348
|
[ -f "$cfg" ] || die "未 adopt(缺 $cfg)"
|
|
1332
|
-
local saved
|
|
1349
|
+
local saved mgr
|
|
1333
1350
|
saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
1334
|
-
|
|
1335
|
-
if [
|
|
1336
|
-
|
|
1351
|
+
mgr="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
|
|
1352
|
+
if [ "$mgr" = "local" ] || [ -z "$mgr" ]; then
|
|
1353
|
+
if [ "$prev" = "$hooksdir" ]; then
|
|
1354
|
+
if [ -n "$saved" ]; then git -C "$repo" config --local core.hooksPath "$saved"; else git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; fi
|
|
1355
|
+
fi
|
|
1337
1356
|
fi
|
|
1338
1357
|
rm -rf "$cfgdir"
|
|
1339
1358
|
echo "reverted:$cfgdir"
|
|
@@ -1348,16 +1367,29 @@ cmd_adopt() {
|
|
|
1348
1367
|
local cfgDef; cfgDef="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1349
1368
|
[ -n "$cfgDef" ] && def="$cfgDef"
|
|
1350
1369
|
fi
|
|
1351
|
-
local
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1370
|
+
local mgr2; mgr2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
|
|
1371
|
+
if [ "$mgr2" = "simple-git-hooks" ]; then
|
|
1372
|
+
local rcf; rcf="$(wt_hooks_rc_dir)/simple-git-hooks-rc.sh"
|
|
1373
|
+
[ -f "$rcf" ] || { echo "FAIL D02 缺 simple-git-hooks RC:$rcf"; bad=1; }
|
|
1374
|
+
grep -rqs "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" || { echo "FAIL D02 shell 未 export SIMPLE_GIT_HOOKS_RC"; bad=1; }
|
|
1375
|
+
python3 - "$repo/package.json" <<'PYM' >/dev/null 2>&1 || { echo "FAIL D02 package.json 未启用 simple-git-hooks"; bad=1; }
|
|
1376
|
+
import json,sys
|
|
1377
|
+
d=json.load(open(sys.argv[1])); c=d.get("simple-git-hooks"); dev=d.get("devDependencies",{}) or {}
|
|
1378
|
+
raise SystemExit(0 if isinstance(c,dict) or "simple-git-hooks" in dev else 1)
|
|
1379
|
+
PYM
|
|
1380
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1381
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 bash "$SELF" guard pre-commit >/dev/null 2>&1 ) \
|
|
1382
|
+
&& { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
|
|
1383
|
+
fi
|
|
1384
|
+
else
|
|
1385
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 本地 core.hooksPath 非绝对($prev)"; bad=1 ;; esac
|
|
1386
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1387
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 缺 $h 钩子"; bad=1; }
|
|
1388
|
+
done
|
|
1389
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1390
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1391
|
+
&& { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
|
|
1392
|
+
fi
|
|
1361
1393
|
fi
|
|
1362
1394
|
if [ "$json" = 1 ]; then
|
|
1363
1395
|
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0","defaultBranch":sys.argv[3]},ensure_ascii=False,separators=(",",":")))' "$repo" "$bad" "$def"
|
|
@@ -1383,34 +1415,78 @@ cmd_adopt() {
|
|
|
1383
1415
|
[ -n "$def" ] || die "默认分支不可解析;请 --default-branch main"
|
|
1384
1416
|
mkdir -p "$hooksdir"
|
|
1385
1417
|
# 安装/刷新本机全局钩子,并让本仓走全局钩子(不改 package.json、不占用仓库钩子目录)
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1418
|
+
# ── 方案:每个仓本地钩子;simple-git-hooks 仓改用它的 RC 注入 ──────────────
|
|
1419
|
+
# 不再写任何全局 git 配置(避免外溢)。
|
|
1420
|
+
if [ -f "$repo/package.json" ] && python3 - "$repo/package.json" <<'PYCHK'
|
|
1421
|
+
import json,sys
|
|
1422
|
+
d=json.load(open(sys.argv[1]))
|
|
1423
|
+
cfg=d.get("simple-git-hooks")
|
|
1424
|
+
dev=d.get("devDependencies",{}) or {}
|
|
1425
|
+
raise SystemExit(0 if isinstance(cfg,dict) or "simple-git-hooks" in dev else 1)
|
|
1426
|
+
PYCHK
|
|
1427
|
+
then
|
|
1428
|
+
# simple-git-hooks:钩子由管理器生成到 .git/hooks,我们不碰;通过 SIMPLE_GIT_HOOKS_RC 注入守卫
|
|
1429
|
+
local rcdir rcf
|
|
1430
|
+
rcdir="$(wt_hooks_rc_dir)"; rcf="$rcdir/simple-git-hooks-rc.sh"
|
|
1431
|
+
mkdir -p "$rcdir"
|
|
1432
|
+
cat > "$rcf" <<'RC'
|
|
1433
|
+
# 由 simple-git-hooks 生成的钩子 source 本文件(SIMPLE_GIT_HOOKS_RC)
|
|
1434
|
+
# 只对已 adopt 的仓生效;未 adopt → 直接返回
|
|
1435
|
+
if command -v dia >/dev/null 2>&1; then
|
|
1436
|
+
dia wt guard "$(basename "$0")" || exit $?
|
|
1437
|
+
fi
|
|
1438
|
+
RC
|
|
1439
|
+
wt_ensure_env_export SIMPLE_GIT_HOOKS_RC "$rcf"
|
|
1440
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
|
|
1441
|
+
import json, sys, datetime
|
|
1442
|
+
cfg, repo, br, tmpl, prev = sys.argv[1:6]
|
|
1443
|
+
json.dump({
|
|
1444
|
+
"schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1445
|
+
"hooksManager": "simple-git-hooks", "hooksPrevious": prev or None,
|
|
1446
|
+
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1447
|
+
}, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1448
|
+
PYG
|
|
1449
|
+
echo "adopted:$repo(hook manager = simple-git-hooks;守卫经 SIMPLE_GIT_HOOKS_RC 注入)"
|
|
1450
|
+
return 0
|
|
1402
1451
|
fi
|
|
1403
|
-
|
|
1404
|
-
|
|
1452
|
+
|
|
1453
|
+
# 其它仓:自己管 <common>/dia/hooks,并把本地 core.hooksPath 指过去。
|
|
1454
|
+
# 若该仓已有**别人**的本地 hooksPath(husky/.githooks 等),不抢占:只报告,交由转为 simple-git-hooks 处理。
|
|
1455
|
+
if [ -n "$prev" ] && [ "$prev" != "$hooksdir" ]; then
|
|
1456
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
|
|
1405
1457
|
import json, sys, datetime
|
|
1406
1458
|
cfg, repo, br, tmpl, prev = sys.argv[1:6]
|
|
1407
1459
|
json.dump({
|
|
1408
1460
|
"schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1409
|
-
"hooksManager": "
|
|
1461
|
+
"hooksManager": "external", "hooksPrevious": prev,
|
|
1462
|
+
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1463
|
+
}, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1464
|
+
PYG
|
|
1465
|
+
echo "[dia-wt] ⚠️ $repo 的 core.hooksPath=$prev 由其它管理器管理,未接管(守卫不生效);建议改用 simple-git-hooks(见 docs/worktree.md)" >&2
|
|
1466
|
+
return 0
|
|
1467
|
+
fi
|
|
1468
|
+
mkdir -p "$hooksdir"
|
|
1469
|
+
local savedPrev=""
|
|
1470
|
+
[ -f "$cfg" ] && savedPrev="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
1471
|
+
[ "$prev" = "$hooksdir" ] && prev="$savedPrev"
|
|
1472
|
+
local chain="$prev"
|
|
1473
|
+
[ -n "$chain" ] || chain="$common/hooks"
|
|
1474
|
+
[ -f "$cfg" ] && cp "$cfg" "$cfgdir/config.json.bak.$(date +%s)"
|
|
1475
|
+
wt_write_hook "$hooksdir" pre-commit "$def" "$chain"
|
|
1476
|
+
wt_write_hook "$hooksdir" pre-push "$def" "$chain"
|
|
1477
|
+
wt_write_hook "$hooksdir" post-checkout "$def" "$chain"
|
|
1478
|
+
git -C "$repo" config --local core.hooksPath "$hooksdir"
|
|
1479
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$prev" "$chain" <<'PYG'
|
|
1480
|
+
import json, sys, datetime
|
|
1481
|
+
cfg, repo, br, tmpl, prev, chain = sys.argv[1:7]
|
|
1482
|
+
json.dump({
|
|
1483
|
+
"schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1484
|
+
"hooksManager": "local", "hooksPath": cfg.rsplit("/",1)[0] + "/hooks",
|
|
1485
|
+
"hooksPrevious": prev or None, "hooksChain": chain,
|
|
1410
1486
|
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1411
1487
|
}, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1412
1488
|
PYG
|
|
1413
|
-
echo "adopted:$repo(守卫 =
|
|
1489
|
+
echo "adopted:$repo(守卫 = 仓库本地钩子 $hooksdir)"
|
|
1414
1490
|
return 0
|
|
1415
1491
|
# 保留首次 adopt 记下的 hooksPrevious(重复 apply 时 prev 已是我们自己的 hooksdir)
|
|
1416
1492
|
local savedPrev=""
|
|
@@ -1467,13 +1543,17 @@ cmd_doctor() {
|
|
|
1467
1543
|
fi
|
|
1468
1544
|
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1469
1545
|
# --fix:无损修复(重写三个守卫钩子 + hooksPath 绝对路径)。只对已 adopt 的仓生效。
|
|
1470
|
-
if [ "$fix" = 1 ] && [ -f "$cfg" ]; then
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1546
|
+
if [ "$fix" = 1 ] && [ -f "$cfg" ] && [ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)" = "simple-git-hooks" ]; then
|
|
1547
|
+
local rcdir rcf; rcdir="$(wt_hooks_rc_dir)"; rcf="$rcdir/simple-git-hooks-rc.sh"
|
|
1548
|
+
mkdir -p "$rcdir"
|
|
1549
|
+
cat > "$rcf" <<'RC'
|
|
1550
|
+
# 由 simple-git-hooks 生成的钩子 source 本文件(SIMPLE_GIT_HOOKS_RC)
|
|
1551
|
+
if command -v dia >/dev/null 2>&1; then
|
|
1552
|
+
dia wt guard "$(basename "$0")" || exit $?
|
|
1553
|
+
fi
|
|
1554
|
+
RC
|
|
1555
|
+
wt_ensure_env_export SIMPLE_GIT_HOOKS_RC "$rcf"
|
|
1556
|
+
[ "$json" = 0 ] && echo "FIX $repo simple-git-hooks RC 已重建"
|
|
1477
1557
|
elif [ "$fix" = 1 ] && [ -f "$cfg" ]; then
|
|
1478
1558
|
local fixChain=""
|
|
1479
1559
|
fixChain="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksChain") or "")' "$cfg" 2>/dev/null || true)"
|
|
@@ -1489,25 +1569,31 @@ cmd_doctor() {
|
|
|
1489
1569
|
local bad=0 mcfg=""
|
|
1490
1570
|
[ -f "$cfg" ] || { echo "FAIL D01 $repo 未 adopt"; bad=1; }
|
|
1491
1571
|
[ -f "$cfg" ] && mcfg="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
|
|
1492
|
-
local
|
|
1493
|
-
ghd="$(wt_global_hooks_dir)"
|
|
1572
|
+
local ghp mgr2
|
|
1494
1573
|
ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
|
|
1495
|
-
# D20
|
|
1496
|
-
[ "$ghp"
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
done
|
|
1500
|
-
# D21 仓库级 hooksPath 外溢:本地设置会覆盖全局(husky/.githooks 等)
|
|
1501
|
-
if [ -n "$prev" ]; then
|
|
1502
|
-
if [ "$prev" = "$hooksdir" ]; then
|
|
1503
|
-
echo "FAIL D21 $repo 旧版本地 hooksPath 残留($prev);跑 dia wt adopt --apply 迁移到全局钩子"; bad=1
|
|
1504
|
-
else
|
|
1505
|
-
echo "WARN D21 $repo 本地 hooksPath=$prev 覆盖全局钩子,本仓守卫不生效"
|
|
1506
|
-
fi
|
|
1574
|
+
# D20 全局配置残留:旧版把守卫装到全局 hooksPath;现在改为每仓本地,必须清掉
|
|
1575
|
+
if [ -n "$ghp" ] && [ -f "$ghp/pre-commit" ] && grep -q "dia-wt guard" "$ghp/pre-commit" 2>/dev/null; then
|
|
1576
|
+
echo "FAIL D20 全局 core.hooksPath 仍指向 dia 旧钩子目录($ghp);执行 git config --global --unset core.hooksPath"
|
|
1577
|
+
bad=1
|
|
1507
1578
|
fi
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1579
|
+
mgr2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
|
|
1580
|
+
if [ "$mgr2" = "simple-git-hooks" ]; then
|
|
1581
|
+
local rcf; rcf="$(wt_hooks_rc_dir)/simple-git-hooks-rc.sh"
|
|
1582
|
+
[ -f "$rcf" ] || { echo "FAIL D02 $repo 缺 simple-git-hooks RC:$rcf"; bad=1; }
|
|
1583
|
+
grep -rqs "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" || echo "WARN D02 $repo shell 未 export SIMPLE_GIT_HOOKS_RC(守卫不会执行)"
|
|
1584
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1585
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 bash "$SELF" guard pre-commit >/dev/null 2>&1 ) \
|
|
1586
|
+
&& { echo "FAIL D03 $repo 守卫探针未拦截 $def 提交"; bad=1; }
|
|
1587
|
+
fi
|
|
1588
|
+
else
|
|
1589
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 $repo 本地 core.hooksPath 非绝对($prev)"; bad=1 ;; esac
|
|
1590
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1591
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 $repo 缺 $h"; bad=1; }
|
|
1592
|
+
done
|
|
1593
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1594
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1595
|
+
&& { echo "FAIL D03 $repo 守卫未拦截 $def 提交"; bad=1; }
|
|
1596
|
+
fi
|
|
1511
1597
|
fi
|
|
1512
1598
|
if wt_is_main_checkout "$repo"; then
|
|
1513
1599
|
[ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ] || echo "WARN D04 $repo 主 checkout 有未提交改动"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# adopt / doctor / gate 回归测试(2026-11-06
|
|
3
|
-
# 覆盖:dry-run、apply(全局钩子)、check/doctor、主 checkout 守卫、链式调用仓库钩子、
|
|
4
|
-
# 非 adopt 仓透传、敏感扫描、adopt --all、gate、revert。
|
|
2
|
+
# adopt / doctor / gate 回归测试(2026-11-06:每仓本地钩子 + simple-git-hooks RC)
|
|
5
3
|
set -uo pipefail
|
|
6
4
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
5
|
WT="$HERE/../bin/dia-wt"
|
|
@@ -9,29 +7,29 @@ TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
|
|
|
9
7
|
PASS=0; FAIL=0
|
|
10
8
|
ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL+1)); }
|
|
11
9
|
|
|
12
|
-
#
|
|
10
|
+
# 隔离 HOME / 全局 git 配置 / 数据根 / shell rc(避免污染真实机器)
|
|
13
11
|
export HOME="$TD/home"; mkdir -p "$HOME"
|
|
14
12
|
export GIT_CONFIG_GLOBAL="$TD/gitconfig"; : > "$GIT_CONFIG_GLOBAL"
|
|
15
13
|
export GIT_CONFIG_NOSYSTEM=1
|
|
16
|
-
export
|
|
17
|
-
|
|
14
|
+
export DIA_ROOT="$TD/dia"
|
|
15
|
+
export DIA_WT_SHELL_RC="$HOME/.zshenv"
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
git -C "$R" config user.email t@example.com; git -C "$R" config user.name t
|
|
21
|
-
echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init
|
|
17
|
+
mk_repo(){ local r="$1"; mkdir -p "$r"; git -C "$r" init -q -b main; git -C "$r" config user.email t@example.com; git -C "$r" config user.name t; echo a > "$r/a.txt"; git -C "$r" add -A; git -C "$r" commit -qm init; }
|
|
22
18
|
|
|
19
|
+
R="$TD/repo"; mk_repo "$R"
|
|
23
20
|
out="$(bash "$WT" adopt "$R" --json)"
|
|
24
21
|
printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默认分支" || bad "dry-run: $out"
|
|
25
22
|
[ ! -f "$R/.git/dia/config.json" ] && ok "dry-run 不落盘" || bad "dry-run 落盘了"
|
|
26
23
|
|
|
27
24
|
bash "$WT" adopt "$R" --apply >/dev/null 2>&1 || bad "apply 失败"
|
|
28
25
|
[ -f "$R/.git/dia/config.json" ] && ok "config 已写" || bad "缺 config"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksManager")=="
|
|
34
|
-
&& ok "config 记录 hooksManager=
|
|
26
|
+
hp="$(git -C "$R" config --local --get core.hooksPath)"
|
|
27
|
+
case "$hp" in */repo/.git/dia/hooks) ok "仓库本地 hooksPath 指向 dia 目录" ;; *) bad "hooksPath=$hp" ;; esac
|
|
28
|
+
for h in pre-commit pre-push post-checkout; do [ -x "$R/.git/dia/hooks/$h" ] || bad "缺 $h"; done
|
|
29
|
+
ok "三个钩子可执行"
|
|
30
|
+
python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksManager")=="local" else 1)' "$R/.git/dia/config.json" \
|
|
31
|
+
&& ok "config 记录 hooksManager=local" || bad "hooksManager 未记录"
|
|
32
|
+
[ -z "$(git config --global --get core.hooksPath 2>/dev/null || true)" ] && ok "未写全局 git 配置" || bad "仍写了全局 hooksPath"
|
|
35
33
|
|
|
36
34
|
bash "$WT" adopt "$R" --apply >/dev/null 2>&1 && ok "重复 apply 幂等" || bad "重复 apply 失败"
|
|
37
35
|
bash "$WT" adopt "$R" --check >/dev/null 2>&1 && ok "check 通过" || bad "check 失败"
|
|
@@ -41,51 +39,55 @@ echo b > "$R/b.txt"; git -C "$R" add b.txt
|
|
|
41
39
|
if git -C "$R" commit -qm should-block 2>/dev/null; then bad "主 checkout main 提交未被拦截"; else ok "主 checkout main 提交被拦截"; fi
|
|
42
40
|
git -C "$R" reset -q
|
|
43
41
|
|
|
44
|
-
# 链式:仓库自己的 .git/hooks/pre-commit
|
|
45
|
-
printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R/.git/hooks/pre-commit"
|
|
46
|
-
chmod +x "$R/.git/hooks/pre-commit"
|
|
42
|
+
# 链式:仓库自己的 .git/hooks/pre-commit 仍要执行
|
|
43
|
+
printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R/.git/hooks/pre-commit"; chmod +x "$R/.git/hooks/pre-commit"
|
|
47
44
|
git -C "$R" worktree add -q -b feat/y "$TD/repo-worktree--y" main
|
|
48
45
|
echo c > "$TD/repo-worktree--y/c.txt"; git -C "$TD/repo-worktree--y" add c.txt
|
|
49
46
|
git -C "$TD/repo-worktree--y" commit -qm feat-y >/dev/null 2>&1
|
|
50
47
|
[ -f "$TD/repo-worktree--y/chain.log" ] && ok "仓库自己的钩子被链式执行" || bad "仓库钩子未执行"
|
|
51
48
|
rm -f "$TD/repo-worktree--y/chain.log"
|
|
52
49
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
bash "$WT" adopt "$
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
# simple-git-hooks 仓:走 RC 注入,不碰仓内文件与 .git/hooks
|
|
51
|
+
RN="$TD/npmrepo"; mk_repo "$RN"
|
|
52
|
+
printf '{"name":"r","devDependencies":{"simple-git-hooks":"^2.11.1"},"simple-git-hooks":{"pre-commit":"npx lint-staged"}}\n' > "$RN/package.json"
|
|
53
|
+
git -C "$RN" add -A; git -C "$RN" commit -qm pkg
|
|
54
|
+
before="$(md5 -q "$RN/package.json" 2>/dev/null || md5sum "$RN/package.json" | cut -d' ' -f1)"
|
|
55
|
+
bash "$WT" adopt "$RN" --apply >/dev/null 2>&1 || bad "npm 仓 adopt 失败"
|
|
56
|
+
after="$(md5 -q "$RN/package.json" 2>/dev/null || md5sum "$RN/package.json" | cut -d' ' -f1)"
|
|
57
|
+
[ "$before" = "$after" ] && ok "不修改仓库 package.json" || bad "package.json 被改动"
|
|
58
|
+
[ -z "$(git -C "$RN" config --local --get core.hooksPath || true)" ] && ok "不占用仓库 hooksPath(交给管理器)" || bad "仍设置了 hooksPath"
|
|
59
|
+
[ -f "$DIA_ROOT/hooks/simple-git-hooks-rc.sh" ] && ok "写入 simple-git-hooks RC" || bad "缺 RC 文件"
|
|
60
|
+
grep -q "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" 2>/dev/null && ok "shell rc 注入 SIMPLE_GIT_HOOKS_RC" || bad "未注入环境变量"
|
|
61
|
+
bash "$WT" doctor "$RN" >/dev/null 2>&1 && ok "npm 仓 doctor 通过" || bad "npm 仓 doctor 失败"
|
|
62
|
+
(cd "$RN" && DIA_WT_GUARD_PROBE=1 bash "$WT" guard pre-commit >/dev/null 2>&1) && bad "guard 未拦截 main 提交" || ok "guard 拦截 main 提交"
|
|
61
63
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
# 全局配置残留检查:旧版全局钩子目录 → doctor FAIL
|
|
65
|
+
LEGACY="$TD/legacy-hooks"; mkdir -p "$LEGACY"
|
|
66
|
+
printf '#!/usr/bin/env bash\n# dia-wt guard(旧)\nexit 0\n' > "$LEGACY/pre-commit"; chmod +x "$LEGACY/pre-commit"
|
|
67
|
+
git config --global core.hooksPath "$LEGACY"
|
|
68
|
+
bash "$WT" doctor "$R" >/tmp/d20.log 2>&1; grep -q "D20" /tmp/d20.log && ok "doctor 报全局钩子残留" || bad "doctor 未报全局残留: $(head -3 /tmp/d20.log | tr "\n" " ")"
|
|
69
|
+
git config --global --unset core.hooksPath
|
|
67
70
|
|
|
68
|
-
#
|
|
71
|
+
# 未 adopt 的仓不受影响
|
|
72
|
+
R3="$TD/repo3"; mk_repo "$R3"; echo z > "$R3/z.txt"; git -C "$R3" add -A
|
|
73
|
+
git -C "$R3" commit -qm ok >/dev/null 2>&1 && ok "未 adopt 仓不受守卫影响" || bad "未 adopt 仓被误拦"
|
|
74
|
+
|
|
75
|
+
# 敏感扫描(worktree push)
|
|
69
76
|
BR="$TD/remote.git"; git init -q --bare "$BR"
|
|
70
77
|
git -C "$R" remote add origin "$BR" 2>/dev/null || true
|
|
71
78
|
git --git-dir="$BR" fetch -q "$R" main:main 2>/dev/null || true
|
|
72
79
|
git -C "$R" fetch -q origin 2>/dev/null || true
|
|
73
80
|
BAD="$(printf '/%s/alice/secret' Users)"; echo "$BAD" > "$TD/repo-worktree--y/bad.txt"
|
|
74
|
-
git -C "$TD/repo-worktree--y" add bad.txt
|
|
75
|
-
git -C "$TD/repo-worktree--y" commit -qm bad >/dev/null 2>&1
|
|
81
|
+
git -C "$TD/repo-worktree--y" add bad.txt; git -C "$TD/repo-worktree--y" commit -qm bad >/dev/null 2>&1
|
|
76
82
|
if git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null; then bad "敏感推送未被拦截"; else ok "敏感推送被拦截"; fi
|
|
77
|
-
git -C "$TD/repo-worktree--y" reset -q --hard HEAD~1
|
|
83
|
+
git -C "$TD/repo-worktree--y" reset -q --hard HEAD~1
|
|
78
84
|
printf 'path=$TD/home/kits/dia.json\n' > "$TD/repo-worktree--y/vars.txt"
|
|
79
85
|
git -C "$TD/repo-worktree--y" add vars.txt && git -C "$TD/repo-worktree--y" commit -qm vars >/dev/null 2>&1
|
|
80
86
|
git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null && ok "相对片段 home/ 不误报" || bad "相对片段被误判"
|
|
81
87
|
|
|
82
|
-
# adopt --all
|
|
88
|
+
# adopt --all
|
|
83
89
|
AR="$TD/allroot"; mkdir -p "$AR/a" "$AR/b"
|
|
84
|
-
for r in a b; do
|
|
85
|
-
git -C "$AR/$r" init -q -b main
|
|
86
|
-
git -C "$AR/$r" config user.email t@example.com; git -C "$AR/$r" config user.name t
|
|
87
|
-
echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init
|
|
88
|
-
done
|
|
90
|
+
for r in a b; do mk_repo "$AR/$r"; done
|
|
89
91
|
git -C "$AR/a" worktree add -q -b feat/w "$AR/a-worktree--w" main
|
|
90
92
|
out="$(DIA_WT_REPOS_ROOT="$AR" bash "$WT" adopt --all --apply --json 2>&1)"
|
|
91
93
|
printf '%s' "$out" | grep -q '"repos":2,"ok":2,"fail":0' && ok "adopt --all 覆盖 2 个主 checkout" || bad "adopt --all: $out"
|
|
@@ -97,16 +99,16 @@ DIA_WT_REPOS_ROOT="$TD/empty-root" bash "$WT" adopt --all >/dev/null 2>&1
|
|
|
97
99
|
|
|
98
100
|
# gate
|
|
99
101
|
rm -f "$TD/repo-worktree--y/chain.log"
|
|
100
|
-
if bash "$WT" gate "$R" y >/tmp/gate.log 2>&1; then ok "gate 成功"; else bad "gate 失败: $(tail -2 /tmp/gate.log | tr
|
|
102
|
+
if bash "$WT" gate "$R" y >/tmp/gate.log 2>&1; then ok "gate 成功"; else bad "gate 失败: $(tail -2 /tmp/gate.log | tr '\n' ' ')"; fi
|
|
101
103
|
if bash "$WT" gate "$R" yb >/dev/null 2>&1; then bad "gate 子串误匹配"; else ok "gate 精确匹配 feature"; fi
|
|
102
104
|
echo dirty >> "$TD/repo-worktree--y/c.txt"
|
|
103
105
|
if bash "$WT" gate "$R" y >/dev/null 2>&1; then bad "worktree 脏时 gate 通过"; else ok "worktree 脏时 gate 失败"; fi
|
|
104
106
|
git -C "$TD/repo-worktree--y" checkout -q -- c.txt
|
|
105
107
|
|
|
106
|
-
# revert
|
|
108
|
+
# revert
|
|
107
109
|
bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
|
|
108
110
|
[ ! -f "$R/.git/dia/config.json" ] && ok "revert 删除本仓配置" || bad "配置残留"
|
|
109
|
-
[ -
|
|
111
|
+
[ -z "$(git -C "$R" config --local --get core.hooksPath || true)" ] && ok "revert 还原本地 hooksPath" || bad "hooksPath 未还原"
|
|
110
112
|
git -C "$R" worktree remove --force "$TD/repo-worktree--y" >/dev/null 2>&1 || true
|
|
111
113
|
|
|
112
114
|
echo; echo "── PASS $PASS · FAIL $FAIL ──"
|