@abelxiaoxing/cadence 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +155 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +83 -15
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +399 -1079
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/openspec-cli.ts +494 -0
  23. package/src/package-delivery.ts +486 -0
  24. package/src/package-verification.ts +509 -0
  25. package/src/route-policy.ts +1 -11
  26. package/src/run-state.ts +4 -16
  27. package/src/run-store.ts +5 -144
  28. package/src/scoped-grep-worker.mjs +23 -0
  29. package/src/scoped-tools.ts +312 -116
  30. package/src/sqlite-schema.ts +171 -0
  31. package/src/storage-schema.ts +352 -0
  32. package/src/subagent-activity.ts +14 -1
  33. package/src/submit-tool.ts +20 -2
  34. package/src/task-ledger.ts +14 -55
  35. package/src/verification-capability.ts +298 -102
  36. package/src/verification-environment.ts +213 -0
  37. package/src/verification-identity.ts +77 -0
  38. package/src/worker-broker.ts +97 -34
  39. package/src/workflow-engine.ts +28 -8285
  40. package/src/workflow-policy.ts +1427 -0
  41. package/src/workflow-state-machine.ts +4478 -0
  42. package/src/workspace-io-entry.mjs +58 -0
  43. package/src/workspace-io-worker.mjs +984 -0
  44. package/src/workspace-io.ts +84 -0
  45. package/src/workspace-store.ts +125 -19
  46. package/skills/abel-workflow/SKILL.md +0 -205
@@ -6,13 +6,21 @@ tags: [abel, implement, openspec, TDD, recovery]
6
6
  argument-hint: "<change_name>"
7
7
  ---
8
8
 
9
- Load the bundled `abel-workflow` Skill and read the complete value inside `<abel-request>` without tokenizing it a second time.
9
+ This procedure applies only when the user explicitly invokes `/abel-implement`.
10
+ Reading this file, mentioning the command, or finding OpenSpec artifacts does not activate it.
11
+ Read the complete value inside `<abel-request>` without tokenizing it a second time.
10
12
 
11
13
  <abel-request>
12
14
  $ARGUMENTS
13
15
  </abel-request>
14
16
 
15
17
  <!-- ABEL:PROMPT:abel-implement -->
18
+
19
+ This stage is scoped to the invoked task.
20
+ Direct answers and same-task continuations stay in this stage.
21
+ If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
22
+ Exit preserves resumable work and never means completion or discard.
23
+ Never start another stage automatically.
16
24
  <!-- ABEL:START -->
17
25
 
18
26
  # Implement outcome
@@ -22,14 +30,22 @@ If it is missing, absent, or ambiguous, stop before execution and ask for the un
22
30
 
23
31
  Implement is a durable, resumable execution stage.
24
32
  The private control plane owns task scheduling, Red-Green-Refactor progression, candidate application, verification attribution, repair, task checkbox updates, AGENTS checkpoints, and the final transaction.
25
- The parent interprets state and makes product decisions; it must not reproduce those mechanics manually.
33
+ The parent interprets state, owns implementation decisions, and follows automatic continuations; it must not reproduce those mechanics manually.
34
+ Design has already established the goal, main choices, and explicit constraints.
35
+ Invoking Implement delegates remaining implementation choices to the parent model's recommended solution.
36
+ Choose the best supported option, record the concise decision and rationale in the same change, and continue without asking the user to select an option or confirm it again.
37
+ Respect explicit Design constraints and non-goals; when a proposed option conflicts, choose a compliant alternative.
38
+ A Worker request is evidence for the parent, not permission for the Worker to widen its own authority.
39
+ Do not end the turn merely because a control call returns a nonterminal state.
40
+ Follow its parent-owned continuation until verified completion or a concrete blocker that cannot be resolved within available capabilities and budgets.
41
+ Keep routine decisions and repairs internal; give brief progress updates during sustained work and include material implementation choices in the final result.
26
42
 
27
43
  ## Trusted admission
28
44
 
29
- Before the first Worker launch, load the current `ready.yaml` and referenced canonical `implement-plan.json`; verify the Gate A binding, raw and canonical hashes, OpenSpec strict/planning status, Requirement → Scenario → Verification → Task traceability, capability closure, exact path/output contracts, repair policy, tracking contract, and sealed AGENTS operations.
45
+ Before the first Worker launch, load the current `ready.yaml` and referenced canonical `implement-plan.json`; verify both current owner-private Gate proofs and the finalization fact binding the exact delivery revision, receipt hash, and canonical plan hash, plus the Gate A binding, raw and canonical hashes, OpenSpec strict/planning status, Requirement → Scenario → Verification → Task traceability, capability closure, exact path/output contracts, repair policy, tracking contract, and sealed AGENTS operations.
30
46
 
31
47
  An invalid or incomplete delivery is one aggregated `delivery-invalid` admission result.
32
- Report every safe diagnostic together.
48
+ Inspect every safe diagnostic together and use the returned parent-owned amendment continuation to repair an executable delivery before resuming.
33
49
  Do not launch a Worker, consume a retry, mutate the main workspace, or infer a missing contract.
34
50
 
35
51
  Gate approval is not tool permission.
@@ -37,7 +53,7 @@ A fresh context validates existing approval facts and does not ask the user to a
37
53
 
38
54
  ## Closed control surface
39
55
 
40
- Use `abel_dispatch` only with the change commands below; never send `version`; never send `action: "run"`; and never send `admit-graph`, `task-attempt`, caller-owned snapshots, completed/blocked arrays, or apply identities.
56
+ For durable run operations, use `abel_dispatch` only with the change commands below; never send `version`; never send `action: "run"`; and never send `admit-graph`, `task-attempt`, caller-owned snapshots, completed/blocked arrays, or apply identities.
41
57
 
42
58
  ```json
43
59
  {"command":"start","stage":"abel-implement","change":"<change>","operationId":"<unique-operation>"}
@@ -48,16 +64,17 @@ Use `abel_dispatch` only with the change commands below; never send `version`; n
48
64
  {"command":"discard","stage":"abel-implement","change":"<change>","operationId":"<unique-operation>"}
49
65
  ```
50
66
 
51
- When the user approves a revised delivery boundary, resume the same run with both `deliveryRevision` and `receiptHash`.
67
+ When a revised delivery is ready, resume the same run; the engine discovers and validates its `deliveryRevision` and `receiptHash` locally.
52
68
  The engine revalidates retained phase/repair facts, invalidates only incompatible tasks, rebuilds tracking, and preserves compatible work.
53
69
 
54
70
  `status` is local and must remain useful when every Worker endpoint is unavailable.
71
+ `discard` is an explicit destructive request; leaving the workflow uses `{"action":"finish"}` and preserves the run instead.
55
72
  Repeating the same operation id returns its committed outcome.
56
73
  Use a new operation id for a new attempt.
57
74
 
58
75
  For an approval-needed retained run, a later explicit Implement invocation must inspect local `status`.
59
76
  When a newer owner-private-proof-bound receipt is present, status exposes its exact pair in `availableDelivery`, adds `resume` to `legalCommands`, and fills the matching `conditionalCommands` arguments.
60
- Resume with that discovered pair; do not require the user to copy revision or hash data from a prior Design conversation.
77
+ Ordinary resume resolves that pair locally; do not require the user to copy revision or hash data from a prior conversation.
61
78
  Discovery never admits the delivery: `resume` still performs the full artifact, traceability, capability, and currentness validation before any Worker starts.
62
79
 
63
80
  ## Execution and verification
@@ -65,7 +82,7 @@ Discovery never admits the delivery: `resume` still performs the full artifact,
65
82
  Before any candidate, the engine records target-contract, task-affected, and full-suite baselines with normalized failure identities.
66
83
  A pre-existing failure remains separate and never satisfies Red or becomes evidence that this change caused it.
67
84
 
68
- For each ready task the engine runs:
85
+ For each ready behavior task the engine runs:
69
86
 
70
87
  1. Red in a disposable child revision; the declared target must fail with the approved identity.
71
88
  2. Green with the minimum approved implementation; the declared target and task-affected contract must pass relative to baseline.
@@ -86,32 +103,93 @@ The main workspace stays unchanged before that transaction.
86
103
  Ordinary failures stay inside this Implement run:
87
104
 
88
105
  - `artifact`, wrong-Red identity, invalid/incomplete patch operation, stale candidate, or transport failure: use the bounded automatic attempt policy; after exhaustion pause with the last safe code and retain the last committed revision.
89
- - Artifact correction alone consumes the sealed `artifactCorrection.maxAttempts` of 2-3 total candidate launches per task phase and operation, including the initial launch; a later `resume` starts a fresh operation budget.
90
- - `environment` or `verification-adapter`: pause without speculative edits.
91
- Resume after the capability is restored; baseline facts and completed phases are reused.
106
+ - Artifact, stale-candidate, and verification correction share the sealed `artifactCorrection.maxAttempts` of 2-3 attempts per verification obligation and phase, including the initial attempt.
107
+ Automatic retries carry the safe failure code, strategy, and available failure identities to the next Worker.
108
+ Exhaustion survives process restart and `resume`.
109
+ Operation ids, route binding, rollback revisions, task renaming, and contract rewording never clear it.
110
+ Dedicated private incident records own the budget; displayed attempt diagnostics are not authority.
111
+ A run shares one cumulative work budget: 24 plus three units per largest admitted phase count, bounded by the host limit captured at run start (default 512).
112
+ Recompiling, renaming or returning to a previous plan size never adds credit; a genuinely larger admitted decomposition may increase capacity without refunding consumption.
113
+ Execution reserves work durably before launch, including nested repair proposals; restart and post-launch cancellation do not refund consumed work.
114
+ - `environment` or `verification-adapter`: the executor pauses without speculative edits.
115
+ The parent inspects local diagnostics and restores available prerequisites using existing authorized capabilities, then resumes; baseline facts and completed phases are reused.
116
+ Do not request user input for a prerequisite the parent can restore.
117
+ Missing external credentials or unavailable services are reported only after local alternatives have been checked.
92
118
  - `pre-existing`: keep as baseline evidence.
93
119
  It does not authorize a code change and does not fail completion unless the change worsens it.
94
120
  - `introduced` in a task-affected contract: reopen the owning task as `repairable`, perform the minimum bounded repair inside its approved paths, record `repair-verified`, and re-run attribution automatically.
95
121
  - `unresolved` full-suite attribution: pause with failure identities and retained cumulative state; resume after evidence resolves ownership.
96
122
  - `workspace-revision-stale`, conflict, cancellation, process interruption, or apply recovery: preserve user edits and resume from the last durable checkpoint.
97
- Cancellation is budget-neutral.
123
+ Cancellation before execution is budget-neutral; cancellation after launch does not count as a product failure and does not refund consumed work.
98
124
  During a partially visible final apply, recovery must settle before pause or discard.
99
- - `result-limit`: pause as `needs-task-split`; never accept a partial patch.
125
+ - `result-limit` or a task-splitting request inside existing authority: automatically try a compact complete patch using exact replacements under the same bounded policy.
126
+ If it still cannot fit, the executor pauses as `needs-task-split` or `task-split-needed` and supplies an amendment continuation.
127
+ The parent recommends a smaller task decomposition, compiles the revised DAG, and resumes; never accept a partial patch or bypass compilation of the task DAG.
100
128
 
101
129
  No item above carries stage-routing metadata or asks the user to restart the approved workflow.
102
130
 
131
+ Treat ordinary recovery as internal work.
132
+ Workers may request additional ordinary regular-file reads within sealed task roots.
133
+ The parent grants exact paths, retains their content dependencies across phases and restart, and binds them to merge and final currentness; hidden files, private-key files, out-of-root paths, and writes do not receive this discovery permission.
134
+ Do not ask permission to correct approved code/tests, use task-wide approved read context, or retry within the sealed policy.
135
+ Use local status and read-only diagnostics to identify environmental prerequisites before asking the user; resume once a concrete capability has been restored, and reuse baseline facts and completed phases.
136
+ When status reports `recovery.exhausted`, do not issue unchanged `resume` calls.
137
+ Inspect retained evidence.
138
+ If status supplies a parent-owned amendment continuation, use it for a concrete plan correction; otherwise explain the specific missing capability or exhausted budget once.
139
+ Rewriting prose or renaming tasks is not a correction and never replenishes execution budgets.
140
+ A rebind may repair endpoint configuration but does not grant a new incident budget; do not repeatedly rebind and resume the same failure.
141
+ If a revised delivery already exists, local status also discovers its exact revision/hash for an exhausted run; resume with `availableDelivery` without asking the user to copy receipt data.
142
+ When `recovery.additionalAttempt` is present, the parent may make a concrete decision to retry once within remaining work capacity.
143
+ Copy that object into an ordinary resume request's optional `recovery` field; do not invent incident keys or failure sequences.
144
+ The default reason is `parent-directed-retry`; `route-changed` and `context-extended` additionally require control-plane evidence.
145
+ This consumes one launch without resetting automatic correction history; another failure pauses again.
146
+ Do not turn the availability of this option into an unconditional retry loop or another user approval round.
147
+ Environment, resource and report protocol failures are unavailable verification, not product failures; restore prerequisites and reverify retained candidates.
148
+ An unavailable endpoint, missing external credential, or irreducibly oversized task is not proof that product Design must restart.
149
+
103
150
  ## The only approval-needed boundary
104
151
 
105
- Use `approval-needed` only when continuing requires authority not present in the sealed delivery: new observable behavior, architecture or policy; a new/changed dependency; an undeclared write/delete/read target; expanded conflict/resource authority; a changed verification contract; an AGENTS target/impact/managed block change; or another irreversible scope decision.
152
+ Use `approval-needed` only when continuing requires authority not present in the sealed delivery: new observable behavior, architecture or policy; a new/changed dependency; an undeclared write/delete target or a read outside sealed discovery roots; expanded conflict/resource authority; a changed verification contract; an AGENTS target/impact/managed block change; or another irreversible scope decision.
106
153
 
107
154
  Classify only codes in the closed authority table.
108
155
  An unknown Worker, verifier, or internal approval code is an `approval-code-invalid` integrity pause; never guess a category or Gate from keywords.
109
156
 
110
- Report the exact missing authority and retained run state.
111
- Do not discard compatible work.
112
- Expose `approval.category`, `approval.requiredGates`, safe `approval.refs`, and `approval.designRequest` as `/abel-design --change <change>` user guidance only; never invoke Design automatically.
113
- Bare `resume` is not immediately executable progress while approval is missing, so keep it out of `legalCommands` and expose it only in `conditionalCommands` with a `deliveryRevision` greater than the retained revision and the matching `receiptHash` precondition.
114
- If the user supplies that newly approved receipt revision, resume the same run with the revision/hash pair.
157
+ Report all known missing authority together using `blockers` and `decisionBatch.items`; merge related choices and reuse accepted same-task decisions.
158
+ The parent resolves new implementation choices with its recommended option under the accepted Design; do not ask the user for another decision round.
159
+ `approval-needed` is an internal requirement to compile updated authority before a Worker continues, not a request for user input.
160
+ `decisionBatch.resolution` identifies the parent as decision owner and `continuation` identifies the automatic next action.
161
+ Follow it in this turn.
162
+ `owner` and `automatic` are status metadata, not tool arguments.
163
+ For amendment use the exact `decisionBatch.continuation` envelope plus `request`; for resume send the closed command envelope with a new operationId.
164
+ A changed batch must cite the new evidence; never ask separately for each task's copy of the same decision.
165
+ Retain the Implement stage and original run.
166
+ `decisionBatch.continuation` supplies the exact `action: "amend"`, change, and batchId for the narrow private artifact revision channel.
167
+ After selecting the recommended solution, use that envelope with a `request` containing the existing closed Design artifact operations: start the same change, retain inherited decisions, record the parent-selected changes, write the required change artifacts, compile, and finalize.
168
+ This is implementation delegation from the accepted Design, not a fabricated new user answer.
169
+ The same channel is available for the allowlisted `delivery-invalid`, `needs-task-split`, and `task-split-needed` technical pauses; these do not need a user decision or a fabricated approval-needed classification.
170
+ Each run permits 64 mutating amendment attempts in a dedicated persistent budget.
171
+ Failed writes/compilations consume that budget, successful operation replay is free, and restart or a new batch does not replenish it.
172
+ Read-only status/preflight does not consume it.
173
+ Fix the exact structured diagnostic before retrying compilation.
174
+ Do not repeatedly submit unchanged invalid artifacts; once `amendmentBudget.exhausted` is true, stop mutation and report the retained result truthfully.
175
+ For example, begin the revision with the returned change and batchId:
176
+
177
+ ```json
178
+ {"action":"amend","change":"<same-change>","batchId":"<current-batch-id>","request":{"operation":"start","change":"<same-change>","operationId":"<unique-operation>"}}
179
+ ```
180
+
181
+ Use the returned private revision runId on subsequent amendment requests.
182
+ This channel does not activate Design, admit evidence packets, allow a different change, or grant product/AGENTS file writes.
183
+ Keep the approved goal and explicit constraints stable.
184
+ Automatic amendments retain Gate A and the structured ChangeContract.
185
+ They cannot renew Gate A, replace behavior decisions, or rewrite proposal/spec artifacts.
186
+ Resolve implementation gaps within the accepted goal, constraints and policy; when no conforming solution exists, report that concrete contract blocker and preserve progress rather than repeatedly attempting an invalid amendment.
187
+ Never weaken acceptance criteria just to obtain a passing result.
188
+ The compiler supplies Gate B automatically.
189
+ Finish all known items in the batch before finalizing, then issue ordinary resume.
190
+ Local delivery discovery supplies the newer verified revision/hash; do not ask the user to copy receipts or switch commands.
191
+ An unchanged resume cannot bypass a pending decision, and stale batch ids or unrelated revision runIds are rejected.
192
+ The user may explicitly choose a full Design discussion, but it is not the recovery instruction for an Implement blocker.
115
193
  A generated artifact defect, endpoint outage, stale snapshot, environment failure, pre-existing failure, introduced in-boundary repair, approved documentation/test edit, or approved AGENTS checkpoint is never `approval-needed`.
116
194
 
117
195
  ## AGENTS and tracking
@@ -120,9 +198,14 @@ Design-time AGENTS read-only authority does not carry into Implement.
120
198
  Gate B seals `none | update-existing | create-index | remove-index`, exact targets, owning task ids, verification, and the complete managed block.
121
199
  The parent control plane applies those operations only in the private cumulative revision, preserves all human text outside `<!-- ABEL:AGENTS-INDEX:START -->` and `<!-- ABEL:AGENTS-INDEX:END -->`, and never gives an AGENTS write path to a Worker.
122
200
 
201
+ `remove-index` deletes a file only if no human content remains.
202
+ Runtime/session ids, credentials, approval state, and dirty-state ledgers never enter an index.
203
+
123
204
  Task checkboxes are parent-owned progress facts.
124
205
  The loader may normalize only `[x]`/`[X]` to `[ ]` for the sealed task ids when checking the delivery hash; every other byte remains integrity-bound.
125
206
 
207
+ When an approved browser E2E check requires `dev-browser` and it is unavailable, pause only that check; continue independent work when safe.
208
+
126
209
  ## Truthful completion
127
210
 
128
211
  Treat `queued`, `connecting`, `waiting-first-response`, `running`, `validating`, `retrying`, `verifying`, `paused`, `approval-needed`, `applying`, and `recovering` as nonterminal activity states.
@@ -131,9 +214,24 @@ Tool-call settlement is not success.
131
214
  Report success only when the durable state is `completed`, final application and post-apply verification committed, and private run content was cleaned.
132
215
  Terminal `completed`, `discarded`, or `rejected` settlement deactivates private dispatch; a nonterminal pause or approval wait keeps it active for direct follow-up.
133
216
 
134
- On a nonterminal result, report the safe code, affected task/scope when present, retained progress, and the exact legal command that continues or inspects the same run.
217
+ On a nonterminal result with an automatic parent continuation, execute it and continue working; do not present internal Gate names, batch ids, receipts, or resume commands as user tasks.
218
+ If automatic recovery is unavailable after investigation or its budget is exhausted, report the concrete blocker, retained progress, and only the external input needed to proceed.
219
+ Respect explicit cancellation and never resume it automatically.
135
220
  Never hide an incomplete run behind a success checkmark.
136
221
 
137
222
  Do not archive, publish, release, stage, or commit implicitly.
138
223
 
139
224
  <!-- ABEL:END -->
225
+
226
+ ## Evidence modes and environment identity
227
+
228
+ Follow the compiled task `verificationMode`.
229
+ The default `behavior` mode retains Red/Green.
230
+ Explicitly approved `mechanical` and `refactor` modes start execution at Green after baseline capture; do not invent a failing test or request a Red Worker.
231
+ The compiler checks mode authority, path suitability and protected verifier inputs.
232
+ All modes retain cumulative verification, output checks and transactional apply.
233
+
234
+ `verification-environment-changed` is an unavailable environment, not a product regression.
235
+ Installed dependency and runner content is bound to evidence; resume in a changed environment rebuilds baseline and revalidates retained phases.
236
+ Reuse compatible sealed candidates.
237
+ Do not edit product code to compensate for missing or drifting tools.
@@ -3,14 +3,16 @@ description: Initialize or safely repair an Abel OpenSpec project
3
3
  argument-hint: "[project-path]"
4
4
  ---
5
5
 
6
- Load the bundled `abel-workflow` Skill before inspecting or writing the target.
7
- If it is unavailable, stop before writes and report one executable package restore or reinstall command.
6
+ This procedure applies only when the user explicitly invokes `/abel-init`.
7
+ Reading this file, mentioning the command, or finding OpenSpec artifacts does not activate it.
8
8
  Read the complete value inside `<abel-request>` without tokenizing it a second time.
9
9
 
10
10
  <abel-request>
11
11
  $ARGUMENTS
12
12
  </abel-request>
13
13
 
14
+ <!-- ABEL:PROMPT:abel-init -->
15
+
14
16
  The request is an optional project path; when absent, use the current working directory.
15
17
  Resolve and report the canonical target root before action.
16
18
 
@@ -34,6 +36,8 @@ Resolve and report the canonical target root before action.
34
36
  5. Resolve and validate the selected schema and templates before claiming readiness.
35
37
  6. Create or repair only verified `<!-- ABEL:AGENTS-INDEX:START -->` … `<!-- ABEL:AGENTS-INDEX:END -->` managed regions.
36
38
  Preserve every byte of human content outside those markers and do not cross nested repositories.
39
+ Keep the index descriptive: file locations and ordinary project commands only; never instruct ordinary tasks to enter an Abel workflow.
40
+ Never record runtime/session ids, credentials, approval state, or dirty-state ledgers.
37
41
  7. Re-run all probes after writes.
38
42
  A second identical Init must produce no additional changes.
39
43
 
@@ -43,3 +47,6 @@ Do not probe `git-commit`, external `dev-browser`, or a dedicated time Skill; no
43
47
 
44
48
  Report one final result with the root, selected toolchain, actions actually taken, OpenSpec capability/schema/template evidence, AGENTS files changed or unchanged, research-Skill paths or remediation, and `ready | partial | paused`.
45
49
  Never report success from the initial probe when the post-write recheck did not pass.
50
+
51
+ Init ends with that report; later ordinary tasks use normal engineering behavior.
52
+ Never start another Abel stage, archive, publish, release, stage, or commit implicitly.
@@ -20,10 +20,12 @@ import {
20
20
  import { tmpdir } from "node:os";
21
21
  import path from "node:path";
22
22
  import { DatabaseSync } from "node:sqlite";
23
-
24
23
  import type { ArtifactStore } from "./artifact-store.ts";
24
+ import { compareCanonicalStrings } from "./canonical.ts";
25
25
  import { isValidRelativePath } from "./contracts.ts";
26
26
  import { observeSafePath } from "./safe-path.ts";
27
+ import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
28
+ import { APPLY_SCHEMA } from "./storage-schema.ts";
27
29
  import type {
28
30
  WorkspaceEntry,
29
31
  WorkspaceRevision,
@@ -548,7 +550,11 @@ export async function verifyCumulativeRevision(
548
550
  );
549
551
  chmodSync(temporary, 0o700);
550
552
  try {
551
- input.workspaceStore.materialize(revision.revisionId, temporary);
553
+ await input.workspaceStore.materializeAsync(
554
+ revision.revisionId,
555
+ temporary,
556
+ input.signal,
557
+ );
552
558
  if (input.signal?.aborted) {
553
559
  return {
554
560
  ok: false,
@@ -629,16 +635,8 @@ export class ApplyTransaction {
629
635
  throw new Error("transaction-database-path-invalid");
630
636
  }
631
637
  this.#database = new DatabaseSync(databasePath);
632
- this.#database.exec(
633
- "PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;",
634
- );
635
- this.#database.exec(`
636
- CREATE TABLE IF NOT EXISTS apply_transaction_journal (
637
- transaction_id TEXT PRIMARY KEY,
638
- journal_json TEXT NOT NULL,
639
- journal_hash TEXT NOT NULL
640
- ) STRICT;
641
- `);
638
+ configureSqlite(this.#database);
639
+ ensureSqliteSchema(this.#database, APPLY_SCHEMA);
642
640
  chmodSync(databasePath, 0o600);
643
641
  }
644
642
 
@@ -986,7 +984,7 @@ export class ApplyTransaction {
986
984
  const deepestFirst = [...journal.createdDirectories].sort(
987
985
  (left, right) =>
988
986
  right.split("/").length - left.split("/").length ||
989
- right.localeCompare(left),
987
+ compareCanonicalStrings(right, left),
990
988
  );
991
989
  for (const relative of deepestFirst) {
992
990
  const observation = observeSafePath(journal.consumerRoot, relative);
@@ -1244,7 +1242,11 @@ export class ApplyTransaction {
1244
1242
  path.join(this.root, "post-apply-verification-"),
1245
1243
  );
1246
1244
  try {
1247
- this.#workspaces.materialize(journal.finalRevisionId, isolatedRoot);
1245
+ await this.#workspaces.materializeAsync(
1246
+ journal.finalRevisionId,
1247
+ isolatedRoot,
1248
+ signal,
1249
+ );
1248
1250
  const execution = Promise.resolve(
1249
1251
  this.#hooks.postApply({
1250
1252
  transactionId,
@@ -3,6 +3,7 @@ import {
3
3
  chmodSync,
4
4
  closeSync,
5
5
  constants,
6
+ copyFileSync,
6
7
  fsyncSync,
7
8
  lstatSync,
8
9
  mkdirSync,
@@ -160,6 +161,22 @@ export class ArtifactStore {
160
161
  return Buffer.from(bytes);
161
162
  }
162
163
 
164
+ /** Clone when supported; always use a distinct inode and verify the copied bytes. */
165
+ copyVerified(hash: string, destination: string): number {
166
+ const source = this.#blobPath(hash);
167
+ const stat = lstatSync(source, { throwIfNoEntry: false });
168
+ if (!stat?.isFile() || stat.isSymbolicLink())
169
+ throw new Error("artifact-unavailable");
170
+ copyFileSync(
171
+ source,
172
+ destination,
173
+ constants.COPYFILE_EXCL | constants.COPYFILE_FICLONE,
174
+ );
175
+ const bytes = readFileSync(destination);
176
+ if (digest(bytes) !== hash) throw new Error("artifact-integrity-invalid");
177
+ return bytes.byteLength;
178
+ }
179
+
163
180
  referenceCount(hash: string): number {
164
181
  requireHash(hash);
165
182
  const referencePath = this.#referencePath(hash);
@@ -1,6 +1,7 @@
1
1
  import { lstatSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
4
+ import { compareCanonicalStrings } from "./canonical.ts";
4
5
 
5
6
  import { isValidRelativePath } from "./contracts.ts";
6
7
  import { observeSafePath } from "./safe-path.ts";
@@ -242,7 +243,7 @@ export function compileCandidatePatch(input: {
242
243
 
243
244
  const diff = [...states.values()]
244
245
  .filter((state) => state.base !== state.current)
245
- .sort((left, right) => left.path.localeCompare(right.path))
246
+ .sort((left, right) => compareCanonicalStrings(left.path, right.path))
246
247
  .map(renderFileDiff)
247
248
  .join("");
248
249
  if (diff.length === 0) throw new Error("candidate-patch-empty");
@@ -0,0 +1,16 @@
1
+ /** Canonical identity ordering is UTF-16 code-unit order, never host collation. */
2
+ export function compareCanonicalStrings(left: string, right: string): number {
3
+ return left < right ? -1 : left > right ? 1 : 0;
4
+ }
5
+
6
+ export function canonicalJson(value: unknown): string {
7
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
8
+ if (value && typeof value === "object") {
9
+ return `{${Object.entries(value as Record<string, unknown>)
10
+ .filter(([, entry]) => entry !== undefined)
11
+ .sort(([left], [right]) => compareCanonicalStrings(left, right))
12
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
13
+ .join(",")}}`;
14
+ }
15
+ return JSON.stringify(value) ?? "null";
16
+ }