Skip to content

CI: setup.sql boots on an empty home - #65

Merged
asubbarao merged 8 commits into
mainfrom
fix/boot-on-empty-home
Oct 19, 2026
Merged

asubbarao merged 8 commits into
mainfrom
fix/boot-on-empty-home

Conversation

@asubbarao

@asubbarao asubbarao commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

server-bootstrap fails on every PR that touches server/ (#56, #58, #64), because setup.sql errors on a machine with no Codex index and no lake folder:

  • agent.titles read ~/.codex/session_index.jsonl directly: now read through cat, so a missing file is an empty view. Nothing else uses the view.
  • lake.sql attached ~/.duck/lake/duckstack/catalog.ducklake before the folder existed: now makes the folder first.

Tested locally with an empty HOME (both pass) and on dev with the real file (view keeps id, thread_name, updated_at). CI on this PR is the real check.

🤖 Generated with Claude Code

Rebased onto main (2026-10-19)

  • reaper: dump what it kills to JSON; kill dev's own jobs after an hour #71 moved host_processes into processes.sql; this PR's edits to that block (no hard-coded home, no SIMILAR TO) were already done there, so the conflicts took main's side.
  • Review fixes: fs.sql's walk joins fs.rules by scope directly (no one-row pruned CTE); lake.sql's mkdir receipt is read_text; the hand-typed lake.agents.ext_fetch CREATE is removed (ext_catalog.sql is self-dispatched, so a missing table is a failed receipt, not a failed boot).
  • Kept: claude_sessions' typed pid column. terminal_sessions selects c.*, every key the session files hold, and joins on pid; reading the files through fs.read would turn those keys into one JSON body and drop terminal_sessions.status. With no session files, the empty typed column is what lets that join bind.

— Opus 5.5

@asubbarao
asubbarao enabled auto-merge October 18, 2026 22:39
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • server/lake.sql:9 check 2 / RULES 18 (no unnamed column0 left behind): read_csv(..., header := false) with && echo ready exists only to give the reader a row, and it leaves an unnamed column0 in the receipt. agent.sql already runs mkdir as a read_text receipt, which needs no echo.
    Rewrite: SELECT content FROM read_text($cmd$mkdir -p "$HOME/.duck/lake/duckstack" |$cmd$);
  • server/agent.sql:27 RULES 19 (never grep/find/cat), RULES 4 / check 2 (a failed reader comes back as a receipt): The view uses cat to do a reader's job, and 2>/dev/null; true throws away the real error, so a corrupt or unreadable index looks the same as a missing one. The file already handles this case the declarative way for agent.lines: it builds the view from listed files and dispatches the statement only when files exist.
    Rewrite: WITH c AS (
    SELECT list(quote_literal(path)) AS files FROM fs.files
    WHERE path = getenv('HOME') || '/.codex/session_index.jsonl'
    ), statements AS (
    SELECT tera_render($view$
    CREATE OR REPLACE VIEW agent.titles AS
    SELECT * FROM read_json([{{ files | join(sep=", ") }}], format := 'newline_delimited');
    $view$, c::JSON, autoescape := false) AS sql FROM c WHERE len(files) > 0
    ), posted AS (
    SELECT array_agg(selfdispatch(sql)) AS receipts FROM statements
    )
    SELECT r.* FROM posted CROSS JOIN UNNEST(receipts) AS t(r);

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • server/lake.sql:9 RULES.md 18 (check 2): read_csv(…, header := false) around mkdir -p … && echo ready returns an unnamed column0 receipt, and the echo ready exists only to give the reader a row. agent.sql already creates folders with read_text.
    Rewrite: SELECT content FROM read_text($cmd$mkdir -p "$HOME/.duck/lake/duckstack" |$cmd$);
  • server/agent.sql:27 RULES.md 4, 19 (check 2): The file is read through a shell cat … 2>/dev/null; true pipe, which hides the missing-file error. That breaks rule 19 (never cat), and on an empty HOME it binds a view whose columns are whatever read_json infers from empty input, not id/thread_name/updated_at. The same file already handles this case properly: agent.lines is posted only when its files are listed.
    Rewrite: WITH c AS (
    SELECT quote_literal(path) AS file FROM fs.files
    WHERE name = 'session_index.jsonl' AND starts_with(path[len(getenv('HOME')) + 1:], '/.codex/')
    ), statements AS (
    SELECT tera_render($view$
    CREATE OR REPLACE VIEW agent.titles AS
    SELECT * FROM read_json({{ file }}, format := 'newline_delimited');
    $view$, c::JSON, autoescape := false) AS sql FROM c
    ), posted AS (
    SELECT array_agg(selfdispatch(sql)) AS receipts FROM statements
    )
    SELECT r.* FROM posted CROSS JOIN UNNEST(receipts) AS t(r);
    -- or put this pair inside the $refresh$ program next to agent.lines, so the view appears once fs.files lists the index

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: failure

  • block sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • warn sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: failure

  • block sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • block sql_pattern_functions__tooling in server/host.sql: Parse with string functions, parsers and extension functions first; a short pattern is allowed only on web pages and log lines, on an explicit petition.
  • block sql_pattern_functions in server/host.sql: Parse with string functions, parsers and extension functions first; a short pattern is allowed only on web pages and log lines, on an explicit petition.
  • warn sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_json_extract in server/host.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_file_too_long in server/host.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_dispatch_shape in server/host.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.
  • warn sql_vague_cte_names in server/host.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: failure

  • block sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • block sql_pattern_functions in server/host.sql: Parse with string functions, parsers and extension functions first; a short pattern is allowed only on web pages and log lines, on an explicit petition.
  • block sql_pattern_functions__tooling in server/host.sql: Parse with string functions, parsers and extension functions first; a short pattern is allowed only on web pages and log lines, on an explicit petition.
  • warn sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_file_too_long in server/host.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_vague_cte_names in server/host.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_json_extract in server/host.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_dispatch_shape in server/host.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: failure

  • block sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • warn sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_vague_cte_names in server/host.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_json_extract in server/host.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_file_too_long in server/host.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • warn sql_dispatch_shape in server/host.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • existing sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • existing sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • existing sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • existing sql_json_extract in server/host.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • existing sql_dispatch_shape in server/host.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.
  • existing sql_file_too_long in server/host.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • existing sql_vague_cte_names in server/host.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • server/fs.sql:67 Shorter version / CLAUDE.md (no disguised singleton joins): The worklist gets a constant 'any' AS scope so it can be joined to a one-row pruned CTE. That is a singleton settings join with an extra CTE in between.
    Rewrite: Drop pruned and join the rules relation by its key directly:
    work AS (
    SELECT w.path, w.action, list(r.name) AS names
    FROM worklist w JOIN fs.rules r USING (scope)
    WHERE w.action IS NOT NULL GROUP BY w.path, w.action
    ) -- 6 lines → 4
  • server/host.sql:224 Shorter version / RULES.md 8 (code as data): Three separate AND NOT starts_with(g.args, …) clauses repeat one expression over a list of prefixes. Line 236 of the same file already holds a prefix list in a comprehension.
    Rewrite: WHEN list_bool_or([ends_with(w, '/codex') FOR w IN string_split(p.args, ' ')])
    AND NOT list_bool_or([starts_with(g.args, k) FOR k IN ['/Applications/ChatGPT.app/', getenv('HOME') || '/.codex/', getenv('HOME') || '/.local/bin/']]) THEN 'codex' -- 3 lines → 2
  • server/host.sql:45 Check 7 (typed NULL columns): UNION ALL BY NAME SELECT NULL::BIGINT AS pid WHERE false declares a column by hand so the view binds when there are no session files.
    Rewrite: Key the sessions by their file name, which always exists: CREATE OR REPLACE VIEW claude_sessions AS SELECT stem::BIGINT AS pid, f.* FROM fs.read f WHERE starts_with(f.folder, getenv('HOME') || '/.claude/sessions') AND f.ext = '.json'; (pid comes from .json, and the row's JSON stays in body).
  • server/lake.sql:13 Check 7 (defined schemas) / Check 5 (new table without a proposal): A hand-typed CREATE TABLE column list (url, fetched_at, response) arrives in a PR whose stated purpose is booting on an empty home. The description never mentions this table, and nothing shows it was proposed.
    Rewrite: Drop lines 12–13 from this PR. If Alok approves lake.agents.ext_fetch, create it with CTAS from the first fetch's reader (CREATE TABLE IF NOT EXISTS lake.agents.ext_fetch AS SELECT … FROM read_text($cmd$curl …|$cmd$)) so DuckDB infers the columns, and ship it in its own PR.
  • server/lake.sql:9 RULES.md 18 (never column0; use the reader's options): read_csv with header := false over mkdir … && echo ready leaves an unnamed column0 in the receipt. agent.sql already has a cleaner shape for the same side-effect mkdir.
    Rewrite: SELECT content FROM read_text($cmd$mkdir -p "$HOME/.duck/lake/duckstack" |$cmd$);

Opus 5.5, duckstack review queue (duckstack-review skill)

asubbarao and others added 8 commits October 19, 2026 01:03
…l when their files are missing

server-bootstrap boots setup.sql with a scratch HOME. agent.titles read ~/.codex/session_index.jsonl directly and the
lake attach needed ~/.duck/lake/duckstack to exist, so every PR touching server/ failed. Tested with an empty HOME:
titles builds empty, the folder is made, the lake attaches. With the real file the view still has id, thread_name, updated_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list statement reads file_last_modified itself, so a rerun writes the same row; never-walked is NULL.
Library is excluded by its rule; the LaunchAgents root and carve-out are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er is absent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ins on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad_text, no hand-typed ext_fetch table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCnrCZ6kWmagfJjoK8jVGQ
@asubbarao
asubbarao force-pushed the fix/boot-on-empty-home branch from 2b5e7ef to 0643770 Compare October 19, 2026 08:06
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • existing sql_file_too_long in server/fs.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • existing sql_new_table_in_file in server/fs.sql: Propose the new TABLE (name, columns, why a view or a direct extension call cannot do it) and create it only on Alok's yes; derive state as views.
  • existing sql_vague_cte_names in server/fs.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • existing sql_json_extract in server/host.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • existing sql_dispatch_shape in server/host.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.
  • existing sql_file_too_long in server/host.sql: Keep a job or loader file to 30-80 lines and one concern; split it, or state the reason and line count in the PR.
  • existing sql_vague_cte_names in server/host.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: success

No findings.

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao
asubbarao merged commit 270d4ba into main Oct 19, 2026
4 checks passed
@asubbarao
asubbarao deleted the fix/boot-on-empty-home branch October 19, 2026 08:11
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