Skip to content

feat(ui): craftar ui --allow-writes — Sync and Discard edit, confirmed in the page (0.37.0) - #76

Merged
llima merged 32 commits into
mainfrom
feat/ui-writes
Oct 24, 2026
Merged

llima merged 32 commits into
mainfrom
feat/ui-writes

Conversation

@llima

@llima llima commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Version bumped to 0.37.0. Merging publishes craftar@0.37.0 to npm after approval in the npm environment.
  • Spec 40 Part C: craftar ui --allow-writes. Typing the flag is the consent. The page then offers Sync for a workspace and Discard edit for one hand-edited file (drift / orphan-drift, offered only from its open diff), each confirmed in the page. Sync never passes --overwrite-drift, and a discard is a drift discard of that one path. Neither is offered for .claude/settings.craftar.example.json, nor for a workspace whose Forge is remote. A write is refused when the workspace changed since the page showed it (a status digest).
  • Two writing routes, POST /api/workspaces/<id>/sync {digest} and …/discard {digest, path}.
    • Requests are checked in spec §6.3's order: method → length → Host → Origin and Sec-Fetch-Site (both required for a POST) → route (405 with Allow) → token → Content-Type (415) → body ≤ 4096 bytes counted as it arrives (413), parsed strictly (400) → a queue of one.
    • Writes then run in §6.4's order: the remote Forge is refused from the configuration, before anything is fetched → load / plan / status → digest (409) → discard row (409 not a hand-edited file, the path never echoed) → unset parameter (409) → syncWorkspace. The answer has the shape of the MCP sync tool's result.
  • Without the flag craftar ui is unchanged, with one exception. The page, its script and stylesheet (pinned by sha256), the 405s and the three terminal lines are byte for byte 0.36.0's, and so is every answer except GET /api/workspaces, which gains writes: false.
  • Shared core, no second copy:
    • syncToolResult (src/core/write-actions.ts) is the one builder of the sync result, for craftar mcp and craftar ui. A discarded orphan-drift reads hand-edited orphan: discarded (DISCARDED_ORPHAN, the words drift discard prints). Every other removal keeps spec 41's orphanReason.
    • orphanDriftBody (src/core/report.ts) is the one reader of an orphan-drift body, used by drift show and the page. drift show's bytes were pinned before the change.
  • Two checks deferred from 0.35.0's review (test-only): an unreadable registry in mcp allow-writes / revoke-writes, and openWorldHint on the writing tools.

Ship notes

  • No run in a real browser. The page was driven in a stub DOM (Sync and Discard edit clicked, with fail-first evidence). Before the rebase onto 0.36.0, curl was run against a real craftar ui --port 0 --allow-writes on a synthetic workspace: 403 with no Origin, 415, 409 with no echo, a drift discard, a stale digest refused, a sync, and SIGINT exiting 0. Windows was not run by hand.
  • The Discard confirmation says only that the file's edit is lost (spec §6.5's wording). The same click also syncs the other pending files, as craftar drift discard does.
  • A body over 4096 bytes keeps nothing past the limit and is drained to its end (up to 1 MiB) before the 413 is sent and the connection closed. Destroying it at once reset the connection on Windows before the answer was read; the first CI run failed there. A test asserts the order (end before the response) and fails against the old server on Linux too.
  • A write that fails inside apply() after its first write answers 500 internal error, as every failure of ours does; the page is not told which files were written (the terminal gets the first line).
  • Content-Type is compared case-insensitively (a media type is), where spec §6.3 says "exactly".
  • The result line of a write is appended at the end of the view and may be below the fold. A 409 for a stale digest offers no Refresh button of its own; the frame's Refresh is on screen.
  • The page's diff route keeps its words for an orphan that agents-md.primary made (spec 41 §4.8), now pinned by a test.
  • Owed in the workspace after ship: spec 40 §19 / At ship, and the node-cli rule and reviewer's craftar ui bullet (POST under --allow-writes, the §6.3 order) together with the statusDigest sentence.

Test plan

  • npm run typecheck: pass
  • npm run build: pass
  • npx vitest run --exclude test/ci.test.ts --maxWorkers=6: 154 files passed (1 skipped), 3633 tests passed, 11 skipped. The oracle was skipped (no fixture in the worktree); no emitter, plan(), status() or apply() code changes.
  • node-cli-reviewer (opus), six rounds over the full range: R3 clean, R4 confirmation clean; R5–R6 over the Windows CI fixes, R6 clean; R7 full range at 1653ae9 clean.
  • Pins: test/ui-writes-pins.test.ts (the read-only page and answers), test/write-pins.test.ts, the drift show pins.
  • CI green on this PR (8/8 at 1653ae9)

llima added 30 commits October 24, 2026 10:53
Spec 40 §12.5: the page constants, the 405 of a non-GET and the reading routes' answers, captured before Part C.
…raftar ui

Spec 40 Part C: the ui write routes answer with the shape of mcp's sync tool; syncToolResult is that shape's one builder.
…the page's

Spec 40 §6.5: the page shows an orphan-drift file's body before a discard; drift show's read and its example-file test move to orphanDriftBody.
Spec 40 §6.4: a path Forge, a fresh digest and, for discard, one hand-edited row; then syncWorkspace, answered with mcp's sync result.
…ontent-Type and a 4096-byte body

Spec 40 §6.3: the checks in order; without the flag every answer is the read-only server's.
…oses

Spec 40 §6.3 step 8: the request is destroyed once the answer has finished, not before.
… list, comments to the file's style

No behaviour change.
…irmed in the page

Spec 40 §6.5: Discard edit only inside an open diff, never for the example file; without the flag the script is the read-only page's, byte for byte.
…vives the reload

Spec 40 §6.5: the page's writing script, driven in a stubbed DOM by the test.
Spec 40 §6.1: typing the flag is the consent; the terminal says writes are allowed, and without it the command is the read-only one.
…openWorldHint on the writing tools

Two checks left open by 0.35.0's review.
Spec 41's orphanReason now gives the removed reason for non-discarded files.
The words craftar drift discard prints for the path it discards; every other removal keeps spec 41's orphanReason.
Spec 40 §6: the Commands row, the status line, Phase 1's shipped list and Upgrading › to 0.37.0.
One spelling of the path under src/ (node-cli rule).
…ion names it

Spec 40 §6.4 step 2: the registry's record can be stale; refuseRemote throws before the Forge is read.
A disk error while hashing is the workspace's state (409), not ours (500).
A remote Forge or an unset declared parameter: the same gates as Sync.
The `to` header in the diff response now uses the same wording as
`craftar diff` for orphans due to agents-md.primary: "written to
AGENTS.md only" instead of "no longer produced".
primaryOrphan answers only for orphan; the orphan-drift header keeps its words.
drift discard and the page's discard print the same words.
orphanDriftBody reads the body since spec 40 Part C.
The backslashes JSON doubles were not regex-escaped; the pins failed on windows-latest only.
…he client

Spec 40 §6.3 step 8: destroying a request with unread body reset the connection on Windows before the answer was read; nothing past 4096 bytes is kept, and draining stops at 1 MiB.
llima added 2 commits October 24, 2026 14:14
Asserts the order, so the reset c886529 fixed is caught on Linux, not only on windows-latest.
Including the one that crosses 4096 bytes.
@llima
llima merged commit 68407ba into main Oct 24, 2026
8 checks passed
@llima
llima deleted the feat/ui-writes branch October 24, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant