otacon 0.1.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/dist/cli/client.js +188 -0
  4. package/dist/cli/client.js.map +1 -0
  5. package/dist/cli/commands/answer.js +63 -0
  6. package/dist/cli/commands/answer.js.map +1 -0
  7. package/dist/cli/commands/ask.js +117 -0
  8. package/dist/cli/commands/ask.js.map +1 -0
  9. package/dist/cli/commands/clean.js +48 -0
  10. package/dist/cli/commands/clean.js.map +1 -0
  11. package/dist/cli/commands/doctor.js +86 -0
  12. package/dist/cli/commands/doctor.js.map +1 -0
  13. package/dist/cli/commands/expose.js +104 -0
  14. package/dist/cli/commands/expose.js.map +1 -0
  15. package/dist/cli/commands/implement-done.js +53 -0
  16. package/dist/cli/commands/implement-done.js.map +1 -0
  17. package/dist/cli/commands/install.js +113 -0
  18. package/dist/cli/commands/install.js.map +1 -0
  19. package/dist/cli/commands/open.js +37 -0
  20. package/dist/cli/commands/open.js.map +1 -0
  21. package/dist/cli/commands/progress.js +45 -0
  22. package/dist/cli/commands/progress.js.map +1 -0
  23. package/dist/cli/commands/start.js +66 -0
  24. package/dist/cli/commands/start.js.map +1 -0
  25. package/dist/cli/commands/status.js +44 -0
  26. package/dist/cli/commands/status.js.map +1 -0
  27. package/dist/cli/commands/submit.js +64 -0
  28. package/dist/cli/commands/submit.js.map +1 -0
  29. package/dist/cli/commands/wait.js +66 -0
  30. package/dist/cli/commands/wait.js.map +1 -0
  31. package/dist/cli/install/assets.js +285 -0
  32. package/dist/cli/install/assets.js.map +1 -0
  33. package/dist/cli/install/locations.js +92 -0
  34. package/dist/cli/install/locations.js.map +1 -0
  35. package/dist/cli/install/tailscale.js +39 -0
  36. package/dist/cli/install/tailscale.js.map +1 -0
  37. package/dist/cli/main.js +73 -0
  38. package/dist/cli/main.js.map +1 -0
  39. package/dist/cli/output.js +39 -0
  40. package/dist/cli/output.js.map +1 -0
  41. package/dist/cli/session.js +77 -0
  42. package/dist/cli/session.js.map +1 -0
  43. package/dist/daemon/activity.js +56 -0
  44. package/dist/daemon/activity.js.map +1 -0
  45. package/dist/daemon/anchor.js +143 -0
  46. package/dist/daemon/anchor.js.map +1 -0
  47. package/dist/daemon/app.js +1081 -0
  48. package/dist/daemon/app.js.map +1 -0
  49. package/dist/daemon/approve.js +71 -0
  50. package/dist/daemon/approve.js.map +1 -0
  51. package/dist/daemon/desktop-notify.js +69 -0
  52. package/dist/daemon/desktop-notify.js.map +1 -0
  53. package/dist/daemon/diff.js +187 -0
  54. package/dist/daemon/diff.js.map +1 -0
  55. package/dist/daemon/linter/index.js +19 -0
  56. package/dist/daemon/linter/index.js.map +1 -0
  57. package/dist/daemon/linter/parse.js +350 -0
  58. package/dist/daemon/linter/parse.js.map +1 -0
  59. package/dist/daemon/linter/rules.js +359 -0
  60. package/dist/daemon/linter/rules.js.map +1 -0
  61. package/dist/daemon/main.js +48 -0
  62. package/dist/daemon/main.js.map +1 -0
  63. package/dist/daemon/notify.js +23 -0
  64. package/dist/daemon/notify.js.map +1 -0
  65. package/dist/daemon/presence.js +37 -0
  66. package/dist/daemon/presence.js.map +1 -0
  67. package/dist/daemon/queue.js +160 -0
  68. package/dist/daemon/queue.js.map +1 -0
  69. package/dist/daemon/store.js +393 -0
  70. package/dist/daemon/store.js.map +1 -0
  71. package/dist/daemon/threads.js +153 -0
  72. package/dist/daemon/threads.js.map +1 -0
  73. package/dist/daemon/transcript.js +89 -0
  74. package/dist/daemon/transcript.js.map +1 -0
  75. package/dist/daemon/ui.js +175 -0
  76. package/dist/daemon/ui.js.map +1 -0
  77. package/dist/shared/config.js +93 -0
  78. package/dist/shared/config.js.map +1 -0
  79. package/dist/shared/gwt.js +69 -0
  80. package/dist/shared/gwt.js.map +1 -0
  81. package/dist/shared/paths.js +67 -0
  82. package/dist/shared/paths.js.map +1 -0
  83. package/dist/shared/question-spec.js +44 -0
  84. package/dist/shared/question-spec.js.map +1 -0
  85. package/dist/shared/types.js +35 -0
  86. package/dist/shared/types.js.map +1 -0
  87. package/dist/shared/version.js +5 -0
  88. package/dist/shared/version.js.map +1 -0
  89. package/dist/ui/assets/arc-HhPfdCPZ.js +1 -0
  90. package/dist/ui/assets/architecture-7EHR7CIX-BPLblcyi.js +1 -0
  91. package/dist/ui/assets/architectureDiagram-3BPJPVTR-D2PIxGOb.js +36 -0
  92. package/dist/ui/assets/array-BifhSqXX.js +1 -0
  93. package/dist/ui/assets/blockDiagram-GPEHLZMM-DQ3Dn17h.js +132 -0
  94. package/dist/ui/assets/c4Diagram-AAUBKEIU-DxITrQgS.js +10 -0
  95. package/dist/ui/assets/channel-ipcU8ZNI.js +1 -0
  96. package/dist/ui/assets/chunk-2J33WTMH-Du1JoPx5.js +1 -0
  97. package/dist/ui/assets/chunk-3OPIFGDE-Dn7x2Yqf.js +62 -0
  98. package/dist/ui/assets/chunk-4BX2VUAB-DVnrE-4n.js +1 -0
  99. package/dist/ui/assets/chunk-55IACEB6-BAhFAimA.js +1 -0
  100. package/dist/ui/assets/chunk-5ZQYHXKU-0hEZptem.js +2 -0
  101. package/dist/ui/assets/chunk-727SXJPM-C1FN_cI3.js +206 -0
  102. package/dist/ui/assets/chunk-AQP2D5EJ-A656OBd4.js +231 -0
  103. package/dist/ui/assets/chunk-BSJP7CBP-D8oMbjm8.js +1 -0
  104. package/dist/ui/assets/chunk-CSCIHK7Q-DjIL8GLi.js +122 -0
  105. package/dist/ui/assets/chunk-FMBD7UC4-Otblfqvz.js +15 -0
  106. package/dist/ui/assets/chunk-KSCS5N6A-BOjTvm3H.js +10 -0
  107. package/dist/ui/assets/chunk-L5ZTLDWV-CaTLaw6L.js +1 -0
  108. package/dist/ui/assets/chunk-LZXEDZCA-Dq5p7qrD.js +2 -0
  109. package/dist/ui/assets/chunk-ND2GUHAM-jZ_NNnWi.js +1 -0
  110. package/dist/ui/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
  111. package/dist/ui/assets/chunk-NZK2D7GU-U_7l_sCh.js +1 -0
  112. package/dist/ui/assets/chunk-O5CBEL6O-MewqqNB7.js +70 -0
  113. package/dist/ui/assets/chunk-QZHKN3VN-DzGPH44B.js +1 -0
  114. package/dist/ui/assets/chunk-WU5MYG2G-DyEIVjoo.js +1 -0
  115. package/dist/ui/assets/chunk-XPW4576I-D5ArxNEF.js +32 -0
  116. package/dist/ui/assets/classDiagram-4FO5ZUOK-Byg2Hl9D.js +1 -0
  117. package/dist/ui/assets/classDiagram-v2-Q7XG4LA2-Byg2Hl9D.js +1 -0
  118. package/dist/ui/assets/cose-bilkent-S5V4N54A-PFXzf7WV.js +1 -0
  119. package/dist/ui/assets/cytoscape.esm-h6BdjjI9.js +321 -0
  120. package/dist/ui/assets/dagre-BM42HDAG-xrCfjZuZ.js +4 -0
  121. package/dist/ui/assets/dagre-Bx709z4p.js +1 -0
  122. package/dist/ui/assets/defaultLocale-C8Fc0cco.js +1 -0
  123. package/dist/ui/assets/diagram-2AECGRRQ-BFf-cyKY.js +43 -0
  124. package/dist/ui/assets/diagram-5GNKFQAL-kNPV4NfV.js +10 -0
  125. package/dist/ui/assets/diagram-KO2AKTUF-ByC1IUwG.js +3 -0
  126. package/dist/ui/assets/diagram-LMA3HP47-DZIJMPK0.js +24 -0
  127. package/dist/ui/assets/diagram-OG6HWLK6-CSDED9A-.js +24 -0
  128. package/dist/ui/assets/dist-YwjsDswi.js +1 -0
  129. package/dist/ui/assets/erDiagram-TEJ5UH35-yuzvjE6J.js +85 -0
  130. package/dist/ui/assets/eventmodeling-FCH6USID-CZR4eNG-.js +1 -0
  131. package/dist/ui/assets/flowDiagram-I6XJVG4X-ApPtVyYM.js +162 -0
  132. package/dist/ui/assets/ganttDiagram-6RSMTGT7-BeMLXtAr.js +292 -0
  133. package/dist/ui/assets/gitGraph-WXDBUCRP-JmTTBa7j.js +1 -0
  134. package/dist/ui/assets/gitGraphDiagram-PVQCEYII-Cjjnjs71.js +106 -0
  135. package/dist/ui/assets/graphlib-B8gBHxth.js +1 -0
  136. package/dist/ui/assets/index-BFQVRcSI.js +11 -0
  137. package/dist/ui/assets/index-Bj_kTrwP.css +1 -0
  138. package/dist/ui/assets/info-J43DQDTF-8vZ3gome.js +1 -0
  139. package/dist/ui/assets/infoDiagram-5YYISTIA-CnMk1cA-.js +2 -0
  140. package/dist/ui/assets/init-D6jRqBbL.js +1 -0
  141. package/dist/ui/assets/ishikawaDiagram-YF4QCWOH-Bl8z6huD.js +70 -0
  142. package/dist/ui/assets/journeyDiagram-JHISSGLW-DYIVfMpS.js +139 -0
  143. package/dist/ui/assets/kanban-definition-UN3LZRKU-BnR0ZzOz.js +89 -0
  144. package/dist/ui/assets/katex-Vhh-h91d.js +257 -0
  145. package/dist/ui/assets/line-DcBdQit6.js +1 -0
  146. package/dist/ui/assets/linear-HKjRHFAO.js +1 -0
  147. package/dist/ui/assets/mermaid-parser.core-DkYXrPlA.js +4 -0
  148. package/dist/ui/assets/mermaid.core-BmkfCI3b.js +9 -0
  149. package/dist/ui/assets/mindmap-definition-RKZ34NQL-sIAd4nDi.js +96 -0
  150. package/dist/ui/assets/ordinal-hYBb2elL.js +1 -0
  151. package/dist/ui/assets/otacon-DPXGiaVj.svg +11 -0
  152. package/dist/ui/assets/packet-YPE3B663-BxbxcfXN.js +1 -0
  153. package/dist/ui/assets/path-BWPyau1x.js +1 -0
  154. package/dist/ui/assets/pie-LRSECV5Y-BJxazjNs.js +1 -0
  155. package/dist/ui/assets/pieDiagram-4H26LBE5-BiOhc9GR.js +30 -0
  156. package/dist/ui/assets/plan-view-CH6NzUDb.js +74 -0
  157. package/dist/ui/assets/purify.es-CDvCXckx.js +3 -0
  158. package/dist/ui/assets/quadrantDiagram-W4KKPZXB-CVyHbWgo.js +7 -0
  159. package/dist/ui/assets/radar-GUYGQ44K-D9ohbnbV.js +1 -0
  160. package/dist/ui/assets/requirementDiagram-4Y6WPE33-Ba24_hqc.js +84 -0
  161. package/dist/ui/assets/rough.esm-CSKSodPl.js +1 -0
  162. package/dist/ui/assets/sankeyDiagram-5OEKKPKP-CxD4wiPL.js +40 -0
  163. package/dist/ui/assets/sequenceDiagram-3UESZ5HK-7qA7lD61.js +162 -0
  164. package/dist/ui/assets/src-IM8AE8MK.js +1 -0
  165. package/dist/ui/assets/stateDiagram-AJRCARHV-DNElRCuH.js +1 -0
  166. package/dist/ui/assets/stateDiagram-v2-BHNVJYJU-D6qTYpY3.js +1 -0
  167. package/dist/ui/assets/timeline-definition-PNZ67QCA-ChYC4Grd.js +120 -0
  168. package/dist/ui/assets/treeView-BLDUP644-Il0KnMi_.js +1 -0
  169. package/dist/ui/assets/treemap-LRROVOQU-CIiKcdRo.js +1 -0
  170. package/dist/ui/assets/vennDiagram-CIIHVFJN-Ulhkum9i.js +34 -0
  171. package/dist/ui/assets/wardley-L42UT6IY-BNd4ljz7.js +1 -0
  172. package/dist/ui/assets/wardleyDiagram-YWT4CUSO-BicXxh84.js +78 -0
  173. package/dist/ui/assets/xychartDiagram-2RQKCTM6-Duf-m_th.js +7 -0
  174. package/dist/ui/index.html +20 -0
  175. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zero Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ <img width="9034" height="1857" alt="github-otacon" src="https://github.com/user-attachments/assets/18796111-63d7-4df6-a2ba-b95f132eabd3" />
2
+
3
+ Plan review surface for coding agents (Claude Code, Codex, OpenCode). Replaces native
4
+ plan modes with one CLI protocol: schema'd concise plans, anchored comments and
5
+ questions from any device (phone included, over Tailscale), revision diffs against
6
+ what you last reviewed, and a mandatory grill-me interview phase before any plan
7
+ reaches review. Approval produces a committed plan artifact for a future implementer
8
+ skill (`snake`) to execute.
9
+
10
+ Behavior spec: [DESIGN.md](DESIGN.md) · tradeoff rationale: [DECISIONS.md](DECISIONS.md)
11
+ · agent conventions: [AGENTS.md](AGENTS.md)
12
+
13
+ Personal tool by/for Zero. Zero API spend by construction — all model work happens in
14
+ your interactive subscription-backed agent session; the daemon, CLI, and UI never call
15
+ an LLM.
16
+
17
+ ## Install
18
+
19
+ One-time machine setup (DESIGN.md §16):
20
+
21
+ ```sh
22
+ npm install -g otacon # one package: CLI + daemon (Node ≥ 20)
23
+ otacon install --all # agent wrappers; or --agent claude|codex|opencode
24
+ otacon install --agent claude --hooks # also register the Claude Code Stop hook
25
+ otacon doctor # verify: node, daemon boots, wrappers, Tailscale
26
+ ```
27
+
28
+ `otacon install` writes the protocol wrapper into each agent's skill location —
29
+ Claude Code: `~/.claude/skills/otacon/SKILL.md` (+ the Stop hook script at
30
+ `~/.claude/hooks/otacon-stop.sh`); Codex: a marked block in `~/.codex/AGENTS.md`;
31
+ OpenCode: `~/.config/opencode/skills/otacon/SKILL.md`. Wrappers are managed files:
32
+ reinstalls overwrite them (outside-the-markers content in Codex's shared file
33
+ survives). `--hooks` merges the Stop hook into `~/.claude/settings.json` additively
34
+ and idempotently, backing the file up first. The daemon is never started by hand —
35
+ any `otacon` command auto-spawns it.
36
+
37
+ Per-repo setup: **none.** The first `otacon start` in a repo creates `.otacon/` and
38
+ gitignores it. Approved plans land committed in `docs/plans/`. `otacon clean` archives
39
+ ended sessions' working state to `.otacon/archive/`.
40
+
41
+ ### Updating
42
+
43
+ ```sh
44
+ npm update -g otacon # the version handshake restarts the daemon on next use
45
+ ```
46
+
47
+ ### Build from source (contributors)
48
+
49
+ For contributors or the bleeding edge only — the published npm package is the
50
+ supported user path. Clone the repo and run from source:
51
+
52
+ ```sh
53
+ git clone https://github.com/zeroliu/otacon && cd otacon
54
+ bun install
55
+ ./bin/otacon doctor # run straight from source
56
+ # — or build a Node artifact and link it onto PATH —
57
+ bun run build && npm link # `otacon` now points at this checkout
58
+ ```
59
+
60
+ (`npm i -g github:zeroliu/otacon` is **not** a supported install — the published
61
+ package ships a prebuilt `dist/`, and a GitHub install would need a build-on-install
62
+ step that is intentionally not wired.)
63
+
64
+ ## Phone access
65
+
66
+ Reviews work from a phone over Tailscale (DESIGN.md §11) — plans never leave your
67
+ devices, and the tailnet is the auth:
68
+
69
+ 1. Install Tailscale on the Mac and the phone; log in (`tailscale up`).
70
+ 2. Enable **HTTPS Certificates** for the tailnet: Tailscale admin console → DNS →
71
+ Enable HTTPS (MagicDNS must be on). This is the one step otacon cannot do for you.
72
+ 3. `otacon expose` — configures `tailscale serve` for the daemon port, verifies the
73
+ tailnet URL actually serves, and prints the HTTPS URL with `verified: true`.
74
+ Bookmark it on the phone.
75
+ 4. Keep the Mac awake while a plan is in review: `caffeinate -i`.
76
+
77
+ If you skip step 2, `tailscale serve` still succeeds but the URL resets every TLS
78
+ handshake — so `otacon expose` reports `verified: false` and links the admin DNS page
79
+ instead of handing you a dead URL. (Just enabled HTTPS? The cert can take a minute to
80
+ provision; re-run `expose`.)
81
+
82
+ On the Mac App Store Tailscale, putting `tailscale` on your `PATH` needs a manual
83
+ launcher — a wrapper script that runs the app-bundle binary (a bare symlink crashes).
84
+ otacon finds the app-bundle binary on its own either way.
85
+
86
+ ---
87
+
88
+ Maintainers cutting a release: see [RELEASING.md](RELEASING.md).
@@ -0,0 +1,188 @@
1
+ // HTTP client for otacond, plus ensureDaemon — the auto-spawn and version
2
+ // handshake every CLI command runs first (DESIGN.md §16).
3
+ //
4
+ // The daemon is spawned by resolved path — the dist/daemon/main.js sibling of
5
+ // this very module — never via PATH (DECISIONS.md "Daemon spawned by resolved
6
+ // file path"), detached with stdout/stderr appended to $OTACON_HOME/daemon.log.
7
+ // Instead of reading the boot line, the CLI re-probes /api/health and watches
8
+ // the child's exit code: exit 0 before health means it lost the spawn race to
9
+ // another otacond (the port is the lock — keep polling the winner); any other
10
+ // exit is a refusal or crash, surfaced with a pointer at the log (DECISIONS.md
11
+ // "Daemon spawn: health re-probe, not the boot line").
12
+ import { spawn } from "node:child_process";
13
+ import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { daemonLogPath, otaconHome, otaconPort } from "../shared/paths.js";
16
+ import { VERSION } from "../shared/version.js";
17
+ import { fail, notice } from "./output.js";
18
+ const PROBE_TIMEOUT_MS = 1500;
19
+ const SPAWN_DEADLINE_MS = 8000;
20
+ const POLL_INTERVAL_MS = 100;
21
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
+ export function baseUrl() {
23
+ return `http://127.0.0.1:${otaconPort()}`;
24
+ }
25
+ /**
26
+ * JSON request/response against the daemon. A connection failure (refused,
27
+ * reset, aborted, truncated body) throws E_DAEMON_DOWN — an actionable exit-1
28
+ * failure per the exit-code contract, not an internal error. `otacon wait`
29
+ * treats exactly that code as "back off and re-park".
30
+ */
31
+ export async function api(method, path, body, signal) {
32
+ try {
33
+ const response = await fetch(`${baseUrl()}${path}`, {
34
+ method,
35
+ headers: body === undefined ? undefined : { "content-type": "application/json" },
36
+ body: body === undefined ? undefined : JSON.stringify(body),
37
+ signal,
38
+ });
39
+ // Every /api response is JSON; a body that fails to parse is a connection
40
+ // truncated mid-response and must NOT pass as an empty result — for events
41
+ // that would print {} as the event while the daemon requeues it.
42
+ const parsed = (await response.json());
43
+ return { status: response.status, body: parsed };
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ fail("E_DAEMON_DOWN", `cannot reach otacond at ${baseUrl()}${path}: ${message}; retry`);
48
+ }
49
+ }
50
+ /** down = nothing answered HTTP; foreign = answered but is not otacond. */
51
+ async function probe() {
52
+ let response;
53
+ try {
54
+ response = await fetch(`${baseUrl()}/api/health`, {
55
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
56
+ });
57
+ }
58
+ catch {
59
+ return { state: "down" };
60
+ }
61
+ try {
62
+ const health = (await response.json());
63
+ if (health.app === "otacond" && typeof health.version === "string") {
64
+ return { state: "up", health };
65
+ }
66
+ }
67
+ catch {
68
+ // non-JSON body: not otacond
69
+ }
70
+ return { state: "foreign" };
71
+ }
72
+ function portConflict() {
73
+ fail("E_PORT_CONFLICT", `port ${otaconPort()} is in use by something that is not otacond; set OTACON_PORT to pick another port`);
74
+ }
75
+ /** dist/daemon/main.js sibling; from a source-tree run (bun) the .ts entry. */
76
+ function daemonEntry() {
77
+ const built = fileURLToPath(new URL("../daemon/main.js", import.meta.url));
78
+ if (existsSync(built))
79
+ return built;
80
+ // Running from src/ (e.g. `bun run src/cli/main.ts`): no built sibling, but
81
+ // process.execPath is bun, which runs the TypeScript entry directly — this
82
+ // is what keeps "local dev behaves identically" true (DECISIONS.md "Daemon
83
+ // spawned by resolved file path").
84
+ return fileURLToPath(new URL("../daemon/main.ts", import.meta.url));
85
+ }
86
+ /** Spawn the daemon detached; returns the child's exit code, undefined while alive. */
87
+ function spawnDaemon() {
88
+ mkdirSync(otaconHome(), { recursive: true });
89
+ const log = openSync(daemonLogPath(), "a");
90
+ const child = spawn(process.execPath, [daemonEntry()], {
91
+ detached: true,
92
+ stdio: ["ignore", log, log],
93
+ });
94
+ child.unref();
95
+ closeSync(log); // the child holds its own descriptor
96
+ let exitCode;
97
+ child.on("error", (error) => {
98
+ // spawn itself failed (ENOENT/EACCES); without a listener this event
99
+ // would crash the CLI outside the JSON-on-stdout contract.
100
+ notice(`failed to spawn otacond: ${error.message}`);
101
+ exitCode = 1;
102
+ });
103
+ child.on("exit", (code) => {
104
+ exitCode = code ?? 1; // signal death counts as failure
105
+ });
106
+ return () => exitCode;
107
+ }
108
+ async function spawnAndAwaitHealth() {
109
+ const exitCode = spawnDaemon();
110
+ const deadline = Date.now() + SPAWN_DEADLINE_MS;
111
+ while (Date.now() < deadline) {
112
+ const result = await probe();
113
+ if (result.state === "up")
114
+ return result.health;
115
+ if (result.state === "foreign")
116
+ portConflict();
117
+ const code = exitCode();
118
+ if (code !== undefined && code !== 0) {
119
+ // Covers the non-HTTP port squatter too: the probe sees "down", the
120
+ // spawned daemon hits EADDRINUSE, fails its own ownership check, exits 1.
121
+ fail("E_DAEMON_START", `otacond exited with code ${code} before becoming healthy; see ${daemonLogPath()} (if the port is taken, set OTACON_PORT)`);
122
+ }
123
+ await sleep(POLL_INTERVAL_MS);
124
+ }
125
+ fail("E_DAEMON_START", `otacond did not become healthy on ${baseUrl()}; see ${daemonLogPath()}`);
126
+ }
127
+ async function shutdownStaleDaemon() {
128
+ // Re-check identity and version immediately before firing the shutdown: the
129
+ // caller probed moments ago, but a peer CLI may have completed the same
130
+ // restart in between — killing the healthy current-version daemon it just
131
+ // spawned would drop parked waiters for nothing (the probe→shutdown TOCTOU).
132
+ const recheck = await probe();
133
+ if (recheck.state === "down")
134
+ return;
135
+ if (recheck.state === "foreign")
136
+ portConflict();
137
+ if (recheck.health.version === VERSION)
138
+ return;
139
+ try {
140
+ await api("POST", "/api/shutdown");
141
+ }
142
+ catch {
143
+ // it may drop the connection while exiting; the down-poll below decides
144
+ }
145
+ const deadline = Date.now() + SPAWN_DEADLINE_MS;
146
+ while (Date.now() < deadline) {
147
+ const result = await probe();
148
+ if (result.state === "down")
149
+ return;
150
+ // A concurrent CLI may have already respawned the current version into
151
+ // the gap; that restart is as good as ours — without this check we would
152
+ // poll a healthy daemon for the full deadline and fail spuriously.
153
+ if (result.state === "up" && result.health.version === VERSION)
154
+ return;
155
+ await sleep(POLL_INTERVAL_MS);
156
+ }
157
+ fail("E_DAEMON_RESTART", "stale otacond did not exit after POST /api/shutdown");
158
+ }
159
+ /** Whole probe→shutdown→respawn cycles before giving up on a version fight. */
160
+ const RESTART_ATTEMPTS = 3;
161
+ /**
162
+ * Health probe → spawn if down → exact-version handshake, restarting a stale
163
+ * daemon via POST /api/shutdown (DESIGN.md §16). Refuses a port held by a
164
+ * non-otacond process. The restart cycle is bounded (peer CLIs of different
165
+ * versions could otherwise ping-pong restarts forever), and the shutdown
166
+ * re-checks the daemon's version right before firing, so a peer's completed
167
+ * restart is adopted instead of killed (DECISIONS.md "Stale-daemon restart").
168
+ */
169
+ export async function ensureDaemon() {
170
+ for (let attempt = 0; attempt < RESTART_ATTEMPTS; attempt++) {
171
+ const current = await probe();
172
+ if (current.state === "foreign")
173
+ portConflict();
174
+ if (current.state === "up") {
175
+ if (current.health.version === VERSION)
176
+ return current.health;
177
+ notice(`restarting stale otacond ${current.health.version} → ${VERSION}`);
178
+ await shutdownStaleDaemon();
179
+ continue; // re-probe: a peer may have respawned the current version
180
+ }
181
+ const health = await spawnAndAwaitHealth();
182
+ if (health.version === VERSION)
183
+ return health;
184
+ // An older CLI won the respawn race; loop to shut its daemon down too.
185
+ }
186
+ fail("E_VERSION_MISMATCH", `daemon on port ${otaconPort()} still runs another version after ${RESTART_ATTEMPTS} restart attempts — a CLI of a different version keeps respawning it; retry`);
187
+ }
188
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/cli/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ // otacon answer <question-id> (--body "…" | --file f.md) [--session id] — the
2
+ // agent's reply to a user question (DESIGN.md §6, §9): the answer lands on the
3
+ // question's thread, the plan and status stay untouched, and the agent goes
4
+ // back to `otacon wait`.
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+ import { api, ensureDaemon } from "../client.js";
9
+ import { fail, printJson, usageError } from "../output.js";
10
+ import { listSessions, realpathOr, resolveSession } from "../session.js";
11
+ export async function answerCommand(argv) {
12
+ const { values, positionals } = parseArgs({
13
+ args: argv,
14
+ options: {
15
+ body: { type: "string" },
16
+ file: { type: "string" },
17
+ session: { type: "string" },
18
+ },
19
+ allowPositionals: true,
20
+ });
21
+ const questionId = positionals[0];
22
+ if (questionId === undefined || positionals.length > 1) {
23
+ usageError("otacon answer takes exactly one <question-id>");
24
+ }
25
+ if ((values.body === undefined) === (values.file === undefined)) {
26
+ usageError("otacon answer requires exactly one of --body or --file");
27
+ }
28
+ let body;
29
+ if (values.body !== undefined) {
30
+ body = values.body;
31
+ }
32
+ else {
33
+ const path = resolve(values.file);
34
+ try {
35
+ body = readFileSync(path, "utf8");
36
+ }
37
+ catch {
38
+ fail("E_NO_FILE", `cannot read answer file ${path}`);
39
+ }
40
+ }
41
+ if (body.trim() === "")
42
+ usageError("the answer body must be non-empty");
43
+ await ensureDaemon();
44
+ const session = resolveSession(await listSessions(), values.session, realpathOr(process.cwd()));
45
+ const response = await api("POST", `/api/sessions/${session.id}/questions/${encodeURIComponent(questionId)}/answer`, { body });
46
+ if (response.status === 200) {
47
+ printJson(response.body);
48
+ return 0;
49
+ }
50
+ const code = response.body.error?.code;
51
+ if (response.status === 404 && code === "E_UNKNOWN_QUESTION") {
52
+ fail("E_UNKNOWN_QUESTION", `session ${session.id} has no question ${questionId}`);
53
+ }
54
+ if (response.status === 404) {
55
+ fail("E_UNKNOWN_SESSION", `daemon no longer knows session ${session.id}`);
56
+ }
57
+ if (response.status === 409) {
58
+ const message = response.body?.error?.message;
59
+ fail("E_SESSION_OVER", message ?? `session ${session.id} is approved — the session is over`);
60
+ }
61
+ fail("E_INTERNAL", `answer failed: ${JSON.stringify(response.body)}`, undefined, 2);
62
+ }
63
+ //# sourceMappingURL=answer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.js","sourceRoot":"","sources":["../../../src/cli/commands/answer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ // otacon ask --question "…" [--options "A|B|C"] [--recommend A] [--multi]
2
+ // [--session id] — post a grill question card to the UI (DESIGN.md §6, §8).
3
+ // Prints the question id; the agent then parks in `otacon wait` and the
4
+ // user's answer arrives as an {"event":"answer"} on stdout there.
5
+ //
6
+ // `--batch <file|->` posts several INDEPENDENT questions in one call (a JSON
7
+ // array of the same specs): the daemon mints them atomically and they render
8
+ // as ordinary cards, each answered instantly. Dependency-first grilling still
9
+ // holds — only independent siblings batch (DESIGN.md §8).
10
+ import { readFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { parseArgs } from "node:util";
13
+ import { parseQuestionSpec } from "../../shared/question-spec.js";
14
+ import { api, ensureDaemon } from "../client.js";
15
+ import { fail, printJson, usageError } from "../output.js";
16
+ import { listSessions, realpathOr, resolveSession } from "../session.js";
17
+ /**
18
+ * Hold one question body to the shared validator (DESIGN.md §6, §8) — the same
19
+ * rules the daemon re-checks — turning its error string into a usage error
20
+ * (E_USAGE, exit 2) carrying the caller's context: a `--batch[i] ` index, or
21
+ * "" for the bare flag form.
22
+ */
23
+ function specOrUsage(raw, where) {
24
+ const spec = parseQuestionSpec(raw);
25
+ if (typeof spec === "string")
26
+ usageError(`${where}${spec}`);
27
+ return spec;
28
+ }
29
+ /**
30
+ * Parse and validate a --batch payload: a non-empty JSON array of question
31
+ * specs. Throws a usage error on a malformed array or member (naming its
32
+ * index) so the agent fixes its file before the daemon sees it; the daemon
33
+ * re-validates and mints the whole batch atomically (a bad member fails it
34
+ * all — no partial queue).
35
+ */
36
+ export function parseBatch(content) {
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(content);
40
+ }
41
+ catch {
42
+ usageError("--batch must be a JSON array of question objects");
43
+ }
44
+ if (!Array.isArray(parsed) || parsed.length === 0) {
45
+ usageError("--batch must be a non-empty JSON array of question objects");
46
+ }
47
+ return parsed.map((raw, i) => specOrUsage(raw, `--batch[${i}] `));
48
+ }
49
+ export async function askCommand(argv) {
50
+ const { values } = parseArgs({
51
+ args: argv,
52
+ options: {
53
+ question: { type: "string" },
54
+ options: { type: "string" },
55
+ recommend: { type: "string" },
56
+ multi: { type: "boolean", default: false },
57
+ session: { type: "string" },
58
+ batch: { type: "string" },
59
+ },
60
+ });
61
+ let payload;
62
+ if (values.batch !== undefined) {
63
+ if (values.question !== undefined ||
64
+ values.options !== undefined ||
65
+ values.recommend !== undefined ||
66
+ values.multi === true) {
67
+ usageError("--batch is exclusive with --question/--options/--recommend/--multi");
68
+ }
69
+ let content;
70
+ try {
71
+ content = readFileSync(values.batch === "-" ? 0 : resolve(values.batch), "utf8");
72
+ }
73
+ catch {
74
+ fail("E_NO_BATCH", `cannot read --batch ${values.batch}; write the questions there or pass -`);
75
+ }
76
+ payload = { questions: parseBatch(content) };
77
+ }
78
+ else {
79
+ if (values.question === undefined || values.question.trim() === "") {
80
+ usageError('otacon ask requires --question "…" (or --batch <file|->)');
81
+ }
82
+ // Assemble the same spec a --batch member is, then hold it to the one shared
83
+ // validator — the only flag-specific bit is splitting --options on "|".
84
+ payload = {
85
+ ...specOrUsage({
86
+ question: values.question,
87
+ ...(values.options !== undefined
88
+ ? { options: values.options.split("|").map((o) => o.trim()) }
89
+ : {}),
90
+ ...(values.recommend !== undefined ? { recommend: values.recommend } : {}),
91
+ ...(values.multi === true ? { multi: true } : {}),
92
+ }, ""),
93
+ };
94
+ }
95
+ await ensureDaemon();
96
+ const session = resolveSession(await listSessions(), values.session, realpathOr(process.cwd()));
97
+ const response = await api("POST", `/api/sessions/${session.id}/ask`, payload);
98
+ if (response.status === 201) {
99
+ // single: {ok, session, id: "q<n>"}; batch: {ok, session, ids: [...]} —
100
+ // now park in `otacon wait`, looping it to drain a batch.
101
+ printJson(response.body);
102
+ return 0;
103
+ }
104
+ const code = response.body.error?.code;
105
+ const message = response.body.error?.message;
106
+ if (response.status === 409) {
107
+ fail(code ?? "E_SESSION_OVER", message ?? `session ${session.id} is over`);
108
+ }
109
+ if (response.status === 404) {
110
+ fail("E_UNKNOWN_SESSION", `daemon no longer knows session ${session.id}`);
111
+ }
112
+ if (response.status === 400) {
113
+ fail("E_BAD_REQUEST", message ?? "daemon rejected the question");
114
+ }
115
+ fail("E_INTERNAL", `ask failed: ${JSON.stringify(response.body)}`, undefined, 2);
116
+ }
117
+ //# sourceMappingURL=ask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask.js","sourceRoot":"","sources":["../../../src/cli/commands/ask.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,kEAAkE;AAClE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,0DAA0D;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAqB,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEzE;;;;;GAKG;AACH,SAAS,WAAW,CAAC,GAAY,EAAE,KAAa;IAC9C,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,UAAU,CAAC,GAAG,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,UAAU,CAAC,kDAAkD,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,UAAU,CAAC,4DAA4D,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAc;IAC7C,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,IAAI,EAAE,IAAI;QACV,OAAO,EAAE;YACP,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC5B,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC3B,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC7B,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;YAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC3B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;KACF,CAAC,CAAC;IAEH,IAAI,OAAgC,CAAC;IACrC,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,IACE,MAAM,CAAC,QAAQ,KAAK,SAAS;YAC7B,MAAM,CAAC,OAAO,KAAK,SAAS;YAC5B,MAAM,CAAC,SAAS,KAAK,SAAS;YAC9B,MAAM,CAAC,KAAK,KAAK,IAAI,EACrB,CAAC;YACD,UAAU,CAAC,oEAAoE,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QACnF,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,YAAY,EAAE,uBAAuB,MAAM,CAAC,KAAK,uCAAuC,CAAC,CAAC;QACjG,CAAC;QACD,OAAO,GAAG,EAAE,SAAS,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/C,CAAC;SAAM,CAAC;QACN,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnE,UAAU,CAAC,0DAA0D,CAAC,CAAC;QACzE,CAAC;QACD,6EAA6E;QAC7E,wEAAwE;QACxE,OAAO,GAAG;YACR,GAAG,WAAW,CACZ;gBACE,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS;oBAC9B,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE;oBAC7D,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1E,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClD,EACD,EAAE,CACH;SACF,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,EAAE,CAAC;IACrB,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,YAAY,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAEhG,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,iBAAiB,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/E,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,wEAAwE;QACxE,0DAA0D;QAC1D,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,IAAI,GAAI,QAAQ,CAAC,IAAI,CAAC,KAAuC,EAAE,IAAI,CAAC;IAC1E,MAAM,OAAO,GAAI,QAAQ,CAAC,IAAI,CAAC,KAA0C,EAAE,OAAO,CAAC;IACnF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,gBAAgB,EAAE,OAAO,IAAI,WAAW,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,mBAAmB,EAAE,kCAAkC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,eAAe,EAAE,OAAO,IAAI,8BAA8B,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,YAAY,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;AACnF,CAAC"}
@@ -0,0 +1,48 @@
1
+ // otacon clean [--all] — archive working state for ended sessions (DESIGN.md
2
+ // §6, §12): for every terminal session in this repo (--all: everywhere), it
3
+ // calls DELETE /api/sessions/:id; the daemon deregisters the session and
4
+ // archives its .otacon/<id>/ dir to .otacon/archive/<id>/, reporting the
5
+ // destination as `archivedTo` (the review UI drives the same route — terminal
6
+ // archives, non-terminal hard-deletes). Committed artifacts under docs/plans/
7
+ // are never touched (DECISIONS.md "clean: daemon deregisters and archives").
8
+ import { parseArgs } from "node:util";
9
+ import { TERMINAL_STATUSES } from "../../shared/types.js";
10
+ import { api, ensureDaemon } from "../client.js";
11
+ import { notice, printJson } from "../output.js";
12
+ import { findRepoRoot, listSessions, realpathOr } from "../session.js";
13
+ export async function cleanCommand(argv) {
14
+ const { values } = parseArgs({
15
+ args: argv,
16
+ options: { all: { type: "boolean", default: false } },
17
+ });
18
+ await ensureDaemon();
19
+ const cwd = realpathOr(process.cwd());
20
+ const root = findRepoRoot(cwd) ?? cwd;
21
+ // Only terminal (ended) sessions qualify — approved, plus implemented /
22
+ // implement_failed once a build finishes (DESIGN.md §12). A terminal session
23
+ // stays terminal, so clean's DELETE always takes the daemon's archive branch,
24
+ // never the non-terminal hard-delete one — a racing status change cannot
25
+ // sweep a live (including `implementing`) session.
26
+ const targets = (await listSessions()).filter((s) => TERMINAL_STATUSES.includes(s.status) && (values.all || realpathOr(s.repo) === root));
27
+ const cleaned = [];
28
+ for (const session of targets) {
29
+ const response = await api("DELETE", `/api/sessions/${session.id}`);
30
+ if (response.status !== 200) {
31
+ notice(`skipping ${session.id}: ${JSON.stringify(response.body)}`);
32
+ continue;
33
+ }
34
+ const pending = response.body.pendingEvents;
35
+ if (typeof pending === "number" && pending > 0) {
36
+ notice(`${session.id}: ${pending} undelivered event(s) archived with it`);
37
+ }
38
+ // The daemon archived the dir and tells us where (null only if it was gone).
39
+ const archivedTo = response.body.archivedTo ?? null;
40
+ cleaned.push({ session: session.id, title: session.title, repo: session.repo, archivedTo });
41
+ }
42
+ if (cleaned.length === 0) {
43
+ notice(values.all ? "no ended sessions to clean" : `no ended sessions for ${root} (try --all)`);
44
+ }
45
+ printJson({ ok: true, cleaned });
46
+ return 0;
47
+ }
48
+ //# sourceMappingURL=clean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clean.js","sourceRoot":"","sources":["../../../src/cli/commands/clean.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAc;IAC/C,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;KACtD,CAAC,CAAC;IACH,MAAM,YAAY,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;IACtC,wEAAwE;IACxE,6EAA6E;IAC7E,8EAA8E;IAC9E,yEAAyE;IACzE,mDAAmD;IACnD,MAAM,OAAO,GAAG,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC,MAAM,CAC3C,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAC3F,CAAC;IAEF,MAAM,OAAO,GAAkF,EAAE,CAAC;IAClG,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,iBAAiB,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACpE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,CAAC,YAAY,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnE,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC;QAC5C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,GAAG,OAAO,CAAC,EAAE,KAAK,OAAO,wCAAwC,CAAC,CAAC;QAC5E,CAAC;QACD,6EAA6E;QAC7E,MAAM,UAAU,GAAI,QAAQ,CAAC,IAAI,CAAC,UAAwC,IAAI,IAAI,CAAC;QACnF,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,yBAAyB,IAAI,cAAc,CAAC,CAAC;IAClG,CAAC;IACD,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,86 @@
1
+ // otacon doctor — verify the machine setup (DESIGN.md §16): Node version,
2
+ // daemon boots and the port is free-or-ours (ensureDaemon does both), wrapper
3
+ // presence per agent, Stop hook registration, Tailscale state. Hard checks
4
+ // (node, daemon) fail the run with exit 1; everything optional — wrappers for
5
+ // agents the user may not use, phone access — is a warning, never a failure.
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { parseArgs } from "node:util";
8
+ import { otaconPort } from "../../shared/paths.js";
9
+ import { ensureDaemon } from "../client.js";
10
+ import { CODEX_BEGIN, MANAGED_MARKER } from "../install/assets.js";
11
+ import { claudeHookScriptPath, claudeSkillPath, codexAgentsPath, opencodeSkillPath, settingsRegisterStopHook, } from "../install/locations.js";
12
+ import { findTailscale, tailscaleStatus } from "../install/tailscale.js";
13
+ import { CliError, printJson } from "../output.js";
14
+ function wrapperCheck(name, path, marker) {
15
+ const present = existsSync(path) && readFileSync(path, "utf8").includes(marker);
16
+ return present
17
+ ? { name, status: "ok", detail: path }
18
+ : {
19
+ name,
20
+ status: "warn",
21
+ detail: `wrapper not installed at ${path}; run otacon install --agent ${name.replace("wrapper-", "")}`,
22
+ };
23
+ }
24
+ function stopHookCheck() {
25
+ const name = "stop-hook";
26
+ if (settingsRegisterStopHook()) {
27
+ return { name, status: "ok", detail: claudeHookScriptPath() };
28
+ }
29
+ return {
30
+ name,
31
+ status: "warn",
32
+ detail: "Stop hook not registered; run otacon install --agent claude --hooks",
33
+ };
34
+ }
35
+ function tailscaleCheck() {
36
+ const name = "tailscale";
37
+ const bin = findTailscale();
38
+ if (bin === undefined) {
39
+ return {
40
+ name,
41
+ status: "warn",
42
+ detail: "tailscale CLI not found — phone access is optional (DESIGN.md §11; otacon expose)",
43
+ };
44
+ }
45
+ const status = tailscaleStatus(bin);
46
+ if (status?.backendState === "Running") {
47
+ return { name, status: "ok", detail: `${bin} (${status.dnsName ?? "no MagicDNS name"})` };
48
+ }
49
+ return {
50
+ name,
51
+ status: "warn",
52
+ detail: `tailscale backend is ${status?.backendState ?? "unreachable"}; run \`tailscale up\` before otacon expose`,
53
+ };
54
+ }
55
+ export async function doctorCommand(argv) {
56
+ parseArgs({ args: argv, options: {} });
57
+ const checks = [];
58
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
59
+ checks.push(nodeMajor >= 20
60
+ ? { name: "node", status: "ok", detail: `node ${process.versions.node}` }
61
+ : { name: "node", status: "fail", detail: `node ${process.versions.node} — otacon needs >=20` });
62
+ try {
63
+ const health = await ensureDaemon();
64
+ checks.push({
65
+ name: "daemon",
66
+ status: "ok",
67
+ detail: `otacond ${health.version} (pid ${health.pid}) on port ${otaconPort()}`,
68
+ });
69
+ }
70
+ catch (error) {
71
+ checks.push({
72
+ name: "daemon",
73
+ status: "fail",
74
+ detail: error instanceof CliError ? `${error.code}: ${error.message}` : String(error),
75
+ });
76
+ }
77
+ checks.push(wrapperCheck("wrapper-claude", claudeSkillPath(), MANAGED_MARKER));
78
+ checks.push(wrapperCheck("wrapper-codex", codexAgentsPath(), CODEX_BEGIN));
79
+ checks.push(wrapperCheck("wrapper-opencode", opencodeSkillPath(), MANAGED_MARKER));
80
+ checks.push(stopHookCheck());
81
+ checks.push(tailscaleCheck());
82
+ const ok = checks.every((c) => c.status !== "fail");
83
+ printJson({ ok, checks });
84
+ return ok ? 0 : 1;
85
+ }
86
+ //# sourceMappingURL=doctor.js.map