Skip to content

kb init: skeleton stage (docs map, doc invariants, seeds) with its own runtime, budget and publisher - #248

Merged
tzhouam merged 2 commits into
mainfrom
kb/init-skeleton
Sep 30, 2026
Merged

tzhouam merged 2 commits into
mainfrom
kb/init-skeleton

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stage 1 of kb init (design kb-init v3): bootstrap a repository's knowledge base from its docs and seed pages, as one human-merged PR. This PR adds the stage and everything later stages reuse.

  • kb_service/init_budget.py: the per-stage budget_usd is a hard ceiling, enforced by reservation. Before dispatch, a generator call reserves its threshold plus one worst-case request, input_bytes × 1.25 × in_price + max_output_tokens × out_price. This is needed because claude's --max-budget-usd only stops the next request (Add a per-call spend threshold (claude --max-budget-usd) and report call cost #241). A generator model with no price is refused before any call. Judge calls are accounted at judge_call_usd.
  • kb_service/init_support.py:
    • InitRuntime: never KbRuntime, so init never touches kb.db.
    • InitRecord: one per stage under <state>/init/<repo>/<stage>.json.
    • UpstreamPin: a bare mirror. The pin is the upstream's current default branch, refreshed via ls-remote --symref, never an assumed main. The export refuses links and escapes. Also provides a PinnedObserver.
    • generate / judge: every model call goes through the budget.
    • Claim, evidence and path checks.
    • The CI knowledge validators, run on the resulting tree.
    • InitPublisher: one deterministic commit on an exact base, pushed to a branch that must be absent, with the PR opened through the owner's gh.
    • Prepared publications: a push that succeeded but whose PR failed is finished on retry with the same commit, with no second push and no model call.
    • Dry runs replace their output directory.
  • kb_service/init_stages.py: run_stage and validate_change. This is the only check_changeset(bootstrap=True) caller, as kb init: let L1 accept new-directory indexes in bootstrap mode, and check index links #243's guard test requires.
    • Model calls: a map call (entry page, architecture page, routes, general/ links), a doc-invariant call, and one adapt call per repos/<other>/ seed page.
    • Screening: every rule is D5-filtered, its evidence is hashed at the pin, its claims are checked at the pin, and it is placed on a running tree before judging. A full page spills the rule to a sibling page, so the judge sees each rule where it will be written. The advisory judge strips fail verdicts.
    • Writing: through apply_operations. Existing pages are only appended to.
    • Routes: start from owners read deterministically from existing sub-directory entry pages, using backticked code paths that exist at the pin inside source_roots. Model owners come after and must be this repository's own pages.
    • Checklist: findings on existing pages go into it.
    • suggest_seeds ranks candidates without a model call.
  • knowledge.init step, kb-init playbook, and CLI kb init REPO --stage S [--dry-run] [--pin SHA] / kb init REPO --suggest-seeds. The CLI dispatches init before any ledger is opened.
  • InitConfig gains generator_call_usd and harness_overhead_bytes. build_doc_corpus accepts optional globs, via doc_files.

Deviations from the spec (kb-init-stage-spec.md, PR C)

  • validate_change lives in init_stages.py, not init_support.py: kb init: let L1 accept new-directory indexes in bootstrap mode, and check index links #243's guard test allows bootstrap=True only in init_stages.py.
  • Seed existence is checked against the knowledge tree mapping at the base SHA, not via validate_seeds(Path). It is the same rule, and no checkout is needed.
  • The skeleton stage creates _routes.yaml only when it is absent. An existing one is left to the modules stage and noted on the checklist.
  • The repository tag must already be in doc/knowledge/SCHEMA.md's taxonomy, because the wiki lint requires it. Otherwise the stage is blocked with an instruction to add the tag in the adapter PR (relevant for jiuwenswarm).
  • The forward-compat facts.attest report at the upstream head (design §9 item 5) is not a separate step. The pin is the default-branch head at stage start, unless --pin names an older commit.
  • Additions from gate review:
    • KB_INIT_GIT_AUTHOR is checked before any model call.
    • A failed push or PR is recorded as blocked.
    • A dry run is refused while a publication is pending.
    • INIT_PATHS allows knowledge/repos/_index.md.

Tests

  • test/test_kb_init_skeleton.py, 30 tests, fully offline, against a toy upstream, a toy knowledge repository, scripted models and a faked gh:
    • An end-to-end dry run produces a tree that passes the L1 changeset (bootstrap), check_tree, check_index_links, the pinned checks, and the real check_knowledge_tree.py / check_wiki_lint.py.
    • A second KB run is append-only.
    • Page overflow: rules that overflow together, and a single rule too big for the page.
    • Publish flow, and recovery after a push whose PR failed.
    • A pending publication survives a dry run.
    • The pin follows the default branch, including a branch that changes after the mirror exists.
    • Fallback routes use normalised roots, including a tree with more than 200 top-level entries.
    • Blocked on a missing tag, an unavailable or unpriced generator, a missing seed, or a missing author.
    • Private upstream → dry run.
    • Budget exhaustion, reservation and charging, price table.
    • CLI never opens kb.db; step and playbook are registered.
  • test/test_kb_init_config.py is extended for the two new fields.
  • The local mirror of CI passes (rc=0): full pytest, doc links and citations, SPEC freshness --strict, knowledge linters.

Part 5/9 of kb init (design v3, sol-approved). Stacked on #239 #240 #241 #242 #243 via kb/init-integration; retarget to main after those merge.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T11:17:05.825940Z 4115751 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41157519bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

self.record = InitRecord(stage="skeleton", repo=lc.repo, pin=pin, kb_base_sha=base_sha,
inputs_digest=digest, started_at=float(int(rt.clock())),
dry_run=self.dry_run, notes=list(self.notes))
self.budget = Budget(init.budget_usd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Carry prior spend into same-stage retries

When a model call fails, the stage is recorded as blocked with its spend, but a retry with the same inputs ignores that record and creates a fresh zero-spend Budget. Repeated retries can therefore spend budget_usd each time while overwriting the record with only the latest attempt's cost, defeating the advertised hard per-stage ceiling. Initialize the retry budget from the matching prior record's spent_usd or prevent rerunning an exhausted stage.

Useful? React with 👍 / 👎.

Comment on lines +799 to +803
seen.add(slug)
owners.append({"owner": slug, "path": page, "signals": signals, "scope_prefixes": prefixes})
if not owners:
owners.append({"owner": _slug(self.lifecycle.repo), "path": f"{self.repo_dir}/{INDEX_NAME}",
"signals": [self.lifecycle.repo], "scope_prefixes": self._root_prefixes()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when a generated owner has no valid prefixes

If the model supplies a valid owner/page but every scope_prefixes entry is absent or malformed, this still appends an owner with an empty prefix list. Because owners is then nonempty, the repository-wide fallback is skipped, leaving changed-file routing unable to select any owner for that repository. Only append generated owners that retain at least one valid prefix, or add the fallback when none of the resulting owners can route files.

Useful? React with 👍 / 👎.

Comment on lines +852 to +853
title = f"kb init({lc.repo}): skeleton"
body = render_pr_body(record, lc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate spend before rendering the PR body

The PR body is rendered before record.spent_usd is copied from the budget in run()'s finally block, so every prepared or published PR reports $0.00 even when generator and judge calls incurred cost. The saved record is corrected afterward, but the already persisted PR body is not; update the record from the budget before calling render_pr_body.

Useful? React with 👍 / 👎.

Comment on lines +462 to +463
self.record.seeds.append({"origin": origin, "kb_sha": self.record.kb_base_sha,
"new_page": page, "new_rule_ids": [c.rule_id for c in found]})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record seed provenance after screening

Seed provenance is recorded for every preliminary candidate before D5, evidence, pinned-claim, and judge screening. If any candidate is later dropped—or all candidates for the seed are dropped—the record and PR body still claim those rule IDs and possibly a nonexistent new_page were produced. Build record.seeds from the kept candidates after screening so the review metadata matches the actual change.

Useful? React with 👍 / 👎.

Comment on lines +359 to +360
if not isinstance(data.get("owners", []), list) or not isinstance(data.get("general_links", []), list):
raise ValueError("owners and general_links must be lists")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate generated general-link entries before use

The map response validator only checks that general_links is a list, but _write_map assumes every element is a mapping and calls .get() on it. A schema-valid model response such as "general_links": ["general/rules.md"] therefore raises an uncaught AttributeError, leaving the stage record in started rather than producing a controlled blocked result. Validate each link object's shape alongside the outer list.

Useful? React with 👍 / 👎.

tzhouam and others added 2 commits September 30, 2026 23:36
Stage 1 of kb init (design kb-init v3): bootstrap a repository's
knowledge base from its docs and seed pages, as one human-merged PR.

- init_budget: hard per-stage ceiling by reservation. A generator call
  reserves threshold + one worst-case request (claude's
  --max-budget-usd only stops the next request); models without a price
  are refused before any call. Judge calls are accounted at a fixed cost.
- init_support: InitRuntime (never KbRuntime, so never kb.db), the
  per-stage InitRecord, the upstream pin (bare mirror; the pin defaults
  to the mirror's HEAD, i.e. the upstream default branch, never an
  assumed main; safe export; PinnedObserver), model calls through the
  budget, claim/evidence and outside-knowledge path checks, the CI
  knowledge validators on the result, and InitPublisher (one
  deterministic commit on an exact base, pushed to a branch that must
  not exist, PR via the owner's gh; or a local dry run). Writes need
  ALLOW_PUSH=1, ALLOW_POST=1 and KB_INIT_GIT_AUTHOR (checked before any
  model call); private upstreams are always dry runs. INIT_PATHS allows
  knowledge/repos/_index.md, the shared list a new repository joins.
- init_stages: run_stage + validate_change (the only
  check_changeset(bootstrap=True) caller). Skeleton: map call (entry
  page, architecture page, routes, general links), doc-invariant rules,
  adapted seed pages. Every rule is D5-filtered, evidenced and checked at
  the pin, then PLACED on a running tree before judging (a page that
  fills up sends the rule to a sibling page), so the advisory judge sees
  each rule where it is written; fail verdicts are stripped. Existing
  pages are only appended to; routes start from owners read
  deterministically from existing sub-directory entry pages (code paths
  they name that exist at the pin); findings on existing pages go to the
  PR checklist; a failed push is recorded as blocked. suggest_seeds
  ranks candidates without a model call.
- InitConfig gains generator_call_usd and harness_overhead_bytes;
  build_doc_corpus takes optional globs (doc_files).
- knowledge.init step, kb-init playbook, `kb init REPO --stage S
  [--dry-run] [--pin SHA]` and `kb init REPO --suggest-seeds`, dispatched
  before any ledger is opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L1 reads any line starting with "## <Word>" as a rule ID
(lifecycle.RULE_HEADING / ANY_RULE_HEADING), so a model-written
"## Overview" or "## SERV-1 — x" in architecture.md or the entry page
intro could collide with real rule IDs or masquerade as a rule. Every
non-rule page the skeleton writes now goes through neutral_headings: an
ATX heading outside code becomes a bold paragraph, any other line that
starts with "#" is escaped, and such a line inside a fence is indented
by one space. No line starts with "#", so the rule regexes (all anchored
at column 0) can never match. The entry page's list heading is bold too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tzhouam
tzhouam changed the base branch from kb/init-integration to main September 30, 2026 15:36
@tzhouam
tzhouam merged commit 6bc50a8 into main Sep 30, 2026
1 check passed
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