Editor polls rebuild the whole site on every tick; on a 2,500-page site the harness pins its CPU and the status poll never answers #358

Closed
opened 2026-10-31 16:42:38 +01:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

On a site with ~2,500 pages the harness pins its CPU answering the SPA's two-second tree and badge polls, each of which rebuilds the page tree, parses every page body and shells out to git. In hosted mode each editor runs on a single core, so the cheap GET /api/status poll starves behind them and the editor shows "Vorschau startet" / "Preview starting" forever although Hugo has been serving for minutes. The poll endpoints need a cost that does not grow with every request, and the SPA must never pile up requests.

Current behavior

Observed on deckle-hosted (deckle-01, site gbv-aktuell, Hugo 0.167.0, harness build f09f77f0, 2026-10-31). The site has 2,573 pages, 2,534 of them under news/. The instance runs under --cpus=1 --memory=1g.

  • 15:50:17 the harness starts listening; 15:55:37 it logs site preview ready (Hugo's first build took 5.5 minutes on the throttled CPU).
  • From the moment the editor tab is open, the harness process consumes 100% of its CPU indefinitely (980 of 1,000 scheduler ticks per 10 s, measured 10 minutes after Hugo was ready and idle). A small site's harness on the same host sits at 1%.
  • Every ~10 s the harness spawns git ls-tree -r -z --name-only refs/remotes/origin/main, git diff --name-status -z and git ls-files: one badge round trip takes about that long, against a 2 s poll interval.
  • 196 TCP connections were open to the harness's port at once: the SPA's poll loops fire on a fixed setInterval with no in-flight guard, so a slow answer never slows the next request, and the requests pile up.
  • The hosted bridge in front of the harness socket refuses new connections once its backlog is full, so the browser's GET /api/status fails. The status store keeps its last known state on failure, which is the initial starting; the pill reads "Vorschau startet" and the preview iframe never receives previewUrl.
  • Hugo itself was listening on its port the whole time; curl of the preview answered.

Where the cost comes from, per poll cycle (every 2 s):

  • GET /api/tree → annotatedTree() → pagetree.Build() reads and parses every page's front matter, then markOutdated().
  • GET /api/badges (fetched after every tree poll) → pagetree.Build() again, ChangedPublishFiles() (git diff + git ls-files), MainContentFiles() (git ls-tree), and pageMediaRefs(root, ".", false), which reads every .md file and runs mediaops.PageRefs over its body.
  • GET /api/publish/pending (every 3 s while visible) repeats the three git calls and the bundle index.
  • GET /api/status, every 1.5 s, is cheap and takes no lock; the Go server answers it on its own goroutine. It starves only because the single core is saturated and the piled-up connections fill the bridge's backlog. There is no server-side queue to remove: the fix for status is cheap polls plus the SPA in-flight guard.

Reproduced in triage (2026-10-31) by driving the handler directly on a generated site of 2,574 pages (2,534 under news/, 5,660 entries under content/), pinned to one core with taskset -c 0. Every request costs the same whether or not anything changed since the previous one:

Request / work One core Git processes
GET /api/tree ~150 ms 0
GET /api/badges ~400 ms 3 (diff, ls-files, ls-tree)
GET /api/publish/pending ~100 ms 3 (same three)
GET /api/status <1 ms 0
pagetree.Build alone 63 ms
pageMediaRefs alone 33 ms
git diff --name-status alone 34 ms
git ls-files --others alone 15 ms
full stat walk of content/ (5,660 entries) 25 ms

Reproduction without the hosted platform: scaffold a site, generate ~2,500 pages under one section, run harness serve with taskset -c 0 (or in a container with --cpus=1), open the editor, and watch the harness's CPU and the time to first /api/tree answer. On a wide machine the symptom is softer but the request duration still grows linearly with page count.

Desired behavior

  • The editor's recurring polls (/api/tree, /api/badges, /api/publish/pending, /api/status) answer in bounded time on a site of several thousand pages, and an idle editor tab leaves the harness close to idle. Work that depends only on the content tree and the git state is done once per change, not once per request: two consecutive polls with no save, no publish, no sync and no change on disk in between must not re-read any page or re-run git.
  • GET /api/status keeps answering from the supervisor's snapshot, without any content or git work, and is answered within its normal latency on a one-core run while a badge request is in progress.
  • A poll loop in the SPA (tree, badges, locks, Hugo status, publish status) never has more than one request in flight: a tick that finds the previous request pending is skipped. A slow harness then degrades to slower polling, not to a growing queue.
  • A change made to content/ by hand (a site developer or their agent editing files while harness serve runs locally) is reflected in the next poll's answer, exactly as today.
  • Correctness of the tree, badges and pending list is unchanged: a save, create, rename, move, delete, discard, publish, translation operation or startup sync is reflected in the next poll's answer exactly as today.

Decisions

  • Reuse, not thinner polling — shortening or lengthening the intervals is not the fix. The intervals are the editor's responsiveness contract; the per-request cost is the defect.
  • Status stays independent — /api/status is the one signal the user has while Hugo starts, and it must stay truthful even when the content endpoints are slow. It takes no lock and does no content or git work; keep it that way.
  • Staleness is decided by a stat walk of content/, not by the mutation path alone — a cached derivation of the content tree or the git state is valid while a fingerprint of content/ (every entry's path, type, size and mtime, files included; no reads, no parses) is unchanged and nothing moved the git state. Why: harness serve also runs locally, where content is edited by hand and must show up at the next poll; the mutating routes alone would miss that. Measured in triage on 2,574 pages on one core: a full walk costs 25 ms, versus ~270 ms of rebuild work per cycle today. A directories-only walk (19 ms) was rejected because an in-place file write changes no directory mtime. A filesystem watcher (inotify) was rejected: it is zero-cost when idle, but Linux-only (the harness also runs on macOS and Windows), subject to fs.inotify.max_user_watches (one watch per directory; 2,577 here, and some containers allow 8,192), races on newly created directories, and would still need the walk as its fallback, i.e. two owners for one rule. The walk may be added as an optimisation later behind the same owner.
  • One fingerprint per cycle — the three poll loops arrive within the same two or three seconds; the walk's result is shared between them for a short window (the implementer picks the window, on the order of a second) so a cycle costs one walk, not three. Why: three walks per cycle would be ~3% of one core; one is ~1%.
  • Git state is part of the key — the badge and pending derivations depend on the worktree (covered by the fingerprint) and on main's tip. Main's tip moves only through publish, sync and the startup sync, which all run under mutMu, and the X-Draft-Tip header the draftTipWriter injects already names the draft tip; invalidate there, or key on the ref itself (reading a ref is far cheaper than spawning git). How the cache is keyed and stored is the implementer's call.
  • Invalidation also follows the mutation path — every write the harness performs goes through the mutating routes (serialised under mutMu, see mutatingRoutes in internal/server/mutation.go), the publish job (which takes mutMu itself) or the startup sync. A mutation invalidates the cache directly so the next poll never answers from a stale entry even inside the fingerprint window.
  • One owner for "is this derived state current" — the rule lives in one function in the server (or a package it imports); the tree, badge and pending handlers all call it, and the web client never re-derives it (CLAUDE.md "Every rule has exactly one owner").
  • Behaviour on a small site is unchanged — the tests that pin today's tree, badge and pending responses keep passing without edits.

Key interfaces

  • pagetree.Build(contentDir, langs, lang, types) — called twice per cycle (tree and badges); its result for an unchanged content tree should be reusable.
  • Handler.annotatedTree(), Handler.handleTree(), Handler.handleBadges(), Handler.handlePublishPending() — the per-request scans live here. Starting hint: internal/server/modules.go, internal/server/git.go, internal/server/pending.go.
  • pageMediaRefs(root, contentPaths, strict) — reads and parses every page body on each badge request; the non-strict (badge) use is the one that must become cached. Starting hint: internal/server/publishset.go.
  • gitrepo.Repo.ChangedPublishFiles(), gitrepo.Repo.MainContentFiles() — spawn git diff, git ls-files and git ls-tree on each call; the results are stable between mutations and content changes. Starting hint: internal/gitrepo/status.go.
  • Handler.ServeHTTP / mutMu — the point every mutation passes; the natural place to invalidate. Starting hint: internal/server/server.go, internal/server/mutation.go.
  • The content fingerprint — a new function (the one owner of the staleness rule) walking content/ with filepath.WalkDir and hashing each entry's path, mode, size and mtime.
  • SPA poll loops that need the in-flight guard: createPageTree (TREE_POLL_INTERVAL_MS, which also fetches badges), createPageLocks, createHugoStatus (POLL_INTERVAL_MS), and the two polling loops in web/src/publish.ts (PUBLISH_POLL_INTERVAL_MS). createModules already has one (inFlight in web/src/stores/modules.ts) and is the pattern to follow. Starting hint: web/src/stores/.

Acceptance criteria

  • On a generated site of 2,500 pages, two consecutive GET /api/tree or GET /api/badges requests with no mutation and no change on disk between them: the second one performs no per-page file reads and spawns no git process (verifiable with strace -f -e trace=openat,execve or a counting test double).
  • On the same site, with the editor's poll loops running against the harness for five minutes with no user action, the harness process's CPU time grows by less than a few seconds in total, not by minutes.
  • On a one-core run (taskset -c 0), GET /api/status answers within its normal latency while a GET /api/badges on the large site is in progress.
  • A save through POST /api/page/save (and each other mutating route, the publish job and the startup sync) is reflected in the next /api/tree, /api/badges and /api/publish/pending answer, as today; the existing tests for those endpoints pass unchanged.
  • A file written, created, renamed or deleted directly under content/ (no API call) is reflected in the next /api/tree and /api/badges answer; a Go test covers an in-place edit of an existing page file and a new page directory with a file inside it.
  • The staleness rule has one owner: one function decides whether the cached derivations are current, and a test asserts that the tree, badge and pending handlers all go through it.
  • Each SPA poll loop named above skips a tick while its previous request is pending; a test with a hanging fetch shows at most one request in flight per loop.
  • A Go benchmark over pagetree.Build plus the badge computation on a generated large site exists next to history_bench_test.go's pattern, so the cost stays visible, and it reports the fingerprint walk's cost separately.
  • docs/architecture.md "Server" describes the cached derivations, the fingerprint and the invalidation points, and a new ADR in docs/adr/ records the decision (stat walk over watcher, with the reasons above). docs/api.md is unchanged because no wire shape changes.

How to verify

  • make test (Go and web suites).
  • The large-site reproduction above: measure /proc/<pid>/stat CPU ticks of the harness over 60 s with an editor tab open, before and after; count git children with pgrep -P <harness pid> over the same window.
  • taskset -c 0 curl -w '%{time_total}' http://127.0.0.1:<port>/api/status while a badge request is running on the large site.
  • Edit a page file under content/ with an editor while the harness runs and the tree is open; the title change shows at the next poll.

Assumptions to check

  • In hosted mode nothing but the harness writes the worktree while the instance runs (the control plane rebases the editor branch in the bare repository, and the harness picks that up through its own sync). The fingerprint covers content/; if anything else can move main's tip or the worktree outside content/ (the media library under assets/media/ feeds the badges too), the key must cover that as well — check whether the library needs to be in the walk or is covered by the mutation path alone.
  • pageMediaRefs's fast path (bytes.Contains(src, LibraryPrefix)) does not save the read: the file is still opened and read for every page.
  • The measured figures come from a 1-CPU, 1 GB container and from a one-core pinned test run; the linear growth with page count holds on any machine, the wall-clock figures do not.
  • mtime resolution: on filesystems with coarse mtimes, a write within the same tick as the previous fingerprint could go unnoticed if size is unchanged; size is in the key to narrow this, and the mutation path covers every harness write regardless.

Out of scope

  • The hosted instance's resource caps (--cpus=1 --memory=1g) and the socat bridge's listen backlog — these are deckle-hosted's and are filed there.
  • Hugo's own first-build time on a large site (hugo server configuration, --renderToMemory, render segments).
  • Changing what the tree, badges or pending list mean, or their wire shapes.
  • A filesystem watcher; see the decision above.
  • The 30-minute idle reaper or any change to how the hosted platform detects an editor's activity.
  • The scaffolded skills: no command, flag, template contract or hand-editing behaviour changes, so scaffold/site/.claude/skills/ and scaffold/site/AGENTS.md need no edit.

Blocked by

None - can start immediately

> *This was generated by AI during triage.* ## Summary On a site with ~2,500 pages the harness pins its CPU answering the SPA's two-second tree and badge polls, each of which rebuilds the page tree, parses every page body and shells out to git. In hosted mode each editor runs on a single core, so the cheap `GET /api/status` poll starves behind them and the editor shows "Vorschau startet" / "Preview starting" forever although Hugo has been serving for minutes. The poll endpoints need a cost that does not grow with every request, and the SPA must never pile up requests. ## Current behavior Observed on deckle-hosted (deckle-01, site `gbv-aktuell`, Hugo 0.167.0, harness build `f09f77f0`, 2026-10-31). The site has 2,573 pages, 2,534 of them under `news/`. The instance runs under `--cpus=1 --memory=1g`. - 15:50:17 the harness starts listening; 15:55:37 it logs `site preview ready` (Hugo's first build took 5.5 minutes on the throttled CPU). - From the moment the editor tab is open, the harness process consumes 100% of its CPU indefinitely (980 of 1,000 scheduler ticks per 10 s, measured 10 minutes after Hugo was ready and idle). A small site's harness on the same host sits at 1%. - Every ~10 s the harness spawns `git ls-tree -r -z --name-only refs/remotes/origin/main`, `git diff --name-status -z` and `git ls-files`: one badge round trip takes about that long, against a 2 s poll interval. - 196 TCP connections were open to the harness's port at once: the SPA's poll loops fire on a fixed `setInterval` with no in-flight guard, so a slow answer never slows the next request, and the requests pile up. - The hosted bridge in front of the harness socket refuses new connections once its backlog is full, so the browser's `GET /api/status` fails. The status store keeps its last known state on failure, which is the initial `starting`; the pill reads "Vorschau startet" and the preview iframe never receives `previewUrl`. - Hugo itself was listening on its port the whole time; `curl` of the preview answered. Where the cost comes from, per poll cycle (every 2 s): - `GET /api/tree` → `annotatedTree()` → `pagetree.Build()` reads and parses every page's front matter, then `markOutdated()`. - `GET /api/badges` (fetched after every tree poll) → `pagetree.Build()` again, `ChangedPublishFiles()` (`git diff` + `git ls-files`), `MainContentFiles()` (`git ls-tree`), and `pageMediaRefs(root, ".", false)`, which reads every `.md` file and runs `mediaops.PageRefs` over its body. - `GET /api/publish/pending` (every 3 s while visible) repeats the three git calls and the bundle index. - `GET /api/status`, every 1.5 s, is cheap and takes no lock; the Go server answers it on its own goroutine. It starves only because the single core is saturated and the piled-up connections fill the bridge's backlog. There is no server-side queue to remove: the fix for status is cheap polls plus the SPA in-flight guard. Reproduced in triage (2026-10-31) by driving the handler directly on a generated site of 2,574 pages (2,534 under `news/`, 5,660 entries under `content/`), pinned to one core with `taskset -c 0`. Every request costs the same whether or not anything changed since the previous one: | Request / work | One core | Git processes | | --- | --- | --- | | `GET /api/tree` | ~150 ms | 0 | | `GET /api/badges` | ~400 ms | 3 (`diff`, `ls-files`, `ls-tree`) | | `GET /api/publish/pending` | ~100 ms | 3 (same three) | | `GET /api/status` | <1 ms | 0 | | `pagetree.Build` alone | 63 ms | | | `pageMediaRefs` alone | 33 ms | | | `git diff --name-status` alone | 34 ms | | | `git ls-files --others` alone | 15 ms | | | full stat walk of `content/` (5,660 entries) | 25 ms | | Reproduction without the hosted platform: scaffold a site, generate ~2,500 pages under one section, run `harness serve` with `taskset -c 0` (or in a container with `--cpus=1`), open the editor, and watch the harness's CPU and the time to first `/api/tree` answer. On a wide machine the symptom is softer but the request duration still grows linearly with page count. ## Desired behavior - The editor's recurring polls (`/api/tree`, `/api/badges`, `/api/publish/pending`, `/api/status`) answer in bounded time on a site of several thousand pages, and an idle editor tab leaves the harness close to idle. Work that depends only on the content tree and the git state is done once per change, not once per request: two consecutive polls with no save, no publish, no sync and no change on disk in between must not re-read any page or re-run git. - `GET /api/status` keeps answering from the supervisor's snapshot, without any content or git work, and is answered within its normal latency on a one-core run while a badge request is in progress. - A poll loop in the SPA (tree, badges, locks, Hugo status, publish status) never has more than one request in flight: a tick that finds the previous request pending is skipped. A slow harness then degrades to slower polling, not to a growing queue. - A change made to `content/` by hand (a site developer or their agent editing files while `harness serve` runs locally) is reflected in the next poll's answer, exactly as today. - Correctness of the tree, badges and pending list is unchanged: a save, create, rename, move, delete, discard, publish, translation operation or startup sync is reflected in the next poll's answer exactly as today. ## Decisions - **Reuse, not thinner polling** — shortening or lengthening the intervals is not the fix. The intervals are the editor's responsiveness contract; the per-request cost is the defect. - **Status stays independent** — `/api/status` is the one signal the user has while Hugo starts, and it must stay truthful even when the content endpoints are slow. It takes no lock and does no content or git work; keep it that way. - **Staleness is decided by a stat walk of `content/`, not by the mutation path alone** — a cached derivation of the content tree or the git state is valid while a fingerprint of `content/` (every entry's path, type, size and mtime, files included; no reads, no parses) is unchanged and nothing moved the git state. Why: `harness serve` also runs locally, where content is edited by hand and must show up at the next poll; the mutating routes alone would miss that. Measured in triage on 2,574 pages on one core: a full walk costs 25 ms, versus ~270 ms of rebuild work per cycle today. A directories-only walk (19 ms) was rejected because an in-place file write changes no directory mtime. A filesystem watcher (inotify) was rejected: it is zero-cost when idle, but Linux-only (the harness also runs on macOS and Windows), subject to `fs.inotify.max_user_watches` (one watch per directory; 2,577 here, and some containers allow 8,192), races on newly created directories, and would still need the walk as its fallback, i.e. two owners for one rule. The walk may be added as an optimisation later behind the same owner. - **One fingerprint per cycle** — the three poll loops arrive within the same two or three seconds; the walk's result is shared between them for a short window (the implementer picks the window, on the order of a second) so a cycle costs one walk, not three. Why: three walks per cycle would be ~3% of one core; one is ~1%. - **Git state is part of the key** — the badge and pending derivations depend on the worktree (covered by the fingerprint) and on main's tip. Main's tip moves only through publish, sync and the startup sync, which all run under `mutMu`, and the `X-Draft-Tip` header the `draftTipWriter` injects already names the draft tip; invalidate there, or key on the ref itself (reading a ref is far cheaper than spawning git). How the cache is keyed and stored is the implementer's call. - **Invalidation also follows the mutation path** — every write the harness performs goes through the mutating routes (serialised under `mutMu`, see `mutatingRoutes` in `internal/server/mutation.go`), the publish job (which takes `mutMu` itself) or the startup sync. A mutation invalidates the cache directly so the next poll never answers from a stale entry even inside the fingerprint window. - **One owner for "is this derived state current"** — the rule lives in one function in the server (or a package it imports); the tree, badge and pending handlers all call it, and the web client never re-derives it (CLAUDE.md "Every rule has exactly one owner"). - **Behaviour on a small site is unchanged** — the tests that pin today's tree, badge and pending responses keep passing without edits. ## Key interfaces - `pagetree.Build(contentDir, langs, lang, types)` — called twice per cycle (tree and badges); its result for an unchanged content tree should be reusable. - `Handler.annotatedTree()`, `Handler.handleTree()`, `Handler.handleBadges()`, `Handler.handlePublishPending()` — the per-request scans live here. Starting hint: `internal/server/modules.go`, `internal/server/git.go`, `internal/server/pending.go`. - `pageMediaRefs(root, contentPaths, strict)` — reads and parses every page body on each badge request; the non-strict (badge) use is the one that must become cached. Starting hint: `internal/server/publishset.go`. - `gitrepo.Repo.ChangedPublishFiles()`, `gitrepo.Repo.MainContentFiles()` — spawn `git diff`, `git ls-files` and `git ls-tree` on each call; the results are stable between mutations and content changes. Starting hint: `internal/gitrepo/status.go`. - `Handler.ServeHTTP` / `mutMu` — the point every mutation passes; the natural place to invalidate. Starting hint: `internal/server/server.go`, `internal/server/mutation.go`. - The content fingerprint — a new function (the one owner of the staleness rule) walking `content/` with `filepath.WalkDir` and hashing each entry's path, mode, size and mtime. - SPA poll loops that need the in-flight guard: `createPageTree` (`TREE_POLL_INTERVAL_MS`, which also fetches badges), `createPageLocks`, `createHugoStatus` (`POLL_INTERVAL_MS`), and the two polling loops in `web/src/publish.ts` (`PUBLISH_POLL_INTERVAL_MS`). `createModules` already has one (`inFlight` in `web/src/stores/modules.ts`) and is the pattern to follow. Starting hint: `web/src/stores/`. ## Acceptance criteria - [ ] On a generated site of 2,500 pages, two consecutive `GET /api/tree` or `GET /api/badges` requests with no mutation and no change on disk between them: the second one performs no per-page file reads and spawns no git process (verifiable with `strace -f -e trace=openat,execve` or a counting test double). - [ ] On the same site, with the editor's poll loops running against the harness for five minutes with no user action, the harness process's CPU time grows by less than a few seconds in total, not by minutes. - [ ] On a one-core run (`taskset -c 0`), `GET /api/status` answers within its normal latency while a `GET /api/badges` on the large site is in progress. - [ ] A save through `POST /api/page/save` (and each other mutating route, the publish job and the startup sync) is reflected in the next `/api/tree`, `/api/badges` and `/api/publish/pending` answer, as today; the existing tests for those endpoints pass unchanged. - [ ] A file written, created, renamed or deleted directly under `content/` (no API call) is reflected in the next `/api/tree` and `/api/badges` answer; a Go test covers an in-place edit of an existing page file and a new page directory with a file inside it. - [ ] The staleness rule has one owner: one function decides whether the cached derivations are current, and a test asserts that the tree, badge and pending handlers all go through it. - [ ] Each SPA poll loop named above skips a tick while its previous request is pending; a test with a hanging fetch shows at most one request in flight per loop. - [ ] A Go benchmark over `pagetree.Build` plus the badge computation on a generated large site exists next to `history_bench_test.go`'s pattern, so the cost stays visible, and it reports the fingerprint walk's cost separately. - [ ] `docs/architecture.md` "Server" describes the cached derivations, the fingerprint and the invalidation points, and a new ADR in `docs/adr/` records the decision (stat walk over watcher, with the reasons above). `docs/api.md` is unchanged because no wire shape changes. ## How to verify - `make test` (Go and web suites). - The large-site reproduction above: measure `/proc/<pid>/stat` CPU ticks of the harness over 60 s with an editor tab open, before and after; count `git` children with `pgrep -P <harness pid>` over the same window. - `taskset -c 0 curl -w '%{time_total}' http://127.0.0.1:<port>/api/status` while a badge request is running on the large site. - Edit a page file under `content/` with an editor while the harness runs and the tree is open; the title change shows at the next poll. ## Assumptions to check - In hosted mode nothing but the harness writes the worktree while the instance runs (the control plane rebases the editor branch in the bare repository, and the harness picks that up through its own sync). The fingerprint covers `content/`; if anything else can move main's tip or the worktree outside `content/` (the media library under `assets/media/` feeds the badges too), the key must cover that as well — check whether the library needs to be in the walk or is covered by the mutation path alone. - `pageMediaRefs`'s fast path (`bytes.Contains(src, LibraryPrefix)`) does not save the read: the file is still opened and read for every page. - The measured figures come from a 1-CPU, 1 GB container and from a one-core pinned test run; the linear growth with page count holds on any machine, the wall-clock figures do not. - mtime resolution: on filesystems with coarse mtimes, a write within the same tick as the previous fingerprint could go unnoticed if size is unchanged; size is in the key to narrow this, and the mutation path covers every harness write regardless. ## Out of scope - The hosted instance's resource caps (`--cpus=1 --memory=1g`) and the socat bridge's listen backlog — these are deckle-hosted's and are filed there. - Hugo's own first-build time on a large site (`hugo server` configuration, `--renderToMemory`, render segments). - Changing what the tree, badges or pending list mean, or their wire shapes. - A filesystem watcher; see the decision above. - The 30-minute idle reaper or any change to how the hosted platform detects an editor's activity. - The scaffolded skills: no command, flag, template contract or hand-editing behaviour changes, so `scaffold/site/.claude/skills/` and `scaffold/site/AGENTS.md` need no edit. ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#358
No description provided.