@abelxiaoxing/cadence 2.0.0 → 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 +61 -41
- package/docs/reuse-and-distribution.md +2 -1
- package/docs/service-management.md +50 -3
- package/package.json +9 -10
- package/src/install-config.mjs +25 -0
- package/src/native-client.ts +31 -3
- package/src/operator-cli.mjs +52 -16
- package/src/service/coordination.ts +35 -16
- package/src/service/private-files.ts +128 -23
- package/src/service-cli.mjs +734 -144
- package/src/service-host.mjs +839 -313
- package/src/task-shell.ts +16 -6
- package/src/task-store.ts +104 -25
- package/src/worker-tools.mjs +42 -17
- package/web/dist/assets/cadence.js +1 -1
- package/web/dist/manifest.json +3 -3
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
|
|
5
|
-
Ordinary conversations, resource discovery
|
|
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;
|
|
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
|
|
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
|
|
118
|
-
bun run
|
|
119
|
-
bun run
|
|
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:
|
|
122
|
-
CADENCE_BROWSER_PATH=/absolute/existing/chromium bun run verify
|
|
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`
|
|
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`
|
|
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
|
|
303
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4
|
-
Opening either view, extension discovery
|
|
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
|
-
|
|
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.
|
|
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 --
|
|
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",
|
|
@@ -97,19 +95,15 @@
|
|
|
97
95
|
"@earendil-works/pi-coding-agent": "*",
|
|
98
96
|
"@earendil-works/pi-tui": "*",
|
|
99
97
|
"typebox": "*",
|
|
100
|
-
"@earendil-works/pi-ai": "*"
|
|
101
|
-
"undici": "^8.9.0"
|
|
98
|
+
"@earendil-works/pi-ai": "*"
|
|
102
99
|
},
|
|
103
100
|
"devDependencies": {
|
|
104
101
|
"@biomejs/biome": "^2.4.16",
|
|
105
|
-
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
106
102
|
"@earendil-works/pi-tui": "0.84.3",
|
|
107
103
|
"@types/node": "^22.15.3",
|
|
108
104
|
"rumdl": "^0.2.10",
|
|
109
105
|
"typescript": "^6.0.3",
|
|
110
106
|
"vitest": "^4.1.8",
|
|
111
|
-
"@earendil-works/pi-ai": "0.84.3",
|
|
112
|
-
"undici": "8.9.0",
|
|
113
107
|
"@types/ws": "8.18.1",
|
|
114
108
|
"react": "19.3.0",
|
|
115
109
|
"react-dom": "19.3.0",
|
|
@@ -145,8 +139,13 @@
|
|
|
145
139
|
"url": "https://github.com/abelxiaoxing/cadence/issues"
|
|
146
140
|
},
|
|
147
141
|
"dependencies": {
|
|
142
|
+
"undici": "8.9.0",
|
|
148
143
|
"ws": "8.21.3"
|
|
149
144
|
},
|
|
145
|
+
"optionalDependencies": {
|
|
146
|
+
"@earendil-works/pi-ai": "0.84.3",
|
|
147
|
+
"@earendil-works/pi-coding-agent": "0.84.3"
|
|
148
|
+
},
|
|
150
149
|
"bin": {
|
|
151
150
|
"cadence-service": "./src/service-cli.mjs"
|
|
152
151
|
}
|
package/src/install-config.mjs
CHANGED
|
@@ -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
|
}
|
package/src/native-client.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Native attachment only. No SDK/auth resolution, worker or storage mutation. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
74
|
+
globalThis.fetch(url, Object.assign({}, init, { dispatcher })),
|
|
47
75
|
});
|
|
48
76
|
const client = new ServiceClient(this.transport);
|
|
49
77
|
this.client = client;
|
package/src/operator-cli.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
268
|
-
|
|
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 =
|
|
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 =
|
|
520
|
+
const workspace = observed(cwd);
|
|
485
521
|
const store = new TaskStore(workspace.root);
|
|
486
522
|
return {
|
|
487
523
|
ok: true,
|