@quolu/lattice 0.67.4 → 0.68.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 (36) hide show
  1. package/README.ja.md +62 -14
  2. package/README.md +60 -10
  3. package/bin/lattice-mcp.mjs +0 -0
  4. package/bin/lattice-scripted-adapter.mjs +0 -0
  5. package/bin/lattice-scripted-worker.mjs +0 -0
  6. package/bin/lattice-work-order-adapter.mjs +0 -0
  7. package/bin/lattice.mjs +0 -0
  8. package/docs/bridge-setup.md +32 -1
  9. package/package.json +9 -5
  10. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  11. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  12. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  13. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  14. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  15. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  16. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  17. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  18. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  19. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  20. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  21. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  22. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  23. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  24. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  25. package/src/bridge-cli.mjs +30 -0
  26. package/src/bridge-hub-heartbeat.mjs +9 -4
  27. package/src/cli-help.mjs +1 -0
  28. package/src/dashboard-delivery.mjs +103 -0
  29. package/src/fs-publish.mjs +16 -0
  30. package/src/git-process.mjs +3 -1
  31. package/src/project-cli.mjs +12 -4
  32. package/src/runtime-os-observation.mjs +1 -1
  33. package/src/runtime-windows-process.mjs +1 -1
  34. package/src/todo-cli.mjs +71 -11
  35. package/src/todo-contracts.mjs +6 -0
  36. package/src/todo-store.mjs +23 -4
package/README.ja.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/og.png" alt="Lattice — 閉ざされて見えた山岳地形から複数の進行経路が現れる" width="100%">
2
+ <img src="https://raw.githubusercontent.com/kitepon/Lattice/main/.github/og.png" alt="Lattice — 閉ざされて見えた山岳地形から複数の進行経路が現れる" width="100%">
3
3
  <br>
4
4
  <sub><em>この画像は、閉ざされているように見えた場所から複数の進行経路が現れ、自律した実行者たちが協調して動き出す瞬間を表しています。</em></sub>
5
5
  </p>
@@ -129,14 +129,49 @@ typed理由つきで分類します——宣言を直せば通るのか、操作
129
129
  ## 所有境界
130
130
 
131
131
  本repoはplan/ToDo/run store、Lattice sensor、schema、migration、release、diagnosticsを
132
- 所有します。[dotagents](https://github.com/kitepon-rgb/dotagents)はkitepon.devを支える内部
133
- toolchainであり、製品横断の工程利用・導入・host統合を所有します。
132
+ 所有します。[dotagents](https://github.com/kitepon/dotagents)はkitepon.devを支える内部toolchainとして、
133
+ 任意の製品横断工場導入とhost統合を統括します。Latticeの製品状態、更新、診断、releaseを所有または
134
+ 制御しません。
134
135
  廃止済みの前身sensorは独立製品として配線せず、Lattice sensorだけを現役面とします。
135
136
  MarkItDownは別区分の第三者CLIです。
136
137
 
137
138
  現在の工程状態と完了証拠の正本は、このrepoのLattice storeです。文書の役割と現行導線は
138
- [docs/README.md](docs/README.md)、製品思想は[PLAN.md](PLAN.md)、公開contractは
139
- [docs/00_product-contract.md](docs/00_product-contract.md)を参照してください。
139
+ [docs/README.md](https://github.com/kitepon/Lattice/blob/main/docs/README.md)、製品思想は
140
+ [PLAN.md](https://github.com/kitepon/Lattice/blob/main/PLAN.md)、公開contractは
141
+ [docs/00_product-contract.md](https://github.com/kitepon/Lattice/blob/main/docs/00_product-contract.md)を参照してください。
142
+
143
+ ## 導入・更新・診断
144
+
145
+ npm packageが単独導入と更新の正規入口です。更新後はtyped discoveryでCLIを確認し、必要なら
146
+ 製品所有のdashboardを新版へ入れ替えます。bridgeを有効化したhostだけ`bridge status`も確認します。
147
+ 常駐bridgeは読み込み済みの旧版を1分以内に自動で置き換えます。
148
+
149
+ ```bash
150
+ npm install -g @quolu/lattice@latest
151
+ lattice --version
152
+ lattice status --json
153
+ lattice bridge status --json
154
+ ```
155
+
156
+ ## Release
157
+
158
+ Latticeのreleaseはこのrepoから行います。`package.json`と`CHANGELOG.md`を更新し、製品gateを通し、
159
+ release commitを既定branchへ着地・pushしてから、同じ版をregistryへ公開して実installを確認します。
160
+ `prepublishOnly`はdirty treeとremote既定branchへ未着地のcommitを拒否します。
161
+
162
+ ```bash
163
+ npm run ci
164
+ npm run verify:release-commit
165
+ npm publish --access public
166
+ npm install -g @quolu/lattice@<version>
167
+ lattice --version
168
+ lattice status --json
169
+ lattice bridge status --json
170
+ ```
171
+
172
+ 製品CI契約は[docs/ci-contract.md](https://github.com/kitepon/Lattice/blob/main/docs/ci-contract.md)、
173
+ dashboard/bridgeの配備固有確認は
174
+ [docs/operations/lattice-kitepon-deployment.md](https://github.com/kitepon/Lattice/blob/main/docs/operations/lattice-kitepon-deployment.md)が正です。
140
175
 
141
176
  CLIの全体像は`lattice --help`、各公開namespaceの正規構文は
142
177
  `lattice <plan|run|event|todo|sensor|factory-diagnostics|runtime-errors|bridge|hooks> --help`で確認できます。
@@ -145,12 +180,24 @@ CLIの全体像は`lattice --help`、各公開namespaceの正規構文は
145
180
 
146
181
  ## 開発
147
182
 
183
+ 製品local gateは次の3入口です。
184
+
148
185
  ```bash
149
186
  npm test
150
187
  npm run check
151
188
  npm run ci
189
+ ```
190
+
191
+ 製品localの保守で必要な時だけ次を使います。
192
+
193
+ ```bash
152
194
  node scripts/reap-orphan-test-daemons.mjs
153
195
  lattice sensor sync . --json
196
+ ```
197
+
198
+ 次は任意の工場統合診断であり、Lattice単独の開発gateまたはrelease gateではありません。
199
+
200
+ ```bash
154
201
  spotter doctor
155
202
  codex-sidecar diagnostics --project . --preset auditor --json
156
203
  ```
@@ -165,7 +212,7 @@ codex-sidecar diagnostics --project . --preset auditor --json
165
212
 
166
213
  Node.js 22.13以上、ただし25.xを除きます(`engines: >=22.13 <25 || >=26`。Node 25.xはV8 turboshaft
167
214
  WASM JITの不具合で同梱sensorが壊れるため、bannerを出して起動を止めます)。境界観測は配布物に同梱したLattice sensorだけを使い、PATH上の
168
- 廃止済みruntimeや旧cache/dataへfallbackしません。Spotterはproject単位で生成stateの所有境界を守ります。
215
+ 廃止済みruntimeや旧cache/dataへfallbackしません。Latticeはproject単位で生成stateの所有境界を守ります。
169
216
 
170
217
  どのrepoでも、Latticeの導入状態はdirectoryの有無を推測せず、最初に次のtyped discoveryで判定します。
171
218
 
@@ -188,7 +235,7 @@ lattice plan create --input .lattice/plan-create.json
188
235
 
189
236
  `invalid`をMarkdown fallbackへ丸めず、`next_action`に従ってstoreを診断してください。
190
237
  discoveryと初期transactionの不変条件は
191
- [ADR 0058](docs/adr/0058-project-discovery-and-initial-authoring.md)が正です。
238
+ [ADR 0058](https://github.com/kitepon/Lattice/blob/main/docs/adr/0058-project-discovery-and-initial-authoring.md)が正です。
192
239
 
193
240
  ## 実行runを端から端まで動かす
194
241
 
@@ -257,7 +304,7 @@ required evidenceを束縛した`todo phase accept`で重監査の判断を記
257
304
  「閉じた」の可視表現なので、監査の記録なしにそこへ行かせません。作成時に拒否はせず、
258
305
  `todo migrate`/`plan create`の結果と最後のdoneのadvisoryで`terminal_audit_required`を通知します。
259
306
  **終端監査はToDoのdispatch可否へ影響しません**(Phaseは重監査の順序だけを制御し、開始順はToDo DAGが決めます)。
260
- 規約は[ADR 0147](docs/adr/0147-audit-is-on-by-default.md)が正です。
307
+ 規約は[ADR 0147](https://github.com/kitepon/Lattice/blob/main/docs/adr/0147-audit-is-on-by-default.md)が正です。
261
308
 
262
309
  ```bash
263
310
  lattice todo phase status --plan <key> # phase無しplanでも暗黙Phaseを返す(implicit: true)
@@ -277,7 +324,8 @@ lattice todo phase baseline --reason <text> --except <監査したいplan_key>
277
324
  一括の入口は、現在監査待ちで一度も監査に触れていないPhaseだけを対象にします。**自動では
278
325
  実行しません**——どれを監査し、どれを歴史として畳むかは人が決め、Latticeは宣言を受け取って
279
326
  記録するだけです。`--except`は「最近の作業でコードも生きているので本当に監査したい」planを
280
- 残すための口です。規約は[ADR 0148](docs/adr/0148-history-closes-unaudited-not-audited.md)が正です。
327
+ 残すための口です。規約は
328
+ [ADR 0148](https://github.com/kitepon/Lattice/blob/main/docs/adr/0148-history-closes-unaudited-not-audited.md)が正です。
281
329
 
282
330
  誤ってphase無しで作ったplanへ後からPhaseを被せる場合は、`revise-phase`の
283
331
  `state_policy: acquire_phase`でdone状態を保ったまま獲得できます(未割当→割当の向きだけを許し、
@@ -310,13 +358,13 @@ LANや外部reverse proxyから閲覧するoptional bridgeは既定で無効で
310
358
  完走したplanは図の場所を取りません。除いた工程は凡例の件数、右ペインの「全工程」一覧、各工程の詳細から
311
359
  辿れ、詳細の前提・後続は除外前の依存関係を示します。総数・進捗・最長依存鎖は除外前の全工程で数えます。
312
360
  凡例の件数バッジを押すと全工程を描いた図へ切り替わります。表示規約は
313
- [ADR 0066](docs/adr/0066-gantt-live-scope-drops-finished-work.md)が正です。
361
+ [ADR 0066](https://github.com/kitepon/Lattice/blob/main/docs/adr/0066-gantt-live-scope-drops-finished-work.md)が正です。
314
362
 
315
363
  右ペインは概要・選択工程・全工程の3面で、いずれもToDo storeを表示します(元plan Markdown本文は
316
364
  再表示しません。元文書へは各工程の詳細が持つ行対応から辿ります)。全工程一覧は動いているplanを
317
365
  最終活動の新しい順で上に、全工程が図から外れた完走planを古い順で下にまとめ、plan内は登録順です。
318
366
  決着済みPhaseと図から外した工程は既定で畳み、開けば読めます。規約は
319
- [ADR 0067](docs/adr/0067-right-pane-shows-the-store-and-orders-by-activity.md)が正です。
367
+ [ADR 0067](https://github.com/kitepon/Lattice/blob/main/docs/adr/0067-right-pane-shows-the-store-and-orders-by-activity.md)が正です。
320
368
 
321
369
  `lattice todo gantt`と`lattice todo gantt status`による静的工程表は廃止済みで、呼ぶと
322
370
  `STATIC_GANTT_RETIRED`が動的dashboardを案内します。
@@ -331,7 +379,7 @@ daemonの生死はdescriptor 1枚では持ちません。daemonは自分のpid
331
379
  旧daemonが観測されないまま生き残ることはありません。descriptorだけを失った場合は2本目を建てず、
332
380
  配信を続けているdaemonを引き取ります。signalを送るのはその場で再認証を通った相手だけで、応答しない
333
381
  pidへは送りません(pid再利用で無関係のprocessを止めうるためです)。規約は
334
- [ADR 0157](docs/adr/0157-dashboard-daemons-are-discoverable-by-record.md)が正です。
382
+ [ADR 0157](https://github.com/kitepon/Lattice/blob/main/docs/adr/0157-dashboard-daemons-are-discoverable-by-record.md)が正です。
335
383
  状態を書き込む`start / block / unblock / done / evidence promote / reopen / note / independence mode /
336
384
  revise / revise-phase / revise-set / phase review|accept|reject|reopen|close-unaudited|baseline /
337
385
  dashboard adopt`では、監査actorとして次の3環境変数を渡せます。未設定ならhost/`session`/USERから
@@ -347,7 +395,7 @@ export LATTICE_TODO_ACTOR_AGENT=<agent-id>
347
395
  blobを書きます。flagの順は問いません。空の設計メモは拒否します。無策なら`NO_PLAN`と書いてください。
348
396
 
349
397
  authoring入口と閉じる口の契約は
350
- [ADR 0181](docs/adr/0181-authoring-entry-accepts-drafts.md)を参照してください。
398
+ [ADR 0181](https://github.com/kitepon/Lattice/blob/main/docs/adr/0181-authoring-entry-accepts-drafts.md)を参照してください。
351
399
 
352
400
  ## 特許
353
401
 
@@ -380,4 +428,4 @@ authoring入口と閉じる口の契約は
380
428
  **MIT License**のままです。upstreamの出自と帰属表示は[`sensor/NOTICE`](sensor/NOTICE)、
381
429
  license本文は[`sensor/LICENSE`](sensor/LICENSE)が持ちます。上記の条件はこれを変更しません。
382
430
 
383
- © 2026 quolu (kitepon-rgb)
431
+ © 2026 quolu (kitepon.dev)
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/og.png" alt="Lattice — several viable routes emerging through an apparently blocked mountain valley" width="100%">
2
+ <img src="https://raw.githubusercontent.com/kitepon/Lattice/main/.github/og.png" alt="Lattice — several viable routes emerging through an apparently blocked mountain valley" width="100%">
3
3
  <br>
4
4
  <sub><em>This image represents several viable paths emerging from terrain that first appeared blocked, as autonomous executors begin moving in coordination.</em></sub>
5
5
  </p>
@@ -231,7 +231,7 @@ live dependency diagram refuses to fold such a plan away, because folding is how
231
231
  says "closed", and nothing gets there without an evidence-bound `phase accept`. Creation is never
232
232
  rejected over it; the requirement is reported instead. And the audit gate never touches dispatch:
233
233
  phases order reviews, the ToDo DAG orders work
234
- ([ADR 0147](docs/adr/0147-audit-is-on-by-default.md)).
234
+ ([ADR 0147](https://github.com/kitepon/Lattice/blob/main/docs/adr/0147-audit-is-on-by-default.md)).
235
235
 
236
236
  **History closes unaudited, never audited.** Work that finished long ago cannot be audited — the
237
237
  code under review has already moved. Demanding an audit there produces either a false finding
@@ -240,7 +240,7 @@ code under review has already moved. Demanding an audit there produces either a
240
240
  incapable of passing as `accepted`** — phase-accept dependencies unlock on `accepted` alone. The
241
241
  bulk entry point never runs by itself, and the machine never infers "old enough to skip"; a human
242
242
  decides what gets audited and what becomes history
243
- ([ADR 0148](docs/adr/0148-history-closes-unaudited-not-audited.md)).
243
+ ([ADR 0148](https://github.com/kitepon/Lattice/blob/main/docs/adr/0148-history-closes-unaudited-not-audited.md)).
244
244
 
245
245
  ## Patent
246
246
 
@@ -259,13 +259,48 @@ Noncommercial use is permitted under the [License](#license) below.
259
259
  ## Ownership boundary
260
260
 
261
261
  This repository owns the plan/ToDo/run store, the bundled sensor, schemas, migrations,
262
- releases, and diagnostics. [dotagents](https://github.com/kitepon-rgb/dotagents) is the
263
- internal toolchain behind kitepon.dev and owns cross-product installation and host integration.
262
+ releases, and diagnostics. [dotagents](https://github.com/kitepon/dotagents) coordinates optional
263
+ cross-product factory installation and host integration; it does not own or control Lattice's
264
+ product state, update, diagnostics, or release.
264
265
 
265
- - Product philosophy: [PLAN.md](PLAN.md)
266
- - Public contract: [docs/00_product-contract.md](docs/00_product-contract.md)
267
- - Immutable decisions: [docs/adr/](docs/adr/)
268
- - Document map: [docs/README.md](docs/README.md)
266
+ - Product philosophy: [PLAN.md](https://github.com/kitepon/Lattice/blob/main/PLAN.md)
267
+ - Public contract: [docs/00_product-contract.md](https://github.com/kitepon/Lattice/blob/main/docs/00_product-contract.md)
268
+ - Immutable decisions: [docs/adr/](https://github.com/kitepon/Lattice/tree/main/docs/adr)
269
+ - Document map: [docs/README.md](https://github.com/kitepon/Lattice/blob/main/docs/README.md)
270
+
271
+ ## Update and diagnostics
272
+
273
+ The npm package is the standalone update path. After updating, typed discovery verifies the
274
+ installed CLI and refreshes the product-owned dashboard when needed. Check the bridge only on a
275
+ host where it is enabled; its supervised process replaces an older loaded version within one minute.
276
+
277
+ ```bash
278
+ npm install -g @quolu/lattice@latest
279
+ lattice --version
280
+ lattice status --json
281
+ lattice bridge status --json
282
+ ```
283
+
284
+ ## Release
285
+
286
+ Maintainers release Lattice from this repository. Update `package.json` and `CHANGELOG.md`, pass the
287
+ product gate, land and push the release commit on the default branch, then publish and verify that
288
+ same version from the registry. `prepublishOnly` rejects a dirty tree or a commit not landed on the
289
+ remote default branch.
290
+
291
+ ```bash
292
+ npm run ci
293
+ npm run verify:release-commit
294
+ npm publish --access public
295
+ npm install -g @quolu/lattice@<version>
296
+ lattice --version
297
+ lattice status --json
298
+ lattice bridge status --json
299
+ ```
300
+
301
+ The product CI contract is [docs/ci-contract.md](https://github.com/kitepon/Lattice/blob/main/docs/ci-contract.md). The deployment-specific
302
+ dashboard and bridge checks are in
303
+ [docs/operations/lattice-kitepon-deployment.md](https://github.com/kitepon/Lattice/blob/main/docs/operations/lattice-kitepon-deployment.md).
269
304
 
270
305
  ## Development
271
306
 
@@ -275,6 +310,21 @@ npm run check # syntax + control-character gate
275
310
  npm run ci # full gate
276
311
  ```
277
312
 
313
+ Product-local maintenance, when relevant:
314
+
315
+ ```bash
316
+ node scripts/reap-orphan-test-daemons.mjs
317
+ lattice sensor sync . --json
318
+ ```
319
+
320
+ These optional factory-integration diagnostics are not Lattice's standalone development or release
321
+ gate:
322
+
323
+ ```bash
324
+ spotter doctor
325
+ codex-sidecar diagnostics --project . --preset auditor --json
326
+ ```
327
+
278
328
  The full gate includes checks that are unusual and deliberate:
279
329
 
280
330
  - **`check:cli-surface`** — every shipped command must have help text *and* be exercised through
@@ -305,4 +355,4 @@ repository and remains under the **MIT License**. Its upstream origin and attrib
305
355
  recorded in [`sensor/NOTICE`](sensor/NOTICE); the license text is
306
356
  [`sensor/LICENSE`](sensor/LICENSE). The terms above do not modify it.
307
357
 
308
- © 2026 quolu (kitepon-rgb)
358
+ © 2026 quolu (kitepon.dev)
File without changes
File without changes
File without changes
File without changes
package/bin/lattice.mjs CHANGED
File without changes
@@ -4,6 +4,37 @@ Latticeのproject dashboardは既定でloopbackだけにbindし、bridge用socke
4
4
  LAN上のreverse proxyなどから閲覧する時だけ、利用者がlisten IPを明示してbridgeを有効化する。
5
5
  `postinstall`では質問やnetwork公開を行わない。
6
6
 
7
+ ## 工程操作から配信漏れを知る
8
+
9
+ `lattice status --json`とToDoの保存操作が、工程結果のstdoutに加え、stderrへ
10
+ `lattice.dashboard_delivery.v1`を返す。工程の保存成功を公開配信の成功と読み替えない。
11
+
12
+ | state | 確認できたこと |
13
+ | --- | --- |
14
+ | `local_only` | PC内の工程表が利用可能。`reason`は`bridge_unconfigured`または`bridge_disabled` |
15
+ | `bridge_only` | bridge設定を利用。hubが未設定のため、その先の公開配信は未確認 |
16
+ | `hub_delivered` | hubが対象工程を受理し、一覧にonlineで掲載し、工程HTMLの中継が成功 |
17
+ | `failed` | 配信を確認できない。`reason`と`next_action`で原因と復旧入口を返す |
18
+
19
+ 有効なbridge設定がある端末では、工程操作が常駐設定・実行体・実processを確認し、欠落や実行pathのずれを
20
+ 正規の`bridge reconfigure`で復旧してから配信を確認する。無効・未設定のbridgeは自動公開しない。
21
+ PCの初期化等で設定自体が失われた時は`local_only`がその事実を示す。公開先を推測して復元はしない。
22
+
23
+ 明示の復旧入口`lattice todo dashboard ensure --json`は、ローカルdaemonから設定済みbridgeとhub配信までを
24
+ 処理する。結果は`lattice.todo_dashboard_ensure_result.v1`で、`activity.delivery`に同じ配信結果を含む。
25
+ 配信失敗では非0を返す。通常の工程保存は配信故障で止めず、保存結果と配信失敗を別々に返す。
26
+
27
+ `bridge setup`/`reconfigure`にhubが設定されている場合は、dashboard起動、hubへの登録、一覧と工程HTMLの
28
+ 確認までを成功条件とする。stdoutの`lattice.bridge_cli_result.v4`は維持し、stderrへ確認結果を返す。通信や中継が
29
+ 失敗した時は`BRIDGE_HUB_DELIVERY_UNCONFIRMED`で非0を返す。通信故障でも保存済みの接続設定は保持する。
30
+ hub URLは端末から直接到達する登録先を使う。reverse proxy経由の登録は接続元がproxyになり、hubから端末への
31
+ 経路を誤る。別の公開HTTPS入口がある構成では、公開HTTPSとSSEの受入も本書後半に従って実施する。
32
+
33
+ 配信確認は全OS共通とし、常駐の復旧はMacのLaunchAgent、WindowsのStartup Folderの既存入口を使う。
34
+ 実行中の版だけが異なる場合は既存の自動更新に任せ、配信確認から再設定を強制しない。
35
+
36
+ ## 共通の設定入口
37
+
7
38
  TTYでは`lattice bridge setup`で安全側既定の対話wizardを開始できる。最初の公開確認は既定で無効を選び、
8
39
  cancelした場合は設定を変更しない。非TTYではhangせず、次の非対話commandを案内する。
9
40
  portを省略するか`auto`にすると、49152–65535から候補を重複なく選び、
@@ -106,7 +137,7 @@ lattice bridge reconfigure --json
106
137
  macOSのLaunchAgent載せ直しでは、`launchctl bootout`のexit 0はunload受付だけである。labelが
107
138
  domainから消える(`launchctl print`が113)前に`bootstrap`すると、launchdは
108
139
  `5 Input/output error`を返す。`reconfigure`はprint 113を待ってから載せる(0.60.7・
109
- [ADR 0179](adr/0179-launchctl-bootout-completes-when-print-returns-113.md))。失敗時は
140
+ [ADR 0179](https://github.com/kitepon/Lattice/blob/main/docs/adr/0179-launchctl-bootout-completes-when-print-returns-113.md))。失敗時は
110
141
  launchctlのexit codeとstderrが`lattice.cli_error.v2`の`detail`に残る。
111
142
 
112
143
  ### 公開面から自分のprojectが消えた時(`last_heartbeat`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.67.4",
3
+ "version": "0.68.0",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -64,13 +64,16 @@
64
64
  "ignore": "^7.0.5",
65
65
  "jsonc-parser": "^3.3.1",
66
66
  "picomatch": "^4.0.3",
67
- "remark-gfm": "^4.0.1",
68
- "remark-parse": "^11.0.0",
69
67
  "sisteransi": "^1.0.5",
70
68
  "tree-sitter-wasms": "^0.1.11",
71
- "unified": "^11.0.5",
72
69
  "web-tree-sitter": "^0.25.3"
73
70
  },
71
+ "devDependencies": {
72
+ "micromark-util-decode-string": "^2.0.1",
73
+ "remark-gfm": "^4.0.1",
74
+ "remark-parse": "^11.0.0",
75
+ "unified": "^11.0.5"
76
+ },
74
77
  "scripts": {
75
78
  "prepack": "npm --prefix sensor run build",
76
79
  "verify:release-commit": "node scripts/verify-release-commit.mjs",
@@ -79,11 +82,12 @@
79
82
  "test:profile": "node scripts/run-product-tests.mjs",
80
83
  "test:sensor": "npm --prefix sensor test",
81
84
  "check": "node scripts/check-syntax.mjs",
82
- "ci": "npm run test && npm run test:sensor && npm run check && npm run check:cli-surface && npm run check:open-questions && npm run check:reachability && npm run verify:todo-store",
85
+ "ci": "npm run check:docs && npm run test && npm run test:sensor && npm run check && npm run check:cli-surface && npm run check:open-questions && npm run check:reachability && npm run verify:todo-store",
83
86
  "verify:todo-store": "node bin/lattice.mjs todo verify --json",
84
87
  "check:cli-surface": "node scripts/verify-cli-surface.mjs",
85
88
  "check:open-questions": "node scripts/verify-open-questions.mjs",
86
89
  "check:reachability": "node scripts/verify-product-reachability.mjs",
90
+ "check:docs": "node scripts/verify-docs.mjs && node scripts/verify-packed-markdown.mjs && node --test test/documentation-ci-contract.test.mjs test/markdown-link-targets.test.mjs",
87
91
  "upstream:check": "node scripts/upstream-check.mjs",
88
92
  "upstream:sync": "node scripts/upstream-sync.mjs"
89
93
  },
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -6,6 +6,8 @@ import * as clack from '@clack/prompts';
6
6
  import packageJson from '../package.json' with { type: 'json' };
7
7
  import { resolveBridgeListenAddress } from './bridge-address.mjs';
8
8
  import { registerBridgeUpstream } from './bridge-registrar.mjs';
9
+ import { ensureTodoDashboardDaemon } from './todo-dashboard-registry.mjs';
10
+ import { verifyBridgeHubDelivery } from './dashboard-delivery.mjs';
9
11
  import {
10
12
  BridgeConfigError, configureBridge, disableBridge, readBridgeConfig, restoreBridgeConfig,
11
13
  normalizeBridgeAllowedHost, withBridgeOperationLock,
@@ -237,6 +239,26 @@ function result(action, config, recovery = null, liveness = null, diagnostics =
237
239
  warnings };
238
240
  }
239
241
 
242
+ /** 設定済みbridgeの常駐が欠けた場合だけ、正規の再設定経路で復旧する。 */
243
+ export async function recoverConfiguredBridge({ config, env = process.env,
244
+ launchAgent = platformLaunchAgent(), runtimeIdentity = readBridgeRuntimeIdentity,
245
+ reconfigure = runBridgeCli } = {}) {
246
+ const diagnostics = await bridgeDiagnostics({ config, launchAgent, env, runtimeIdentity });
247
+ if (diagnostics === null) return;
248
+ if (diagnostics.persistence.state === 'installed'
249
+ && diagnostics.persistence.node_exists && diagnostics.persistence.bridge_exists
250
+ && diagnostics.runtime?.state === 'running'
251
+ && diagnostics.drift.every((field) => field === 'version')) return;
252
+ let errorOutput = '';
253
+ const code = await reconfigure({ argv: ['reconfigure', '--json'], env,
254
+ stdout: { write() {} }, stderr: { write(chunk) { errorOutput += chunk; } },
255
+ launchAgent, runtimeIdentity });
256
+ if (code !== 0) {
257
+ const error = JSON.parse(errorOutput);
258
+ throw new BridgeConfigError(error.code, error.message, error.detail);
259
+ }
260
+ }
261
+
240
262
  export async function collectBridgeSetupWizard({ input, output, prompts = clack } = {}) {
241
263
  const common = { input, output };
242
264
  const enabled = await prompts.confirm({ ...common,
@@ -287,6 +309,7 @@ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
287
309
  stop: stopBridgeDaemon, clearStop: clearBridgeStopControl },
288
310
  launchAgent = platformLaunchAgent(), runtimeIdentity = readBridgeRuntimeIdentity,
289
311
  bridgePath = DEFAULT_BRIDGE_PATH,
312
+ ensureDashboard = ensureTodoDashboardDaemon, verifyDelivery = verifyBridgeHubDelivery,
290
313
  prompts = clack, probe = probeBridgeListener, interfaces = networkInterfaces() } = {}) {
291
314
  if (!Array.isArray(argv)) {
292
315
  return fail(stderr, 'USAGE', 'usage: lattice bridge <setup|reconfigure|status|disable|register> [options] --json');
@@ -392,6 +415,7 @@ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
392
415
  reuseCurrentPort: options.port === undefined,
393
416
  });
394
417
  try {
418
+ if (configured.hub !== null) await ensureDashboard({ env });
395
419
  if (!agentSnapshot.loaded && current !== null) {
396
420
  const requestStop = daemon.requestStop ?? daemon.stop ?? requestBridgeDaemonStop;
397
421
  await requestStop({ env, listen: current.listen });
@@ -414,6 +438,12 @@ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
414
438
  return configured;
415
439
  });
416
440
  } else return fail(stderr, 'USAGE', 'unknown bridge command or options');
441
+ // hubを指定した設定は、保存・常駐起動だけでは成功にしない。通信故障時も設定は
442
+ // 保持し、次の工程操作から同じ接続を復旧できるようにする。
443
+ if ((command === 'setup' || command === 'reconfigure') && config.hub !== null) {
444
+ const delivery = await verifyDelivery({ config, env });
445
+ stderr.write(`${JSON.stringify(delivery)}\n`);
446
+ }
417
447
  // Liveness and the persistence/runtime diagnostics are only meaningful for
418
448
  // a read: the mutating commands have just reconfigured the daemon and the
419
449
  // socket may not have settled yet.
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { randomBytes } from 'node:crypto';
19
19
  import { hostname } from 'node:os';
20
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
20
+ import { link, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
21
21
  import path from 'node:path';
22
22
 
23
23
  import { bridgeConfigPaths } from './bridge-config.mjs';
@@ -68,13 +68,18 @@ export async function readOrCreateBridgeHubTerminalId({ env = process.env } = {}
68
68
  if (error?.code !== 'ENOENT') throw error;
69
69
  }
70
70
  const terminalId = randomBytes(16).toString('hex');
71
+ const temporary = `${ref}.${terminalId}.tmp`;
72
+ await writeFile(temporary, `${JSON.stringify({ schema: TERMINAL_IDENTITY_SCHEMA, terminal_id: terminalId })}\n`,
73
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' });
71
74
  try {
72
- await writeFile(ref, `${JSON.stringify({ schema: TERMINAL_IDENTITY_SCHEMA, terminal_id: terminalId })}\n`,
73
- { encoding: 'utf8', mode: 0o600, flag: 'wx' });
75
+ // 完成済みのbytesだけを、既存のIDを上書きせず公開する。
76
+ await link(temporary, ref);
74
77
  return terminalId;
75
78
  } catch (error) {
76
79
  if (error?.code !== 'EEXIST') throw error;
77
- // Lost the create race to another process; loop back and read what it wrote.
80
+ // 同時生成の勝者が公開した完成済みIDを読む。
81
+ } finally {
82
+ await unlink(temporary);
78
83
  }
79
84
  }
80
85
  }
package/src/cli-help.mjs CHANGED
@@ -110,6 +110,7 @@ Write commands:
110
110
  reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]
111
111
  evidence promote --plan <key> --task <id> --evidence <file>
112
112
  # done状態と完了時刻を維持し、最新doneへ追記eventで証拠を再束縛する
113
+ start --plan <key> --task <id> --rebind --reason <text> # 改訂でcarryされたin-progressへ現版のstart束縛を積み直す(ADR 0191)
113
114
  dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
114
115
  dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
115
116
  # 開発中に発見した依存を明示接続する(同一plan内・plan跨ぎとも)。依存の自動推定は行わない
@@ -0,0 +1,103 @@
1
+ import { BridgeConfigError, readBridgeConfig } from './bridge-config.mjs';
2
+ import { createBridgeHubHeartbeatController } from './bridge-hub-heartbeat.mjs';
3
+
4
+ // 配信結果は全OS共通。常駐の復旧はbridge CLIがOS固有の入口へ委ねる。
5
+
6
+ const SCHEMA = 'lattice.dashboard_delivery.v1';
7
+ const SETUP = 'lattice bridge setup --listen <IP> --dashboard --hub <URL> --json';
8
+
9
+ function receipt(state, reason, projectIds = [], urls = [], nextAction = null) {
10
+ return { schema: SCHEMA, state, reason, project_ids: projectIds, urls, next_action: nextAction };
11
+ }
12
+
13
+ function unconfirmed(reason, projectIds = []) {
14
+ return new BridgeConfigError('BRIDGE_HUB_DELIVERY_UNCONFIRMED',
15
+ '工程の公開配信を確認できません。接続設定は保存済みです。', {
16
+ reason, project_ids: projectIds, next_action: 'lattice todo dashboard ensure --json',
17
+ });
18
+ }
19
+
20
+ /** 登録の受付、一覧への掲載、工程HTMLの中継を同じ呼出しで確認する。 */
21
+ export async function verifyBridgeHubDelivery({ config, env = process.env, projectId = null,
22
+ heartbeat = async () => createBridgeHubHeartbeatController({ env }).tick({ config }),
23
+ fetchImpl = fetch, timeoutMs = 5_000 } = {}) {
24
+ if (config.hub === null) return receipt('bridge_only', 'hub_unconfigured', [], [], SETUP);
25
+ const delivered = await heartbeat();
26
+ const result = delivered?.result;
27
+ const registered = result?.registered;
28
+ if (!['accepted', 'partial'].includes(delivered?.state)
29
+ || result?.schema !== 'lattice.bridge_hub_registration_result.v2'
30
+ || !Array.isArray(registered) || !registered.every((id) => typeof id === 'string')
31
+ || !Array.isArray(result.rejected)) {
32
+ throw unconfirmed(delivered?.state ?? 'heartbeat_missing');
33
+ }
34
+ const projectIds = projectId === null ? registered : [projectId];
35
+ if (projectIds.length === 0 || projectIds.some((id) => !registered.includes(id))
36
+ || (projectId === null && result.rejected.length > 0)) {
37
+ throw unconfirmed('project_not_registered', projectIds);
38
+ }
39
+ const request = (url, accept) => fetchImpl(url, { redirect: 'error',
40
+ headers: { accept }, signal: AbortSignal.timeout(timeoutMs) });
41
+ let listing;
42
+ try {
43
+ const response = await request(new URL('projects/', config.hub.url), 'application/json');
44
+ if (response.status !== 200) throw unconfirmed('hub_listing_unavailable', projectIds);
45
+ listing = await response.json();
46
+ } catch (error) {
47
+ if (error?.code === 'BRIDGE_HUB_DELIVERY_UNCONFIRMED') throw error;
48
+ throw unconfirmed('hub_listing_unavailable', projectIds);
49
+ }
50
+ if (!Array.isArray(listing) || projectIds.some((id) =>
51
+ !listing.some((entry) => entry.project_id === id && entry.status === 'online'))) {
52
+ throw unconfirmed('project_not_visible', projectIds);
53
+ }
54
+ const urls = [];
55
+ for (const id of projectIds) {
56
+ const url = new URL(`projects/${encodeURIComponent(id)}/`, config.hub.url);
57
+ try {
58
+ const response = await request(url, 'text/html');
59
+ if (response.status !== 200 || !response.headers.get('content-type')?.includes('text/html')) {
60
+ throw unconfirmed('project_unreachable', [id]);
61
+ }
62
+ const html = await response.text();
63
+ if (!/<title>Lattice — [^<]+依存工程図<\/title>/u.test(html)) {
64
+ throw unconfirmed('project_response_invalid', [id]);
65
+ }
66
+ } catch (error) {
67
+ if (error?.code === 'BRIDGE_HUB_DELIVERY_UNCONFIRMED') throw error;
68
+ throw unconfirmed('project_unreachable', [id]);
69
+ }
70
+ urls.push(url.href);
71
+ }
72
+ return receipt('hub_delivered', null, projectIds, urls);
73
+ }
74
+
75
+ export async function ensureProjectDashboardDelivery({ projectId, env = process.env,
76
+ readConfig = readBridgeConfig, verify = verifyBridgeHubDelivery,
77
+ recover = async (options) => {
78
+ const { recoverConfiguredBridge } = await import('./bridge-cli.mjs');
79
+ await recoverConfiguredBridge(options);
80
+ } } = {}) {
81
+ const config = await readConfig({ env });
82
+ if (config === null || !config.enabled) {
83
+ return receipt('local_only', config === null ? 'bridge_unconfigured' : 'bridge_disabled',
84
+ [projectId], [], SETUP);
85
+ }
86
+ await recover({ config, env });
87
+ return verify({ config: await readConfig({ env }), env, projectId });
88
+ }
89
+
90
+ /** stdoutの工程契約を変えず、保存と配信の結果を別のtyped receiptで伝える。 */
91
+ export async function reportProjectDashboardDelivery({ projectId, env, stderr,
92
+ ensureDelivery = ensureProjectDashboardDelivery }) {
93
+ let delivery;
94
+ try { delivery = await ensureDelivery({ projectId, env }); }
95
+ catch (error) {
96
+ // 外部境界の故障だけを配信失敗へ写す。内部の型・実装不整合は隠さない。
97
+ if (typeof error?.code !== 'string') throw error;
98
+ delivery = receipt('failed', error.code, [projectId], [],
99
+ error.detail?.next_action ?? 'lattice bridge status --json');
100
+ }
101
+ stderr?.write(`${JSON.stringify(delivery)}\n`);
102
+ return delivery;
103
+ }
@@ -0,0 +1,16 @@
1
+ import { rename } from 'node:fs/promises';
2
+ import { setTimeout } from 'node:timers/promises';
3
+
4
+ /** Windowsは読み手が開いている宛先へのrenameを一時的にEPERMで拒否する。
5
+ * 同じatomic renameを待って再実行し、恒久的な拒否は元のエラーで返す。 */
6
+ export async function renamePublishedFile(source, destination, {
7
+ renameFile = rename, platform = process.platform, wait = setTimeout, attempts = 8,
8
+ } = {}) {
9
+ for (let attempt = 0; ; attempt++) {
10
+ try { return await renameFile(source, destination); }
11
+ catch (error) {
12
+ if (platform !== 'win32' || error?.code !== 'EPERM' || attempt + 1 >= attempts) throw error;
13
+ await wait(Math.min(64, 2 ** attempt));
14
+ }
15
+ }
16
+ }
@@ -42,7 +42,9 @@ export function gitCatFileBatch(objectNames, { cwd, maxBodyBytes }) {
42
42
  maxBuffer: (maxBodyBytes + 256) * objectNames.length,
43
43
  stdio: ['pipe', 'pipe', 'ignore'],
44
44
  });
45
- if (result.status !== 0 || result.stdout === undefined || result.stdout === null) {
45
+ // Linux / Node 24ではmaxBuffer超過時にもstatus=0と完全stdoutを返しつつ、errorだけを
46
+ // ENOBUFSにする競合がある。statusだけで成功判定するとprocess自身が報告した失敗を失う。
47
+ if (result.error || result.status !== 0 || result.stdout === undefined || result.stdout === null) {
46
48
  throw new Error(`git cat-file --batch failed: status=${result.status ?? 'signal'}`);
47
49
  }
48
50
  const stdout = result.stdout;
@@ -24,6 +24,7 @@ import {
24
24
  import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
25
25
  import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
26
26
  import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
27
+ import { reportProjectDashboardDelivery } from './dashboard-delivery.mjs';
27
28
  import { resolveProjectIdentity } from './project-identity.mjs';
28
29
  import {
29
30
  buildTodoPlan,
@@ -336,8 +337,9 @@ async function resolveProjectState({ cwd, cliVersion, diagnoseStructureArtifacts
336
337
  }
337
338
  }
338
339
 
339
- export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
340
- ensureDashboardActivity = ensureTodoDashboardActivity }) {
340
+ export async function runProjectStatus({ cwd, stdout, stderr = process.stderr, cliVersion, env = process.env,
341
+ ensureDashboardActivity = ensureTodoDashboardActivity,
342
+ reportDelivery = reportProjectDashboardDelivery }) {
341
343
  const state = await resolveProjectState({
342
344
  cwd, cliVersion, diagnoseStructureArtifacts: true,
343
345
  });
@@ -349,13 +351,19 @@ export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.
349
351
  repoRoot: state.repoRoot, projectId: state.store.project_id, env,
350
352
  });
351
353
  const actorSession = env.LATTICE_TODO_ACTOR_SESSION;
352
- await ensureDashboardActivity({
354
+ const activity = await ensureDashboardActivity({
353
355
  repoRoot: state.repoRoot, projectId: state.store.project_id,
354
356
  displayName: identity.displayName,
355
357
  sessionId: isTodoIdentifier(actorSession) ? actorSession : `status-${process.pid}`, env,
356
358
  });
357
- } catch {
359
+ if (activity !== undefined) {
360
+ await reportDelivery({ projectId: state.store.project_id, env, stderr });
361
+ }
362
+ } catch (error) {
358
363
  // dashboardはdiscoveryの副作用。statusをdashboard故障で止めない(ADR 0181)。
364
+ stderr.write(`${JSON.stringify({ schema: 'lattice.dashboard_delivery.v1', state: 'failed',
365
+ reason: error.code ?? 'DASHBOARD_FAILED', project_ids: [state.store.project_id], urls: [],
366
+ next_action: 'lattice todo dashboard ensure --json' })}\n`);
359
367
  }
360
368
  }
361
369
  stdout.write(`${JSON.stringify(state.result)}\n`);
@@ -16,7 +16,7 @@ export function osObservationEnvironment() { return { ...process.env, LC_ALL: 'C
16
16
 
17
17
  /** processのstart identity(darwin/linuxはps lstart、win32はPowerShell StartTime Ticks)の生文字列を返す。 */
18
18
  export async function observeStartIdentityRaw(pid) {
19
- const executable = process.platform === 'win32' ? 'powershell.exe' : '/bin/ps';
19
+ const executable = process.platform === 'win32' ? 'pwsh.exe' : '/bin/ps';
20
20
  const args = process.platform === 'win32'
21
21
  ? ['-NoProfile', '-NonInteractive', '-Command',
22
22
  `[System.Diagnostics.Process]::GetProcessById(${pid}).StartTime.ToUniversalTime().Ticks`]
@@ -37,7 +37,7 @@ export async function observeWindowsWorkerProcess(pid) {
37
37
  }
38
38
  const filter = JSON.stringify(`ProcessId=${pid}`);
39
39
  const script = `Get-CimInstance Win32_Process -Filter ${filter} | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress`;
40
- const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', script], {
40
+ const { stdout } = await execFileAsync('pwsh.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
41
41
  encoding: 'utf8',
42
42
  });
43
43
  const row = JSON.parse(stdout.trim() || 'null');
package/src/todo-cli.mjs CHANGED
@@ -901,6 +901,47 @@ async function startTask({
901
901
  structureContext });
902
902
  }
903
903
 
904
+ // plan改訂でcarryされたin-progress ToDoの再束縛入口(ADR 0191)。task状態・開始時刻は
905
+ // 変えず、現plan_version・現actorのstart束縛だけをjournalへ積む。runtime intakeは
906
+ // literal startだけを束縛に使いcarryを推定しないため、改訂を跨いだ進行中工程の
907
+ // 再開はこの入口だけが正規経路になる。advisory・independence gateは初回startで
908
+ // 済んでいるため再適用しない。
909
+ async function rebindStart({ repoRoot, env, planKey, taskId, reason }) {
910
+ const store = await readTodoStore({ repoRoot });
911
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
912
+ if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
913
+ const task = member.tasks.find(({ task_id: candidate }) => candidate === taskId);
914
+ if (task === undefined) {
915
+ throw new TodoStoreError('TASK_NOT_FOUND', 'task_not_found', undefined, {
916
+ requested_task_id: taskId,
917
+ });
918
+ }
919
+ if (task.status !== 'in-progress') {
920
+ throw new TodoStoreError('TASK_START_BINDING_UNSUPPORTED', 'rebind_requires_in_progress', undefined, {
921
+ task_id: taskId, status: task.status,
922
+ });
923
+ }
924
+ let noteContext;
925
+ try {
926
+ ({ context: noteContext } = await readTodoNoteContext({ repoRoot, store, planKey, taskId }));
927
+ } catch (error) {
928
+ if (!['NOTE_LOG_CORRUPT', 'NOTE_PROJECTION_INVALID'].includes(error?.code)) throw error;
929
+ noteContext = {
930
+ schema: 'lattice.todo_note_context.unreadable.v1',
931
+ project_id: member.plan.project_id,
932
+ plan_key: planKey,
933
+ task_id: taskId,
934
+ code: error.code,
935
+ reason: error.detail?.reason ?? 'note_context_unreadable',
936
+ next_action: `lattice todo note list --plan ${planKey} --json`,
937
+ };
938
+ }
939
+ return mutate({
940
+ repoRoot, env, planKey, taskId, kind: 'start',
941
+ payload: { start_mode: 'rebind', reason }, evidenceRef: null, noteContext,
942
+ });
943
+ }
944
+
904
945
  async function retractStart({ repoRoot, env, planKey, taskId, reason }) {
905
946
  const actor = mutationActor(env);
906
947
  const store = await readTodoStore({ repoRoot });
@@ -3483,23 +3524,34 @@ function writesTodoStore(argv) {
3483
3524
  }
3484
3525
  }
3485
3526
 
3486
- async function ensureActiveProjectDashboard({ repoRoot, env }) {
3527
+ async function ensureActiveProjectDashboard({ repoRoot, env, stderr, strict = false }) {
3487
3528
  if (env.LATTICE_DASHBOARD_AUTOSTART === '0') return null;
3488
3529
  let actor;
3489
- try { actor = mutationActor(env); } catch { return null; }
3530
+ try { actor = mutationActor(env); } catch (error) { if (strict) throw error; return null; }
3490
3531
  const sessionId = actor.session;
3491
3532
  let store;
3492
3533
  try { store = await readTodoStoreStable({ repoRoot }); }
3493
- catch { return null; }
3534
+ catch (error) { if (strict) throw error; return null; }
3494
3535
  let identity;
3495
3536
  try { identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env }); }
3496
- catch { return null; }
3537
+ catch (error) { if (strict) throw error; return null; }
3497
3538
  try {
3498
- return await ensureTodoDashboardActivity({
3539
+ const activity = await ensureTodoDashboardActivity({
3499
3540
  repoRoot, projectId: store.project_id, displayName: identity.displayName, sessionId, env,
3500
3541
  });
3501
- } catch {
3542
+ const { reportProjectDashboardDelivery } = await import('./dashboard-delivery.mjs');
3543
+ const delivery = await reportProjectDashboardDelivery({ projectId: store.project_id, env, stderr });
3544
+ if (strict && delivery.state === 'failed') {
3545
+ throw new TodoStoreError('DASHBOARD_DELIVERY_FAILED', 'dashboard_delivery_failed', undefined,
3546
+ { delivery });
3547
+ }
3548
+ return { ...activity, delivery };
3549
+ } catch (error) {
3502
3550
  // dashboardは副作用。start/doneをdashboard故障で止めない(ADR 0181)。
3551
+ if (strict) throw error;
3552
+ stderr?.write(`${JSON.stringify({ schema: 'lattice.dashboard_delivery.v1', state: 'failed',
3553
+ reason: error.code ?? 'DASHBOARD_FAILED', project_ids: [store.project_id], urls: [],
3554
+ next_action: 'lattice todo dashboard ensure --json' })}\n`);
3503
3555
  return null;
3504
3556
  }
3505
3557
  }
@@ -3703,7 +3755,8 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3703
3755
  // コマンドの副作用でしか daemon を ensure できず、OS再起動後に誰も書込まなければ
3704
3756
  // 公開工程図が503のまま沈黙した。launchd等の周期実行から叩く冪等な入口。
3705
3757
  action = async (repoRoot) => {
3706
- const ensured = await ensureActiveProjectDashboard({ repoRoot, env });
3758
+ const ensured = await ensureActiveProjectDashboard({ repoRoot,
3759
+ env: { ...env, LATTICE_DASHBOARD_AUTOSTART: '1' }, stderr, strict: true });
3707
3760
  return { schema: 'lattice.todo_dashboard_ensure_result.v1',
3708
3761
  ensured: ensured !== null, activity: ensured };
3709
3762
  };
@@ -3970,11 +4023,18 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3970
4023
  action = (repoRoot) => phaseBaseline({ repoRoot, env, reason: argv[3], exceptPlanKeys });
3971
4024
  } else if (argv[0] === 'start') {
3972
4025
  const flags = matchFlagCommand(argv, ['start'], {
3973
- known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed'],
4026
+ known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed', 'rebind', 'reason'],
3974
4027
  required: ['plan', 'task'],
3975
- booleans: ['parallel-frontier', 'serial-confirmed'],
4028
+ booleans: ['parallel-frontier', 'serial-confirmed', 'rebind'],
3976
4029
  });
3977
- if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
4030
+ if (flags !== null && flags.rebind === true) {
4031
+ if (isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
4032
+ && typeof flags.reason === 'string' && flags.reason.length > 0) {
4033
+ action = (repoRoot) => rebindStart({
4034
+ repoRoot, env, planKey: flags.plan, taskId: flags.task, reason: flags.reason,
4035
+ });
4036
+ }
4037
+ } else if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
3978
4038
  && (flags['override-reason'] === undefined || (typeof flags['override-reason'] === 'string'
3979
4039
  && flags['override-reason'].length > 0))) {
3980
4040
  action = (repoRoot) => startTask({
@@ -4099,7 +4159,7 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
4099
4159
  // `readTodoStoreStable` to read (2026-08-10 P0: `todo verify` was
4100
4160
  // unreachable for exactly the case it exists to diagnose).
4101
4161
  if (writesTodoStore(argv)) {
4102
- await ensureActiveProjectDashboard({ repoRoot, env });
4162
+ await ensureActiveProjectDashboard({ repoRoot, env, stderr });
4103
4163
  }
4104
4164
  const result = atomicCommit
4105
4165
  ? await commitTodoStoreMutation({ repoRoot, argv, action, env })
@@ -532,6 +532,12 @@ function validPayload(event) {
532
532
  && isTodoDigest(payload.target_event_digest)
533
533
  && nullableText(payload.reason) && payload.reason !== null;
534
534
  }
535
+ // rebind: plan改訂でcarryされたin-progress ToDoへ、現plan_versionの明示的start束縛を積み直す
536
+ // (runtime intakeはliteral startだけを束縛に使い、carryを推定しない。ADR 0191)
537
+ if (event.kind === 'start' && payload?.start_mode === 'rebind') {
538
+ return exactRecord(payload, ['start_mode', 'reason'])
539
+ && nullableText(payload.reason) && payload.reason !== null;
540
+ }
535
541
  if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
536
542
  if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
537
543
  && nullableText(payload.reason) && payload.reason !== null
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { fsyncDirectory } from './fs-dir-sync.mjs';
3
+ import { renamePublishedFile } from './fs-publish.mjs';
3
4
  import {
4
5
  lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir,
5
6
  } from 'node:fs/promises';
@@ -678,6 +679,17 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
678
679
  if (state === undefined) fail('STORE_INCONSISTENT', 'event_task_missing');
679
680
  const dependenciesDone = localPredecessors(plan, event.task_id).every((id) => states.get(id)?.status === 'done');
680
681
  if (event.kind === 'start') {
682
+ if (event.payload.start_mode === 'rebind') {
683
+ // 改訂でcarryされたin-progressの再束縛。状態・開始時刻は動かさず、start束縛だけを
684
+ // 現actor・現plan_versionのeventへ更新する(ADR 0191)
685
+ if (state.status !== 'in-progress') {
686
+ fail('STORE_INCONSISTENT', 'invalid_rebind_start_transition');
687
+ }
688
+ authoredStartBindings.set(event.task_id, {
689
+ actor: structuredClone(event.actor), event_digest: event.event_digest,
690
+ });
691
+ continue;
692
+ }
681
693
  if (event.payload.start_mode === 'historical_import') {
682
694
  if (!importedGenesis || state.status !== 'pending') {
683
695
  fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
@@ -1125,6 +1137,7 @@ function validateMergedTransition(store, member, event) {
1125
1137
  const phaseAcceptReady = mergedPhaseAcceptReady(store, targetKey);
1126
1138
  const dependenciesDone = taskDependenciesDone && phaseAcceptReady;
1127
1139
  if (event.kind === 'start' && event.payload.start_mode !== 'historical_import'
1140
+ && event.payload.start_mode !== 'rebind'
1128
1141
  && (!phaseAcceptReady || (!taskDependenciesDone && event.payload.override_reason === null))) {
1129
1142
  fail('STORE_INCONSISTENT', 'invalid_start_transition');
1130
1143
  }
@@ -1198,7 +1211,10 @@ function readEvidenceBlob(absoluteRepo, oid) {
1198
1211
  const [entry] = gitCatFileBatch([oid], {
1199
1212
  cwd: absoluteRepo, maxBodyBytes: TODO_LIMITS.narrativeSectionBytes + 1,
1200
1213
  });
1201
- if (entry.missing || entry.type !== 'blob') throw new Error('not blob');
1214
+ // child_processのmaxBufferはprocess出力の総量を制限するだけで、証拠1件の意味上限ではない。
1215
+ // 契約上限はbytesで明示判定し、process bufferの打切りを意味判定に使わない。
1216
+ if (entry.missing || entry.type !== 'blob'
1217
+ || entry.bytes.length > TODO_LIMITS.narrativeSectionBytes) throw new Error('not bounded blob');
1202
1218
  if (evidenceBlobCache.size >= EVIDENCE_BLOB_CACHE_LIMIT) {
1203
1219
  evidenceBlobCache.delete(evidenceBlobCache.keys().next().value);
1204
1220
  }
@@ -1267,7 +1283,10 @@ function pinnedSourceLine(repoRoot, source, cache = null) {
1267
1283
  }
1268
1284
  if (blob === undefined) {
1269
1285
  const blobEntry = entries.shift();
1270
- if (blobEntry.missing || blobEntry.type !== 'blob') throw new Error('not blob');
1286
+ // commitとblobを同じbatchで読むと総buffer予算は2件分になる。source 1件の意味上限は
1287
+ // cacheへ入れる前に実bytesで判定する。
1288
+ if (blobEntry.missing || blobEntry.type !== 'blob'
1289
+ || blobEntry.bytes.length > TODO_LIMITS.narrativeSectionBytes) throw new Error('not bounded source blob');
1271
1290
  blob = blobEntry.bytes;
1272
1291
  cache?.blobs.set(objectSpec, blob);
1273
1292
  }
@@ -1704,7 +1723,7 @@ async function atomicWrite(absolute, bytes) {
1704
1723
  process.stderr.write(`lattice: fsync skipped (WSL2/NTFS EIO) ${absolute}\n`);
1705
1724
  }
1706
1725
  await handle.close(); handle = null;
1707
- await rename(temporary, absolute);
1726
+ await renamePublishedFile(temporary, absolute);
1708
1727
  await fsyncDirectory(path.dirname(absolute));
1709
1728
  } finally {
1710
1729
  if (handle) await handle.close();
@@ -1725,7 +1744,7 @@ async function atomicWriteMode(absolute, bytes, mode) {
1725
1744
  process.stderr.write(`lattice: fsync skipped (WSL2/NTFS EIO) ${absolute}\n`);
1726
1745
  }
1727
1746
  await handle.close(); handle = null;
1728
- await rename(temporary, absolute);
1747
+ await renamePublishedFile(temporary, absolute);
1729
1748
  await fsyncDirectory(path.dirname(absolute));
1730
1749
  } finally {
1731
1750
  if (handle) await handle.close();