@abelxiaoxing/cadence 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,29 +1,24 @@
1
1
  # Cadence
2
2
 
3
3
  `@abelxiaoxing/cadence` is a shared Pi task workspace with two independently usable clients: native `/cadence` and a browser WebUI.
4
- One explicitly started service owns tasks, bounded subagent collaboration, real parent checks and retained history.
5
- Ordinary conversations, resource discovery, installation and opening history never start work or a service.
4
+ One persistent service owns tasks, bounded subagent collaboration, real parent checks and retained history.
5
+ Ordinary conversations, resource discovery and opening history never start work or a service.
6
+ On Linux, Pi's user-scope install registers that service as a systemd user unit and starts its WebUI on the default address.
6
7
 
7
8
  This is a trusted workflow assistant, **not a sandbox, certified executor or transactional application system**.
8
9
  Code workers edit the chosen working copy directly; recovery can require human work.
9
10
  Model prose, tool results and human Done do not certify checks or code.
10
11
 
11
- ## Upgrading from 1.x
12
-
13
- Version 2.0.0 replaces the four Abel workflow prompts (`/abel-init`, `/abel-design`, `/abel-implement`, `/abel-diagnose`) with native `/cadence` and the WebUI backed by one explicitly started service.
14
- This is a breaking change, not an in-place continuation of the 1.x workflow.
15
- Stop older Cadence sessions before switching; existing records and credentials are not imported, migrated, deleted or replayed automatically.
16
- Node `>=22.19.0` and the declared external peers, including Undici, are required; `ws` is now a production dependency.
17
- Installation still does not start the service or execute tasks; follow the loading and explicit startup instructions below.
18
-
19
12
  ## Load and start explicitly
20
13
 
21
14
  The package can be loaded as a reviewed local package, extracted tarball or installed npm package through Pi's package mechanism.
22
15
  For a local checkout, add its absolute or relative path to Pi's `packages` setting, or try `pi -e ./cadence`.
23
16
  Pi loads `src/index.ts`; the service, worker and readonly operator are ordinary JavaScript with no TypeScript/source-tree fallback.
24
- The manifest does not impose a Pi version range; public Pi peers stay external.
17
+ The manifest does not impose a Pi version range; inside Pi the extension uses Pi's own public packages.
25
18
  Development pins, declared Node baseline (`>=22.19.0`) and actual tested runtime identities are separate facts, not universal compatibility promises.
26
- Required installed peers include public Pi packages, Typebox and Undici; `ws` is the sole production dependency.
19
+ Required installed peers are public Pi packages and Typebox; `undici` and `ws` are exact production dependencies.
20
+ Pi's package installer omits peers and supplies only its own packages and Typebox to the extension, so any other extension import must be a production dependency.
21
+ The standalone owner and workers run outside Pi and import `pi-coding-agent`/`pi-ai` by bare name, so they load the exact tested copy (0.84.3) from optional dependencies; an install that omits optional dependencies cannot start an owner.
27
22
  Browser assets are prebuilt, not compiled by consumer postinstall.
28
23
 
29
24
  Before using the shared owner, stop older Cadence sessions and choose one coordination state-home for the checkout.
@@ -38,8 +33,21 @@ cadence-service token-link --workspace /absolute/project --reveal
38
33
  Use the installed `cadence-service` bin, or `node /absolute/cadence/src/service-cli.mjs` followed by those arguments.
39
34
  Default binding is numeric loopback `127.0.0.1:17423`; `--foreground` is available for an explicitly configured supervisor.
40
35
  Start reads the existing Pi agent directory (or an explicit private configuration), never creates another login/profile.
36
+ A directory that contains the private state home (the home directory and its ancestors, such as `C:\Users\<you>`) cannot be a workspace, so commands there use `~/cadence` instead of refusing; only `start` creates it, and `/cadence` started there attaches to that owner.
41
37
  See [service management](docs/service-management.md) for configuration, model/auth policy, Stop, diagnosis and informed recovery.
42
38
 
39
+ ### Linux autostart after `pi install`
40
+
41
+ `pi install npm:@abelxiaoxing/cadence` (user scope) writes `~/.local/share/systemd/user/cadence.service`, enables it and starts the owner for `~/cadence` on `http://127.0.0.1:17423`.
42
+ Set an existing absolute `CADENCE_WORKSPACE` at install to choose another workspace, or `CADENCE_AUTOSTART=0` to skip registration.
43
+ Enter the WebUI once with the link from `cadence-service token-link --workspace ~/cadence --reveal`; installation prints that command, not the token.
44
+ Each `pi update` restarts the unit through orderly Stop, so running work is stopped rather than detached; `systemctl --user disable --now cadence` or `mask` is kept across upgrades.
45
+ Run `cadence-service autostart remove` before `pi remove`.
46
+ The npm client hides dependency install-script output, and with `strict-allow-scripts` it skips those scripts entirely.
47
+ Approve Cadence once with `npm install-scripts approve @abelxiaoxing/cadence` inside `~/.pi/agent/npm` (or `npm config set allow-scripts=@abelxiaoxing/cadence --location=user`), or run `cadence-service autostart install`; `/cadence` names that command while no unit is registered.
48
+ Until the optional SDK and Pi's `auth.json` exist, registration or start waits instead of quarantining an owner.
49
+ See [the systemd user unit](docs/service-management.md#linux-systemd-user-unit) for exact behavior and limits.
50
+
43
51
  ### Native terminal
44
52
 
45
53
  In a trusted, idle Pi TUI, type `/cadence [goal]` using fresh native Enter.
@@ -114,24 +122,20 @@ Outbound provider proxy policy is independent of inbound HTTPS and refuses unsup
114
122
  ## Development and evidence
115
123
 
116
124
  ```sh
117
- bun run verify # default fast verification; no browser
118
- bun run test # reviewed fast root regressions
119
- bun run test:target test/service-contract.test.ts
125
+ bun run check
126
+ bun run check:web
127
+ bun run build:check
128
+ bun run lint
129
+ bun run check:agents
130
+ bun run test:service
120
131
  bun run test:web
121
- bun run test:integration # explicit slow SDK/PTY/history lane
122
- CADENCE_BROWSER_PATH=/absolute/existing/chromium bun run verify:full
132
+ CADENCE_BROWSER_PATH=/absolute/existing/chromium bun run test:browser
133
+ CADENCE_BROWSER_PATH=/absolute/existing/chromium bun run verify
123
134
  bun pm pack --destination /absolute/private-temp
124
135
  ```
125
136
 
126
137
  Build/verification use a fresh private directory with `TMPDIR`, `TEMP` and `TMP` set explicitly.
127
- `verify` runs root/Web type checks, exact generated-byte checks, lint, Agent pins, fast root regressions, Web unit tests and the real pack audit.
128
- The fast root lane uses an explicit reviewed list in `test/fast.config.ts`, with at most four parallel workers; no test is selected from mutable timing caches.
129
- SDK/provider cycles, PTYs, large-history/native stress scenarios and real browsers are not part of the default command or fast CI matrix.
130
- `test:integration` selects the remaining root tests serially; new root tests stay there until explicitly promoted to the fast list.
131
- `verify:full` runs fast verification, those remaining tests and real-browser scenarios once each; it can take tens of minutes and must be requested explicitly.
132
- Fast verification is not full integration/browser qualification.
133
- Missing required browser in the full lane is non-green, never an automatic download.
134
- For a focused slow regression, pass its file to `test:target`; `test:service` and `test:browser` remain separate explicit lanes.
138
+ `verify` includes native/unit tests, web tests, required real-browser scenarios and exact real-pack auditing; missing required browser is non-green, never an automatic download.
135
139
  Source/extracted PTYs, fabricated loopback providers and isolated HTTPS are distinct from physical humans/devices, paid accounts, package-manager installation and native Windows/macOS/ConPTY qualification.
136
140
  Those additional lanes remain unqualified unless their own receipts exist.
137
141
  No passing gate authorizes commit, push, publication, installation, deployment or cleanup.
@@ -149,7 +153,7 @@ Historical logs are not runtime dependencies, and removing development tooling m
149
153
  | Owner | Responsibility |
150
154
  | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
151
155
  | `src/index.ts`, `src/task-shell.ts`, `src/native-*.ts` | Nonactivating Pi entry, raw native consent, typed service attachment, paging and verified export; no embedded executor |
152
- | `src/service/host.ts`, `src/service/cli.ts` | Real single-owner composition and explicit management; generated management entry stays SDK-free |
156
+ | `src/service/host.ts`, `src/service/cli.ts`, `src/service/autostart.ts` | Real single-owner composition, explicit management and the generated Linux user unit; management entry stays SDK-free |
153
157
  | `src/service/{coordination,private-files,owner,recovery}.ts` | Canonical overlap exclusion, exact locks, original-operation lifetime and informed local recovery |
154
158
  | `src/service/{admission,task-actions,graph,scheduler}.ts` | Context-bound actions, actual current views, finite graph scheduling, check/review barriers |
155
159
  | `src/service/{model-config,model-context,model-choice,auth-network}.ts`, `src/sdk-network.ts` | Existing Pi sources, explicit exact model/thinking, original OAuth writeback and strict shared network policy |
@@ -186,6 +190,7 @@ Use their current declarations rather than copying DTO parsing, reducers, a seco
186
190
  - Independent readonly assignments may overlap; writers and native parent checks exclude readers and other writers across tasks.
187
191
  Reserve bounded slots/bytes before copying/auth/effects; dependencies use exact committed predecessor content only after actual native and recording settlement.
188
192
  Overflow, stale dependencies, first failure or wait expiry closes admission without retry; independent already-running siblings retain their own outcomes.
193
+ Each assignment or check observes the working copy at its baseline, immediately before its native launch and after exit; the steps between recheck ownership, sources and definitions without another Git/file capture.
189
194
  - Register the original accepted control flow before its first effect, in addition to separate native/auth/storage originals.
190
195
  Reporting timeout, a caught promise, PID absence or an empty roster cannot release resources.
191
196
  Stop publishes one shared attempt, quiesces all admissions, requests stops before awaiting children and joins permitted original continuation/safety writes.
@@ -209,6 +214,7 @@ A fresh check may account for exactly one owned generation advance bound to its
209
214
  Do not replace this proof with a general before≤after comparison.
210
215
  Changes to files, definitions, epoch/cwd/environment or a foreign generation stale the current obligation without rewriting historical native exit or PASS.
211
216
  Human Review binds exact selected run/graph/check/change identities and recaptures under a short exclusive barrier only after confirmation, not throughout the human modal.
217
+ One accepted Review folds its fixed committed snapshot (plan proof, selected run, check results) once; every barrier validation still rechecks raw task bytes, changes, configuration, choice and active work.
212
218
  Acknowledgment permits reviewed-with-issues, never changes failed/partial/canceled/native/recording/check facts or revives blocked execution.
213
219
  Unavailable concurrent views may display conservative stale status but cannot advance authoritative review revisions and invalidate an otherwise unchanged accepted Review.
214
220
 
@@ -235,6 +241,13 @@ Only the serialized history writer assigns committed sequence and persisted cont
235
241
  Commit actual admission/auth/launch/result facts in order; an index, observer callback or candidate reducer cannot authorize effects.
236
242
  Quota/exhaustion/refusal stops unsafe work without eviction; torn tails retain a verified prefix and visible gaps, while interior corruption refuses affected ranges.
237
243
  Derived indexes are optional accelerators: readers validate original batches/artifacts, use bounded fallback and never repair on view.
244
+ Current/plan/review reads replay a task's batches, so per-file cost bounds them: on a slow host (Windows Node 22) that replay once exceeded the 100ms read quantum after review, leaving the task `unavailable`.
245
+ `PrivateFiles` reuses a directory walk only within one synchronous `DirectoryWalks` scope (a read budget or lease check), after re-observing that directory's identity; an await or later call walks again, and the quantum is never raised instead.
246
+ When a batch settles a task's publication, the owner writer remembers in process memory that task's fold state (check facts, latest run, plan/lineage progress with original admission locators) under its chain head's sequence and hash, a bounded number of tasks and states.
247
+ Readers in that process resume from the newest remembered state at or before the snapshot whose chain link still matches and fold only newer original batches; an evicted, restarted, foreign-process or diverged miss folds the original chain, and nothing is persisted, authenticated or repaired.
248
+ A quantum that makes no semantic progress because its wall-clock time ran out (a host stall: GC, scheduling, scanning) is repeated with a fresh quantum at most twice before its refusal stands; byte exhaustion and every other refusal stay final, and no progress is ever faked.
249
+ The wall-clock quantum belongs only to client-facing cooperative reads: owner-internal authority reads (current views, admission, publication, Review and receipt facts) are bounded by the same bytes but never by time, so host speed cannot turn an owner's own decision into a refusal.
250
+ Plan proofs and Review selections read their small plan text through its content/part indexes under the validated snapshot, verifying the original chunk batches, artifacts and aggregate hash; absent or faulty indexes fall back to the complete validated-prefix scan, which no longer runs per current view.
238
251
  Prepared journal heads avoid redundant mutable legacy hints; no-root compatibility and its original integrity checks remain.
239
252
 
240
253
  New history uses private format3 at `service-v1/history-v2/`, with exclusive private read-proof key creation before the final format marker.
@@ -276,16 +289,22 @@ Byte values are UTF-8/encoded bounds where specified; they are not targets to si
276
289
 
277
290
  ### Validation and regression lessons
278
291
 
279
- | Condition | Required behavior |
280
- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
281
- | Missing/unsafe/corrupt/partial selected state | Absent or fixed refusal; no bootstrap, adoption, repair or cleanup on observation |
282
- | Native exit0 with recording/stdin/final/cleanup failure | Preserve native zero independently; no workflow success |
283
- | Stale grant, selected task bytes or model/source identity | Refuse and require fresh observation/confirmation |
284
- | Unknown original stop/auth/storage or changed lease | Retain fences/charges; no guessed process signaling or automatic release |
285
- | Failed/unrun/stale check, partial child or incomplete coverage | Explicit issues review; preserve independent recorded outcomes |
286
- | Lost reply or detached view | Observe original receipt/run; no mutation replay or implicit Cancel |
287
- | Invalid read proof or changed source/key | Refuse; never rebase or interpret budget exhaustion as absence |
288
- | Dirty/partial/extra compiled assets or unsafe archive | Refuse in place; no source refill, wildcard serving or fallback build |
292
+ | Condition | Required behavior |
293
+ | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
294
+ | Missing/unsafe/corrupt/partial selected state | Absent or fixed refusal; no bootstrap, adoption, repair or cleanup on observation |
295
+ | Native exit0 with recording/stdin/final/cleanup failure | Preserve native zero independently; no workflow success |
296
+ | Stale grant, selected task bytes or model/source identity | Refuse and require fresh observation/confirmation |
297
+ | Unknown original stop/auth/storage or changed lease | Retain fences/charges; no guessed process signaling or automatic release |
298
+ | Failed/unrun/stale check, partial child or incomplete coverage | Explicit issues review; preserve independent recorded outcomes |
299
+ | Lost reply or detached view | Observe original receipt/run; no mutation replay or implicit Cancel |
300
+ | Invalid read proof or changed source/key | Refuse; never rebase or interpret budget exhaustion as absence |
301
+ | Dirty/partial/extra compiled assets or unsafe archive | Refuse in place; no source refill, wildcard serving or fallback build |
302
+ | Extension import outside Pi aliases declared only as a peer | Exact production dependency; Pi's install omits peers and Pi fails to start |
303
+ | Install without the optional SDK or Pi `auth.json` | No unit registration or start; Pi sources refuse unregistered, no SDK quarantines |
304
+ | Package files hard-linked into a package manager's store (Bun) | Packaged reads ignore link count, bind exact bodies; other files stay single-link |
305
+ | Host refuses during startup | Fixed code/stage only; unregistered exits `refused`, registered stays `quarantined` |
306
+ | Workspace containing the private state home | Land on `~/cadence` (only start creates it); never move state into the working copy |
307
+ | Upgrade changes the unit workspace or Node path | Stop with the loaded unit definition before rewriting, not only SIGTERM |
289
308
 
290
309
  Good: current context → explicit grant → durable acceptance → genuine original native/storage settlement → exact retained dependency → real checks → human review.
291
310
  Base: a client leaves and another reattaches to the same accepted run without launching it again.
@@ -299,9 +318,8 @@ Correct: expose bounded continuations/full exports and report the exact executed
299
318
  ### Development and verification practice
300
319
 
301
320
  Read affected source and adjacent tests, identify the actual owner of a contract, then make the smallest coherent change.
302
- Use ordinary source editing and directly affected checks while developing; run the fast `verify` command at stable boundaries.
303
- Request the explicit integration/browser lanes when those capabilities need qualification; do not silently start the entire suite after a local edit or a publication request.
304
- Keep fast-check results separate from real native/SDK/browser evidence, and never replace a required integration result with unit mocks or citation counts.
321
+ Use ordinary source editing and directly affected checks while developing; independently review complete capabilities and run full `verify` at stable integration boundaries.
322
+ Do not run the entire suite after every local edit, but do not replace final integration with unit mocks or citation counts.
305
323
  On this development host create fresh0700 disk roots under `/home/mirako/abel` and explicitly set TMPDIR/TEMP/TMP; `/tmp` is inode-exhausted.
306
324
  Use fabricated credentials, isolated browser/profile/state roots and local fixtures; never personal credentials, automatic downloads or unknown process cleanup.
307
325
  Outer command budgets must cover whole suites and preserve original runner waits; existing SDK20s/child15s, PTY90s/interaction15s and browser120s/action15s fixture budgets are not increased to obtain green.
@@ -316,11 +334,13 @@ Regression owners:
316
334
  - `service-history-*`: durable ordering, cold/no-index/restarted proof/search/export, corruption/torn/quota and expected-byte publication.
317
335
  - `lightweight-shell`, `native-client`, `native-history`, `terminal-integration`: native consent, shared service, Unicode/paging, detach/cancel and actual source/extracted PTYs.
318
336
  - `test/web/`: adapted pure/SSR tests separately from actual built-browser contexts, HTTPS, mixed clients, focus/viewports and complete downloads.
319
- - `package-*`, `distribution`, `service-package`, `independence`: exact tar/member/body/import/notice closure and nonactivating installation/discovery.
337
+ - `package-*`, `distribution`, `service-package`, `independence`: exact tar/member/body/import/notice closure and nonactivating discovery.
338
+ - `service-autostart`, `install-config`: Pi user-scope gating, unit escaping, upgrade/disable/mask/foreign handling and orderly removal.
339
+ - `workspace-landing`: home-directory redirection to `~/cadence` across management, native attach and observation.
320
340
 
321
341
  Regenerate operator/service/native/browser outputs only through their canonical builders; `--check` variants compare new private output without overwriting canonical bytes.
322
342
  Review the independently enumerated package member list and every emitted body, not a count or an expected list generated from the tar under test.
323
- Keep public Pi peers external, the exact ws runtime edge and all upstream/font/vendor notices.
343
+ Keep public Pi peers external for the extension, the exact Undici/ws runtime edges, the exact optional SDK pins for the standalone owner and all upstream/font/vendor notices.
324
344
  Preserve original license bytes through their precise Git attributes; line-ending normalization must not invalidate audited source/notice hashes.
325
345
 
326
346
  ### Prior integrated acceptance snapshot
@@ -35,7 +35,8 @@ Provisioned official notice bodies were reused locally; builds never download li
35
35
  ## Runtime/package closure
36
36
 
37
37
  Only the native extension's explicit import closure, maintained SDK worker/shared JS capture modules, generated management/host/operator executables, Agents, independent skills/configuration, documentation, notices and complete built assets ship.
38
- Service/worker execution uses ordinary Node with declared external Pi/Typebox/Undici peers and exact production `ws`; no source-tree or TypeScript fallback, SDK bundle or consumer compilation.
38
+ Service/worker execution uses ordinary Node with exact production `undici` and `ws` and the Pi SDK (`pi-coding-agent`, `pi-ai`) from exact 0.84.3 optional dependencies, because Pi's installer omits peers; no source-tree or TypeScript fallback, SDK bundle or consumer compilation.
39
+ Pi's package installer omits peers and aliases only its own packages and Typebox, so every other runtime import reachable from `src/index.ts` must be a production dependency; otherwise Pi fails to load the extension at startup.
39
40
  The retired shell-local controller is not a shipped competing executor.
40
41
 
41
42
  `provenance/package-members.json` is a reviewed source/import/asset policy, not generated from the archive being tested.
@@ -1,7 +1,7 @@
1
1
  # Explicit service management
2
2
 
3
- The native `/cadence` view and browser are clients of the same explicitly started owner.
4
- Opening either view, extension discovery, installation and login do not start it.
3
+ The native `/cadence` view and browser are clients of the same owner, started explicitly or by the [Linux systemd user unit](#linux-systemd-user-unit).
4
+ Opening either view, extension discovery and login do not start it.
5
5
  Use one coordination state-home for the checkout; a different state-home is not a recovery route.
6
6
  Stop older Cadence sessions before starting the shared service.
7
7
 
@@ -20,7 +20,47 @@ The equivalent package-relative command is `node <cadence>/src/service-cli.mjs .
20
20
  Defaults are loopback `127.0.0.1:17423`, normal Pi agent configuration, and detached service lifetime.
21
21
  An explicit port0 selects and persists a positive port once; restart uses the selected configuration, not a fallback endpoint.
22
22
  Missing/corrupt existing setup is preserved, not filled in or reset.
23
- There is no login autostart, supervisor installation, provider login or credential migration.
23
+ A workspace that contains the private state home (the home directory and its ancestors, such as `C:\Users\<you>` with `%LOCALAPPDATA%` or `~` with `~/.local/state`) would expose private history and locks to change observation, so every command there uses `~/cadence` (`%USERPROFILE%\cadence`) instead of refusing.
24
+ Only `start` creates it; `status` reports `absent` without creating it, an explicit `--workspace` adds one informational line, and JSON output names the effective `workspace` only when it differs.
25
+ `/cadence` started in such a directory attaches to the same owner, and real project directories are never redirected.
26
+ Apart from the Linux user unit below, there is no supervisor installation, provider login or credential migration.
27
+
28
+ ## Linux systemd user unit
29
+
30
+ On Linux, Pi's user-scope install (`pi install npm:@abelxiaoxing/cadence`, npm in `<agent-dir>/npm`) and every `pi update` run the package postinstall, which calls `cadence-service autostart refresh`.
31
+ Refresh acts only when the package is exactly Pi's managed user copy; checkouts, project (`-l`), temporary and global installs, other platforms and `CADENCE_AUTOSTART=0` are left alone.
32
+
33
+ The first refresh writes `~/.local/share/systemd/user/cadence.service`, enables it for `default.target` and starts it.
34
+ The workspace is `~/cadence` (created if missing) or an existing absolute `CADENCE_WORKSPACE`, which lands on `~/cadence` with a note when it contains the unit's state home; later refreshes keep the unit's workspace unless that variable is set again.
35
+ The unit runs `start --foreground --workspace` without `--port`, so a new workspace selects `http://127.0.0.1:17423`.
36
+ A port saved by an earlier start of that workspace still wins, and an occupied endpoint fails the unit instead of falling back.
37
+ It bakes the installer's PATH (minus npm lifecycle entries), LANG/LC_ALL, an absolute XDG_STATE_HOME and a custom PI_CODING_AGENT_DIR, so shell management sees the same state home and Pi sources.
38
+ Proxy variables are not baked; add a validated pair with `systemctl --user edit cadence`.
39
+ Browser entry still requires the explicitly revealed token link; installation prints the `token-link --reveal` command, never the token.
40
+
41
+ An upgrade refresh stops an active unit with its loaded definition, rewrites the file and starts it again.
42
+ `ExecStop` is the authenticated orderly Stop, so admitted work is stopped, not detached.
43
+ A disabled unit is refreshed but not re-enabled, and restarted only if it was running; a masked unit is never enabled or started; a unit file Cadence did not generate is left unchanged.
44
+ `KillMode=mixed` sends systemd's SIGTERM only to the management client.
45
+ An owner still settling after `TimeoutStopSec=90` (for example a quarantined one) is SIGKILLed, which is not orderly Stop and can leave recovery fences.
46
+ `Restart=on-failure` is bounded to three starts in five minutes; a restart never removes fences, so a crashed owner keeps refusing until informed recovery.
47
+
48
+ Pi's installer omits peers, so the standalone owner and workers load the Pi SDK (`pi-coding-agent`, `pi-ai`) from exact 0.84.3 optional dependencies.
49
+ Package files may be hard links into the package manager's store (Bun's default install); Cadence binds their exact content, not their link count.
50
+ Missing or unusable Pi sources refuse before an owner registers; a new owner without the SDK quarantines.
51
+ Refresh therefore does not register the unit while the SDK is unresolvable (for example, optional dependencies omitted), and enables without starting it while `auth.json` is missing; the unit carries `ConditionPathExists=` for all three files.
52
+ After logging in to Pi, start it with `systemctl --user start cadence`.
53
+ Without linger, user units start at login and stop at logout through the same Stop; `loginctl enable-linger` keeps the owner running and starts it at boot.
54
+
55
+ ```sh
56
+ cadence-service autostart install [--workspace /absolute/project]
57
+ cadence-service autostart status
58
+ cadence-service autostart remove
59
+ ```
60
+
61
+ `install` registers, enables and starts the unit from any package location unless it is masked.
62
+ `remove` disables it through `ExecStop` and deletes only the generated file; workspace, state home, history and Pi sources stay.
63
+ Because npm runs no uninstall script, run `remove` before `pi remove`; a left-behind unit skips start once its CLI path disappears.
24
64
 
25
65
  ## Status, departure and Stop
26
66
 
@@ -34,6 +74,13 @@ There is no login autostart, supervisor installation, provider login or credenti
34
74
  - `unconfirmed` means startup/Stop acknowledgment or settlement was not established within its reporting bound.
35
75
  It is not permission to restart, kill a guessed PID or remove a lock.
36
76
  `quarantined` preserves uncertain process/auth/storage work and prevents unsafe writers.
77
+ - A host that refuses during `start` reports `{"status":"refused","error":<code>,"stage":<stage>}` and exits without registering an owner.
78
+ Stages are `package` (installed web assets or Agent profiles), `pi-agent-directory`, `pi-auth`, `pi-models` and `service`; stderr names the selected Pi source to fix.
79
+ `quarantined` with stage `pi-runtime` or `service` means the owner registered first; it keeps its fences for informed recovery.
80
+ Reports carry fixed codes and stages only, never paths, configuration bytes or OS error text.
81
+ - The selected Pi agent directory must be a real directory owned by you with mode 0700; `auth.json` and `models.json` must be your own single-link regular files with mode 0600.
82
+ `models.json` may use only fields the pinned SDK supports.
83
+ `supportsMidConvoSystemMessages` from newer Pi is accepted because the pinned SDK has no system-role conversation message; other unknown fields refuse rather than being silently ignored.
37
84
  - A foreground owner can be managed from another terminal.
38
85
  Ctrl-C, SIGTERM, machine crash or terminal destruction is **not** the authenticated orderly Stop route and can leave recovery fences.
39
86
  A recovered `stopped` registration means acknowledged fence release, not retroactive native closure certification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Shared Pi task workspace: native terminal and WebUI, persistent service, checks and human review.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -71,12 +71,10 @@
71
71
  "postinstall": "node src/install-config.mjs",
72
72
  "pack:check": "node scripts/pack-check.mjs",
73
73
  "traceability:check": "node scripts/traceability-check.mjs",
74
- "verify": "bun run check && bun run check:web && bun run build:web:check && bun run lint && bun run check:agents && bun run test && bun run test:web && bun run pack:check",
75
- "verify:full": "bun run verify && bun run test:integration && bun run test:browser",
74
+ "verify": "bun run check && bun run check:web && bun run build:web:check && bun run lint && bun run check:agents && bun run test && bun run test:web && bun run test:browser && bun run pack:check",
76
75
  "check": "node scripts/check-syntax.mjs && bun scripts/build-operator.mjs --check && bun scripts/build-service.mjs --check && tsc --noEmit",
77
76
  "lint": "biome check . && rumdl check .",
78
- "test": "vitest run --config test/fast.config.ts --reporter=dot",
79
- "test:integration": "vitest run --config test/integration.config.ts --reporter=verbose",
77
+ "test": "vitest run --no-file-parallelism",
80
78
  "test:target": "vitest run",
81
79
  "check:agents": "node scripts/check-agents.mjs",
82
80
  "doctor": "node src/operator-cli.mjs doctor",
@@ -101,13 +99,11 @@
101
99
  },
102
100
  "devDependencies": {
103
101
  "@biomejs/biome": "^2.4.16",
104
- "@earendil-works/pi-coding-agent": "0.84.3",
105
102
  "@earendil-works/pi-tui": "0.84.3",
106
103
  "@types/node": "^22.15.3",
107
104
  "rumdl": "^0.2.10",
108
105
  "typescript": "^6.0.3",
109
106
  "vitest": "^4.1.8",
110
- "@earendil-works/pi-ai": "0.84.3",
111
107
  "@types/ws": "8.18.1",
112
108
  "react": "19.3.0",
113
109
  "react-dom": "19.3.0",
@@ -146,6 +142,10 @@
146
142
  "undici": "8.9.0",
147
143
  "ws": "8.21.3"
148
144
  },
145
+ "optionalDependencies": {
146
+ "@earendil-works/pi-ai": "0.84.3",
147
+ "@earendil-works/pi-coding-agent": "0.84.3"
148
+ },
149
149
  "bin": {
150
150
  "cadence-service": "./src/service-cli.mjs"
151
151
  }
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import path from "node:path";
@@ -40,6 +41,22 @@ export function ensureUserConfig(home = homedir()) {
40
41
  }
41
42
  }
42
43
 
44
+ /** Pi install/update hook. The management client decides whether this is Pi's
45
+ * Linux user-scope install and refreshes the systemd user unit; only its human
46
+ * guidance (stderr) reaches the installer. */
47
+ export function refreshAutostart(run = spawnSync) {
48
+ if (process.platform !== "linux" || process.versions.bun) return;
49
+ run(
50
+ process.execPath,
51
+ [
52
+ fileURLToPath(new URL("./service-cli.mjs", import.meta.url)),
53
+ "autostart",
54
+ "refresh",
55
+ ],
56
+ { stdio: ["ignore", "ignore", "inherit"] },
57
+ );
58
+ }
59
+
43
60
  if (
44
61
  process.argv[1] &&
45
62
  path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
@@ -54,4 +71,12 @@ if (
54
71
  );
55
72
  process.exitCode = 0;
56
73
  }
74
+ try {
75
+ refreshAutostart();
76
+ } catch (error) {
77
+ process.stderr.write(
78
+ `Cadence could not refresh its systemd user unit: ${error instanceof Error ? error.message : String(error)}\n`,
79
+ );
80
+ process.exitCode = 0;
81
+ }
57
82
  }
@@ -1,6 +1,10 @@
1
1
  /** Native attachment only. No SDK/auth resolution, worker or storage mutation. */
2
2
 
3
- import { Agent } from "undici";
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { isAbsolute, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Agent, Dispatcher1Wrapper } from "undici";
4
8
  import { nativeBuild } from "./native-build.ts";
5
9
  import { ServiceCoordination } from "./service/coordination.ts";
6
10
  import { ServiceClient } from "./service-client.ts";
@@ -10,10 +14,27 @@ import {
10
14
  type SessionBootstrap,
11
15
  } from "./service-contract.ts";
12
16
  import { HttpServiceTransport } from "./service-http-client.ts";
17
+ import { effectiveWorkspace } from "./task-store.ts";
13
18
 
14
19
  export const MANAGEMENT_GUIDANCE =
15
20
  "Cadence owner absent, unavailable or incompatible. Explicitly run cadence-service status --workspace <path>, cadence-service diagnose --workspace <path>, or cadence-service start --workspace <path>. Stop older Cadence sessions first. For retained fences use cadence-service recover inspect; resolve requires a local terminal and separate process, credential and storage acknowledgments. Never treat unavailable/quarantined as stopped. No service was started and no embedded executor is available.";
16
21
 
22
+ /** Linux: npm may skip or hide the Pi install hook that registers the user
23
+ * unit, so name the explicit command when it is absent. Reads one path only. */
24
+ export function autostartHint(): string {
25
+ if (process.platform !== "linux") return "";
26
+ const data = process.env.XDG_DATA_HOME;
27
+ const unit = join(
28
+ data && isAbsolute(data) ? data : join(homedir(), ".local", "share"),
29
+ "systemd",
30
+ "user",
31
+ "cadence.service",
32
+ );
33
+ if (existsSync(unit)) return "";
34
+ const cli = fileURLToPath(new URL("./service-cli.mjs", import.meta.url));
35
+ return `\nTo run the shared service and WebUI (default http://127.0.0.1:17423) at login: node ${JSON.stringify(cli)} autostart install`;
36
+ }
37
+
17
38
  export class NativeConnection {
18
39
  private transport?: HttpServiceTransport;
19
40
  private network?: Agent;
@@ -28,10 +49,17 @@ export class NativeConnection {
28
49
  // Failed detach keeps its exact transport/slot. Explicit re-entry may retry
29
50
  // detach, but never forget it and allocate a replacement on uncertainty.
30
51
  await this.detach();
31
- const access = new ServiceCoordination(cwd).localAccess();
52
+ // Pi started in the home directory attaches to the ~/cadence landing owner,
53
+ // the same one the management client starts there. Observation only.
54
+ const access = new ServiceCoordination(
55
+ effectiveWorkspace(cwd).workspace,
56
+ ).localAccess();
32
57
  this.epoch = access.epoch;
33
58
  const network = new Agent();
34
59
  this.network = network;
60
+ // Node 22/24 built-in fetch bundles undici 6/7, whose legacy handlers an
61
+ // undici 8 Agent rejects. The public wrapper adapts them on every line.
62
+ const dispatcher = new Dispatcher1Wrapper(network);
35
63
  this.transport = new HttpServiceTransport({
36
64
  origin: access.endpoint,
37
65
  localCapability: access.capability,
@@ -43,7 +71,7 @@ export class NativeConnection {
43
71
  // process routes provider auth through a proxy. Public per-request Node
44
72
  // dispatcher only: never replace/restore the parent's global dispatcher.
45
73
  fetch: (url, init) =>
46
- globalThis.fetch(url, Object.assign({}, init, { dispatcher: network })),
74
+ globalThis.fetch(url, Object.assign({}, init, { dispatcher })),
47
75
  });
48
76
  const client = new ServiceClient(this.transport);
49
77
  this.client = client;
@@ -136,11 +136,8 @@ function safeDirectory(path, create = false, inspectExisting) {
136
136
  at = join(at, part);
137
137
  let existing;
138
138
  try {
139
- const st = lstatSync(at);
140
- if (!st.isDirectory())
141
- throw new Error("Unsafe directory");
142
139
  const checked = lstatSync(at);
143
- if (checked.isSymbolicLink() || inspectExisting && !checked.isDirectory())
140
+ if (!checked.isDirectory() || checked.isSymbolicLink())
144
141
  throw new Error("Unsafe directory");
145
142
  existing = checked;
146
143
  } catch (error) {
@@ -154,7 +151,8 @@ function safeDirectory(path, create = false, inspectExisting) {
154
151
  if (!object(mkdirError) || mkdirError.code !== "EEXIST")
155
152
  throw mkdirError;
156
153
  }
157
- if (!lstatSync(at).isDirectory() || lstatSync(at).isSymbolicLink())
154
+ const created = lstatSync(at);
155
+ if (!created.isDirectory() || created.isSymbolicLink())
158
156
  throw new Error("Unsafe directory");
159
157
  }
160
158
  if (existing)
@@ -162,13 +160,29 @@ function safeDirectory(path, create = false, inspectExisting) {
162
160
  }
163
161
  return true;
164
162
  }
165
- function readBounded(path, max) {
163
+ var transientWindows = new Set(["EPERM", "EACCES", "EBUSY"]);
164
+ function retryWindowsFileOperation(operation, recheck) {
165
+ if (process.platform !== "win32")
166
+ return operation();
167
+ const deadline = performance.now() + 2000;
168
+ for (let delay = 2;; delay = Math.min(delay * 2, 50)) {
169
+ try {
170
+ return operation();
171
+ } catch (error) {
172
+ if (!object(error) || !transientWindows.has(String(error.code)) || performance.now() >= deadline)
173
+ throw error;
174
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
175
+ recheck?.();
176
+ }
177
+ }
178
+ }
179
+ function readBounded(path, max, packaged = false) {
166
180
  if (!safeDirectory(dirname(path)))
167
181
  return;
168
182
  let fd;
169
183
  try {
170
184
  const st = lstatSync(path);
171
- if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max)
185
+ if (!st.isFile() || st.isSymbolicLink() || !packaged && st.nlink !== 1 || st.size > max)
172
186
  throw new Error("Unsafe or oversized file");
173
187
  fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
174
188
  } catch (error) {
@@ -178,7 +192,7 @@ function readBounded(path, max) {
178
192
  }
179
193
  try {
180
194
  const st = fstatSync(fd);
181
- if (!st.isFile() || st.nlink !== 1 || st.size > max)
195
+ if (!st.isFile() || !packaged && st.nlink !== 1 || st.size > max)
182
196
  throw new Error("Unsafe or oversized file");
183
197
  const buffer = Buffer.alloc(max + 1);
184
198
  let size = 0;
@@ -256,6 +270,23 @@ function canonicalAmbientRoot(path) {
256
270
  return join(canonicalAmbientRoot(parent), full.slice(parent.length));
257
271
  }
258
272
  }
273
+ function privateRootFor(canonicalRoot, stateHome) {
274
+ return join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(canonicalRoot).digest("hex"));
275
+ }
276
+ function contains(root, path) {
277
+ const within = relative(root, resolve(path));
278
+ return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
279
+ }
280
+ function stateHomeInside(root, stateHome = defaultStateHome()) {
281
+ const canonical = realpathSync(root);
282
+ return contains(canonical, privateRootFor(canonical, stateHome));
283
+ }
284
+ var LANDING_WORKSPACE = "cadence";
285
+ function effectiveWorkspace(requested, stateHome, home = homedir()) {
286
+ if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
287
+ return { workspace: requested, redirected: false };
288
+ return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
289
+ }
259
290
 
260
291
  class TaskStore {
261
292
  root;
@@ -264,9 +295,8 @@ class TaskStore {
264
295
  constructor(root, stateHome = defaultStateHome()) {
265
296
  this.root = realpathSync(root);
266
297
  this.tasks = join(this.root, ".cadence", "assistant", "tasks");
267
- this.privateRoot = join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(this.root).digest("hex"));
268
- const within = relative(this.root, resolve(this.privateRoot));
269
- if (!within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within))
298
+ this.privateRoot = privateRootFor(this.root, stateHome);
299
+ if (contains(this.root, this.privateRoot))
270
300
  throw new Error("Private state home must be outside the working copy");
271
301
  }
272
302
  get lockPath() {
@@ -318,7 +348,7 @@ class TaskStore {
318
348
  };
319
349
  let fd;
320
350
  try {
321
- fd = openSync(this.lockPath, "wx", 384);
351
+ fd = retryWindowsFileOperation(() => openSync(this.lockPath, "wx", 384));
322
352
  } catch {
323
353
  throw new Error("Checkout busy or unresolved lock; inspect and explicitly recover only a dead owner");
324
354
  }
@@ -338,7 +368,7 @@ class TaskStore {
338
368
  if (lock.pid !== process.pid || lock.host !== hostname())
339
369
  throw new Error("Only the owning process may release a lock");
340
370
  this.assertOwner(lock);
341
- unlinkSync(this.lockPath);
371
+ retryWindowsFileOperation(() => unlinkSync(this.lockPath), () => this.assertOwner(lock));
342
372
  }
343
373
  recover(expected, acknowledgeEscapedProcesses) {
344
374
  if (!acknowledgeEscapedProcesses || expected.host !== hostname())
@@ -418,7 +448,12 @@ class TaskStore {
418
448
  const latest = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
419
449
  if ((latest ? contentIdentity(latest) : undefined) !== expectedIdentity)
420
450
  throw new Error("Task content changed; replacement preserved without overwrite");
421
- renameSync(temporary, join(dir, "task.json"));
451
+ retryWindowsFileOperation(() => renameSync(temporary, join(dir, "task.json")), () => {
452
+ this.assertOwner(lock);
453
+ const now = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
454
+ if ((now ? contentIdentity(now) : undefined) !== expectedIdentity)
455
+ throw new Error("Task content changed; replacement preserved without overwrite");
456
+ });
422
457
  return next;
423
458
  } finally {
424
459
  if (!held)
@@ -459,8 +494,9 @@ class TaskStore {
459
494
  var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
460
495
 
461
496
  // src/operator-tools.ts
497
+ var observed = (cwd) => resolveWorkspace(effectiveWorkspace(cwd).workspace);
462
498
  function inspectConsumer(cwd) {
463
- const workspace = resolveWorkspace(cwd);
499
+ const workspace = observed(cwd);
464
500
  const store = new TaskStore(workspace.root);
465
501
  const tasks = store.list();
466
502
  const checks = store.checks();
@@ -481,7 +517,7 @@ function inspectConsumer(cwd) {
481
517
  };
482
518
  }
483
519
  function inspectRuns(cwd) {
484
- const workspace = resolveWorkspace(cwd);
520
+ const workspace = observed(cwd);
485
521
  const store = new TaskStore(workspace.root);
486
522
  return {
487
523
  ok: true,