@cr1992/agentkit 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/)。
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 1.1.1 - 2026-09-08
8
+
9
+ - 修复 `doctor` 对已回收 record 仍生成需要活树才能收敛的 metadata finding。`stack_parent`、
10
+ `history_operation` 和 `review_refresh` 三个分支缺少 `worktree_state` 过滤,对已删除目录报
11
+ `STACK_PARENT_ADVANCED` 等提示,其补救动作(managed rebase/retarget、`rebase --continue`、
12
+ `refresh-review`)在回收后都无法执行,噪声永远清不掉;其中 error 级的还会按「任何 error 都暂停
13
+ `spawn/adopt`」把后续派工钉死。生命周期与 watcher 两个收集器早已是这个口径,本次补齐 metadata。
14
+ - `manage-worktrees` 补齐 `--no-watch` 的使用判据:只用于确定不会合入,或武装失败原因为永久性
15
+ (否则 pending 会积一条清不掉的 `AUTO_RECLAIM_NOT_ARMED`)。明确禁止因 watcher 活不过会话或
16
+ 为压后台进程数而关闭——前者是 `watch-service` 的职责,后者不成立(`disabled` 持久且不重试)。
17
+ - `watch-service status` 进入强制流程表的「恢复/盘点」行,不再只以表外散文形式存在。
18
+ - 四个 SKILL.md 对 `docs/<域>/` 的引用从相对链接改为 `agentkit docs <域> <主题>` 命令。宿主把 Skill
19
+ 基目录报成安装路径(常为软链),Read 工具按词法折叠 `..`,原 `../docs/...` 链接在安装态全部不可达,
20
+ agent 只剩摘要表。`validate-skills` 新增安装态检查:SKILL.md 相对链接越出 Skill 目录即报错,
21
+ `agentkit docs` 引用的主题必须真实存在。
22
+
23
+ ## 1.1.0 - 2026-09-07
24
+
25
+ - 修复从待回收 worktree 自身执行 `reclaim` 时,目录删除后误判本地分支已不存在的问题;未完成的
26
+ branch cleanup 现在返回非零并可幂等重试。
27
+ - `manage-worktrees` 持久化评审 watcher 的 pending/disabled 意图,让 `doctor` 与 `resume-all` 不再忽略未武装记录。
28
+ - 新增 macOS `watch-service` LaunchAgent,为已登记 watcher 提供跨 Agent 会话与重启后的自动恢复触发。
29
+
5
30
  ## 1.0.0 - 2026-09-04
6
31
 
7
32
  - 将四个 Agent 工程 Skill 收敛为一个零依赖 Node.js CLI 与四个薄壳。
package/README.en.md CHANGED
@@ -12,7 +12,7 @@ the `orchestrate-subagents` control plane, which then chooses lightweight or ful
12
12
  rerouting from effective capabilities, task scale, and local model policy. Providers remain independently usable and
13
13
  compose through frozen Artifact, Binding, and Evidence envelopes.
14
14
 
15
- [Detailed collaboration contracts and safety boundaries (Chinese, v1.0.0)](./docs/architecture/skill-system-architecture.md)
15
+ [Detailed collaboration contracts and safety boundaries (Chinese, v1.1.0)](./docs/architecture/skill-system-architecture.md)
16
16
 
17
17
  ## Included skills
18
18
 
@@ -39,6 +39,18 @@ npm install -g @cr1992/agentkit
39
39
  agentkit doctor
40
40
  ```
41
41
 
42
+ On macOS, explicitly install the per-repository user service when worktree merge watchers must recover across agent
43
+ sessions and restarts:
44
+
45
+ ```bash
46
+ agentkit worktree watch-service install
47
+ agentkit worktree watch-service status
48
+ ```
49
+
50
+ The npm install does not create a persistent service. The service pins the Node and agentkit paths present at install
51
+ time; rerun `watch-service install` after moving either installation, and inspect `program_available` in
52
+ `status --json`. Other platforms currently use `agentkit worktree resume-all` for manual recovery.
53
+
42
54
  ### Skills
43
55
 
44
56
  Install all four skills:
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  其内部再按有效能力、任务规模、本地模型配置和验收证据选择轻量或完整运行方式及后续重路由。
12
12
  各 provider 仍可独立使用,并通过冻结的 Artifact、Binding 和 Evidence envelope 按需组合。
13
13
 
14
- [Agent Skills 协作契约与安全边界(v1.0.0)](./docs/architecture/skill-system-architecture.md)
14
+ [Agent Skills 协作契约与安全边界(v1.1.0)](./docs/architecture/skill-system-architecture.md)
15
15
 
16
16
  ## 包含的 Skill
17
17
 
@@ -37,6 +37,17 @@ npm install -g @cr1992/agentkit
37
37
  agentkit doctor
38
38
  ```
39
39
 
40
+ macOS 上若要让 worktree 合入监听跨 Agent 会话和重启持续恢复,可按仓库显式安装用户级维护器:
41
+
42
+ ```bash
43
+ agentkit worktree watch-service install
44
+ agentkit worktree watch-service status
45
+ ```
46
+
47
+ 它不会随 npm install 自动常驻。服务固定安装时的 Node 与 agentkit 路径;升级 agentkit、切换 Node
48
+ 安装位置后应重新执行 `watch-service install`,`status --json` 的 `program_available` 可检查原路径。
49
+ 其他平台当前使用 `agentkit worktree resume-all` 手工恢复。
50
+
40
51
  ### Skill
41
52
 
42
53
  安装全部 Skill:
@@ -6,7 +6,15 @@
6
6
 
7
7
  监听绑定“冻结的 head SHA 已成为目标 ref 祖先”这一事实,不绑定 change request 载体。载体改为他人代推、
8
8
  聚合 MR 或其他分支时不解除监听。进入 `ready_for_review` 默认武装;`submit` 只是其中一个入口。电脑重启后
9
- 由 `resume-all` 恢复 stale watcher;change request 已关闭且明确不会合入时用 `unwatch`。
9
+ 由 `resume-all` 恢复 stale watcher;macOS 要让这一步无需新 Agent 会话触发,显式安装用户级维护器:
10
+
11
+ ```bash
12
+ agentkit worktree watch-service install
13
+ ```
14
+
15
+ 维护器定期执行有限的 `resume-all`,实际 watcher 仍用 token/event CAS 与 heartbeat 裁决;它不放宽任何
16
+ 回收前置条件。`watch-service status` 检查 plist、launchd job 与当前 Node/runtime 路径,`uninstall` 解除。
17
+ 未安装时只有进程级自动回收,不得称作跨会话保证。change request 已关闭且明确不会合入时用 `unwatch`。
10
18
 
11
19
  ## 已推送成果
12
20
 
@@ -20,6 +28,11 @@ agentkit worktree reclaim <task-or-id> --pushed <sha-or-unique-prefix>
20
28
  final_snapshot -> reclaim_ready -> git worktree remove -> branch cleanup -> reclaimed
21
29
  ```
22
30
 
31
+ 命令可以从待回收 worktree 自身发起;runtime 在删除目录前冻结 primary worktree 作为后续仓库级 Git
32
+ 操作的稳定 cwd。branch probe 只有在 `show-ref` 明确返回“不存在”时才记为 `absent`,cwd、权限或其他
33
+ 执行错误一律 fail closed。目录已删除但 branch cleanup 未完成时保留 `BRANCH_PENDING`,命令返回非零,
34
+ 从任一仍存在的 worktree 重跑同一 `reclaim` 完成对账。
35
+
23
36
  终态事件将非 `abandoned` 任务收敛为 `done`,并以最终 source HEAD 闭合 ownership epoch。旧版本若留下
24
37
  `reclaimed` 但状态或 epoch 未闭合,重复同一命令会追加 reconciliation event 后再对账。
25
38
 
@@ -88,7 +101,8 @@ agentkit worktree reclaim old-task \
88
101
 
89
102
  - `abandoned` 只冻结写入,不等于已回收;`doctor` 持续报告残留树和断裂替代关系。
90
103
  - 禁止 `rm -rf`、`git worktree remove --force` 和 `branch -D`。
91
- - branch cleanup 失败时保留 `BRANCH_PENDING`;修复占用后重跑相同 `reclaim`,工具重新验证 branch tip。
104
+ - branch cleanup 失败时保留 `BRANCH_PENDING` 并返回非零;修复占用后从仍存在的 worktree 重跑相同
105
+ `reclaim`,工具重新验证 branch tip。
92
106
  - `git worktree remove` 失败返回非零 `KEEP` 并记录原始错误。即使 Git 登记已解除,物理目录仍在也不能
93
107
  标为 `reclaimed`;先恢复权限或登记关系,不用强删掩盖孤儿目录。
94
108
  - 含 submodule 的树逐个验证已初始化工作区干净,再自动 deinit 并清理该树私有元数据。submodule 脏、
@@ -13,7 +13,29 @@ agentkit worktree touch <selector> --status ready_for_review
13
13
  `ready_for_review` 默认按该树登记的 `base_ref` 武装监听,Profile `default_base` 只在 record 没有 base 时
14
14
  兜底;显式 target 与既有人工 target 仍然优先。HEAD 前进时同一 event 使旧 watcher 失效,
15
15
  再尝试冻结新 HEAD。worktree dirty、HEAD 未推送或远端主干不可刷新时 fail-soft,状态更新成功但明确
16
- 回报未武装原因。补齐前提后重跑,或用 `--no-watch` 明确接受人工回收。
16
+ 回报未武装原因,并把 `review_watch=auto/pending` 与原因写入 trace;`resume-all` 会重试。`--no-watch`
17
+ 则持久登记 `disabled`,与启动失败严格区分。`doctor` 对 pending、缺失 intent 和显式退出分别报告
18
+ `AUTO_RECLAIM_NOT_ARMED`、`AUTO_RECLAIM_INTENT_MISSING` 与 `AUTO_RECLAIM_DISABLED`。
19
+
20
+ `--no-watch` 只用于两种情况:change request 确定不会合入(与 `unwatch` 同一判据);或该树的合入在结构上
21
+ 无法被 watcher 观测且原因是永久的(例如仓库没有可刷新的远端 target),此时留在 pending 只会积一条永远清不
22
+ 掉的 `AUTO_RECLAIM_NOT_ARMED`(error),按强制流程会挡住后续 `spawn/adopt`。除此之外一律不要关:watcher
23
+ 活不过当前会话是 `watch-service` 要解决的问题,不是关 watcher 的理由;也不能拿它临时压后台进程数——
24
+ `disabled` 是持久策略,`resume-all` 不重试,没有“解除节流”这一步。dirty、未 push、网络等**可恢复**原因
25
+ 导致的武装失败,留在 pending 等重试,不要转成 `disabled`。
26
+
27
+ 进程级 watcher 只保证当前宿主允许后台进程存活期间的即时回收。需要跨 Agent 会话或机器重启:
28
+
29
+ ```bash
30
+ agentkit worktree watch-service install
31
+ agentkit worktree watch-service status --json
32
+ ```
33
+
34
+ macOS adapter 安装用户级 LaunchAgent,每 60 秒(可用 `--interval-seconds` 调整)从 primary worktree
35
+ 运行一次 `resume-all`。安装是显式动作,不随 npm install 自动发生,不保存调用会话的环境变量或凭证。
36
+ 服务固定安装时的 Node 与 manager script 绝对路径;升级 agentkit 或切换 Node 安装位置后重跑 install,
37
+ `status --json` 的 `program_available` 会检查这两个路径。卸载使用 `watch-service uninstall`。其他平台仍可
38
+ 手工运行 `resume-all`,在相应 service adapter 落地前不宣称跨会话保证。
17
39
 
18
40
  人工 `watch --target` 可改默认目标;已经显式武装的 target 不被静默改写,换目标先 `unwatch`。陈旧
19
41
  watcher 不能覆盖并发 rearm 或 `merge_detected`。旧 SHA 合入不代表新 HEAD 已完成。
@@ -88,5 +110,7 @@ agentkit worktree touch <selector> \
88
110
  未给 target 时使用 record 的 `base_ref`;远端 MR target 不同时先核对 provider/UI,再显式传
89
111
  真实目标。`--mr` 只接受 HTTP(S) URL;纯文本载体使用 `--change-ref`。
90
112
 
91
- watcher 只在冻结 SHA 成为目标 ref 祖先后回收。重启后 `resume-all` 恢复 stale watcher;change request
92
- 关闭且确定不会合入时 `unwatch`。详细回收见 [reclaim-and-watch.md](reclaim-and-watch.md)。
113
+ watcher 只在冻结 SHA 成为目标 ref 祖先后回收。`resume-all` 恢复 stale watcher 和已登记的 pending
114
+ intent;对没有 intent 的 legacy 评审记录只报告 skipped,要求重新 `touch ready_for_review`,不猜测它
115
+ 是否曾显式 `--no-watch`。change request 关闭且确定不会合入时 `unwatch`。详细回收见
116
+ [reclaim-and-watch.md](reclaim-and-watch.md)。
@@ -346,7 +346,7 @@ export function readJsonFileOrDie(path, label) {
346
346
  export function parseArgs(argv) {
347
347
  const flags = new Map();
348
348
  const positionals = [];
349
- const booleanFlags = new Set(['json', 'all', 'present', 'archived', 'verbose', 'recover-lock', 'no-watch', 'abort-on-conflict', 'no-rerere', 'recompose', 'scan-conflicts', 'abort', 'continue', 'pause-before-push']);
349
+ const booleanFlags = new Set(['json', 'all', 'present', 'archived', 'verbose', 'quiet', 'recover-lock', 'no-watch', 'abort-on-conflict', 'no-rerere', 'recompose', 'scan-conflicts', 'abort', 'continue', 'pause-before-push']);
350
350
  for (let index = 0; index < argv.length; index++) {
351
351
  const value = argv[index];
352
352
  if (!value.startsWith('--')) {
@@ -35,10 +35,18 @@ export function createCommands(deps) {
35
35
  buildListing,
36
36
  learningRoot,
37
37
  isSettledWorktreeState,
38
+ watchServiceStatus,
38
39
  } = deps;
39
40
 
40
41
  function collectDoctorRecordMetadataFindings(loaded, listing, recordsById, record, findings) {
41
- if (record.storage_class === 'ephemeral' && record.worktree_state !== 'reclaimed') findings.push({ code: 'EPHEMERAL_WORKTREE', severity: 'warning', worktree_id: record.worktree_id, path: record.path });
42
+ // 本收集器的每条 finding 都要求一棵活树才能收敛:commit+push、rebase --continue、
43
+ // refresh-review --continue/--abort、managed rebase 或 retarget、补记 base_reason。
44
+ // record 一旦 reclaimed,目录已经删除,这些提示既没有补救动作也永远不会消失;其中 error 级的
45
+ // (MANAGED_HISTORY_OPERATION_PENDING / REVIEW_REFRESH_PENDING / STACK_PARENT_*)还会按
46
+ // SKILL 的「任何 error 都暂停 spawn/adopt」把后续派工钉死。生命周期与 watcher 两个收集器
47
+ // 已经是这个口径,这里补齐;archived 更早一步在调用方 continue,所以只需排除 reclaimed。
48
+ if (record.worktree_state === 'reclaimed') return;
49
+ if (record.storage_class === 'ephemeral') findings.push({ code: 'EPHEMERAL_WORKTREE', severity: 'warning', worktree_id: record.worktree_id, path: record.path });
42
50
  if (record.history_operation) {
43
51
  findings.push({
44
52
  code: 'MANAGED_HISTORY_OPERATION_PENDING',
@@ -102,7 +110,6 @@ export function createCommands(deps) {
102
110
  }
103
111
  }
104
112
  if (
105
- record.worktree_state !== 'reclaimed' &&
106
113
  loaded.profile.default_base &&
107
114
  record.base_ref &&
108
115
  record.base_ref !== loaded.profile.default_base
@@ -243,10 +250,45 @@ export function createCommands(deps) {
243
250
 
244
251
  /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {Record<string,any>} record @param {Record<string,any>[]} findings */
245
252
  function collectDoctorRecordWatcherFindings(loaded, record, findings) {
246
- if (record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state) && record.worktree_state !== 'reclaimed') {
253
+ const reviewLifecycle = ['ready_for_review', 'integrating'].includes(record.task_status)
254
+ && !['reclaimed', 'archived'].includes(record.worktree_state);
255
+ const activeWatch = record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)
256
+ ? record.auto_reclaim
257
+ : null;
258
+ if (reviewLifecycle && !activeWatch) {
259
+ if (record.review_watch?.policy === 'disabled') {
260
+ findings.push({
261
+ code: 'AUTO_RECLAIM_DISABLED',
262
+ severity: 'warning',
263
+ worktree_id: record.worktree_id,
264
+ path: record.path,
265
+ detail: `已显式关闭跨会话回收(${record.review_watch.reason ?? 'no reason'});合入后需要人工 reclaim。`,
266
+ });
267
+ } else if (record.review_watch?.policy === 'auto') {
268
+ findings.push({
269
+ code: 'AUTO_RECLAIM_NOT_ARMED',
270
+ severity: 'error',
271
+ worktree_id: record.worktree_id,
272
+ path: record.path,
273
+ target_ref: record.review_watch.target_ref ?? null,
274
+ head_sha: record.review_watch.head_sha ?? null,
275
+ detail: record.review_watch.reason ?? '自动回收 intent 已登记,但 watcher 尚未武装;运行 resume-all 重试。',
276
+ });
277
+ } else {
278
+ findings.push({
279
+ code: 'AUTO_RECLAIM_INTENT_MISSING',
280
+ severity: 'error',
281
+ worktree_id: record.worktree_id,
282
+ path: record.path,
283
+ detail: `评审态 record 没有 watcher 或显式退出记录;运行 touch ${record.task} --status ready_for_review 重新登记。`,
284
+ });
285
+ }
286
+ return;
287
+ }
288
+ if (activeWatch) {
247
289
  const heartbeat = readWatcherHeartbeat(loaded.context.common_dir, record.worktree_id);
248
290
  const health = watcherHealth(record, heartbeat);
249
- const advance = record.auto_reclaim.target_advance;
291
+ const advance = activeWatch.target_advance;
250
292
  if (advance) {
251
293
  const prediction = advance.prediction?.state ?? 'unknown';
252
294
  findings.push({
@@ -258,7 +300,7 @@ export function createCommands(deps) {
258
300
  severity: 'warning',
259
301
  worktree_id: record.worktree_id,
260
302
  path: record.path,
261
- target_ref: record.auto_reclaim.target_ref,
303
+ target_ref: activeWatch.target_ref,
262
304
  target_sha: advance.target_sha,
263
305
  recorded_base_sha: advance.recorded_base_sha,
264
306
  prediction: advance.prediction,
@@ -462,6 +504,21 @@ export function createCommands(deps) {
462
504
  }
463
505
  }
464
506
  collectDoctorRecordFindings(loaded, listing, recordsById, findings);
507
+ const expectsDurableWatch = listing.rows.some((row) => row.record
508
+ && ['ready_for_review', 'integrating'].includes(row.record.task_status)
509
+ && (row.record.review_watch?.policy === 'auto'
510
+ || (row.record.auto_reclaim && !['disarmed', 'reclaimed'].includes(row.record.auto_reclaim.state))));
511
+ if (expectsDurableWatch) {
512
+ const service = watchServiceStatus(loaded);
513
+ if (service.supported && (!service.installed || !service.loaded || !service.program_available)) {
514
+ findings.push({
515
+ code: 'AUTO_RECLAIM_SERVICE_INACTIVE',
516
+ severity: 'warning',
517
+ path: service.plist_path ?? loaded.context.primary_worktree,
518
+ detail: '当前 watcher 只有进程级存活保证;运行 agentkit worktree watch-service install 启用跨会话恢复。',
519
+ });
520
+ }
521
+ }
465
522
  collectDoctorSupersessionFindings(listing, findings);
466
523
  collectDoctorSessionFindings(listing, findings);
467
524
  collectDoctorRuntimeFindings(loaded, findings);
@@ -4,7 +4,7 @@
4
4
  import { spawn } from 'node:child_process';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
7
- import { tmpdir } from 'node:os';
7
+ import { homedir, tmpdir } from 'node:os';
8
8
  import { basename, dirname, join, resolve } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
 
@@ -25,6 +25,7 @@ import { createCommands as createLifecycleCommands } from './worktree-lifecycle.
25
25
  import { createCommands as createReclaimCommands } from './worktree-reclaim.mjs';
26
26
  import { createCommands as createReviewRefreshCommands } from './worktree-review-refresh.mjs';
27
27
  import { createCommands as createReviewWatchCommands } from './worktree-review-watch.mjs';
28
+ import { createCommands as createWatchServiceCommands } from './worktree-watch-service.mjs';
28
29
 
29
30
  export { runFileCapture, runFileTry } from './worktree-process.mjs';
30
31
  export {
@@ -71,12 +72,16 @@ const dependencies = {
71
72
  statSync,
72
73
  writeFileSync,
73
74
  tmpdir,
75
+ homedir,
74
76
  basename,
75
77
  dirname,
76
78
  join,
77
79
  resolve,
78
80
  fileURLToPath,
79
81
  managerScript,
82
+ processPlatform: process.platform,
83
+ processExecPath: process.execPath,
84
+ processGetuid: () => typeof process.getuid === 'function' ? process.getuid() : 0,
80
85
  ...mergePreview,
81
86
  ...profile,
82
87
  ...provider,
@@ -119,6 +124,8 @@ const artifactCommands = createArtifactCommands(dependencies);
119
124
  assignNew(dependencies, artifactCommands, 'worktree-artifact');
120
125
  const learningCommands = createLearningCommands(dependencies);
121
126
  assignNew(dependencies, learningCommands, 'worktree-learning');
127
+ const watchServiceCommands = createWatchServiceCommands(dependencies);
128
+ assignNew(dependencies, watchServiceCommands, 'worktree-watch-service');
122
129
  const doctorCommands = createDoctorCommands(dependencies);
123
130
 
124
131
  export const batchFingerprint = batchCommands.batchFingerprint;
@@ -126,7 +133,7 @@ export const verifyArtifactEnvelope = artifactCommands.verifyArtifactEnvelope;
126
133
 
127
134
  function cmdCapabilities(args) {
128
135
  rejectUnknownFlags(args.flags, ['json']);
129
- console.log(JSON.stringify({ skill: 'manage-worktrees', runtime_version: '1.3.0', contracts: { worktree_binding: [1], artifact_ref: [1], reflection_record: [1], improvement_proposal: [1], batch_result: [1] }, features: ['git-common-dir-ledger', 'ownership-epochs', 'artifact-verification', 'incident-reflection', 'proposed-only-improvement', 'batch-integrate', 'batch-conflict-scan', 'declared-post-integrate-steps', 'batch-result', 'evidence-archive-reclaim', 'durable-pushed-ref-proof', 'auto-armed-review-watch', 'review-target-advance-prediction', 'explicit-review-refresh', 'managed-history-rewrite', 'stack-parent-attribution', 'structured-change-registration'], content_digest: worktreeSkillDigest() }, null, 2));
136
+ console.log(JSON.stringify({ skill: 'manage-worktrees', runtime_version: '1.4.0', contracts: { worktree_binding: [1], artifact_ref: [1], reflection_record: [1], improvement_proposal: [1], batch_result: [1] }, features: ['git-common-dir-ledger', 'ownership-epochs', 'artifact-verification', 'incident-reflection', 'proposed-only-improvement', 'batch-integrate', 'batch-conflict-scan', 'declared-post-integrate-steps', 'batch-result', 'evidence-archive-reclaim', 'durable-pushed-ref-proof', 'auto-armed-review-watch', 'persistent-review-watch-intent', 'launchd-watch-service', 'review-target-advance-prediction', 'explicit-review-refresh', 'managed-history-rewrite', 'stack-parent-attribution', 'structured-change-registration'], content_digest: worktreeSkillDigest() }, null, 2));
130
137
  }
131
138
 
132
139
  function usage() {
@@ -165,7 +172,9 @@ doctor [--json] [--verbose]
165
172
  rebuild [<selector>] [--id <uuid>] [--recover-lock]
166
173
  watch <selector> [--target <remote/ref>] [--interval-ms <ms>] [--change-ref <text>] [--notify auto|off] [--id <uuid>]
167
174
  submit <selector> [--title <text>] [--description <text>] [--target <branch>] [--remote <name>] [--interval-ms <ms>] [--notify auto|off] [--id <uuid>]
168
- resume-all [--json]
175
+ resume-all [--json] [--quiet]
176
+ watch-service install|status|uninstall [--json] [--interval-seconds <seconds>]
177
+ macOS 用户级 LaunchAgent 定期运行 resume-all;显式安装,不在 npm install 时自动常驻
169
178
  unwatch <selector> [--id <uuid>]
170
179
  reclaim <selector> --pushed <sha> [--id <uuid>]
171
180
  reclaim <selector> --superseded-by <replacement-selector> [--discard <exact-old-head>] [--id <uuid>] [--replacement-id <uuid>]
@@ -211,6 +220,7 @@ function main(argv = process.argv.slice(2)) {
211
220
  submit: reviewCommands.cmdSubmit,
212
221
  watch: reviewCommands.cmdWatch,
213
222
  'resume-all': reviewCommands.cmdResumeAll,
223
+ 'watch-service': watchServiceCommands.cmdWatchService,
214
224
  unwatch: reviewCommands.cmdUnwatch,
215
225
  'watch-worker': reviewCommands.cmdWatchWorker,
216
226
  reclaim: reclaimCommands.cmdReclaim,
@@ -281,19 +281,49 @@ export function createCommands(deps) {
281
281
  return refs.out.split('\n').filter(Boolean).filter((ref) => ref !== ownBranch).sort();
282
282
  }
283
283
 
284
+ /**
285
+ * 回收可以从目标 worktree 自身发起。目标目录一旦被移除,命令启动时的 current_worktree
286
+ * 就不再是合法 cwd;后续仓库级 Git 操作必须固定使用不会被本次回收删除的 primary worktree。
287
+ * @param {ReturnType<typeof loadRepositoryProfile>} loaded
288
+ */
289
+ function repositoryGitCwd(loaded) {
290
+ return loaded.context.primary_worktree ?? loaded.context.current_worktree;
291
+ }
292
+
293
+ /**
294
+ * `git show-ref --verify --quiet` 的退出码 1 才表示 ref 不存在。ENOENT、权限错误或其他
295
+ * operational failure 不能降级成 absent,否则会产生“branch=absent”假成功。
296
+ * @param {ReturnType<typeof loadRepositoryProfile>} loaded
297
+ * @param {Record<string,any>} record
298
+ */
299
+ function inspectLocalBranch(loaded, record) {
300
+ if (!record.branch) return { state: 'absent', reason: null };
301
+ const checked = gitTry(
302
+ ['show-ref', '--verify', '--quiet', `refs/heads/${record.branch}`],
303
+ repositoryGitCwd(loaded),
304
+ );
305
+ if (checked.ok) return { state: 'exists', reason: null };
306
+ const status = checked.error && typeof checked.error === 'object' && 'status' in checked.error
307
+ ? checked.error.status
308
+ : null;
309
+ if (status === 1) return { state: 'absent', reason: null };
310
+ return {
311
+ state: 'error',
312
+ reason: commandFailureReason(checked, `unable to inspect local branch ${record.branch}`),
313
+ };
314
+ }
315
+
284
316
  /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {Record<string,any>} record */
285
317
  function localBranchExists(loaded, record) {
286
- return Boolean(record.branch) && gitTry(
287
- ['show-ref', '--verify', '--quiet', `refs/heads/${record.branch}`],
288
- loaded.context.current_worktree,
289
- ).ok;
318
+ return inspectLocalBranch(loaded, record).state !== 'absent';
290
319
  }
291
320
 
292
321
  /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {Record<string,any>} record @param {string} pushed */
293
322
  function attemptLocalBranchCleanup(loaded, record, pushed) {
294
323
  const checkedAt = new Date().toISOString();
295
324
  const previousAttempts = Number(record.branch_cleanup?.attempts ?? 0);
296
- if (!record.branch || !localBranchExists(loaded, record)) {
325
+ const branchState = inspectLocalBranch(loaded, record);
326
+ if (branchState.state === 'absent') {
297
327
  return {
298
328
  status: 'absent',
299
329
  branch: record.branch ?? null,
@@ -302,7 +332,17 @@ export function createCommands(deps) {
302
332
  reason: null,
303
333
  };
304
334
  }
305
- if (!gitTry(['merge-base', '--is-ancestor', record.branch, pushed], loaded.context.current_worktree).ok) {
335
+ if (branchState.state === 'error') {
336
+ return {
337
+ status: 'failed',
338
+ branch: record.branch,
339
+ attempts: previousAttempts + 1,
340
+ checked_at: checkedAt,
341
+ reason: branchState.reason,
342
+ };
343
+ }
344
+ const cwd = repositoryGitCwd(loaded);
345
+ if (!gitTry(['merge-base', '--is-ancestor', record.branch, pushed], cwd).ok) {
306
346
  return {
307
347
  status: 'failed',
308
348
  branch: record.branch,
@@ -311,7 +351,7 @@ export function createCommands(deps) {
311
351
  reason: `local branch tip is not merged into pushed sha: ${pushed}`,
312
352
  };
313
353
  }
314
- const removed = gitTry(['branch', '-D', '--', record.branch], loaded.context.current_worktree);
354
+ const removed = gitTry(['branch', '-D', '--', record.branch], cwd);
315
355
  if (removed.ok) {
316
356
  return {
317
357
  status: 'deleted',
@@ -332,8 +372,8 @@ export function createCommands(deps) {
332
372
 
333
373
  /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {Record<string,any>} record @param {string} pushed */
334
374
  function reconcileReclaimedBranchCleanup(loaded, record, pushed) {
335
- const branchExists = localBranchExists(loaded, record);
336
- if (['deleted', 'absent'].includes(record.branch_cleanup?.status) && !branchExists) {
375
+ const branchState = inspectLocalBranch(loaded, record);
376
+ if (['deleted', 'absent'].includes(record.branch_cleanup?.status) && branchState.state === 'absent') {
337
377
  return { record, branch_cleanup: record.branch_cleanup, changed: false };
338
378
  }
339
379
  const cleanup = attemptLocalBranchCleanup(loaded, record, pushed);
@@ -511,7 +551,8 @@ export function createCommands(deps) {
511
551
  branch_cleanup_changed: terminal.changed || reconciled.changed,
512
552
  };
513
553
  }
514
- const registeredAtStart = parseWorktrees(loaded.context.current_worktree)
554
+ const repositoryCwd = repositoryGitCwd(loaded);
555
+ const registeredAtStart = parseWorktrees(repositoryCwd)
515
556
  .find((worktree) => worktree.path === canonicalSelectorPath(record.path));
516
557
  if (!registeredAtStart && existsSync(record.path)) {
517
558
  return {
@@ -538,14 +579,14 @@ export function createCommands(deps) {
538
579
  }, { pushed, evidence: options.evidence ?? null });
539
580
  }
540
581
 
541
- const live = parseWorktrees(loaded.context.current_worktree).find((worktree) => worktree.path === canonicalSelectorPath(record.path));
582
+ const live = parseWorktrees(repositoryCwd).find((worktree) => worktree.path === canonicalSelectorPath(record.path));
542
583
  if (live) {
543
584
  const submodules = reclaimSubmodules(live.path);
544
585
  if (submodules.reason) return { reclaimed: false, reason: submodules.reason, record };
545
- const removed = gitTry(['worktree', 'remove', live.path], loaded.context.current_worktree);
586
+ const removed = gitTry(['worktree', 'remove', live.path], repositoryCwd);
546
587
  if (!removed.ok) {
547
588
  const detail = commandFailureReason(removed, 'git worktree remove refused');
548
- const stillRegistered = parseWorktrees(loaded.context.current_worktree)
589
+ const stillRegistered = parseWorktrees(repositoryCwd)
549
590
  .some((worktree) => worktree.path === canonicalSelectorPath(record.path));
550
591
  const residue = !stillRegistered && existsSync(record.path)
551
592
  ? '; Git registration was removed but the physical directory remains'
@@ -569,7 +610,7 @@ export function createCommands(deps) {
569
610
  };
570
611
  }
571
612
  }
572
- gitTry(['worktree', 'prune'], loaded.context.current_worktree);
613
+ gitTry(['worktree', 'prune'], repositoryCwd);
573
614
  const branchCleanup = attemptLocalBranchCleanup(loaded, record, pushed);
574
615
  record = appendReclaimEvent(loaded.context.common_dir, record, 'reclaimed', (next) => {
575
616
  const completedAt = new Date().toISOString();
@@ -659,6 +700,7 @@ export function createCommands(deps) {
659
700
  }
660
701
  if (result.branch_cleanup?.status === 'failed') {
661
702
  log(`目录已回收 ${result.record.worktree_id.slice(0, 8)};本地分支 ${result.record.branch} 清理待重试: ${result.branch_cleanup.reason}`);
703
+ process.exitCode = 1;
662
704
  return;
663
705
  }
664
706
  const recovery = result.record.evidence_archive?.archive_ref
@@ -48,14 +48,103 @@ export function createCommands(deps) {
48
48
  reclaimRecord,
49
49
  } = deps;
50
50
 
51
+ /** @param {Record<string,any>} record */
52
+ function activeAutoReclaim(record) {
53
+ return record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)
54
+ ? record.auto_reclaim
55
+ : null;
56
+ }
57
+
58
+ /**
59
+ * review_watch 是“应不应该自动回收”的持久意图;auto_reclaim 是某一次实际 watcher 的租约。
60
+ * 两者分开后,启动前置条件失败、宿主杀进程和显式 --no-watch 不再都坍缩成字段缺失。
61
+ * @param {ReturnType<typeof loadRepositoryProfile>} loaded
62
+ * @param {Record<string,any>} record
63
+ * @param {{flags:Map<string,unknown>}} args
64
+ * @param {{head:string|null}} snapshot
65
+ */
66
+ function reviewWatchIntent(loaded, record, args, snapshot) {
67
+ const previous = record.review_watch?.policy === 'auto'
68
+ ? record.review_watch
69
+ : record.auto_reclaim ?? null;
70
+ const targetRef = aliasedFlag(args.flags, 'target', 'watch-target')
71
+ ?? previous?.target_ref
72
+ ?? record.base_ref
73
+ ?? loaded.profile.default_base
74
+ ?? null;
75
+ const changeRefValue = aliasedFlag(args.flags, 'change-ref', 'mr');
76
+ return {
77
+ policy: 'auto',
78
+ state: 'pending',
79
+ target_ref: targetRef,
80
+ head_sha: snapshot.head,
81
+ interval_ms: parseWatchInterval(flag(args.flags, 'interval-ms') ?? previous?.interval_ms),
82
+ change_ref: changeRefValue
83
+ ? oneLine(changeRefValue, 'change-ref', 1000)
84
+ : previous?.change_ref ?? null,
85
+ notify: parseNotifyMode(flag(args.flags, 'notify') ?? previous?.notify),
86
+ reason: null,
87
+ source: 'auto_touch',
88
+ updated_at: new Date().toISOString(),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * @param {ReturnType<typeof loadRepositoryProfile>} loaded
94
+ * @param {Record<string,any>} record
95
+ * @param {Record<string,any>} intent
96
+ * @param {'pending'|'disabled'} state
97
+ * @param {string} reason
98
+ * @param {string|null} [disarmToken]
99
+ */
100
+ function persistReviewWatchIntent(loaded, record, intent, state, reason, disarmToken = null) {
101
+ const now = new Date().toISOString();
102
+ return appendTraceEvent({
103
+ commonDir: loaded.context.common_dir,
104
+ worktreeId: record.worktree_id,
105
+ eventType: state === 'disabled' ? 'review_watch_disabled' : 'review_watch_pending',
106
+ actor: record.agent,
107
+ details: {
108
+ policy: state === 'disabled' ? 'disabled' : 'auto',
109
+ target_ref: intent.target_ref,
110
+ head_sha: intent.head_sha,
111
+ reason,
112
+ },
113
+ mutate(current) {
114
+ if (current.worktree_state === 'reclaimed' || current.worktree_state === 'archived') {
115
+ throw new WorktreeTraceError('WATCH_INTENT_SETTLED', `已结算 record 不能更新 watch intent: ${record.worktree_id}`);
116
+ }
117
+ const next = structuredClone(current);
118
+ if (disarmToken) {
119
+ if (next.auto_reclaim?.token !== disarmToken || ['disarmed', 'reclaimed'].includes(next.auto_reclaim?.state)) {
120
+ throw new WorktreeTraceError('WATCHER_CHANGED', `watch token 已被并发更新或解除: ${record.worktree_id}`);
121
+ }
122
+ next.auto_reclaim.state = 'disarmed';
123
+ next.auto_reclaim.disarmed_at = now;
124
+ next.auto_reclaim.disarm_reason = 'explicit_no_watch';
125
+ }
126
+ next.review_watch = {
127
+ ...intent,
128
+ policy: state === 'disabled' ? 'disabled' : 'auto',
129
+ state,
130
+ reason,
131
+ updated_at: now,
132
+ };
133
+ next.updated_at = now;
134
+ return next;
135
+ },
136
+ }).record;
137
+ }
138
+
51
139
  function autoArmReviewWatch(loaded, record, args, snapshot) {
140
+ const existing = activeAutoReclaim(record);
141
+ const intent = reviewWatchIntent(loaded, record, args, snapshot);
52
142
  if (args.flags.get('no-watch')) {
143
+ persistReviewWatchIntent(loaded, record, intent, 'disabled', 'explicit_no_watch', existing?.token ?? null);
144
+ if (existing?.token) removeWatcherHeartbeat(loaded.context.common_dir, record.worktree_id, existing.token);
53
145
  log('watch 未武装:--no-watch 显式退出;合入后需要人工回收。');
54
146
  return;
55
147
  }
56
- const existing = record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)
57
- ? record.auto_reclaim
58
- : null;
59
148
  // 原子失效旧 watcher 后仍沿用它的 target/interval/change-ref 默认值,避免自动重冻结
60
149
  // 把人工显式选择的 target 静默改回该树登记的 base。
61
150
  const previous = record.auto_reclaim ?? null;
@@ -63,8 +152,11 @@ export function createCommands(deps) {
63
152
  log('watch 保持原冻结 SHA:目标分支已确认包含该 head,自动回收已进入提交阶段。');
64
153
  return;
65
154
  }
66
- const skip = (reason) => log(`watch 未武装:${reason};自动回收保持关闭,可补齐前提后重新 touch。`);
67
- const targetRef = aliasedFlag(args.flags, 'target', 'watch-target') ?? previous?.target_ref ?? record.base_ref ?? loaded.profile.default_base;
155
+ const skip = (reason) => {
156
+ persistReviewWatchIntent(loaded, record, intent, 'pending', reason);
157
+ log(`watch 未武装:${reason};自动回收保持关闭,已登记 pending,resume-all/维护服务会重试。`);
158
+ };
159
+ const targetRef = intent.target_ref;
68
160
  if (!targetRef || !targetRef.includes('/')) {
69
161
  skip(`无法确定远端主干 target(Profile default_base=${loaded.profile.default_base ?? 'null'})`);
70
162
  return;
@@ -102,11 +194,9 @@ export function createCommands(deps) {
102
194
  targetRef,
103
195
  targetSha: refreshedTarget.target_sha,
104
196
  headSha: snapshot.head,
105
- intervalMs: parseWatchInterval(flag(args.flags, 'interval-ms') ?? previous?.interval_ms),
106
- changeRef: aliasedFlag(args.flags, 'change-ref', 'mr')
107
- ? oneLine(aliasedFlag(args.flags, 'change-ref', 'mr'), 'change-ref', 1000)
108
- : previous?.change_ref ?? null,
109
- notifyMode: parseNotifyMode(flag(args.flags, 'notify') ?? previous?.notify),
197
+ intervalMs: intent.interval_ms,
198
+ changeRef: intent.change_ref,
199
+ notifyMode: intent.notify,
110
200
  explicitConfig: flag(args.flags, 'config') ? loaded.profile_path : null,
111
201
  previousHealth: health.reason,
112
202
  armedBy: 'auto_touch',
@@ -197,6 +287,18 @@ export function createCommands(deps) {
197
287
  disarm_reason: null,
198
288
  pid: null,
199
289
  };
290
+ next.review_watch = {
291
+ policy: 'auto',
292
+ state: 'armed',
293
+ target_ref: options.targetRef,
294
+ head_sha: options.headSha,
295
+ interval_ms: options.intervalMs,
296
+ change_ref: options.changeRef,
297
+ notify: options.notifyMode,
298
+ reason: null,
299
+ source: options.armedBy ?? existing?.armed_by ?? 'explicit',
300
+ updated_at: now,
301
+ };
200
302
  next.updated_at = now;
201
303
  return next;
202
304
  },
@@ -465,24 +567,71 @@ export function createCommands(deps) {
465
567
  }
466
568
 
467
569
  function cmdResumeAll(args) {
468
- rejectUnknownFlags(args.flags, ['json', 'config']);
570
+ rejectUnknownFlags(args.flags, ['json', 'quiet', 'config']);
469
571
  if (args.positionals.length) die('resume-all 不接受 selector;它只扫描已 arm record。', 2);
470
572
  const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
471
573
  const result = { resumed: [], healthy: [], skipped: [] };
472
- const records = loadRecords(loaded.context.common_dir).filter((record) =>
473
- record.auto_reclaim &&
474
- !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state) &&
475
- // archive 的前置条件本身已经要求 watcher 先 disarm,正常路径不会走到这里;这里保留
476
- // 一道防线,避免归档记录被任何遗留/异常状态误当作待恢复 watcher。
477
- record.worktree_state !== 'reclaimed' && record.worktree_state !== 'archived');
574
+ const records = loadRecords(loaded.context.common_dir).filter((record) => {
575
+ if (record.worktree_state === 'reclaimed' || record.worktree_state === 'archived') return false;
576
+ if (activeAutoReclaim(record)) return true;
577
+ if (!['ready_for_review', 'integrating'].includes(record.task_status)) return false;
578
+ // auto intent 要重试;没有任何 intent 的 legacy/live record 也必须进入 skipped,不能继续 0/0/0 隐身。
579
+ return record.review_watch?.policy === 'auto' || !record.review_watch;
580
+ });
478
581
  for (const record of records) {
582
+ const existing = activeAutoReclaim(record);
583
+ if (!existing) {
584
+ const intent = record.review_watch;
585
+ if (!intent) {
586
+ result.skipped.push({ worktree_id: record.worktree_id, task: record.task, reason: 'review watch intent missing; touch ready_for_review again' });
587
+ continue;
588
+ }
589
+ const snapshot = liveGitSnapshot(record);
590
+ let reason = null;
591
+ if (!snapshot.present) reason = 'worktree missing';
592
+ else if (snapshot.dirty !== false) reason = 'worktree dirty';
593
+ else if (!snapshot.head || snapshot.head !== intent.head_sha) reason = 'live head differs from pending intent';
594
+ else {
595
+ const upstream = gitTry(['rev-parse', '@{upstream}^{commit}'], record.path);
596
+ if (!upstream.ok || upstream.out !== snapshot.head) reason = 'current head not fully pushed to upstream';
597
+ }
598
+ if (!intent.target_ref || typeof intent.target_ref !== 'string') reason ??= 'target ref missing';
599
+ const intervalMs = Number(intent.interval_ms);
600
+ if (!Number.isInteger(intervalMs) || intervalMs < WATCH_MIN_INTERVAL_MS || intervalMs > WATCH_MAX_INTERVAL_MS) reason ??= 'interval invalid';
601
+ let refreshed = null;
602
+ if (!reason) {
603
+ refreshed = refreshTargetRef(intent.target_ref, loaded.context.current_worktree);
604
+ if (!refreshed.ok) reason = `target ref unavailable: ${intent.target_ref}`;
605
+ }
606
+ if (reason) {
607
+ result.skipped.push({ worktree_id: record.worktree_id, task: record.task, reason });
608
+ continue;
609
+ }
610
+ try {
611
+ const started = startWatcher(loaded, record, {
612
+ targetRef: intent.target_ref,
613
+ targetSha: refreshed.target_sha,
614
+ headSha: intent.head_sha,
615
+ intervalMs,
616
+ changeRef: intent.change_ref ?? null,
617
+ notifyMode: intent.notify ?? 'auto',
618
+ explicitConfig: flag(args.flags, 'config') ? loaded.profile_path : null,
619
+ previousHealth: intent.reason ?? 'pending intent',
620
+ armedBy: intent.source ?? 'auto_touch',
621
+ });
622
+ result.resumed.push({ worktree_id: record.worktree_id, task: record.task, pid: started.pid, previous_health: intent.reason ?? 'pending intent', dirty: false });
623
+ } catch (error) {
624
+ result.skipped.push({ worktree_id: record.worktree_id, task: record.task, reason: error instanceof Error ? error.message : String(error) });
625
+ }
626
+ continue;
627
+ }
479
628
  const heartbeat = readWatcherHeartbeat(loaded.context.common_dir, record.worktree_id);
480
629
  const health = watcherHealth(record, heartbeat);
481
630
  if (health.healthy) {
482
631
  result.healthy.push({ worktree_id: record.worktree_id, task: record.task, pid: heartbeat.state.pid });
483
632
  continue;
484
633
  }
485
- const auto = record.auto_reclaim;
634
+ const auto = existing;
486
635
  const intervalMs = Number(auto.interval_ms);
487
636
  const snapshot = liveGitSnapshot(record);
488
637
  let reason = null;
@@ -512,6 +661,7 @@ export function createCommands(deps) {
512
661
  result.skipped.push({ worktree_id: record.worktree_id, task: record.task, reason: error instanceof Error ? error.message : String(error) });
513
662
  }
514
663
  }
664
+ if (args.flags.get('quiet')) return;
515
665
  if (args.flags.get('json')) {
516
666
  console.log(JSON.stringify(result, null, 2));
517
667
  return;
@@ -526,17 +676,45 @@ export function createCommands(deps) {
526
676
  rejectUnknownFlags(args.flags, ['id', 'config']);
527
677
  const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
528
678
  let record = selectRecord(loadRecords(loaded.context.common_dir), args.positionals[0] ?? null, flag(args.flags, 'id'));
529
- const token = record.auto_reclaim?.token;
530
- if (!token || ['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)) {
531
- log(`watcher 未 arm: ${record.worktree_id.slice(0, 8)}`);
679
+ if (record.worktree_state === 'reclaimed' || record.worktree_state === 'archived') {
680
+ log(`已结算,无需 unwatch: ${record.worktree_id.slice(0, 8)}`);
532
681
  return;
533
682
  }
534
- if (record.auto_reclaim.state === 'merge_detected') {
683
+ const active = activeAutoReclaim(record);
684
+ const token = active?.token;
685
+ if (!token) {
686
+ if (record.review_watch?.policy === 'disabled') {
687
+ log(`watcher 未 arm: ${record.worktree_id.slice(0, 8)}`);
688
+ return;
689
+ }
690
+ const snapshot = liveGitSnapshot(record);
691
+ const intent = {
692
+ ...(record.review_watch ?? {}),
693
+ target_ref: record.review_watch?.target_ref ?? record.base_ref ?? null,
694
+ head_sha: record.review_watch?.head_sha ?? snapshot.head,
695
+ interval_ms: record.review_watch?.interval_ms ?? parseWatchInterval(null),
696
+ change_ref: record.review_watch?.change_ref ?? null,
697
+ notify: record.review_watch?.notify ?? 'auto',
698
+ source: record.review_watch?.source ?? 'explicit',
699
+ };
700
+ persistReviewWatchIntent(loaded, record, intent, 'disabled', 'explicit_unwatch');
701
+ log(`auto-reclaim 已明确关闭: ${record.worktree_id.slice(0, 8)}`);
702
+ return;
703
+ }
704
+ if (active.state === 'merge_detected') {
535
705
  die('目标分支已确认包含冻结的 MR head,自动回收已进入提交阶段,不能再 unwatch。');
536
706
  }
537
707
  record = appendReclaimEvent(loaded.context.common_dir, record, 'auto_reclaim_disarmed', (next) => {
538
708
  next.auto_reclaim.state = 'disarmed';
539
709
  next.auto_reclaim.disarmed_at = new Date().toISOString();
710
+ next.auto_reclaim.disarm_reason = 'explicit_unwatch';
711
+ next.review_watch = {
712
+ ...(next.review_watch ?? {}),
713
+ policy: 'disabled',
714
+ state: 'disabled',
715
+ reason: 'explicit_unwatch',
716
+ updated_at: new Date().toISOString(),
717
+ };
540
718
  });
541
719
  removeWatcherHeartbeat(loaded.context.common_dir, record.worktree_id, token);
542
720
  log(`auto-reclaim watcher 已解除: ${record.worktree_id.slice(0, 8)}`);
@@ -0,0 +1,224 @@
1
+ // @ts-check
2
+
3
+ const DEFAULT_INTERVAL_SECONDS = 60;
4
+ const MIN_INTERVAL_SECONDS = 30;
5
+ const MAX_INTERVAL_SECONDS = 60 * 60;
6
+
7
+ /** @param {string} value */
8
+ function xmlEscape(value) {
9
+ return value
10
+ .replaceAll('&', '&amp;')
11
+ .replaceAll('<', '&lt;')
12
+ .replaceAll('>', '&gt;');
13
+ }
14
+
15
+ /** @param {string} repositoryId */
16
+ export function launchAgentLabel(repositoryId) {
17
+ const suffix = String(repositoryId).toLowerCase().replaceAll(/[^a-z0-9]/gu, '').slice(0, 32);
18
+ if (!suffix) throw new Error('repository id 不能生成 launchd label。');
19
+ return `io.github.cr1992.agentkit.worktree.${suffix}`;
20
+ }
21
+
22
+ /**
23
+ * 生成固定 argv 的 LaunchAgent;不经过 shell,也不把调用会话的环境变量或凭证写进 plist。
24
+ * @param {{label:string,nodePath:string,managerScript:string,workingDirectory:string,intervalSeconds:number,logPath:string,configPath?:string|null}} options
25
+ */
26
+ export function renderLaunchAgentPlist(options) {
27
+ const args = [options.nodePath, options.managerScript, 'resume-all', '--quiet'];
28
+ if (options.configPath) args.push('--config', options.configPath);
29
+ const argumentXml = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
30
+ return `<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ <dict>
34
+ <key>Label</key>
35
+ <string>${xmlEscape(options.label)}</string>
36
+ <key>ProgramArguments</key>
37
+ <array>
38
+ ${argumentXml}
39
+ </array>
40
+ <key>WorkingDirectory</key>
41
+ <string>${xmlEscape(options.workingDirectory)}</string>
42
+ <key>RunAtLoad</key>
43
+ <true/>
44
+ <key>StartInterval</key>
45
+ <integer>${options.intervalSeconds}</integer>
46
+ <key>AbandonProcessGroup</key>
47
+ <true/>
48
+ <key>ProcessType</key>
49
+ <string>Background</string>
50
+ <key>StandardOutPath</key>
51
+ <string>/dev/null</string>
52
+ <key>StandardErrorPath</key>
53
+ <string>${xmlEscape(options.logPath)}</string>
54
+ </dict>
55
+ </plist>
56
+ `;
57
+ }
58
+
59
+ /** @param {unknown} raw */
60
+ export function parseServiceInterval(raw) {
61
+ const value = raw === null || raw === undefined ? DEFAULT_INTERVAL_SECONDS : Number(raw);
62
+ if (!Number.isInteger(value) || value < MIN_INTERVAL_SECONDS || value > MAX_INTERVAL_SECONDS) {
63
+ throw new Error(`--interval-seconds 必须是 ${MIN_INTERVAL_SECONDS}-${MAX_INTERVAL_SECONDS} 的整数。`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ /**
69
+ * @param {Record<string,any>} deps
70
+ */
71
+ export function createCommands(deps) {
72
+ const {
73
+ processPlatform,
74
+ processExecPath,
75
+ processGetuid,
76
+ homedir,
77
+ randomUUID,
78
+ existsSync,
79
+ mkdirSync,
80
+ readFileSync,
81
+ renameSync,
82
+ rmSync,
83
+ writeFileSync,
84
+ dirname,
85
+ join,
86
+ managerScript,
87
+ runFileCapture,
88
+ loadRepositoryProfile,
89
+ readRepositoryIdentity,
90
+ ensureRepositoryIdentity,
91
+ traceLayout,
92
+ flag,
93
+ rejectUnknownFlags,
94
+ die,
95
+ log,
96
+ } = deps;
97
+
98
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {boolean} createIdentity */
99
+ function descriptor(loaded, createIdentity) {
100
+ const identity = createIdentity
101
+ ? ensureRepositoryIdentity(loaded.context)
102
+ : readRepositoryIdentity(loaded.context);
103
+ if (!identity) return null;
104
+ const label = launchAgentLabel(identity.repository_id);
105
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
106
+ const logPath = join(traceLayout(loaded.context.common_dir).root, 'watch-service.log');
107
+ const uid = processGetuid();
108
+ return {
109
+ label,
110
+ plist_path: plistPath,
111
+ log_path: logPath,
112
+ domain: `gui/${uid}`,
113
+ service_target: `gui/${uid}/${label}`,
114
+ working_directory: loaded.context.primary_worktree,
115
+ config_path: loaded.profile_source === 'explicit' ? loaded.profile_path : null,
116
+ };
117
+ }
118
+
119
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded */
120
+ function watchServiceStatus(loaded) {
121
+ if (processPlatform !== 'darwin') {
122
+ return { supported: false, installed: false, loaded: false, platform: processPlatform, reason: 'macOS LaunchAgent adapter only' };
123
+ }
124
+ const value = descriptor(loaded, false);
125
+ if (!value) {
126
+ return { supported: true, installed: false, loaded: false, platform: processPlatform, reason: 'repository identity missing' };
127
+ }
128
+ const probe = runFileCapture('/bin/launchctl', ['print', value.service_target], { cwd: loaded.context.primary_worktree });
129
+ return {
130
+ supported: true,
131
+ installed: existsSync(value.plist_path),
132
+ loaded: probe.ok,
133
+ platform: processPlatform,
134
+ label: value.label,
135
+ plist_path: value.plist_path,
136
+ log_path: value.log_path,
137
+ node_path: processExecPath,
138
+ manager_script: managerScript,
139
+ program_available: existsSync(processExecPath) && existsSync(managerScript),
140
+ reason: probe.ok ? null : (probe.out || 'launchd job not loaded'),
141
+ };
142
+ }
143
+
144
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {number} intervalSeconds */
145
+ function install(loaded, intervalSeconds) {
146
+ if (processPlatform !== 'darwin') die(`watch-service install 当前只支持 macOS;${processPlatform} 可继续手工运行 resume-all。`, 2);
147
+ const value = descriptor(loaded, true);
148
+ const plist = renderLaunchAgentPlist({
149
+ label: value.label,
150
+ nodePath: processExecPath,
151
+ managerScript,
152
+ workingDirectory: value.working_directory,
153
+ intervalSeconds,
154
+ logPath: value.log_path,
155
+ configPath: value.config_path,
156
+ });
157
+ mkdirSync(dirname(value.plist_path), { recursive: true });
158
+ mkdirSync(dirname(value.log_path), { recursive: true });
159
+ const previous = existsSync(value.plist_path) ? readFileSync(value.plist_path, 'utf8') : null;
160
+ const temporary = `${value.plist_path}.${processGetuid()}.${randomUUID()}.tmp`;
161
+ writeFileSync(temporary, plist, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
162
+
163
+ const wasLoaded = runFileCapture('/bin/launchctl', ['print', value.service_target], { cwd: value.working_directory }).ok;
164
+ if (wasLoaded) {
165
+ const stopped = runFileCapture('/bin/launchctl', ['bootout', value.service_target], { cwd: value.working_directory });
166
+ if (!stopped.ok) {
167
+ rmSync(temporary, { force: true });
168
+ die(`无法重载现有 LaunchAgent:${stopped.out || 'launchctl bootout failed'}`);
169
+ }
170
+ }
171
+ renameSync(temporary, value.plist_path);
172
+ const started = runFileCapture('/bin/launchctl', ['bootstrap', value.domain, value.plist_path], { cwd: value.working_directory });
173
+ if (!started.ok) {
174
+ if (previous === null) rmSync(value.plist_path, { force: true });
175
+ else writeFileSync(value.plist_path, previous, { encoding: 'utf8', mode: 0o600 });
176
+ if (wasLoaded && previous !== null) runFileCapture('/bin/launchctl', ['bootstrap', value.domain, value.plist_path], { cwd: value.working_directory });
177
+ die(`LaunchAgent 安装失败:${started.out || 'launchctl bootstrap failed'}`);
178
+ }
179
+ const kicked = runFileCapture('/bin/launchctl', ['kickstart', '-k', value.service_target], { cwd: value.working_directory });
180
+ if (!kicked.ok) die(`LaunchAgent 已加载但首次执行失败:${kicked.out || 'launchctl kickstart failed'}`);
181
+ return { ...watchServiceStatus(loaded), interval_seconds: intervalSeconds };
182
+ }
183
+
184
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded */
185
+ function uninstall(loaded) {
186
+ if (processPlatform !== 'darwin') die(`watch-service uninstall 当前只支持 macOS;当前平台 ${processPlatform}。`, 2);
187
+ const value = descriptor(loaded, false);
188
+ if (!value) return { supported: true, installed: false, loaded: false, removed: false };
189
+ const loadedBefore = runFileCapture('/bin/launchctl', ['print', value.service_target], { cwd: value.working_directory }).ok;
190
+ if (loadedBefore) {
191
+ const stopped = runFileCapture('/bin/launchctl', ['bootout', value.service_target], { cwd: value.working_directory });
192
+ if (!stopped.ok) die(`LaunchAgent 停止失败:${stopped.out || 'launchctl bootout failed'}`);
193
+ }
194
+ const installedBefore = existsSync(value.plist_path);
195
+ rmSync(value.plist_path, { force: true });
196
+ return { supported: true, installed: false, loaded: false, removed: installedBefore || loadedBefore, label: value.label, plist_path: value.plist_path };
197
+ }
198
+
199
+ function cmdWatchService(args) {
200
+ rejectUnknownFlags(args.flags, ['json', 'config', 'interval-seconds']);
201
+ const action = args.positionals[0] ?? 'status';
202
+ if (args.positionals.length > 1 || !['install', 'status', 'uninstall'].includes(action)) {
203
+ die('watch-service 用法:watch-service install|status|uninstall [--json] [--interval-seconds <seconds>]', 2);
204
+ }
205
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
206
+ let result;
207
+ if (action === 'install') {
208
+ let intervalSeconds;
209
+ try { intervalSeconds = parseServiceInterval(flag(args.flags, 'interval-seconds')); }
210
+ catch (error) { die(error instanceof Error ? error.message : String(error), 2); }
211
+ result = install(loaded, intervalSeconds);
212
+ } else if (action === 'uninstall') {
213
+ result = uninstall(loaded);
214
+ } else {
215
+ result = watchServiceStatus(loaded);
216
+ }
217
+ if (args.flags.get('json')) console.log(JSON.stringify(result, null, 2));
218
+ else if (action === 'install') log(`跨会话 watch-service 已安装 label=${result.label} interval=${result.interval_seconds}s`);
219
+ else if (action === 'uninstall') log(`跨会话 watch-service ${result.removed ? '已卸载' : '原本未安装'}`);
220
+ else log(`watch-service supported=${result.supported} installed=${result.installed} loaded=${result.loaded}${result.reason ? ` reason=${result.reason}` : ''}`);
221
+ }
222
+
223
+ return { cmdWatchService, watchServiceStatus };
224
+ }
@@ -23,7 +23,7 @@ metadata:
23
23
  下文多行示例采用 POSIX 续行语法;PowerShell/cmd 保留相同 argv 即可。不要把 `python3`、`bash`、
24
24
  `~` 展开或 `/tmp` 视为跨平台常量。
25
25
 
26
- 不要假设目标仓库有同名脚本。仓库可以通过根目录 `.worktree-trace.json` 提供 Profile,但 portable core 不执行 Profile 中的任意 shell command。配置 schema 和项目适配边界见 [Profile 配置](../docs/worktree/profile.md)。
26
+ 不要假设目标仓库有同名脚本。仓库可以通过根目录 `.worktree-trace.json` 提供 Profile,但 portable core 不执行 Profile 中的任意 shell command。配置 schema 和项目适配边界见 Profile 配置(`agentkit docs worktree profile`)。
27
27
 
28
28
  所有命令都必须从**目标 Git 仓库的任意 worktree 内**执行:manager 以当前工作目录解析 repository
29
29
  identity、primary worktree 和 Profile。
@@ -58,14 +58,18 @@ manager 的 trace/common-dir 状态只应由受信 operator/controller 与 runti
58
58
 
59
59
  | 阶段 | 最小命令面 | 硬门禁与按需文档 |
60
60
  |---|---|---|
61
- | 恢复/盘点 | `resume-all` → `list` → `doctor` | `list/doctor` 只读;`UNTRACKED` 不等于无人使用;`EPHEMERAL` 优先 commit+push;任何 error 都暂停 `spawn/adopt` |
61
+ | 恢复/盘点 | `watch-service status` → `resume-all` → `list` → `doctor` | 常驻服务未安装时只报告并建议 `watch-service install`,不得用 `--no-watch` 顶替;`list/doctor` 只读;`UNTRACKED` 不等于无人使用;`EPHEMERAL` 优先 commit+push;任何 error 都暂停 `spawn/adopt` |
62
62
  | 碰撞扫描 | `scan --target <paths>` | `COLLIDE`、范围不确定或重做昂贵时隔离;`CLEAR` 仅在 owner 明确且改动小时共享;低置信后缀匹配先人工核对路径基准 |
63
- | 交付身份 | 选择复用/并存/替代 | branch、MR 或目录名不同不能单独证明独立交付;同一 agent 有存量树或需要替代时读[交付身份](../docs/worktree/delivery-identity.md) |
64
- | 创建/接管 | `spawn <semantic-slug>` / `adopt <path>` | 使用真实 agent-id,不猜身份;task 至少两个 lowercase 语义分词;base、root、堆叠、rebase/retarget 读[创建与堆叠](../docs/worktree/spawn-and-stack.md) |
65
- | 更新/交接 | `touch` / `handoff` / `refresh-review` | `ready_for_review` 默认武装冻结 HEAD;handoff 只接受 clean、已 push 边界,禁止 stash 搬运;读[评审生命周期](../docs/worktree/review-lifecycle.md) |
63
+ | 交付身份 | 选择复用/并存/替代 | branch、MR 或目录名不同不能单独证明独立交付;同一 agent 有存量树或需要替代时读交付身份(`agentkit docs worktree delivery-identity`) |
64
+ | 创建/接管 | `spawn <semantic-slug>` / `adopt <path>` | 使用真实 agent-id,不猜身份;task 至少两个 lowercase 语义分词;base、root、堆叠、rebase/retarget 读创建与堆叠(`agentkit docs worktree spawn-and-stack`) |
65
+ | 更新/交接 | `touch` / `handoff` / `refresh-review` | `ready_for_review` 默认武装冻结 HEAD;handoff 只接受 clean、已 push 边界,禁止 stash 搬运;读评审生命周期(`agentkit docs worktree review-lifecycle`) |
66
66
  | 审计/提交 | `audit` / `submit` | portable core 不决定 MR/PR 或直推策略;先服从目标仓规则,再原子登记 change request 与 watcher target |
67
- | 批量集成 | `plan-batch` → `batch-integrate` → `batch-step` → `batch-result` | 同仓同 target:≥3 默认聚合,2 个仅在碰撞时;只合成冻结 SHA,不代跑门禁;合同/顺序/target 变化即重规划;读[批量集成](../docs/worktree/batch-integration.md) |
68
- | 回收 | `reclaim` / `archive` | 必须有远端/其他 ref 可达证明或明确 archive evidence;dirty、stash、submodule、权限或 branch-tip 异常一律 `KEEP`;替代树默认先归档,`--discard` 需人工明确授权;读[回收与看护](../docs/worktree/reclaim-and-watch.md) |
67
+ | 批量集成 | `plan-batch` → `batch-integrate` → `batch-step` → `batch-result` | 同仓同 target:≥3 默认聚合,2 个仅在碰撞时;只合成冻结 SHA,不代跑门禁;合同/顺序/target 变化即重规划;读批量集成(`agentkit docs worktree batch-integration`) |
68
+ | 回收 | `reclaim` / `archive` | 必须有远端/其他 ref 可达证明或明确 archive evidence;dirty、stash、submodule、权限或 branch-tip 异常一律 `KEEP`;替代树默认先归档,`--discard` 需人工明确授权;读回收与看护(`agentkit docs worktree reclaim-and-watch`) |
69
+
70
+ `resume-all` 只提供当次恢复。跨 Agent 会话、注销或重启后仍要自动恢复 watcher,靠的是常驻维护器:
71
+ macOS 由用户一次性显式执行 `watch-service install`。未安装时不得把进程级 watcher 描述成跨会话保证,
72
+ 也不得因此改用 `--no-watch`(判据见评审生命周期)——那只会把待回收的树变成无人看护。
69
73
 
70
74
  不得按目录年龄、名称或“看起来干净”删除。禁止 `rm -rf`、`git worktree remove --force`、`branch -D`
71
75
  和 feature owner 手工 rebase。watcher 的 `merge-tree` 结果只是目标前进后的只读成本预判,不保证逐
@@ -38,7 +38,7 @@ controller 始终拥有闭环对象、完成标准、任务图、依赖/barrier
38
38
 
39
39
  派生后维护“活跃 Agent”“本批已完成”,存在并行时再维护“工作进展”;在派发、批次收口、用户询问
40
40
  和最终答复时更新,无活跃 worker 明示为 0,不虚构百分比。字段与状态变换见
41
- [编排运行时](../docs/orchestrate/orchestration-runtime.md)「派发台账的三张表」。台账也是回收清单:收口前
41
+ 编排运行时(`agentkit docs orchestrate orchestration-runtime`)「派发台账的三张表」。台账也是回收清单:收口前
42
42
  确认没有孤儿 worker/worktree/运行时资源。任务图与台账只写仓库外会话状态;完整档用 mechanical
43
43
  ledger,轻量档用单一 JSON 快照,闭环后清理。
44
44
 
@@ -48,7 +48,7 @@ ledger,轻量档用单一 JSON 快照,闭环后清理。
48
48
  只有 `allowed` 可用。输入已随合同完整提供、节点不依赖额外宿主能力时 requirements 可为空,
49
49
  此时记 `not_required` 及依据即可。同机、本地或过去恒过都不构成豁免。探针只在比 controller 自做或
50
50
  缩小节点更便宜时派发,并计入 worker 与 Token 预算。能力键前缀、binding 与复用规则、反应矩阵见
51
- [编排运行时](../docs/orchestrate/orchestration-runtime.md)「Worker 有效能力预检」。
51
+ 编排运行时(`agentkit docs orchestrate orchestration-runtime`)「Worker 有效能力预检」。
52
52
 
53
53
  ### 显式轻量档
54
54
 
@@ -59,7 +59,7 @@ ledger,轻量档用单一 JSON 快照,闭环后清理。
59
59
  轻量档仍冻结每个节点的目标、范围、事实/假设、输出合同、验收、证据和停止条件,并在仓库外维护
60
60
  单一 JSON 快照;它只记录事实,不伪造 journal。资格失效时停止新派发,把现状和产物收养进完整 ledger。
61
61
  Reflection 使用 `agentkit orchestrate reflection record/propose`,通用 Skill 缺口不得写入宿主能力缓存。
62
- 快照 schema、收养步骤和 Reflection 约束见 [编排运行时](../docs/orchestrate/orchestration-runtime.md)。
62
+ 快照 schema、收养步骤和 Reflection 约束见 编排运行时(`agentkit docs orchestrate orchestration-runtime`)。
63
63
 
64
64
  ### 完整档机械台账工具
65
65
 
@@ -68,7 +68,7 @@ review-view / diff` 固定公共 Task Contract,再以仓库外 state root 初
68
68
  脚本不直接派发 Agent,controller 把宿主回执写入 `dispatch-record`。节点必须选择
69
69
  `worker_self_check`、`controller_recheck`、`independent_evidence`,只读 critic/scout 用
70
70
  `not_applicable`;依赖/barrier 未通过不派下游。命令、schema、`--expected-revision`、state root 信任
71
- 边界和批级熔断见 [编排运行时](../docs/orchestrate/orchestration-runtime.md)。
71
+ 边界和批级熔断见 编排运行时(`agentkit docs orchestrate orchestration-runtime`)。
72
72
  Reflection 只记证据化观察,Improvement Proposal 永远保持 `proposed`,不能改变当前运行。
73
73
 
74
74
  ## 1. 先过闸门
@@ -81,7 +81,7 @@ Reflection 只记证据化观察,Improvement Proposal 永远保持 `proposed`
81
81
 
82
82
  规模来自真正独立且单个上下文装不下的工作块,不来自话题数;删掉不影响答案的节点。常规盘点/审计
83
83
  一轮总 Agent 数 `≤10`。扫描与核验配比、
84
- 合并判据与各档规模见 [任务类型剧本](../docs/orchestrate/task-playbooks.md)。
84
+ 合并判据与各档规模见 任务类型剧本(`agentkit docs orchestrate task-playbooks`)。
85
85
 
86
86
  ## 3. 派发前决定环境
87
87
 
@@ -98,7 +98,7 @@ Reflection 只记证据化观察,Improvement Proposal 永远保持 `proposed`
98
98
 
99
99
  仓库 dirty、任务大或包含新文件本身都不足以要求隔离;先确认第二写入者、归属冲突或路径相交。
100
100
  裁决隔离后使用 `manage-worktrees`,它不改变任务图、权限或验收。不可用时必须先读
101
- [无 manage-worktrees 时的隔离下限](../docs/orchestrate/isolation-fallback.md)。
101
+ 无 manage-worktrees 时的隔离下限(`agentkit docs orchestrate isolation-fallback`)。
102
102
 
103
103
  共享树中 worker 不得切换分支,只按路径 stage / commit;禁止 `git add -A`、`git commit -am`、裸 `stash`、`reset --hard`、`checkout -- .` 和并发 merge,controller 是唯一 integrator。
104
104
 
@@ -107,7 +107,7 @@ Reflection 只记证据化观察,Improvement Proposal 永远保持 `proposed`
107
107
  2-3 个独立任务用宿主轻量并行;循环、条件、逐项管线或预算驱动才使用结构化编排。多 stage 默认逐条
108
108
  pipeline,只有跨条去重、聚合、比较或早退才设 barrier。截断、采样或不重试必须披露覆盖缺口。
109
109
  barrier、critic 面板与 loop-until-dry 的具体用法见
110
- [编排运行时](../docs/orchestrate/orchestration-runtime.md)「编排原语选择」。具体 API 服从宿主编排工具说明。
110
+ 编排运行时(`agentkit docs orchestrate orchestration-runtime`)「编排原语选择」。具体 API 服从宿主编排工具说明。
111
111
 
112
112
  ## 5. 派发可验收的任务契约
113
113
 
@@ -118,17 +118,17 @@ stop_conditions`;写任务再冻结权限、精确仓库/workdir/branch 和 wr
118
118
  controller 圆场。
119
119
 
120
120
  派发写 worker、完整档、独立 Evidence 或失败后重派前,必须读取
121
- [节点派发合同](../docs/orchestrate/dispatch-contract.md) 并使用完整 envelope。轻量只读节点可只使用上述最小合同。
121
+ 节点派发合同(`agentkit docs orchestrate dispatch-contract`) 并使用完整 envelope。轻量只读节点可只使用上述最小合同。
122
122
 
123
123
  ## 6. 控制运行态与升级
124
124
 
125
125
  用户可见台账只用 `运行中 / 阻塞 / 待验收`;`partial` 由 controller 决定继续、转阻塞或停止后判
126
126
  未通过。只有 controller 能给出 `通过 / 未通过 / 取消`。台账术语不进用户汇报,对外说法见
127
- [面向用户的汇报词表](../docs/orchestrate/user-facing-reporting.md)。
127
+ 面向用户的汇报词表(`agentkit docs orchestrate user-facing-reporting`)。
128
128
 
129
129
  只在结论交付、需要输入、scope/写入冲突、预算将尽、新依赖、失败或置信不足时介入;“等待中”不是
130
130
  状态包。能力故障、业务失败、中断与 contract gap 分开处理,不按失败次数机械升档。失败、重派、中断
131
- 或 controller 接手时读取[失败路由与恢复](../docs/orchestrate/failure-routing-and-recovery.md)。合同失真、
131
+ 或 controller 接手时读取失败路由与恢复(`agentkit docs orchestrate failure-routing-and-recovery`)。合同失真、
132
132
  新授权、不可逆动作、资源/结论冲突或重试耗尽必须升级,不得自行扩 scope。
133
133
 
134
134
  ## 7. 验证、收敛与停止
@@ -139,7 +139,7 @@ controller 圆场。
139
139
  复核记录;`independent_evidence` 绑定唯一 Artifact 与标准 Evidence;只读 critic/scout 用
140
140
  `not_applicable`,附一份 report 即收口。失败、不可判定、安全阻塞、
141
141
  human gate 或错绑 Evidence 均不能通过。节点专属验收合同只能用 `agentkit contract project` 从公共
142
- 合同切出 acceptance 子集;完整门禁见 [编排运行时](../docs/orchestrate/orchestration-runtime.md)「合同投影」。
142
+ 合同切出 acceptance 子集;完整门禁见 编排运行时(`agentkit docs orchestrate orchestration-runtime`)「合同投影」。
143
143
 
144
144
  快速演进分支的迭代期只读 review 最多算 `controller_recheck`;到 RC/合入候选才冻结唯一 Artifact,
145
145
  运行 `verify-agent-output` 生成 `independent_evidence`。
@@ -153,7 +153,7 @@ human gate 或错绑 Evidence 均不能通过。节点专属验收合同只能
153
153
  独立 reviewer 是高成本资源,不与 worker 1:1 配对。对同一 Artifact 默认只允许一次 primary review;
154
154
  第二次必须由 `undecidable`、证据冲突或协议歧义触发,使用不同 lens 且可能改变处置。smoke 未通过、
155
155
  同 lens 重复、输入超出预算或已有 `blocked_safety` 时不派。考虑任何 critic/reviewer 时必须读取
156
- [Reviewer 数量与 Token 预算](../docs/orchestrate/review-budget.md),冻结 `extensions.review_policy`,并在派发前运行
156
+ Reviewer 数量与 Token 预算(`agentkit docs orchestrate review-budget`),冻结 `extensions.review_policy`,并在派发前运行
157
157
  `agentkit orchestrate review-budget evaluate`。提高 reviewer 数量或输入上限属于 re-contract。
158
158
 
159
159
  最终答复前用 `闭环项 / 标准 / 证据 / 结果` 审计:闭环对象与外部状态已重查;必要节点已验收且活跃
@@ -163,11 +163,11 @@ human gate 或错绑 Evidence 均不能通过。节点专属验收合同只能
163
163
  ## 8. 模型路由与宿主自适配
164
164
 
165
165
  controller 持续掌握目标、任务图、re-plan、失败分类和最终验收;worker 不自选模型或下一次配置。
166
- 派发、首次建 tier、失败后调档或切模型时读[模型路由](../docs/orchestrate/model-routing-config.md),按任务类型
167
- 选档再读[任务剧本](../docs/orchestrate/task-playbooks.md):使用本地配置中最低可靠 tier,model/effort 只取
166
+ 派发、首次建 tier、失败后调档或切模型时读模型路由(`agentkit docs orchestrate model-routing-config`),按任务类型
167
+ 选档再读任务剧本(`agentkit docs orchestrate task-playbooks`):使用本地配置中最低可靠 tier,model/effort 只取
168
168
  用户配置与实时 schema,每次保存精确配置、attempt lineage、理由和预算;扩大授权或显著增费先确认。
169
169
 
170
170
  `agents/openai.yaml` 和缓存都不是能力证明。runtime/path/tool 要求先做有效能力预检;能力快照的创建、
171
- 刷新和复用读[宿主能力缓存](../docs/orchestrate/host-capability-cache.md)。实时 schema 优先,unknown 不能被
171
+ 刷新和复用读宿主能力缓存(`agentkit docs orchestrate host-capability-cache`)。实时 schema 优先,unknown 不能被
172
172
  缓存补成 allowed,缓存不能扩大授权。`protocol_version`、`runtime_version`、`content_digest` 分别表示
173
173
  协议兼容性、实现版本和精确安装内容,不得互相替代。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr1992/agentkit",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Agent engineering toolkit: orchestration, git worktree isolation, independent verification and bounded verify loops.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,8 +51,8 @@ verification run,消费其标准 Evidence Package。Loop 不执行或复制完
51
51
  - runtime 只生成 `embedded_verification_record`,不能导出、转换或冒充 Evidence Package;
52
52
  - 无独立 reviewer 时停止,不用实现者自审产生 pass。
53
53
 
54
- 进入 embedded L1 前读取 [内嵌 review 适配器](../docs/loop/embedded-review-adapter.md)。
55
- verifier 行为语义以 [验收协议](../docs/verify/verification-protocol.md) 为唯一真源;独立安装本 Skill
54
+ 进入 embedded L1 前读取 内嵌 review 适配器(`agentkit docs loop embedded-review-adapter`)。
55
+ verifier 行为语义以 验收协议(`agentkit docs verify verification-protocol`) 为唯一真源;独立安装本 Skill
56
56
  时,adapter 只携带绑定和最低输入要求,不重定义 verdict。
57
57
 
58
58
  ## Runtime 流程
@@ -79,8 +79,8 @@ run-embedded-l0 → 新上下文 reviewer → record-embedded-review
79
79
  ```
80
80
 
81
81
  所有状态写命令可带 `--expected-revision <n>`;revision 不匹配时拒绝写入。详细状态迁移见
82
- [Loop 状态机](../docs/loop/loop-state-machine.md),恢复和熔断见
83
- [恢复与熔断](../docs/loop/recovery-and-fuses.md)。只有发生复制/移动、journal 恢复、
82
+ Loop 状态机(`agentkit docs loop loop-state-machine`),恢复和熔断见
83
+ 恢复与熔断(`agentkit docs loop recovery-and-fuses`)。只有发生复制/移动、journal 恢复、
84
84
  reflection 或提案时才使用 `adopt-root / record-reflection / convergence-report / propose-improvement --help`。
85
85
 
86
86
  ## 决策规则
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "package_name": "@cr1992/agentkit",
4
- "package_version": "1.0.0",
4
+ "package_version": "1.1.1",
5
5
  "cli": {
6
6
  "command": "agentkit",
7
7
  "entry": "bin/agentkit.mjs"
@@ -31,8 +31,8 @@ controller 必须先提供:
31
31
  4. clean workdir,`HEAD == artifact_sha`;
32
32
  5. 新上下文 reviewer,或由用户中继的第二会话。只有当前实现者上下文时停止,不得伪造独立验收。
33
33
 
34
- 详细 envelope 与字段规则见 [Evidence schema](../docs/verify/evidence-schema.md)。进入 L1 前必须完整读取
35
- [验收协议](../docs/verify/verification-protocol.md)。
34
+ 详细 envelope 与字段规则见 Evidence schema(`agentkit docs verify evidence-schema`)。进入 L1 前必须完整读取
35
+ 验收协议(`agentkit docs verify verification-protocol`)。
36
36
 
37
37
  ## 标准流程
38
38
 
@@ -56,7 +56,7 @@ agentkit verify validate --run <run-dir>
56
56
  ```
57
57
 
58
58
  所有子命令支持 `--help`;写命令支持 `--expected-revision`。尚未准备输入、happy path 失败或需要完整
59
- 诊断时,先读 [输入准备与诊断命令](../docs/verify/input-preparation.md)。readiness/preflight 失败属于环境
59
+ 诊断时,先读 输入准备与诊断命令(`agentkit docs verify input-preparation`)。readiness/preflight 失败属于环境
60
60
  前提,不得记录为 Artifact verdict。
61
61
 
62
62
  ## 状态与裁决