Skip to content

feat(prd-intake): the PRD becomes an input, and devagent up brings the factory up - #373

Merged
linhdmn merged 1 commit into
mainfrom
feat/prd-intent-lane-and-factory-lifecycle
Sep 14, 2026
Merged

linhdmn merged 1 commit into
mainfrom
feat/prd-intent-lane-and-factory-lifecycle

Conversation

@linhdmn

@linhdmn linhdmn commented Sep 14, 2026

Copy link
Copy Markdown
Member

Why

The driver had no route from what the operator writes into what it builds. docs/PRD.md is a state document by policy — and a dirty one blocks every iteration ([prd-fresh] … operator mid-edit), while an empty deterministic lane left the LLM free to select the loop itself (issue #355: weeks of loop plumbing, not product). Starting the driver on top of that meant SELFBUILD_* exports, a devagent on PATH that a Go-only checkout never installed, and make loop-start by hand.

Three changes, one outcome: write what you want built, type one word, get tested PRs.

What

PRD intake — the PRD becomes an input (#370)

  • internal/prdintake: an OPEN checkbox is an instruction. Each - [ ] item becomes a queue row carrying its heading as context, its indented sub-bullets as acceptance criteria, and its section body as the task-PRD sidecar (PrdMarkdown), so the worker reads the spec instead of one bullet. Row id = PRD-<8hex> of the normalized text: re-running over an unchanged PRD queues nothing, and the row's own criterion makes the shipped PR tick the box it built.
  • Not work, ever: - [x] (shipped state), blockquotes (state notes), struck lines, plain prose bullets, fenced examples. Policy stays intact — the PRD is still a state doc, with one documented exception.
  • internal/loopdriver/intake.go runs it at iteration head, after the PRD-currency gate (a draft is never read as intent) and before pickIssue/claimQueueTask, so the iteration that reads your edit builds it. SELFBUILD_PRD_INTAKE=0 / SELFBUILD_PRD_INTAKE_MAX control it; failures are logged and never fail an iteration.
  • Phase 2a already outranked the tracker, so operator intent now outranks LLM self-selection. devagent prd-intake --dry-run|--json|--max N runs the same step by hand.
  • selfbuild queue is empty: every pick falls back to LLM self-selection, so the loop ships loop plumbing #355's other half: the empty-lane fallback now logs its remedy and breadcrumbs queue-empty into events, instead of reading as LLM-driven progress.

devagent up / devagent down — one command (#371)

Gate → state dirs → PRD intake → lane census → daemon → detached driver → report. up launches the driver from its own executable with SELFBUILD_DEVAGENT_BIN pinned to match (the PATH trap), refuses to start behind a dirty docs/PRD.md (the driver would skip every iteration silently), prints pending rows / open PRD items / open selfbuild issues (the number that predicts whether the next ten hours ship product or plumbing), and is idempotent against a live loop lock. down signals recorded pids only — never pkill -f (#354). make up|down are now thin wrappers over both; --scout adds the researcher as a second recorded child.

Green up means a running factory. The first smoke caught up printing a healthy pid for 15 s after the driver printed max iterations reached: up is the driver's parent, so a child that halts at its own gate exits 0 and stays a zombie whose pid answers any liveness probe. up now reaps its own child and withholds the verdict until the loop lock and a phase-naming heartbeat exist — driver running (pid 31815) — holds the loop lock, iteration 2, phase queue-empty — or fails the run with the halt line read from .selfbuild/logs/. --wait 0 skips the proof for scripted starts.

The scout writer is alive again (#372)

runScoutOnce/runScoutLoop ported: lock → config → queue-depth check before the paid dispatch → prompt → workers.GetWorker().Spawn → extract → parse → queue.EnqueueTask (+ task-PRD sidecar) → heartbeat. devagent scout --once/--interval no longer exits 3 while the com.devagent.scout LaunchAgent ticks every 30 min against a dead queue writer; an empty ResultText with a nonzero exit is an error, never a silent fallback. Also: devagent create --scout/--tracker stopped baking the deleted dist/src/cli.js into plist argv, where cobra would choke on it.

Verification

  • gofmt -l, go vet ./..., golangci-lint run (0 issues), full go test ./... green — 24 new tests: parser contract (fences, blockquotes, struck, nesting, re-wrap stability), the 120-word dispatch cap, ingest idempotency/reword/per-pass cap/missing-PRD, same-iteration claim + per-iteration idempotency + off-switch + dry-run writes nothing, and both up health-receipt directions plus down's recorded-pid stop.
  • Live, with the built binary: scratch docs/PRD.md item → prd-intake (once, then idempotent) → queue list → loop --dry-run claimed it and published .selfbuild/goals/loop-1.md + an ok ledger row; up proved a live driver and refused a dead one with its cause; down stopped a real session-leader pid and left nothing; scout --once --dry-run previewed a cycle with no AI call and no writes.

Docs

FR-SIMPLE-07/08 + §12 command rows + §21 status note + footer, docs/SELF-BUILD-LOOP.md (one-command section, up step table, the intake policy exception, SELFBUILD_PRD_INTAKE* knobs), README "Run the automated driver", docs/PRD.html regenerated with pandoc, and the 2026-09-14 cold-start research pass committed so its citations resolve.

…ctory up

The driver had no route from what the operator writes into what it builds.
docs/PRD.md is a state document by policy and a dirty one *blocks* every
iteration, the deterministic lane was empty so the LLM could only find itself
to work on (#355: weeks of loop plumbing), and starting the driver meant
assembling SELFBUILD_* exports, a `devagent` on PATH, and `make loop-start` by
hand. Three changes, one value: write what you want built, type one word, get
tested PRs.

internal/prdintake (#370): an OPEN checkbox in docs/PRD.md is an instruction.
Each `- [ ] item` becomes a queue row carrying its heading as context, its
indented sub-bullets as acceptance criteria, and its section body as the
task-PRD sidecar, so the worker reads the spec rather than one bullet; the row
id is a content hash, so re-running intake over an unchanged PRD queues
nothing and the shipped PR ticks the box it built. `- [x]`, blockquotes,
struck lines, plain bullets and fenced examples stay exactly what policy says
they are — never work. The driver ingests at iteration head, after the
PRD-currency gate (a draft mid-edit is never read as intent) and before the
pick, so the iteration that reads your edit is the iteration that builds it
(`SELFBUILD_PRD_INTAKE=0` opts out); phase 2a already outranks the tracker, so
operator intent now outranks LLM self-selection. `devagent prd-intake
--dry-run|--json|--max N` runs the same step by hand. An empty lane is now a
named state instead of silent progress: the fallback logs and breadcrumbs
`queue-empty` with the refill instructions.

`devagent up` / `devagent down` (#371): the prerequisite gate (a missing worker
CLI aborts instead of burning tokens), the state/queue dirs, PRD intake, a
lane census (pending rows / open PRD items / open selfbuild issues — the
number that predicts whether the next ten hours ship product or plumbing), a
refusal to start behind a dirty docs/PRD.md (the driver would skip every
iteration and say nothing), the daemon, then the detached driver launched from
its own executable with SELFBUILD_DEVAGENT_BIN pinned to match — the PATH trap
that made a Go-only checkout silently shell out to a stale CLI. It ends in a
HEALTH receipt, not a spawn receipt: `up` is the driver's parent, so a driver
that halts at its own gate exits 0 and stays a pid that answers any liveness
probe (measured: 15 seconds of "healthy" after `max iterations reached`), so
`up` reaps its own child and waits until the loop lock AND a phase-naming
heartbeat exist, or fails the run with the halt line from the driver's log.
`--scout` adds the researcher as a second recorded child; `down` signals
recorded pids only (never `pkill -f`, #354); `make up|down` now delegate to it.

internal/scout (#372): `runScoutOnce`/`runScoutLoop` are ported at last, so
`devagent scout --once/--interval` does its real cycle (lock -> config ->
depth check BEFORE the paid dispatch -> prompt -> workers.GetWorker().Spawn ->
extract -> parse -> enqueue + task-PRD sidecar -> heartbeat) instead of
exiting 3 while the scout LaunchAgent ticks every 30 minutes and the queue
writer stays silently dead. `devagent create --scout/--tracker` also stopped
baking the deleted `dist/src/cli.js` into plist argv, where cobra would choke
on it.

Verification: gofmt/go vet/golangci-lint clean; full `go test ./...` green
(24 new tests: parser contract incl. fences/blockquotes/struck/nesting, goal
shape at the 120-word dispatch cap, ingest idempotency + reword + per-pass
cap, same-iteration claim, per-iteration idempotency, off-switch and failure
paths, dry-run writes nothing, up's step plan/idempotency/gate refusal/
empty-lane hint/dirty-PRD refusal/both health-receipt directions, down's
recorded-pid stop); live smokes end to end — a scratch PRD item queued ->
claimed -> published as goals/loop-1.md, `up` printing `driver running (pid
31815) — holds the loop lock, iteration 2, phase queue-empty` and then
`✗ driver pid 27840 is not running: exited: max iterations reached`, `down`
leaving no surviving process, and `scout --once --dry-run` previewing a cycle
with no AI call and no writes.

docs/PRD.md grows FR-SIMPLE-07/08 + §12 rows + the status note and footer (and
the gap-audit blockquote staged in the tree), docs/SELF-BUILD-LOOP.md gains the
one-command section, the step table and the intake policy exception, README
gains the driver path, PRD.html regenerated with pandoc, and the 2026-09-14
cold-start research pass that shaped the health receipt is committed so its
citations resolve.
@linhdmn
linhdmn merged commit e1e7888 into main Sep 14, 2026
7 checks 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