Commit Graph
100 Commits
Author SHA1 Message Date
kyle 7f4f0d50bf docs: P3.1 search filters and multi-query merge
Native /v1/search knobs (domains, time, topic, exact match, min_score)
plus multi-query URL dedupe. Tavily passthrough; SearXNG approximations
with warnings. No crawl/map/research. Claude reviews, titan implements.
2026-09-29 15:03:14 -07:00
kyleandClaude Opus 5.5 5a59f7ae22 Merge P6: asynchronous batch fetch (POST /v1/batch, status, one result, cancel; sift batch)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:28:23 -07:00
kyleandClaude Opus 5.5 a452aa16a1 P6 review: runner shutdown, cancel-while-busy, concurrent submits, batch-off 503, transactional store, CLI 202
Owner review of p6 (Ornith tasks 01-09), with an independent reviewer pass whose findings I
confirmed against the code. Every fix has a regression test that fails on the old code.

internal/batch
- Close let the workers keep draining every live job under the cancelled context, failing
  every target; they now stop at once, store nothing on a failure during shutdown, and leave
  the job live for MarkInFlightFailed("restart").
- A job cancelled while its in-flight target came back busy stayed in the rotation forever
  (depth gauge stuck, job held in memory); it now leaves. The task 03 design had this gap: mine.
- Submit's idempotency lookup, caps and create ran unlocked: 20 concurrent submits by one
  caller made 8 jobs. Serialised with submitMu.
- A failed target insert left a queued job no worker would run (holding the caller's cap);
  it is marked failed.
- A second concurrent cancel returned true and counted twice.
- Config: zero OR negative values take the defaults (negative Workers panicked the
  WaitGroup; a negative TTL or byte budget would sweep or evict everything).
- Retry timing used the pre-fetch time (a 5 s busy answer made RetryWait moot).
- The rotation cursor is kept within [0, len(live)] (it was absolute and drifted on removal).
- The unused in-memory callers map is gone (Submit racing Close wrote to a nil map).
internal/dcache
- PutBatchTargets, SweepBatch and eviction are transactional; the sweep never removes a
  queued or running job.
internal/api
- Every /v1/batch endpoint answers 503 when batch is off; POST panicked on the nil runner.
- GET /{id}/{idx} bounds idx by the rows actually returned.
- batchFetch copies a *fetch.Error instead of mutating one the PDF pool may share.
internal/config
- ParseSize no longer overflows to a negative size.
cmd/sift
- sift batch treated 202 Accepted (submit) as an error and exited 1 with no message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:27:47 -07:00
kyleandornith-1.5-35b-a3b be48ecb550 cli: sift batch submit|status|get|cancel; README and skill
Add `sift batch` so agents reach the batch endpoint through the CLI like
`fetch` and `search`. `batchcmd.go` has `parseBatchArgs` (verb plus the
unchanged args after it; submit takes one arg, status a job id optionally
preceded by `--results`, get a job id plus a non-negative-integer idx, cancel
a job id, else a usage error), `batchMain` dispatch, one small function per
verb, and `batchDo` (bearer header, decode the envelope, print it on stdout,
on non-2xx the error to stderr and `exitFor`). `submitMain` reads the file or
stdin and validates locally: a JSON object with a non-empty `targets` array,
or a bare array of targets or URL strings wrapped into `{"targets": [...]}`.

README gains a "Batch" section (endpoints, serial-per-job/one-worker/
round-robin model, caps, idempotency, lease/TTL/restart, summary-vs-results,
redaction, metrics); SKILL.md gains a "Batch" section.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 23:13:48 -07:00
kyleandornith-1.5-35b-a3b 484c33f986 serve: [batch] config; open the store, reap jobs killed by a restart, start the runner
Add the [batch] config block (Batch type + File.Batch) and ParseSize (binary
KiB/MiB/GiB byte counts) in internal/config. serve now opens the dcache store
when batch is on, marks the queued/running jobs a restart killed as failed
(never resumed) and observes them, builds the runner from the config with the
runner's defaults filling zeros, and closes the runner on both shutdown paths.
Batch is on by default; disabled = true leaves srv.Batch nil so the endpoints
answer 503.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 23:06:46 -07:00
kyleandornith-1.5-35b-a3b 692a334c09 api: /v1/batch — submit, status summary, one result, cancel
The HTTP surface over s.Batch. New internal/api/batch.go: batchSubmit (POST)
decodes {"targets":[{url,mode,question,pages,grep,jq,context,recover}]} flat as
/v1/fetch does, 400 "invalid batch" on bad json / empty list / empty url / bad
mode, then PreCheck per target -> 422 "blocked: target <idx>: <reason>" with no
job, then Submit mapping ErrTooManyTargets->422, ErrCallerBusy->429 Retry-After
30, ErrBusy->429 Retry-After 60, ErrNoTargets->400, and 202
{success,job_id,status,targets,reused}. batchStatus (GET), batchResult
(GET {id}/{idx}) and batchCancel (DELETE) share ownership: unknown or not
c.Name -> 404, never 403; status is a summary (no inline results unless
?results=1), idx validated 0..targets_total-1 else 404, cancel of a terminal
job -> 409 else 200 {success,status:cancelled}. api.go scoped to rule 2:
batchMaxBody (256 KiB), the four routes in New, and ServeHTTP applying the
per-path cap once and collapsing every batch path to a fixed metrics label so a
job id never becomes a label. Each handler logs one batch line, never a URL,
hostname or idem_key.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 22:59:05 -07:00
kyleandornith-1.5-35b-a3b 72e0828360 api: NewBatchRunner — the batch fetch adapter, retry on busy, outcome metrics
Wire the batch runner to fetchTarget and feed the sift_batch_* metrics.
New internal/api/batch.go: NewBatchRunner overwrites cfg.Hooks (Job, Target,
JobSeconds, Depth, Inflight → s.Prom) and sets the runner's FetchFunc to
batchFetch, which tags the context as a batch fetch, runs under requestDeadline,
and maps fetchTarget's errors to what the runner retries (429/503 →
ErrRetryLater) and what the metrics classify (batchOutcome). Added the Batch
field to Server (nil → task 07 answers 503 "batch disabled") and, in
extractPDF, charged the PDF pool to c.Name+"/batch" under the batch mark so a
batch never takes a caller's interactive slot while the single-flight key is
unchanged. No HTTP endpoints.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 22:43:28 -07:00
kyleandornith-1.5-35b-a3b 0d923bc366 api: v1fetch's per-target pipeline becomes fetchTarget (no behaviour change)
Move v1fetch's inline per-target pipeline into a new internal/api/target.go
method, `fetchTarget(ctx, c, t batch.Target) (batch.Result, error)`, so the
batch worker can drive the same work. The body moves unchanged in order:
question truncation, mode default/check, wantCtx, doFetch, the recover path,
the PDF branch (render, pages/grep errors, question-chunk limit), the
clean/raw branch, digest, context search, the fetch log line and the
envelope. `content` is returned as Result.Content; every other envelope key
goes in Result.Meta, never content. v1fetch keeps reqN/caller/decode/400,
builds a batch.Target from the request, calls fetchTarget, writeFetchErr on
error and writeJSON(w, 200, env) on success where env is a copy of Meta plus
"content". doFetch takes context.Context instead of *http.Request; scrape
passes r.Context(). No new behaviour, fields or JSON keys.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 22:34:56 -07:00
kyleandornith-1.5-35b-a3b e0168f7d65 metrics: sift_batch_* series (jobs, targets, depth, in-flight, durations, bytes)
New internal/metrics/batch.go adds the batch series to metrics.Set, registered
in New: BatchJobsTotal and BatchTargetsTotal counters (label "outcome", any
value outside the known set folded to "other" so a raw string never becomes a
label), BatchQueueDepth and BatchTargetsInflight gauges, BatchJobSeconds and
BatchTargetSeconds histograms (the existing request buckets) and
BatchResultBytes (1K,4K,16K,64K,256K,1M). The Observe…/Set… methods are
nil-receiver safe like ObservePDF and flow through the existing /metrics
handler; no new endpoint or Snapshot change.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 22:21:59 -07:00
kyleandornith-1.5-35b-a3b 816a76fe5b batch: workers — round-robin, serial per job, busy retry, cancel in flight, hooks
Start cfg.Workers goroutines in New that drain one rotation of live jobs in
submit order, each taking the next job with a pending target and none in flight,
so targets alternate across jobs and each job stays serial. One target per fetch
under the Close-cancelled context: panic recovers into ErrInternal (never
echoed), ErrRetryLater retries after RetryWait (bounded by RetryFor), otherwise
the target is stored done/failed and the hooks fire. Terminal status is set once
per job; cancelling a running job stores the in-flight target without overwriting
cancelled and leaves later targets pending.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 22:18:35 -07:00
kyleandClaude Opus 5.5 0e691a6b4f Merge master into p6: task 03 design section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:49:42 -07:00
kyleandClaude Opus 5.5 38f0689ae3 P6 task 03: an explicit Design section (state, worker loop, Cancel, Close) after the first attempt spent its whole reply deliberating the concurrency
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:49:35 -07:00
kyleandornith-1.5-35b-a3b e2ddb2e9d7 batch: runner core — types, Submit with idempotency and caps, Status, Touch, Cancel
New package internal/batch holding the runner core with no workers yet (task 03
adds them; until then every job stays queued, so a queued job counts as live for
the caps). batch.go defines the public surface: Options/Target/Result/TargetError,
FetchFunc, Job/TargetStatus, Hooks and Config with its defaults (TTL 24h, 32
targets/job, 1 job/caller, 8 total, worker clamped to 2, 64 MiB, 2 s / 10 m retry).

Submit runs in the given order: empty → ErrNoTargets; idempotency via hex SHA-256
of `c.Name + NUL + json.Marshal(targets)` (a live BatchJobByIDem hit returns the id
reused, touching no caps); then TargetsPerJob / JobsPerCaller / JobsTotal; then it
stores the job queued (random 16-byte hex id, CreatedAt = Lease = now) and its
targets (pending, Options = json of t.Options) before returning. The submitting
caller is held in memory keyed by job id, never persisted (jobs never resume after
a restart). Status maps the rows and decodes Result from the result column without
writing; Touch moves the lease; Cancel sets a queued job cancelled at once and
leaves its targets pending; Close is idempotent. Store errors are Warn-logged with
the job id, never a URL or host.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 21:30:35 -07:00
kyleandClaude Opus 5.5 013a39cb4b Merge master into p6: the task 02 split, guard.PreCheck, AGENTS.md rule 6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:14:22 -07:00
kyleandClaude Opus 5.5 103ec3d05a P6 during the run: split the runner task (02 core, 03 workers); guard.PreCheck as an owner commit; AGENTS.md: write code into files early
Task 02 (the whole runner) ended at 21:08 with no file and no commit: Ornith wrote the
implementation out in its reply (~700 lines), OpenCode compacted the session, and the model
then emitted a malformed tool call. It had also looked for the given test at a wrong path
("_files/…" relative to the plan: my ambiguity).

- 02-runner → 02-runner-core (types, Submit, Status, Touch, Cancel; no goroutines) and
  03-runner-workers; the given test split to match (runner_core_test.go gains three tests:
  queued storage, cancel while queued, Status never moves the lease); later tasks renumbered
  04-09 and every cross-reference checked.
- Copy paths spelled from the repo root.
- guard.PreCheck (+ its test) is committed here by the owner: the old task 05 told the
  implementer to edit internal/guard/, which AGENTS.md forbids. My contradiction.
- AGENTS.md rule 6: write code into files early, in small steps.
Both runner test files compile against a panic-only skeleton (the core file also alone) and
fail there on panic: todo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:14:08 -07:00
kyleandornith-1.5-35b-a3b 2529cb8e84 dcache: batch job and target tables with a sliding lease, sweep and byte budget
Add the batch job store to internal/dcache (storage only; no HTTP, no worker).
New internal/dcache/batch.go holds the BatchJob/BatchTarget types and all
accessors; Open gains the batch_job/batch_target tables and indices, and DB
gains BatchH/BatchM counters.

Sliding lease: writes (UpdateBatchTarget, SetBatchJobStatus, TouchBatchJob,
MarkInFlightFailed) bump the job lease to now; reads never write. BatchJobByIDem
returns the newest (caller, idem_key) job with lease+ttl>now and status
queued/running/done/partial. UpdateBatchTarget counts targets_done exactly once
per target entering done/failed and adds len(result) to nbytes. SweepBatch
removes by lease; EvictBatchOldest evicts whole non-live jobs oldest-lease-first;
MarkInFlightFailed reconciles in-flight jobs on restart. Batch nbytes are kept out
of Size() and the content evict union.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 20:38:42 -07:00
kyleandClaude Opus 5.5 583a35883d PLAN: future work — digest retrieval misses terse summary facts (RP2040 PIO count, prod check after P5b)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:34:57 -07:00
kyleandClaude Opus 5.5 94da9d9127 skill: confirm numbers and counts from a digest with --grep; the question refusal is now only past 5000 chunks
Found in the P5b prod check: "how many PIO state machines in total?" on the RP2040 datasheet
came back 4 (bm25 and hybrid); the answer, 8, is a terse bullet on p. 9 that --grep finds at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:33:19 -07:00
kyleandClaude Opus 5.5 bdc093ee02 Merge the P6 plan (asynchronous batch fetch), revised after owner review; Kyle to re-review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:18:24 -07:00
kyleandClaude Opus 5.5 6e0949d58f Merge P5c: POST /v1/extract and sift extract (PDFs already on a host)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:56:53 -07:00
kyleandClaude Opus 5.5 a804f5ac93 P5c review: bound concurrent uploads; warnings is a list; sift extract keeps a base-URL path
Owner review of p5c (tasks 01-02 by Ornith):
- extract.go: an upload (up to 100 MiB) is read whole before the PDF pool's limits apply, so
  any number of concurrent uploads could buffer at once in a 1 GiB service. At most 2 bodies
  are read at a time now; a third waits up to 5 s, then 429 busy with Retry-After 10.
- extract.go: `warnings` was null when empty; it is [] as in /v1/fetch.
- extractcmd.go: extractURL replaced the client base URL's path; it now appends /v1/extract
  like fetch and search do.
Regression tests in extract_review_test.go (api and cmd/sift).

End-to-end (local serve -> Dixie; sift extract with SIFT_CLIENT): Llama 2 paper, 13.6 MB /
77 pages: --pages 1 ok, repeat served from the content-hash cache, --grep 2 hits, --question
answered with page citations in 16.8 s. Log lines carry bytes/pages/mode/cached only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:56:49 -07:00
kyleandornith-1.5-35b-a3b 3a3a470cd7 cli: sift extract uploads a local PDF; README and skill
`sift extract <file.pdf>` stats the file (refusing over 100 MiB before the
upload), streams it as the POST body with Content-Type application/pdf and the
ContentLength set, and prints the /v1/extract answer exactly as `sift fetch`
prints a PDF URL — same content output, same errors and exitFor codes.
parseExtractOpts reuses splitAroundPositional; extractURL sends only the set
options with mode=digest when a question is given. main.go gains the case and a
help line; README and skill document the command.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 19:52:09 -07:00
kyleandornith-1.5-35b-a3b 7c40531891 api: POST /v1/extract answers an uploaded PDF like a fetched one (content-addressed cache)
Add POST /v1/extract: auth the caller, read the PDF body, hash it, and answer
it on the PDF fetch path exactly as /v1/fetch answers the same PDF by URL. The
hash is the content-addressed cache key ("upload:"+hash under the caller class),
so identical content is extracted once with no TTL or revalidation. The api.go
diff is scoped to the rule-1 changes: the DefaultPDFMaxUpload const, the
PDFMaxUpload Server field, the route, and the two ServeHTTP lines.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 19:37:45 -07:00
kyleandClaude Opus 5.5 0481b6e32f Merge P5b: long-document questions (page-aware chunks, BM25 first, cached embeddings built in the background)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:02:55 -07:00
kyleandClaude Opus 5.5 64d08a358d P5b review: background embedder re-queues a failed document and waits for a busy gate; no empty vectors cached
Owner review of p5b (tasks 01-03 by Ornith):
- background.go: `seen` was never cleared, so a document whose background embedding failed
  (Dixie hiccup) or was abandoned could never be queued again: every later question on it stayed
  BM25 for the life of the process. Dedupe now covers only queued and in-progress documents.
- background.go: acquireGate gave up after one retry and dropped the document; the task said to
  keep retrying once a second. It now waits (background work has no deadline).
- retrieval.go/background.go: a vector the server left out is not cached as an empty vector.
- bm25.go: Scores on a nil *BM25 dereferenced it; returns nil.
Regression tests in review_test.go; both fail on the old code.

End-to-end against Dixie (local serve): Karpathy's thesis, 95 pages / 51 chunks: bm25 in 19 s,
hybrid (51 embedded) after the background; RP2040 datasheet, 642 pages / 284 chunks (a 422
before P5b): bm25 in 13.5 s, all 284 embedded within 90 s, hybrid answer correct with its quote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:02:51 -07:00
kyleandornith-1.5-35b-a3b 8cdb3e8537 api: questions on long PDFs are answered (retrieval in the digest meta); README and skill
The digest now handles long documents itself (task 02), so the API's refusal
limit is only a safety cap: DefaultPDFMaxQuestionChunks 200 -> 5000 (the 422
message text is unchanged). In v1fetch the digest-meta object gains "retrieval"
and "embedded" next to chunks_used/chunks_total, only when the digest ran
without a fallback. The PDF section explains a long-doc question is answered
first from BM25 while its chunks embed in the background and hybrid on later
questions; skill/SKILL.md gains one sentence. Given tests byte-identical.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 18:52:28 -07:00
kyleandornith-1.5-35b-a3b f6644c9278 digest: cache chunk embeddings; long documents answer from BM25 while embedding in the background
Cached chunk embeddings change what a long document answers with: BM25 first,
embedding uncached chunks (or the whole document) in the background so the next
question on the same doc scores hybrid. selectText now returns its mode and the
number of chunks that had a vector at answer-build time; the digest cache stores
only hybrid answers, never a provisional bm25 one. The background worker batches
through a Dixie gate and WaitBackground polls for true idle.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 18:44:28 -07:00
kyleandClaude Opus 5.5 300b46bb67 PLAN: future work — paywalled pages to archive snapshots (Kyle, possible P7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:05:55 -07:00
kyleandClaude Opus 5.5 7b7eb4e19b P5b README: changes during the run (KV overflow on run 1; my fixture-size bug in task 02's given test)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:58:12 -07:00
kyleandornith-1.5-35b-a3b 7595ad8519 digest: SplitPages keeps every chunk on its page; BM25 scorer
SplitPages is Split's budget/line splitting plus page awareness: a "--- page N ---"
line sets the current page and a chunk that starts after the first marker but not on
a marker line is prefixed "--- page N (cont.) ---\n\n" for the page its first line
belongs to, so every chunk names its page. BM25 (k1=1.2, b=0.75) ranks chunks
lexically with counted term frequencies and ln(1+(N-df+0.5)/(df+0.5)) IDF.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 17:57:27 -07:00
kyleandClaude Opus 5.5 d52a4fa1e6 P5b given test: the embedding-cache test used a 12-page document (4.6k tokens), under SmallDoc, so the rules make it "whole", not "hybrid"; use 30 pages (11.7k tokens, 10 chunks) and assert the precondition
Found by Ornith in the p5b/02 session (it measured the document); my test's fault, not the task's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:57:14 -07:00
kyleandClaude Opus 5.5 218eaa8b53 P6 plan, owner review folded in: caller travels with the job, 1–2 round-robin workers, busy retry, summary reads, local-only tests, 8 tasks
Kyle: "Fold them into the plan." What changed and why is in docs/P6.md §14. In short:
- FetchFunc gets the *auth.Caller (the given api test could not compile, and a fetch with no
  caller would bypass per-caller gates, the pool limit, the cache class and redaction);
- store reads never bump the lease (task 01's rule contradicted its own test); the API Touches;
- 1-2 workers round-robin over live jobs, so a batch holds at most 2 of the 8 global slots;
  busy targets retry in the runner (bounded); batch PDFs use their own pool name; 210 s deadline;
- the api tests no longer touch the network or race the cancel; private names via fake DNS;
- failed/cancelled jobs are not reused; GET is a summary plus GET …/{idx} and ?results=1;
- the 256 KiB body cap moves into ServeHTTP; a fixed metrics label for /v1/batch/{id};
- guard.PreCheck (given verbatim) replaces guard.ParseURL at submit;
- 5 tasks became 8 (refactor, wiring, endpoints and serve/config split), each restating the
  APIs it uses; every given test compiles against a panic-only skeleton and fails on it.

P6 starts after P5c: task 04 moves v1fetch, which P5b changes. Recheck the api given tests
against post-P5c master before the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:55:50 -07:00
kyleandDeepSeek V4 Pro 63e70e19fc P6 plan: asynchronous batch fetch endpoint — phase spec, task plan, given tests
Co-Authored-By: DeepSeek V4 Pro <noreply@deepseek.com>
2026-09-26 12:09:38 -07:00
kyleandClaude Opus 5.5 b1da18d49e PLAN: future work — asynchronous batch fetch endpoint (Kyle, after P5c)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:03:43 -07:00
kyleandClaude Opus 5.5 49d96e197b P5b and P5c plans: long-document retrieval (BM25 first, background embedding) and /v1/extract + sift extract; given tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:01:18 -07:00
kyleandClaude Opus 5.5 1311cf38d4 pdfdoc test: RE2 timing bound 1 s -> 10 s (race detector made it flaky: 1.0-1.9 s)
The check distinguishes linear time from catastrophic backtracking, which would never
finish on a 1 MB input, so a generous bound loses nothing. Reported by talos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:53:52 -07:00
kyleandClaude Opus 5.5 c89281200a docs: bring README, skill and dist up to date through P5a
README gains API, CLI and configuration sections and the PDF range/grep/question limits; the skill covers PDF busy and failure results; dist/sift.service matches the Nix unit (MemoryMax=1G, secret.env); the example config drops the unused [upstreams] block; stale task comments in api/pdf.go.

Gate overridden by the owner: TestGrepLimits (given test) exceeds its 1 s budget under -race on this host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 10:21:51 -07:00
kyleandClaude Opus 5.5 01c8d9daeb Merge P5a: PDF documents (extraction in a child, pool, cache, map, pages, grep, cited digests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:48:53 -07:00
kyle 7bfe567223 Merge remote-tracking branch 'origin/master' into p5a 2026-09-26 04:48:21 -07:00
kyleandClaude Opus 5.5 aafc1d1c28 P5a review: outlines from named destinations; pool race; fast over-limit; revalidation under the limiter; Accept pdf
- pdftext/outline: resolve named destinations through the catalog /Dests and the /Names /Dests
  tree, resolve indirect /Dest, /A and /Title, and treat reader.Null (what Dict.Get returns for
  a missing key) as absent. Before this every LaTeX and Acrobat PDF had an empty outline.
  pdftest gains NamedDests; TestOutlineNamedDestinations.
- api: pool() always goes through its Once (data race on concurrent first use);
  TestDefaultPoolConcurrentFirstUse. Revalidation of a stale cache entry counts against the
  caller/host limit.
- pdftext/runner: output over MaxText kills the child at once instead of waiting for the
  deadline; TestRunnerTooMuchTextFailsFast.
- fetch: Accept includes application/pdf;q=0.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:48:17 -07:00
kyleandClaude Opus 5.5 2eb8c843ef p5a: pdftest gains NamedDests (owner review); review notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:48:13 -07:00
kyleandornith-1.5-35b-a3b 7e78de633f P5a: digests cite pages; sift_pdf_* metrics; sift fetch --pages/--grep; skill, unit and README
Digest systemPrompt gains a page-cite instruction and PromptVersion is 3.
Add the sift_pdf_* metric series (extract_total{outcome}, extract_seconds,
pages_total, pool_wait_seconds, cache_hits_total) to internal/metrics and wire
ObservePDF/ObservePDFWait/PDFCacheHit into internal/api/pdf.go. Refactor
cmd/sift/fetchcmd.go to a fetchOpts struct with parseFetchOpts and fetchBody,
adding --pages/--grep. Update skill/SKILL.md, set nix MemoryMax to 1G, and add a
PDF documents section to the README.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 04:36:49 -07:00
kyleandornith-1.5-35b-a3b 09f738bcac api: PDF cache with revalidation; pages and grep; refuse questions on very long documents
Answer extracted PDFs from the document cache: fresh for PDFTTL, then a
conditional GET (304 reuses the stored text, a 200 is extracted and stored
without a second GET). Store after extraction with the response hash, ETag,
fetch time and extracted pages. Route pages/grep through pdfdoc.Options with
400s for bad ranges and non-PDFs, and refuse a question when the document
exceeds PDFMaxQuestionChunks with a pointer to grep and pages.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 04:23:21 -07:00
kyleandornith-1.5-35b-a3b 93ec310113 api: serve PDFs — extraction in the child under the pool, full text or map, errors
Route "pdf" through clean.Kind in doFetch/cleanFetched: extract under the pool
(keyed by caller class + document hash, one per caller), map the five pdftext
errors plus a pool panic to 422, ErrBusy to 503+Retry-After 10, ErrCallerBusy to
429, and answer with the whole text or the map. Adds RetryAfter to fetch.Error
and the matching header. No cache, pages or grep — those are task 07.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-26 04:04:11 -07:00
kyle 9c858aded9 Merge remote-tracking branch 'origin/master' into p5a 2026-09-26 03:12:35 -07:00
kyleandClaude Opus 5.5 c8d0e2b44a run-plan: per-session timeout (SESSION_MAX, default 90 min)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 03:12:26 -07:00
kyleandClaude Opus 5.5 5ce625be33 p5a: split task 06 (too large: looped 3h) into 06-api-pdf and 07-api-cache-pages; old 07 is 08
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 03:12:07 -07:00
kyleandornith-1.5-35b-a3b 41a68feb06 pdfdoc: page markers, the map for long documents, page ranges, grep
Pure render function on a *pdftext.Doc with no I/O: the whole text with
page markers when it fits, otherwise a map (title, outline with page
numbers, leading pages and how to ask for more), a page range, or grep
results with page numbers. Every length is in runes.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-25 23:52:11 -07:00
kyleandornith-1.5-35b-a3b bb9a6544d1 pdfpool: a few extraction slots, a short queue, single-flight, one job per caller
Generic in-process scheduler for the expensive PDF-extraction child process.
A pool runs at most `slots` jobs, keeps at most `queue` waiting, and refuses a
waiter past `wait`. Do() applies six rules in order: join an existing running
or queued job (no slot, allowed while the caller holds another, fn may be nil);
refuse a caller that already has a job in flight; take a free slot, else queue
(first-come-first-served, else ErrBusy); run fn under context.WithoutCancel so
the flight outlives a caller that leaves; broadcast the result to every sharer
with no caching and a recovered panic returned as an error. Running counts only
jobs holding a slot; Queued counts the line; Waiting(key) counts joiners.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-25 23:32:16 -07:00
kyleandornith-1.5-35b-a3b b93b7d5816 dcache: PDF document cache (per URL and caller class, whole-document eviction)
Add pdf_doc/pdf_page tables plus PDFEntry, PDFKey, PutPDF (one transaction,
old pages deleted first), GetPDF (page-ordered, updates last_hit) and TouchPDF
(304 revalidation moves only fetched_at). PDF documents count toward Size() and
are evicted whole by oldest use; PDFH/PDFM counters added. evict() now breaks
last_hit ties by rowid so same-second inserts evict in insertion order.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-25 23:05:12 -07:00
kyleandornith-1.5-35b-a3b f57aaef59e pdftext: extract PDF text in a re-exec'd child (sift pdftext) with a deadline
The parser is pure Go but a goroutine cannot be killed, so extraction runs in
a child process: the binary re-executed as `sift pdftext`, reading the PDF on
stdin and writing JSON Lines on stdout, killed at a deadline by the parent.

pdftext.go holds the types, the five errors, Extract (reader.Open with
ErrWrongPassword -> ErrEncrypted, per-page recover, trailer /Info /Title, and
rule-3 UTF-16BE/Latin-1 text decode) and ChildMain (soft 768 MiB memory limit,
-no env read, no network, exit codes 0/3/4/1). outline.go walks /Outlines ->
/First -> /Next with /First children at level+1, resolving /Dest and /GoTo /A
dests to 1-based pages, bounded at 10,000 items and never visiting an object
twice. runner.go is the Runner: env-isolated child (nil env is an empty
environment), 60 s deadline, 64 MiB output cap, 200 MiB stdin cap, stderr
capped at 4 KiB, exit-code to error mapping, and Doc parsing with ErrNoText.
cmd/sift/main.go adds the unlisted `pdftext` case. tools.go is removed; the
go-pdfkit modules stay vendored.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-25 22:58:05 -07:00
kyleandornith-1.5-35b-a3b 13365a2daa clean, fetch: recognise PDFs; 100 MB / 120 s for PDFs; FetchIf revalidates
Recognise PDFs in clean.Kind (declared types and %PDF- sniffing on
octet-stream/bodyless bodies), give PDF responses the 100 MB body cap and
120 s deadline while web pages keep 5 MB / 20 s, and add FetchIf, which
sends If-None-Match / If-Modified-Since and maps a 304 to a Resource with
Status 304 and an empty Body. Fetch and FetchIf share one doFetch path for
guard, client, limits and error mapping.

Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
2026-09-25 22:40:04 -07:00
kyleandClaude Opus 5.5 6a3b6dab71 run-plan: give opencode an empty stdin (1.15 waits for EOF on a piped stdin and hangs in the background)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:38:33 -07:00
kyleandClaude Opus 5.5 7fa5bd21b2 p5a: fix a deadlock in the given TestFetchIf (lock held across a request); run notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 21:45:57 -07:00
kyleandClaude Opus 5.5 cfde35397a P5a plan: PDF documents — seven Ornith tasks with acceptance tests first
docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables,
04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI,
skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the
intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted
fixtures.

Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via
tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for
the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard;
TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and
writeTimeout 120→240 s for the PDF path's worst case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:47:38 -07:00
kyleandClaude Opus 5.5 3f2b78d4aa docs: P5a — PDF documents (pure-Go extract in a re-exec'd child, per-page cache, map, pages/grep, 2-worker pool)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:19:39 -07:00
kyle 4372673184 p2.7: prometheus /metrics
Default GET /metrics is Prometheus text; JSON stays at /metrics.json
and ?format=json. :8443 needs the metrics caller bearer; loopback
stays open. Restricted callers have no per-caller series and no
tavily:restricted gauge.
2026-09-23 21:49:38 -07:00
kyle c19a58d1e3 docs: P2.7 prometheus /metrics
Design only. JSON stays at /metrics.json and ?format=json.
2026-09-23 21:36:48 -07:00
kyle e62cfc8660 p3: no Tavily daily cap for restricted callers
Restricted searches skip the cap check. Credits land on
tavily:restricted so they never raise the fleet row. Fleet at cap
still falls through.
2026-09-23 16:27:07 -07:00
kyle 1c57845ebd p3: keep original URLs on RRF/dedupe; rerank=false is not a merge
CanonicalURL is the dedupe key only. Hits keep the first source's
original URL, including a trailing slash. Dedupe has the same rule.
2026-09-23 16:04:45 -07:00
kyle b6e1cdf825 p3: map Brave LLM Context; RRF merge; per-source no_rerank
Live llm/context hits live in grounding.generic plus a URL-keyed
sources map, not a sources array. Fan-in uses reciprocal-rank fusion
instead of cosine. no_rerank keeps that source's own order as a prefix.
2026-09-23 16:02:21 -07:00
kyle eb4e46d036 p3: blind titles, cap snippets, equalise k; abort cached dumps
Exempt title from the vendor-token check. Truncate snippets to 160
chars and clip all four packs to the same per-query k. run.py fails
cached replies with a rerun message instead of a config mismatch.
2026-09-23 15:32:45 -07:00
kyle 7558eca46d p3: bake-off collector that refuses a mislabelled dump
run.py records one named config at a time. Sift dumps fail if
sizes.sources_ok does not match --config. Hermes Tavily uses the
live-tool request shape, not sift. Raw dumps stay gitignored.
2026-09-23 15:29:46 -07:00
kyle 35e2356541 p3: actually blind bake-off packs
Rewrite A-D to [{rank, url, title, snippet}] only. Fail closed if
searxng/brave/tavily/hermes appear outside url or snippet. Self-test
and a 28-query fake-dump dry run pass.
2026-09-23 14:13:26 -07:00
kyle 9a8dae3948 p3: harvest 28 bake-off queries with targets and blind protocol
Kyle-approved fleet harvest. Sibling targets (domain/page or open).
Runner labels A-D; mapping stays in a sealed key file until scoring.
2026-09-23 14:09:42 -07:00
kyle 8ca3adec9e cli: do not fall through to ~/.config from a non-default HERMES_HOME
A restricted profile shares the Unix user with the fleet. If its
$HERMES_HOME/sift/client.toml is missing, returning ~/.config/sift
would silently use the fleet key. Non-default HERMES_HOME now pins
the profile path so loadClient errors instead.
2026-09-23 12:39:17 -07:00
kyle 429f6b207e p3: tavily parse errors degrade; retry DNS on search
A 200 with a bad Tavily body is tavily: error, not an empty success
that would cache for the hour. Upstream clients stay in the source
list if DNS is down at boot and retry the pin on Search; /healthz
lists them as unresolved until a pin lands.
2026-09-23 12:22:31 -07:00
kyle f748e695b1 p2.6: warn when permit_restricted has no key
CheckMappings now logs WARN for each permit_restricted name missing
from keys.toml, so a missing straylight-gs key is visible at start
and on reload instead of passing silently.
2026-09-23 12:19:10 -07:00
kyle d7873403ff p3: search API, pinned sources, cache, CLI
POST /v1/search and /v2/search fan in SearXNG, Brave LLM Context, and
Tavily from config-fixed endpoints. Public sources refuse private A
records; SearXNG is a loopback-only client, not the fetch guard.

Cache keys include caller class. Restricted callers search like the
fleet; logs omit query, hosts, URLs, and upstream errors. Degraded
results are not stored. Tavily sends search_depth=basic and drops
answer. Missing keys surface in /healthz.

Default-on sources stay off until the bake-off. Query harvest is not
in this commit.
2026-09-23 12:17:51 -07:00
kyle 3687636ed7 p2.6: partition dcache by caller class; pin restricted workspace
Digest, chunk-embed, and question-embed keys include fleet|restricted so
a fleet caller cannot observe a restricted cache hit. Digest.Run takes
the class from permit_restricted.

A permit_restricted caller whose keys.toml workspace is not
guardian-star is refused at Search (no Honcho request) and flagged at
serve start and keys reload.

docs: P3 restricted callers search like the fleet; cache/logs isolate.
2026-09-23 11:51:39 -07:00
kyle 7be4ed1d43 p2.5: redact reason/err for permit_restricted logs
Guard messages embed the hostname (resolve <host>, no addresses for
<host>). Restricted callers now log a coarse reason class and err=-.
Test dumps slog records and forbids the hostname anywhere in them.

docs: P3 review — fail-closed search, cache class, public dialer
2026-09-23 11:47:37 -07:00
kyle 2d376d3ad1 docs: P3 spec — search API, sources, cache, bake-off
No code. Default-on sources left to the bake-off. Restricted-caller
paid-engine question left open for Kyle.
2026-09-23 11:43:03 -07:00
kyle 85113f9c0b p2.5: log host=- for permit_restricted callers
Scrape, fetch, refused, fetch-failed, and internal lines redact the
host for callers in context.permit_restricted. final_url stays in the
response. Test captures slog records, not the journal.
2026-09-23 11:40:45 -07:00
kyle e36da19f57 p2.4: restricted workspace for permitted callers
guardian-star context is allowed only when the authenticated caller is in
permit_restricted and mapped to that workspace. Fleet callers remapped to it
still refuse. CLI client.toml: SIFT_CLIENT, then $HERMES_HOME/sift, then ~/.config.
2026-09-23 08:33:58 -07:00
kyle 14eb7808ce docs: skill matches shipped P2 (no search, recover, fallback cap)
sift search is P3 and not built — keep web_search until then.
Recover ladder is Wayback then archive.today. digest.fallback
means clean text capped at ~6k tokens.
2026-09-23 06:00:50 -07:00
kyle 767c12c1af p2.3: pin chunk 0, hybrid score, keep question on retry
Question-mode selection always keeps the lead chunk, then fills
the budget by 0.7 cosine + 0.3 term overlap. prompt_version 2.
too-large embed retry no longer halves the question (index 0).
2026-09-23 03:18:29 -07:00
kyle 5dbc7e4094 p2.2: cap embed inputs at 3000 runes, retry too-large 500
Link-heavy chunks estimated under EmbedMaxTok still overflow
Dixie's 2048-token embed slot. Cap every embed input at 3000
runes. A 500 whose body contains 'too large' halves inputs and
retries once. Fallback logs the truncated upstream body.
2026-09-23 03:03:11 -07:00
kyle 16a7d678d3 p2.1: honcho-embed, wayback http/204, memento, keys owner
Embed requests send dixie.embed_model (default honcho-embed).
Wayback accepts http://web.archive.org, rewrites to https, skips
non-200 closest.status. Memento-Datetime is parsed as an HTTP date
and future/garbage values become empty snapshot_at. keys add writes
via temp+fchown/chmod/rename so root does not clobber keys.toml.
Digest fallback is capped at the 6k-token budget.
2026-09-23 02:41:46 -07:00
kyle 2db001e0d7 p2: 120s write timeout and flags after the URL
WriteTimeout 30s was cutting digest mid-flight (queue+fetch+digest)
and tailscale then 502'd. 120s write, 90s handler deadline.
sift fetch URL --question now parses; extra positionals error.
2026-09-23 00:28:59 -07:00
kyle a992216ec0 p2: widen document fence for space and zero-width
Strip U+200B–U+200D, U+2060, U+FEFF before fencing. Match
(?i)<\s*/?\s*document so </ document> cannot sit outside the frame.
2026-09-23 00:09:14 -07:00
kyle ff87a04f94 p2: fence document tags, atomic fleetctx, harden workspace
Neutralise </document and <document in page text. Counters are
atomic.Uint64. Workspace names are [A-Za-z0-9_-] and PathEscaped;
foreign workspace_id rows are dropped. Wayback closest.url must be
https://web.archive.org. archive.today snapshot_at is Memento-Datetime
or null.
2026-09-23 00:01:04 -07:00
kyle 2f9d3d97e6 p2: digest with in-sift FIFO, SQLite caches, Honcho context, recover
Dixie is capped (max_inflight 2, queue_depth 8, queue_wait 20s) and
never queued on llama-server. Streaming chat, thinking off, 45s
fallback to clean. Digest and chunk-embedding caches are
content-addressed SQLite (Honcho context is not cached). Recover
uses the guarded Fetcher. guardian-star is refused in code.
2026-09-22 23:54:49 -07:00
kyleandClaude Opus 5.5 505e113b64 docs: P2 scope line matches §2a caches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 23:47:24 -07:00
kyleandClaude Opus 5.5 9f8d9f4f13 docs: P2 — bounded in-sift queue; digest + chunk-embedding caches pulled forward
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 23:47:09 -07:00
kyleandClaude Opus 5.5 cb0cb83fb9 docs: P2 spec — digest (bounded, streaming, thinking off), Honcho context (guardian-star refused), recover ladder
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 23:43:40 -07:00
kyle 0598c8985b p1.1: note IPv4-only self-IP assumption
Trace via 1.1.1.1 never learns a delegated v6 prefix.
2026-09-22 23:01:19 -07:00
kyle 447842f860 p1.1: dynamic self-IP deny (DDNS/trace, keep previous, atomic swap)
Discover WAN via ddns:<host> then trace:<url> before listen. Persist to
self_ip.json. Previous addresses stay denied for keep_previous. Failure
keeps the last known set. /healthz reports age and source.
2026-09-22 22:59:08 -07:00
kyle dca635fb62 p1: vendorHash = null when vendor/ is committed
buildGoModule refuses a hashed fetch if vendor/ exists. Keep vendor/;
the check path already uses GOPROXY=off GOFLAGS=-mod=vendor.
2026-09-22 22:27:24 -07:00
kyle fac1462e7b p1: ExecStart must invoke sift serve
P1 split the binary into subcommands. Without `serve`, the unit exits 2
on `unknown command`.
2026-09-22 22:11:28 -07:00
kyleandClaude Opus 5.5 d404657820 docs: P1.1 dynamic self-IP deny (DDNS/trace discovery, keep previous, atomic swap)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:06:20 -07:00
kyle 8e3a6cd339 p1: cap jq output, log refusals, fail contract without SDK
jq results are bounded (10k items / output cap) and return 400 when
exceeded. 401/403 log caller, host, and reason. make contract exits 1
unless firecrawl-py is present (SIFT_CONTRACT_SKIP_SDK=1 to skip).
2026-09-22 22:02:38 -07:00
kyle 9444a35322 p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests
Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification
is ignored. No 502. keys add writes the secret only to --out. Release binary
has no -test-allow-prefix.
2026-09-22 21:54:01 -07:00
kyleandClaude Opus 5.5 6cb206fb27 docs: P1 spec — clean fetch, auth, CLI, contract facts from firecrawl-py 4.17.0, talos wiring
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:38:29 -07:00
kyle 099b94979c p0: IANA special-purpose deny table, extra deny prefixes
Block 6to4/Teredo/NAT64-local/site-local/benchmarking/reserved
ranges from the IANA registries (families, not decoded embeds).
Guard.DenyPrefixes / -deny-prefix covers hairpin WAN /32s; home
addresses stay on-box, not in git. Control is tcp/tcp4/tcp6 only.
2026-09-22 21:33:52 -07:00
kyleandClaude Opus 5.5 3b41d6ffbc plan: UA decided — default sift/<ver>, config override, measure bot-blocks
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:28:04 -07:00
kyle 2607d56e4c p0: skeleton, dialer-Control URL guard, flake, Makefile
Go service with /healthz, SSRF guard (parse + resolve-all + Control),
exhaustive tests, NixOS module, and Makefile as source of truth for
build flags. nix flake check runs make test lint.
2026-09-22 21:26:13 -07:00
kyleandClaude Opus 5.5 7d4f090a12 skill: sift as the explicit first step for fetch/search; plan §4.3 CLI client, --recover, UA policy
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:25:57 -07:00
kyleandClaude Opus 5.5 1e915ce715 plan: Makefile for non-Nix hosts (source of truth for build flags)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:15:29 -07:00
kyleandClaude Opus 5.5 e9aea1829e plan: Go instead of Python (Kyle's preference); dialer-Control SSRF check, gojq
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:05:59 -07:00
kyleandClaude Opus 5.5 94876864e5 sift: implementation plan
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:00:00 -07:00