@khorsheed/dsh-ankh-guard 0.1.1 → 0.2.0
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 +24 -0
- package/README.en.md +75 -29
- package/README.i18n.yaml +2 -2
- package/README.md +74 -29
- package/lib/cli.js +2349 -182
- package/lib/client.js +257 -0
- package/lib/exit-agent.js +5 -2
- package/lib/index.js +721 -38
- package/lib/invariant.js +1 -1
- package/lib/preflight-runner.js +125 -47
- package/lib/processes-BjZgJjQr.js +344 -0
- package/lib/restart-context-D6nISh28.js +1245 -0
- package/lib/restart-context-DUyExi9O.js +1245 -0
- package/lib/{state-Dhx9VG44.js → state-4f7yny39.js} +60 -13
- package/lib/state-CZMypGkB.js +323 -0
- package/lib/test-seam-DnvLWTeO.js +119 -0
- package/lib/test-seam-cli.js +24 -0
- package/lib/test-seam-dwvaKjRp.js +459 -0
- package/lib/test-seam.js +2 -0
- package/lib/types/browser-handoff.d.ts +55 -0
- package/lib/types/browser-handoff.js +489 -0
- package/lib/types/cli.d.ts +34 -4
- package/lib/types/cli.js +1421 -166
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +264 -0
- package/lib/types/deployment-proof.d.ts +24 -0
- package/lib/types/deployment-proof.js +314 -0
- package/lib/types/exit-agent.js +2 -0
- package/lib/types/git.d.ts +12 -3
- package/lib/types/git.js +69 -7
- package/lib/types/index.d.ts +66 -3
- package/lib/types/index.js +157 -39
- package/lib/types/launch-spec.d.ts +263 -0
- package/lib/types/launch-spec.js +823 -0
- package/lib/types/preflight-runner.d.ts +23 -12
- package/lib/types/preflight-runner.js +152 -57
- package/lib/types/processes.d.ts +38 -6
- package/lib/types/processes.js +236 -10
- package/lib/types/restart-context.d.ts +50 -0
- package/lib/types/restart-context.js +106 -0
- package/lib/types/restart-request.d.ts +32 -0
- package/lib/types/restart-request.js +128 -0
- package/lib/types/state-files.d.ts +30 -0
- package/lib/types/state-files.js +55 -0
- package/lib/types/state.d.ts +29 -2
- package/lib/types/state.js +52 -7
- package/lib/types/temp-artifact.d.ts +15 -0
- package/lib/types/temp-artifact.js +17 -0
- package/lib/types/test-seam-cli.d.ts +3 -0
- package/lib/types/test-seam-cli.js +27 -0
- package/lib/types/test-seam.d.ts +55 -0
- package/lib/types/test-seam.js +112 -0
- package/lib/types/transition.d.ts +118 -0
- package/lib/types/transition.js +717 -0
- package/package.json +30 -11
- package/scripts/dsh-watchdog.sh +1388 -80
- package/scripts/install-launchd.sh +43 -5
- package/scripts/install-systemd.sh +43 -5
- package/scripts/on-install.js +1 -1
- package/skills/dsh-self-restart-guard/SKILL.md +38 -12
- package/lib/processes-hCAmwma-.js +0 -127
- package/lib/restart-context-DmnQXNf-.js +0 -421
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.2.0(2026-09-10)
|
|
4
|
+
|
|
5
|
+
- **BREAKING**:minHost 前移至 `0.1.2-rc.1`;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 线(末版 `0.1.1`)
|
|
6
|
+
- 新增进程内 `requestRestart` 触发缝与 boot 代际浏览器刷新通道:应用内服务可直接驱动受门禁约束的重启,重启后按 boot 代际刷新已登记标签页
|
|
7
|
+
- preflight 快照跳过运行时条目:socket/FIFO(含指向它们的符号链接)不再导致 `reconfigure` 拒绝;顶层 `scratch/` 目录从 home 复制中排除;复制耗时超过凭证窗口一半时提前警告
|
|
8
|
+
- 适配宿主 0.1.2 线:dev/peer 依赖面迁移到 0.1.2 API(`@deepseek-ai/dsh-skill` 归位 0.1.2 范围),`dsh.compat` 地板同步前移
|
|
9
|
+
- preflight 执行面成为 launch spec 的显式契约:持久化 source/built、runner 可执行方式/路径/内容 SHA、实际 dsh 安装锚点与 target command SHA;`reconfigure` 在隔离 home 先运行与该 SHA 绑定的一等 candidate command probe,再用同一 source/npm-built 模块面做 composition preflight,不再从 `process.execArgv` 猜测或误读 checkout source
|
|
10
|
+
- candidate probe 是调用方提供并与 target command 摘要共同绑定的信任边界;Guard 不声称能从任意 shell command 自动证明 argv 同源,随包 Skill 负责按同一 DSH executable/launcher argv 生成 `--dump-config` probe
|
|
11
|
+
- preflight snapshot 复制物理节点并把 pnpm/Cordis 链接图重建到副本内,保留合法依赖循环;外部 target 在哈希命名区域按 canonical path 去重物化,链接逐条审计确保可写解析结果不逃出 snapshot。经内部/外部链接写入不会触达原字节,悬空/不可解析链接、特殊文件、可写逃逸或复制失败均在运行 candidate、创建 cutover、停止 previous 前 fail closed
|
|
12
|
+
- cutover 回执把 target 的 readiness/canary 保留在 `targetValidation`,previous 恢复验证单列在 `recovery.validation`;只有 target-scoped credential 时明确记录 recovery canary skipped 并继续要求稳定 ownership,不再把 target canary fail 与 restored/previous readiness 混成一个字段
|
|
13
|
+
- `reconfigure --transition-file` 新增版本无关的可逆状态隔离:在 copy-on-write 优先的 live-home 副本上完成 target preflight,previous 停止后才按哈希绑定计划原子 quarantine;target 失败时先保留其替代内容、恢复 previous 原字节再启动旧宿主,任何无法证明的回滚都会停机等待用户
|
|
14
|
+
- 重启凭证改为执行证据:CLI `record` 默认拒绝自我声明,`--run -- PROGRAM ...` 以原样 argv 执行并只在 exit 0、HEAD 不变且工作树全净时记录;任何 staged/unstaged/untracked 输入都会让 verify/restart/canary 拒绝
|
|
15
|
+
- checkpoint 在干净树上直接记录现有 HEAD,不再造空提交;脏树默认拒绝,复核后显式 `--include-dirty` 才用临时 index 提交完整快照,hook/commit 失败不污染调用者 staging area
|
|
16
|
+
- `schedule-exit` 无存活 watchdog 时从警告升级为硬拒绝;稳定态从耐久 active launch spec 取 repo/harness/profile/port,并拒绝冲突参数或过期 supervisor command,防止复活已拒绝的启动配置
|
|
17
|
+
- detached `supervise` 等到 watchdog 已持久化 pidfile claim 才返回成功,关闭“刚 supervise 就 schedule-exit”竞态;输出区分 `exit-agent pid`,watchdog 生命周期日志带时间戳
|
|
18
|
+
- 新增通用启动配置事务切换:`configure-launch`/`reconfigure` 持久化并原子选择 previous/target 完整 launch spec,分别记录 credential/rollback repo 与宿主 `harnessRoot`;资料不足时拒绝从旧 command record 或目标 repo 伪造 previous
|
|
19
|
+
- launchd/systemd 前台等待者在 successor 退出后重新读取耐久 launch state 与回执;target 失败恢复 previous 后,外层 supervisor 不会复活等待前缓存的 target
|
|
20
|
+
- watchdog 就绪探针区分 transport-up 与 ready:裸 401 不再健康;最终进程输出的同 authority 启动 URL 必须完成临时 Cookie jar 的 303 交换和认证后 `/` 200;通过 ownership 稳定窗口和 canary 后才开始浏览器交接。客户端改用 Cordis Context + 空 inject 兼容 rc.2/Alpha.4,并以持有式长轮询登记和恢复所有仍响应的标签页;现有 Cookie 刷新或 `location.replace()` 一次性 URL 交换后恢复不带 query/fragment 的原 pathname,仅在原页缺失/超时时 system-open 兜底。一个真实页面 ACK 解除终态门禁,其他慢标签页在状态压缩后仍可恢复;opener 成功不等同接管成功,Bearer URL 与原始 capability 不进入耐久状态、日志或回执
|
|
21
|
+
- 新增脱敏耐久回执 `launch-cutover.json`,记录 supervisor/child PID、配置摘要、认证、重试、canary 与恢复结果;会话仅在回执终态后唤醒
|
|
22
|
+
- 修复嵌套 watchdog 接管的进程所有权漏洞:`reconfigure` 在旧宿主仍存活时固化 supervisor、直接 child 与 listener 的 PID/start identity;successor 对旧 supervisor 做可消费控制请求的有界等待,超时只终止先冻结再复核的精确 identity,并冻结/复核后代亲缘后回收,拒绝按端口误杀或把旧 listener 的 HTTP 200 当成 target ready
|
|
23
|
+
- cutover 就绪现在同时要求目标 child 存活、唯一 listener 属于该 child 树、child/listener identity 在稳定窗口内不变且 retry 为 0;目标在 provisional ready 后退出、`EADDRINUSE` 或旧 listener 持续 200 都会计入失败并执行完整 spec 恢复策略
|
|
24
|
+
- 进程发现优先使用 `/usr/sbin/lsof`、`/usr/bin/lsof` 等绝对路径,不依赖 dsh 工具环境的 PATH;Linux identity 使用 boot/start tick,macOS 优先使用 `proc_pidinfo` 微秒启动时间;新增耐久 `abort-cutover` 与显式 `restore-previous` 控制命令,独立原子 marker 保证并发时 restore 单调优先,回执记录 ownership、稳定性证明、分角色失败计数与控制结果
|
|
25
|
+
|
|
3
26
|
## 0.1.1(2026-08-23)
|
|
4
27
|
|
|
28
|
+
- 修复:schedule-exit 与 restart 的竞态——schedule-exit 现在全程持 restart.lock(读→写→拉起),不再误杀并发重启刚拉起的新实例
|
|
5
29
|
- 修复:随包 skill 在目录里可见、调用即炸——宿主在 load 时才校验注册的 `source` 字段,之前没传;已补 `source: 'runtime'`,并加了真实 SkillRegistry 往返测试(list + load)防回归
|
|
6
30
|
|
|
7
31
|
## 0.1.0(2026-08-22)
|
package/README.en.md
CHANGED
|
@@ -12,17 +12,22 @@ When the agent wants to restart after editing code, this plugin asks one questio
|
|
|
12
12
|
|
|
13
13
|
One rule at the core: **prove the code is good before you allow a restart.**
|
|
14
14
|
|
|
15
|
-
After
|
|
15
|
+
After `record --run -- PROGRAM ...` executes the build/test argv and observes exit 0, the plugin records a credential bound to the current git commit, valid for 10 minutes (`maxAgeMinutes`). On a restart request it checks four things:
|
|
16
16
|
|
|
17
17
|
1. does a credential exist;
|
|
18
18
|
2. is it younger than `maxAgeMinutes`;
|
|
19
|
-
3. does the current HEAD match the commit the credential was recorded on
|
|
19
|
+
3. does the current HEAD match the commit the credential was recorded on;
|
|
20
|
+
4. is the checkout free of staged, unstaged, and untracked inputs — any one invalidates it.
|
|
21
|
+
|
|
22
|
+
That ten-minute window constrains build evidence which has not yet survived a real boot; it does not make byte-identical deployed software become untested every ten minutes. Only after a watchdog restart passes ownership/readiness, composition preflight, and the post-restart canary does the guard promote the credential into a durable `provenDeployment`. A later `schedule-exit` may reuse it after credential expiry, but only after recomputing and matching the credential-repo and harness clean HEADs, complete launch spec, profile configuration, directly installed packages and `file:` archives, host installation metadata, and the explicitly bound source/built preflight runner and anchor. Any byte, link target, launch input, or repository-state drift fails closed and requires build + test again. A legacy `last-good-boot.json` is never promoted implicitly; enabling this path requires one full gate and successful canary under the new protocol.
|
|
20
23
|
|
|
21
24
|
That one rule catches a whole class of incidents: broken builds, missed config registration, wrong imports — all of these fail the build/typecheck, so no credential exists and the restart is refused before it can hurt.
|
|
22
25
|
|
|
23
26
|
A green build still says nothing about the profile composition: bad patch YAML, a missing built file, a duplicate loader entry id, a typert manifest ownership mismatch, a plugin whose apply throws — all of these fail only at boot. So a second gate runs after the credential check, before anything is stopped: `preflight` deep-dry-runs the exact composition in a subprocess (full plugin tree booted through the same engine, then disposed), and a composition that does not boot means the running instance is never stopped. See [preflight: the composition gate](#preflight-the-composition-gate).
|
|
24
27
|
|
|
25
|
-
The restart itself is handed to a watchdog: a detached supervisor that brings the host back if it dies, rolls back to the last known-good revision (the healthy-boot stamp — the last revision that actually came up in this deployment — else the checkpoint, else the green credential's HEAD) if it can't come up
|
|
28
|
+
The restart itself is handed to a watchdog: a detached supervisor that brings the host back if it dies, rolls back to the last known-good revision (the healthy-boot stamp — the last revision that actually came up in this deployment — else the checkpoint, else the green credential's HEAD) if it can't come up, and stops at a crash page after four consecutive failures. A boot failure whose origin lives outside the checkout (a freshly installed plugin is the common case) cannot be fixed by reverting the repository — so the watchdog instead rolls back the **profile composition**: every healthy boot snapshots the profile's composition inputs (the bundles layer and the profile manifest), and an out-of-repo boot failure restores that snapshot (unmounting the newest plugin change; the failing inputs are kept under `composition-backup-*`) and reports the recovery, naming what was unmounted, through the restart-report channel. Every rollback leaves `guard-backup-*` recovery anchors for the discarded HEAD and any uncommitted work. `checkpoint` records the existing clean HEAD before a batch; a dirty tree is refused unless the complete path set is reviewed and explicitly approved with `--include-dirty`. `reset` hard-resets to that point (anchored the same way), and `canary` re-verifies after a restart. Checkpoints and credentials persist in a state file that survives restarts, so the canary runs after the new instance is up.
|
|
29
|
+
|
|
30
|
+
Readiness is application-aware. Any HTTP response, including a naked 401, proves only that the transport is up. A public root is ready at HTTP 200. A protected root is ready only after the watchdog extracts the final process's same-authority launch URL from that process's output and uses a temporary cookie jar to prove launch URL → 303 → authenticated `/` → 200. HTTP success alone is insufficient: the spawned direct child must still be alive, the port's sole listener must belong to that child's process tree, child/listener PIDs and start identities must remain unchanged through the stability window, and the current retry must be zero. Browser handoff is a separate evidence plane and runs only after that proof and the canary pass. Every responsive registered original tab waits before shutdown, reloads if its cookie remains valid, or receives the final process's one-time URL in memory and calls `location.replace()`; after the cookie exchange it returns to its credential-free same-origin pathname. One real authenticated page acknowledgement gates terminal readiness, while slower registered tabs remain recoverable through the exact final listener. Bearer URLs are absent from durable state and logs.
|
|
26
31
|
|
|
27
32
|
## Install and load
|
|
28
33
|
|
|
@@ -37,13 +42,13 @@ The package declares `dsh.bundle`, so the add reconciles its `cordis.patch.yml`
|
|
|
37
42
|
|
|
38
43
|
Config (all optional): `stateDir` (default `$DSH_HOME/state`, else `<cwd>/.dsh-guard-state`), `repoDir` (default the process cwd), `maxAgeMinutes` (credential freshness, default 10), `reportRestartContext` (`followup` autonomous report / `step` ride the next turn / `off`, default `followup`), `resumeInterrupted` (resume restart-interrupted sessions and queue a continue turn, default true), `resumeDelayMs` (default 5000), `resumeMaxSnapshotAgeMs` (default 600000).
|
|
39
44
|
|
|
40
|
-
Runtime needs: `node`, `bash`, `lsof` on macOS/Linux for listener discovery (`--pid` bypasses it), and `pgrep` for descendant reaping (
|
|
45
|
+
Runtime needs: `node`, `bash`, `lsof` on macOS/Linux for listener discovery (`--pid` bypasses it), and `pgrep` for descendant reaping (watchdog cleanup and `restart`'s forced-kill escalation walk the child tree instead of assuming a process group). The guard probes system absolute paths such as `/usr/sbin/lsof` and `/usr/bin/lsof` before falling back to PATH, so dsh's restricted tool PATH cannot silently disable listener checks. No build step for consumers — the published `lib/` is the runnable artifact.
|
|
41
46
|
|
|
42
47
|
## Prerequisites for a self-restart (for the agent driving it)
|
|
43
48
|
|
|
44
|
-
- **git is required.** The credential, checkpoints, and rollback are all git-based: the credential binds HEAD, a checkpoint
|
|
49
|
+
- **git is required.** The credential, checkpoints, and rollback are all git-based: the credential binds a clean HEAD, a checkpoint names a real commit (reusing clean HEAD or creating an explicitly approved dirty snapshot), and rollback is a reset. If the deployment directory is not a git repository, `git init` it and make an initial commit before `record` — otherwise the gate refuses with "current git HEAD unavailable". The `git init` is not ceremony: with a repository in place, the checkpoint/rollback recovery anchors actually work.
|
|
45
50
|
- **Full-access (unsandboxed) permissions.** The restart loop spawns detached processes, kills processes, and binds ports; sandboxed tool runners (workspace-write and the like) deny those operations with EPERM and the instance dies at the shell layer. The agent CANNOT switch its own sandbox — that is the point of the sandbox; `/permission` is a user-typed command, and per-command escalation prompts the user for approval. For a permanent deployment the easier official path is starting the instance with `DSH_PERMISSION_MODE=danger-full-access` (the base bundle's deployment-level switch — sandbox and approval policy both open), so every session starts unsandboxed. Otherwise, before initiating a self-restart, ask the user to switch THIS session: `/permission danger-full-access` — the settings page only affects NEW sessions, and an open persistent terminal (PTY) fences the switch until closed. (`verify` and `record` print this hint too.)
|
|
46
|
-
- **The first restart after install must be driven by the CLI.** The running instance has not loaded the plugin yet — composition changes need a boot — and no watchdog exists yet, so a bare exit leaves the service DOWN with nothing to bring it back. Right after the add, run `dsh-ankh-guard supervise --port N --start "CMD"` (it adopts the running instance and respawns ANY exit from then on), or drive the first restart with `dsh-ankh-guard restart --port N --start "CMD" --rollback` (it owns the whole stop→start→canary loop in a detached process), or install the launchd/systemd supervisor. The adoption takeover files a report record addressed to the session that ran `supervise` (via `$DSH_SESSION_ID`), so that first bounce reports back like a scheduled restart — the driving session does not park silently. `verify`/`record
|
|
51
|
+
- **The first restart after install must be driven by the CLI.** The running instance has not loaded the plugin yet — composition changes need a boot — and no watchdog exists yet, so a bare exit leaves the service DOWN with nothing to bring it back. Right after the add, run `dsh-ankh-guard supervise --port N --start "CMD"` (it adopts the running instance and respawns ANY exit from then on), or drive the first restart with `dsh-ankh-guard restart --port N --start "CMD" --rollback` (it owns the whole stop→start→canary loop in a detached process), or install the launchd/systemd supervisor. The adoption takeover files a report record addressed to the session that ran `supervise` (via `$DSH_SESSION_ID`), so that first bounce reports back like a scheduled restart — the driving session does not park silently. `verify`/`record` warn while no live watchdog exists; `schedule-exit` hard-refuses instead of scheduling a guaranteed outage.
|
|
47
52
|
|
|
48
53
|
## Known install pitfalls
|
|
49
54
|
|
|
@@ -51,23 +56,26 @@ Runtime needs: `node`, `bash`, `lsof` on macOS/Linux for listener discovery (`--
|
|
|
51
56
|
- **pnpm blocks dependency build scripts by default.** If the add fails on a build-script interception, allow the toolchain entries via `allowBuilds` and retry.
|
|
52
57
|
- **A root-owned npm cache** (one `sudo npm …` in the past) fails the prepare build with EPERM: `sudo chown -R $(id -u):$(id -g) ~/.npm`.
|
|
53
58
|
- **`--start` does not run from your cwd.** The watchdog `cd`s into the dsh home (else `/tmp`) before launching, so the start command must be self-contained — absolute paths, or an explicit `cd` inside it.
|
|
54
|
-
- **Put `--no-open` in the start command.**
|
|
59
|
+
- **Put `--no-open` in the start command.** It prevents the host itself from opening a tab on every respawn. During a protected launch cutover, `reconfigure` first asks an existing tab to wait and recover in place. If that tab cannot acknowledge within the handoff timeout, the watchdog opens the final process URL in a fallback tab; a successful `open` call is only an attempted fallback, not proof that a browser took over. A candidate process's URL is never reused. The preflight dry-run never opens a browser.
|
|
60
|
+
- **The first rollout of original-tab handoff necessarily falls back.** A page loaded by the old plugin has no handoff client. To validate in-place recovery, first deploy this ankh-guard version to the previous host, refresh the original tab so the new client is active, and only then run the host cutover. Falling back to a new tab during the bootstrap deployment is expected.
|
|
55
61
|
- **Supervision adopted from a sandboxed session stays sandboxed.** A watchdog spawned from inside a workspace-write sandbox passes that profile to every respawned instance (nested sandbox-exec then fails, and every command degrades to approvals). For a permanent deployment, use the layered shape (the launchd/systemd installer) so the watchdog chain starts outside any sandbox.
|
|
56
62
|
|
|
57
63
|
## CLI
|
|
58
64
|
|
|
59
|
-
The primary interface is the CLI, usable even when the instance is down. Use the `dsh-ankh-guard` bin (or `node lib/cli.js`).
|
|
65
|
+
The primary interface is the CLI, usable even when the instance is down. Use the `dsh-ankh-guard` bin (or `node lib/cli.js`). `--repo` always names the credential/rollback repository; `--harness-root` names the host root used by preflight and the child, and the two can — and usually do — differ. State commands take `--state-dir "$DSH_HOME/state"`.
|
|
60
66
|
|
|
61
67
|
```sh
|
|
62
68
|
dsh-ankh-guard verify # is it safe to restart right now
|
|
63
|
-
dsh-ankh-guard record build+test
|
|
69
|
+
dsh-ankh-guard record build+test --run -- sh -c 'pnpm run build && pnpm run test'
|
|
64
70
|
dsh-ankh-guard checkpoint --message "what changed" # checkpoint before editing
|
|
65
71
|
dsh-ankh-guard preflight # deep dry-run: does the profile composition boot
|
|
66
72
|
dsh-ankh-guard canary --port 3080 # confirm after restart
|
|
67
73
|
dsh-ankh-guard supervise --port 3080 --start "CMD" # hand the port to a watchdog
|
|
74
|
+
dsh-ankh-guard reconfigure --start "NEW CMD" --repo "<credential repo>" \
|
|
75
|
+
--harness-root "<host root>" --on-failure restore-previous
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
Full commands: `verify`, `record`, `status`, `clear`, `checkpoint`, `reset`, `canary`, `preflight`, `restart`, `schedule-exit`, `supervise`.
|
|
78
|
+
Full commands: `verify`, `record`, `status`, `clear`, `checkpoint`, `reset`, `canary`, `preflight`, `restart`, `schedule-exit`, `configure-launch`, `launch-status`, `reconfigure`, `abort-cutover`, `restore-previous`, `supervise`.
|
|
71
79
|
|
|
72
80
|
### preflight: the composition gate
|
|
73
81
|
|
|
@@ -77,17 +85,17 @@ Full commands: `verify`, `record`, `status`, `clear`, `checkpoint`, `reset`, `ca
|
|
|
77
85
|
- `1` — a composition verdict: the tree a restart would boot is broken; the output names the failing layer.
|
|
78
86
|
- `3` — preflight itself could not execute (missing app layout, infrastructure crash) — **not** a verdict on the composition.
|
|
79
87
|
|
|
80
|
-
`schedule-exit` and `
|
|
88
|
+
`schedule-exit`, `restart`, and `reconfigure` run this gate after the credential check, before anything is stopped. A composition failure refuses with the preflight's diagnostics; an infrastructure failure also refuses — worded differently and with the manual override (stop the instance by hand, let the watchdog respawn it) — because the guard will not stop a healthy instance it cannot prove will come back. The CLI emits `composition preflight START` before waiting, then emits PASS or refusal only after the runner settles. The gate locates the dsh app to dry-run via `--harness-root`, then the durable selected launch spec, `DSH_HARNESS`, and finally the conventional `~/code/deepseek-harness` checkout; the credential `--repo` never participates in host resolution. Where none resolves (a pure npm deployment with no harness checkout), there is no engine to boot the profile with, so the gate warns once and proceeds. Flags: `--profile NAME` (default `$DSH_PROFILE`, else `web`) and `--preflight-timeout-ms MS` (default 120000); `DSH_PREFLIGHT_COMMAND` replaces the resolved app bin wholesale (test hook). A managed shell/tool invoking the command needs a separate, longer wait budget: at least 180000 ms for the default preflight, or at least 30000 ms beyond a custom `--preflight-timeout-ms`. A caller timeout is not a guard refusal, and without `exit scheduled` no restart was authorized; inspect durable markers/receipts before retrying. Run it by hand any time with `dsh-ankh-guard preflight --profile web --harness-root "$DSH_HARNESS"`.
|
|
81
89
|
|
|
82
90
|
### The self-restart protocol
|
|
83
91
|
|
|
84
|
-
Six steps for a safe restart after editing code:
|
|
92
|
+
Six steps for a safe restart after editing code. A pure restart with no file, dependency, profile, installed-artifact, or launch-spec change skips step 1. When the new guard has already written a same-fingerprint `provenDeployment` after a successful canary, `schedule-exit` can also reuse steps 3–4. Without that proof, with legacy state, or after any fingerprint drift, the complete sequence remains mandatory:
|
|
85
93
|
|
|
86
|
-
1. **checkpoint** —
|
|
94
|
+
1. **checkpoint** — record clean HEAD; a dirty tree is refused until the complete snapshot is reviewed and approved with `--include-dirty`: `dsh-ankh-guard checkpoint --message "<batch>"`
|
|
87
95
|
2. **modify** — make the change; register every surface it needs (aggregates, paths, bundle rows, dependencies).
|
|
88
96
|
3. **build + test** — the narrow full set for the changed surface; no green, no credential.
|
|
89
|
-
4. **record** — `dsh-ankh-guard record build+test --
|
|
90
|
-
5. **verify** — `dsh-ankh-guard verify` must exit 0; a denial (missing
|
|
97
|
+
4. **record** — make the guard execute the evidence: `dsh-ankh-guard record build+test --run -- sh -c 'pnpm run build && pnpm run test'`
|
|
98
|
+
5. **verify** — `dsh-ankh-guard verify` must exit 0. It prefers a fresh credential and accepts a proven deployment only with stable durable launch state and an exact current fingerprint; a denial (missing proof, fingerprint drift, HEAD mismatch, or dirty checkout) means clean up, rebuild, and re-record.
|
|
91
99
|
6. **restart + canary** — after the new instance is up, `dsh-ankh-guard canary --port N` confirms it.
|
|
92
100
|
|
|
93
101
|
### supervise: seamless restart
|
|
@@ -95,18 +103,47 @@ Six steps for a safe restart after editing code:
|
|
|
95
103
|
`restart` runs the whole kill → start → probe → canary loop in one CLI process (use `--delay-ms` so the scheduling turn finishes first). For deployments where nobody should touch a terminal, `supervise` hands the job to a **watchdog** — a detached supervisor process that survives the instance:
|
|
96
104
|
|
|
97
105
|
```sh
|
|
98
|
-
dsh-ankh-guard supervise --port 3080 --start "CMD" --state-dir "$DSH_HOME/state"
|
|
106
|
+
dsh-ankh-guard supervise --port 3080 --start "CMD" --state-dir "$DSH_HOME/state" \
|
|
107
|
+
--repo "<credential repo>" --harness-root "<host root>"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`supervise` also needs the dsh home the supervised instance boots with (the watchdog exports it as the instance's `DSH_HOME`): `--home DIR` wins, else `$DSH_HOME`; with neither set it refuses loudly — a home guessed from `--state-dir` would silently boot the instance on the wrong profiles/credentials. First-time persistence also requires an explicit `--harness-root` or `DSH_HARNESS`; it never guesses the host root from the credential repo.
|
|
111
|
+
|
|
112
|
+
It spawns `scripts/dsh-watchdog.sh` (ships with the package) detached with `--wait-owner`: the watchdog idles while the current instance runs, takes over the port when the instance exits (intentional restart or crash), respawns it, runs the guard canary on intentional restarts (a `restart-requested.json` marker), and clears the marker on pass. Two consecutive boot failures roll the checkout back to the last known-good revision — the healthy-boot stamp (`last-good-boot.json`, written every time the instance comes up, so it names the last revision that genuinely ran in this deployment), else the guard checkpoint, else the credential's HEAD — but only when the boot failure's error subject is a path inside the repository. When the subject lives outside the checkout (a broken profile overlay or an installed plugin), a checkout reset cannot help, so the watchdog instead restores the last healthy **profile composition**: the snapshot of the profile's composition inputs (`last-good-composition/`, taken at every healthy boot) replaces the live bundles layer and manifest, unmounting the newest plugin change, with the failing inputs preserved under `composition-backup-*` and the recovered report naming exactly what was unmounted. The same exemption logic covers a start command that does not bind the supervised port: when the boot window times out while the instance is listening elsewhere — or fails with `EADDRINUSE` naming a port this watchdog does not own — the watchdog names the bound port and skips both rollbacks, because resetting files cannot change a command-line argument. `EADDRINUSE` on the supervised port keeps its free-and-retry escape hatch, now bounded at five attempts. Every reset (watchdog, CLI, or service) first creates `guard-backup-*` branch anchors for the discarded HEAD and for uncommitted tracked changes, so recovery never depends on the reflog. Four failures serve a crash page on the port with a retry button (SIGUSR1 to the watchdog). A `watchdog-stop` marker exits the watchdog for good. The instance itself can adopt supervision before a self-restart — the user never starts the watchdog by hand.
|
|
113
|
+
|
|
114
|
+
When a watchdog is already supervising, the restart trigger is `schedule-exit`: it gets the port, credential repo, host root, and profile from the durable active launch spec, rejects conflicting explicit flags, and verifies the supervisor's complete command before writing the restart marker and spawning a detached process explicitly labelled `exit-agent pid`. It prefers a credential within the ten-minute freshness window. Once that expires, only an exact `provenDeployment` may take the pure-restart fast path; the selected evidence SHA is copied into the short-lived restart marker and the new watchdog rechecks that same SHA and the live fingerprint during canary, closing the check-to-stop evidence replacement window. A successful canary then promotes or retains the proof. For an agent Bash/tool call, set that call's `timeoutMs` to 180000; this is tool metadata, not a CLI flag, and keeps the caller alive longer than the 120-second preflight gate. The managed shell cannot reap the exit agent, so the scheduled kill lands after the scheduling turn ends. The watchdog respawns, runs the canary, and the new instance reports via `last-restart.json`; watchdog lifecycle lines are timestamped. With no live watchdog, `schedule-exit` hard-refuses: establish supervision first or use `restart`, which owns the complete single-shot loop. The latter lacks durable supervisor/launch ownership and therefore still requires a fresh credential.
|
|
115
|
+
|
|
116
|
+
### reconfigure: transactional launch changes
|
|
117
|
+
|
|
118
|
+
`schedule-exit` is the same-launch fast path. If the command, dsh home, credential/rollback repository, host root, or profile changes, use `reconfigure`; changing the online port is deliberately refused because it needs a separately supervised traffic cutover.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
dsh-ankh-guard reconfigure \
|
|
122
|
+
--start "<complete target command>" \
|
|
123
|
+
--repo "<target credential/rollback repo>" \
|
|
124
|
+
--harness-root "<target host root>" \
|
|
125
|
+
--preflight-surface built \
|
|
126
|
+
--preflight-install-anchor "<actual npm toolchain>/node_modules/@deepseek-ai/dsh/package.json" \
|
|
127
|
+
--candidate-probe-command "<one-shot validation using the same target argv>" \
|
|
128
|
+
--transition-file "<optional state-transition plan.json>" \
|
|
129
|
+
--on-failure restore-previous \
|
|
130
|
+
--browser-handoff required \
|
|
131
|
+
--state-dir "$DSH_HOME/state"
|
|
99
132
|
```
|
|
100
133
|
|
|
101
|
-
`
|
|
134
|
+
The recovery choice is mandatory and therefore approved before the old host stops: `restore-previous` restores the entire previous launch specification, while `wait-for-user` parks for intervention without resetting any repository. The full previous/target pair separately persists command, home, credential repo, harness root, profile, and port. Each new spec also binds the composition preflight's `source|built` surface, runner executable/path/content SHA, the actual `@deepseek-ai/dsh/package.json` install anchor, and the target command SHA. A built successor resolves modules from its npm toolchain; the guard never chooses checkout source merely because its own process happens to use tsx. `reconfigure` additionally requires the caller to supply a one-shot `--candidate-probe-command` committed with the target command SHA. It runs that probe against an isolated home before composition preflight runs on the bound execution surface. The guard binds and executes both command digests; it cannot prove that an arbitrary successful shell command was automatically derived from the target argv. The bundled Skill constructs a DSH `--dump-config` probe from the same executable and launcher argv; integrations outside that caller trust boundary must establish the same semantic provenance themselves. Either failure occurs before previous stops. The mode-0600 `launch-spec.json` contains the commands and selected side; the receipt contains only bindings, hashes, and PASS outcomes, never probe/start commands or bearer URLs.
|
|
135
|
+
|
|
136
|
+
The atomic selected-side rename is the configuration commit point. If no complete durable previous spec exists, initialize it first with the real current values and explicit preflight surface/install anchor through `configure-launch`: legacy `instance-launch.json` cannot supply the missing roles, and a target `--repo` is never backfilled into previous. A replacement watchdog then atomically claims `watchdog.pid` while the old host is still serving. During preparation the guard captures PID/start identities for the old supervisor, its direct child, and its listener. The successor waits for the exact supervisor identity to yield with a 15-second default bound (configurable through `--supervisor-yield-timeout-ms`) and consumes abort/restore throughout that wait. On timeout it may retire only the frozen, revalidated old supervisor tree. It then stops only the proven child/listener identities and confirms port release; it never chooses or kills an arbitrary process found by port. PID reuse, a hung watchdog, a short-lived `reconfigure` caller, an outer launchd/systemd waiter, and nested shells therefore cannot blur ownership or suspend a cutover forever.
|
|
102
137
|
|
|
103
|
-
|
|
138
|
+
For a protected target, the watchdog accepts a launch URL only from the final process's output, only for the exact supervised loopback authority, and without depending on a parameter name. It proves 303 cookie exchange and authenticated root 200 with a temporary jar, then spends a three-second default stability window proving that the child is alive, its sole listener remains in that child tree, PID/start identities do not change, and retry is zero. The browser half uses held long polls on a same-origin plugin route rather than a permanent 500 ms loop. The proven previous listener stores only hashes of per-tab capabilities and puts every responsive registered tab into a waiting state. Only after ownership-stable service readiness and canary success does the final listener tell each tab to reload when its cookie is still accepted, or return that final process's same-origin one-time URL for an in-memory `location.replace()` after 401. The authenticated page acknowledges and returns to the original safe pathname with query and fragment discarded. One real ACK gates terminal ready; other registered, unacknowledged tabs remain eligible after state compaction. If no original tab registered or none acknowledges in time, the watchdog requests one system-open fallback and still waits for its authenticated page acknowledgement; opener exit 0 alone never completes handoff. Server readiness/canary and browser handoff are recorded separately, and all required evidence must complete before session wake-up. A naked 401, an old listener's 200, a target's transient 200, or a subsequent target exit can never become ready or hand a rejected target URL to the browser. No raw browser capability or bearer URL enters a state file, durable log, or receipt. `launch-cutover.json` records redacted configuration summaries, process identities, authentication, handoff, stability, and per-role failures. Target readiness/canary remains under `targetValidation`; previous recovery readiness and a passing, failing, or explicitly skipped recovery canary (when only a target-scoped credential exists) live under `recovery.validation`. A restored receipt can no longer carry an unqualified target canary failure beside previous readiness. `launch-status` prints the receipt without exposing either command. During a transaction, `abort-cutover --state-dir "$DSH_HOME/state"` applies the pre-approved recovery policy, while `restore-previous --state-dir "$DSH_HOME/state"` explicitly authorizes stopping the proven target and restoring the complete previous spec. The two actions use separate atomic markers and restore always wins on read, so even concurrent sessions cannot let a later ordinary abort downgrade an explicit restore.
|
|
104
139
|
|
|
105
|
-
When a
|
|
140
|
+
When a candidate cannot read a reconstructible projection or cache left by the previous host, `--transition-file` can submit a reviewed schema-v1 quarantine plan. A plan accepts only non-overlapping, symlink-free paths below `home` that exclude guard state, with an explicit `quarantine` operation; it contains no host-version or filename knowledge. Example: `{"schemaVersion":1,"home":"/absolute/dsh-home","operations":[{"kind":"quarantine","path":"storages/<reconstructible-cache>","expect":"present"}]}`. Each `expect` is `present` or `absent`; isolated preflight and live apply must observe that same state or refuse before previous stops or target starts. The plan must cover both old paths that need to leave before target starts and new output paths that must leave before previous can recover after a target failure; list the latter explicitly with `expect: "absent"` even when they do not exist at preparation. Do not use this mechanism for authoritative logs, credentials, or irreplaceable data. Formats that require content transformation need a separate reversible migration tool and review.
|
|
141
|
+
|
|
142
|
+
The guard first copies the live home's physical files with copy-on-write preference, then rebuilds pnpm/Cordis links as relative links whose targets are snapshot-owned copies; legitimate internal dependency cycles remain intact. External targets enter a hash-named, deduplicated materialization area inside the snapshot. A linked `node_modules` target brings its ancestor resolution layer once so Node's package lookup semantics survive. A post-copy `realpath` audit requires every writable link target to remain under the snapshot root. Runtime entries without copyable semantics (sockets, FIFOs, and links to them) are skipped and counted, and the top-level `scratch/` tree never joins the copy; dangling or unresolvable links, any other special files, writable escapes, and read/copy failures make `reconfigure` fail closed before it runs the candidate, creates a cutover, or stops previous. It then applies the same quarantine and runs target composition preflight in that safe copy. If the copy cannot be prepared or the target does not boot, previous keeps serving and the live home stays unchanged. Only after the successor owns supervision and has stopped and revalidated the previous process tree does it apply the hash-bound durable plan with same-filesystem renames. If the target is rejected, the watchdog first stops its proven process, retains replacements it created at transitioned paths under `launch-transitions/<cutover>/rejected-target/`, restores the exact previous bytes and records the result, and only then permits previous to start. Any unproven step parks at `awaiting-user` instead of exposing previous to mixed state. After target success, the quarantined previous content remains in the cutover directory for operator disposition; it is never deleted automatically.
|
|
106
143
|
|
|
107
144
|
**The restart report reaches the model by itself — and waits for its owner.** After a scheduled restart (a pending `last-restart.json` record), the plugin queues the report as the next turn via `agent.followup` — the official wake-the-agent seam the schedule system uses for reminders — so the agent reports the restart result without any user message. Session restore after a restart is lazy (an agent is created only when the UI or an RPC touches the session), so the report goes ONLY to the session that scheduled the exit (`schedule-exit` records `$DSH_SESSION_ID` as the initiator), whenever it resumes — no other session is ever woken for reporting, and the record stays pending until its owner resumes or the next restart replaces it (new `exitAt`). A record without an initiator is claimed by the first root agent created. Only root agents, once (acknowledged on delivery). Config `reportRestartContext`: `followup` (default, autonomous), `step` (ride the first step of whatever turn comes next), or `off`.
|
|
108
145
|
|
|
109
|
-
**Interrupted sessions resume and continue by themselves.** At SIGTERM the plugin snapshots which root sessions had a live turn (plus the restart's initiator) into `interrupted-sessions.json`; on the next restart boot — a cold start drops the snapshot — it resumes those sessions via `ctx.agents.resume` and queues a "continue" followup for the interrupted ones (their logs were closed with `reason.kind === 'interrupted'` by crash-recovery repair), so a self-restart no longer silently pauses every other session. Config `resumeInterrupted` (default true) and `resumeDelayMs` (default 5000, lets the app's services come up first).
|
|
146
|
+
**Interrupted sessions resume and continue by themselves.** At SIGTERM the plugin snapshots which root sessions had a live turn (plus the restart's initiator) into `interrupted-sessions.json`; on the next restart boot — a cold start drops the snapshot — it resumes those sessions via `ctx.agents.resume` and queues a "continue" followup for the interrupted ones (their logs were closed with `reason.kind === 'interrupted'` by crash-recovery repair), so a self-restart no longer silently pauses every other session. One boundary: a turn **parked on user input** (an open `ask_user_question` call or an undecided approval, read from the repaired log tail) is not interrupted work — the card persists and the user answers whenever — so such sessions are neither resumed nor continued. Config `resumeInterrupted` (default true) and `resumeDelayMs` (default 5000, lets the app's services come up first).
|
|
110
147
|
|
|
111
148
|
### supervise: one port, one owner
|
|
112
149
|
|
|
@@ -117,12 +154,16 @@ A port must have exactly one supervision owner, but the owner itself should be s
|
|
|
117
154
|
- **C — layered (recommended)**: launchd supervises the watchdog, the watchdog supervises the instance. One owner per port, and the owner is supervised. macOS: `scripts/install-launchd.sh --start "CMD"` generates a `com.dsh.watchdog.plist` (whose `ProgramArguments` run the CLI in the foreground) into `~/Library/LaunchAgents` and bootstraps it; `--force` replaces a running detached watchdog; `--uninstall` removes the job. systemd: `scripts/install-systemd.sh --start "CMD"` generates and enables the user unit `~/.config/systemd/user/dsh-watchdog.service` — `Restart=on-failure` is the counterpart of launchd's `SuccessfulExit: false`, `StartLimitIntervalSec=0` disables the start rate limit (the default puts a repeatedly restarting unit into `failed` and stops trying, which ends supervision silently), `--print` writes the unit to stdout without touching systemctl, and `--force`/`--uninstall` match the launchd installer. A user unit stops when the session ends; surviving logout needs an administrator to run `loginctl enable-linger <user>`. Both platforms run the same command:
|
|
118
155
|
|
|
119
156
|
```sh
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
# The installers initialize this once, then every KeepAlive start follows the
|
|
158
|
+
# selected durable launch specification:
|
|
159
|
+
dsh-ankh-guard configure-launch --if-absent --port 3093 --start "<start command>" \
|
|
160
|
+
--home "$DSH_HOME" --state-dir "$DSH_HOME/state" \
|
|
161
|
+
--repo "<credential repo>" --harness-root "<host root>" \
|
|
162
|
+
--preflight-surface built --preflight-install-anchor "<dsh package.json>" &&
|
|
163
|
+
exec dsh-ankh-guard supervise --foreground --state-dir "$DSH_HOME/state"
|
|
123
164
|
```
|
|
124
165
|
|
|
125
|
-
`--foreground` runs the watchdog inline (adopting the port) and exits with it, so a dead watchdog triggers the external supervisor's restart. On TERM/INT or any exit the watchdog reaps what it spawned — the instance child and the give-up crash page — and removes its own pidfile, then exits non-zero; under the installed plist's `KeepAlive SuccessfulExit: false` a killed watchdog restarts the whole chain, while a deliberate `watchdog-stop` (exit 0) stays down. If a live detached watchdog already holds the pidfile, `--foreground` waits for it to exit and then takes over
|
|
166
|
+
`--foreground` runs the watchdog inline (adopting the port) and exits with it, so a dead watchdog triggers the external supervisor's restart. On TERM/INT or any exit the watchdog reaps what it spawned — the instance child and the give-up crash page — and removes its own pidfile, then exits non-zero; under the installed plist's `KeepAlive SuccessfulExit: false` a killed watchdog restarts the whole chain, while a deliberate `watchdog-stop` (exit 0) stays down. If a live detached watchdog already holds the pidfile, `--foreground` waits for it to exit and then takes over. If the cutover failed and restored previous during that wait, it rereads `launch-spec.json` and the receipt before spawning, so it cannot revive the pre-wait target snapshot. Exiting 0 instead would read as an intentional stop, idle the launchd job, and silently leave the other watchdog unsupervised. The detached form (`supervise` without `--foreground`) is a debug / one-shot tool — the instance adopting supervision ahead of a self-restart, or a quick manual session — not a production supervision shape, because nothing supervises the detached watchdog itself.
|
|
126
167
|
|
|
127
168
|
The checkpoint/rollback round trip:
|
|
128
169
|
|
|
@@ -143,6 +184,8 @@ dsh-ankh-guard restart \
|
|
|
143
184
|
|
|
144
185
|
Mounted as a cordis plugin (base bundle), the same surface is available as the `selfRestartGuard` service for in-app gates. Config: `maxAgeMinutes` (default 10), `stateDir`, `repoDir`, `reportRestartContext` (default `followup`), `fallbackGraceMs` (default 300000).
|
|
145
186
|
|
|
187
|
+
Beyond the verify/record/canary gates, the service exposes `requestRestart({ start, profile, initiator })` — the in-process restart trigger for UI-grade callers (e.g. the mode switcher): `initiator` is required (the requesting session's real id) and the port comes from the launch record. With no live watchdog it drives `restart`; supervised instances drive the `reconfigure` transactional cutover (the only safe way to change the launch command under a watchdog). The credential/preflight/marker/lock gate chain is the CLI's own, refusals return a structured `{ accepted, stage, reason }` and never stop the instance. On the browser side, beside the cutover receipt channel there is a boot-generation channel: after an ordinary restart or a crash respawn, open tabs notice the process's boot id changed over the handoff long-poll and reload once to pick up the new bundle; a sustained disconnection (~5 s) raises a neutral overlay that never promises auto-recovery, and transient blips stay silent.
|
|
188
|
+
|
|
146
189
|
## Model Experience
|
|
147
190
|
|
|
148
191
|
A shipped skill, plus two followup messages, no tool schema. The `dsh-self-restart-guard` skill is registered at apply: the full restart protocol rides the skill catalog, so an agent discovers it exactly when a task involves restarting the instance — no per-session push notice. Every boot records the registration outcome (`skill-registration.json`), surfaced as the `skill:` line in `check-env`, and a composition without the skill capability now warns in the boot log — a migration or repackaging that drops the skill shows up there instead of vanishing silently. After a restart, the restart report / interrupted-session continuation reaches only the initiating session and the sessions the restart interrupted, as plugin-sourced followup user messages; every other session stays untouched.
|
|
@@ -153,8 +196,11 @@ None.
|
|
|
153
196
|
|
|
154
197
|
## Compatibility
|
|
155
198
|
|
|
156
|
-
- npm release line (`@deepseek-ai/dsh@0.1.
|
|
157
|
-
-
|
|
199
|
+
- npm release line (`@deepseek-ai/dsh@0.1.2-rc.1`): ⚠️ degraded — everything works; the composition-preflight gate runs through the standalone `preflight-runner` (composing through the published `@deepseek-ai/dsh-app-boot` primitives with a drift tripwire, since 0.1.2-rc.1 still does not export `composeProfile`) wherever a dsh app layout resolves — `--harness-root`, the durable launch spec, `DSH_HARNESS`, or the default checkout. On a pure npm deployment with no harness checkout the gate reports a notice and proceeds instead. The original-tab bridge feature-probes the optional WebServer/connection authentication seams; hosts without token auth naturally take the existing-cookie path. Every other capability is intact on the npm line. minHost moves up to 0.1.2-rc.1 — older hosts stay on the previous release line.
|
|
200
|
+
- Historical verification: a live npm-host 0.1.1-rc.2 → 0.1.2-alpha.4 isolated cutover passed (transition preflight removed a v3 whole-unit projection cache with an old-schema record from a home copy, live apply quarantined the old file, and target completed Token URL → 303 → cookie 200, the ownership stability window, and canary at zero retries; the old file remained byte-exact in the cutover directory. An untransitioned control over the same home failed on the missing Alpha.4 record fields, demonstrating that acceptance covered the real schema break).
|
|
201
|
+
- source line (deepseek-harness master, fork or upstream): ✅ (verifiedHost: 0.1.2-rc.1) — the gate runs through the standalone `preflight-runner` (resolves the published `@deepseek-ai/dsh-app-boot` etc. from the live checkout), so no fork patch is required.
|
|
202
|
+
|
|
203
|
+
**Version line mapping**: 0.2.0 and up support host `0.1.2-rc.1` and later; hosts on `0.1.0-rc.6` ~ `0.1.1-rc.2` stay on the 0.1.x release line (last release `0.1.1`).
|
|
158
204
|
|
|
159
205
|
## Known Limitations and Deferred Work
|
|
160
206
|
|
|
@@ -164,9 +210,9 @@ None.
|
|
|
164
210
|
- **A SIGKILL crash writes no interrupted-session snapshot** — interrupted-session auto-continue covers graceful stops (SIGTERM: scheduled exits, watchdog takeovers); crash-interrupted sessions still resume lazily on open.
|
|
165
211
|
- **The watchdog needs a supervisor to outlive the instance** — `supervise` spawns it detached (setsid); a watchdog spawned from inside a process that is about to die must be orphaned first, so the app adopts supervision *before* exiting.
|
|
166
212
|
- **The guard watches the checkout, not who else works on it** — concurrent self-modifying sessions share the tree; rollbacks are anchored and recoverable, but nothing serializes the sessions themselves.
|
|
167
|
-
- **
|
|
168
|
-
- **`restart`/`supervise` discover the listener via `lsof`** (macOS/Linux with lsof); other platforms need `--pid`.
|
|
169
|
-
- **Kills are per-pid with a descendant sweep, never per process group** — the instance is not setsid'd, so `restart`, `schedule-exit`'s exit agent, and
|
|
213
|
+
- **Dirty-tree checkpoints refuse by default** — `--include-dirty` commits the full staged/unstaged/untracked path set, so use it only after path-by-path review, explicit user approval, and where repository policy permits; pure restarts skip the checkpoint.
|
|
214
|
+
- **`restart`/`supervise` discover the listener via `lsof`** (macOS/Linux with lsof); the guard prefers absolute system paths, while other platforms need `--pid`.
|
|
215
|
+
- **Kills are per-pid identity with a descendant sweep, never per process group** — the instance is not setsid'd, so `restart`, `schedule-exit`'s exit agent, and watchdog cleanup target recorded child/listener identities. Cutover forced paths `SIGSTOP` first, then revalidate a Linux boot/start-tick token or macOS `proc_pidinfo` microsecond start time; a mismatch is only resumed and refused. Descendants are likewise frozen and their parentage rechecked while walking `pgrep -P`. Ordinary non-cutover port recovery retains a bounded listener-cleanup escape hatch; cutovers never select or kill a process by port.
|
|
170
216
|
|
|
171
217
|
## Changelog
|
|
172
218
|
|
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/ankh-guard/README.en.md
|
|
5
|
-
packages/ankh-guard/README.en.md:
|
|
6
|
-
packages/ankh-guard/README.md:
|
|
5
|
+
packages/ankh-guard/README.en.md: 941f3ceb4d5738244b46f4492eb8fc6c823fd967
|
|
6
|
+
packages/ankh-guard/README.md: ab62e07753b0e0e24ff833aae5aafd600e184e95
|