product-docs (Prody) — run bilans
Functional, business-audience product documentation generated and maintained in a dedicated docs repository from a multi-repo product catalog. Newest run first.
2026-09-17 — profile 2: a one-product catalog documented in French from a local source snapshot, converged (run 01a0afa6-aafb)
- Status: validated end to end —
catalog_ingest→scan_hints→campaign(cut once by the wall, then complete) →scope_check→page_lint→gateconverged →mr_gate(off) →publish_gate(off) → done. - Versions: bot product-docs 1.1.1 (
dsl: 2, wave 3b of #1344) · iterion5fa790d82for the bots; the engine the branch binaryv3.157.0+c31559517(built on the wave-3a branch from main at v3.157.0; main is at v3.159.0 as this is written, one PR of engine work ahead), served through the host's Anthropic-compatible facade (z.ai) as the runs' provenance records. - Method: launched FROM a scratch DOCS repository holding
catalog.yaml(one productgreet,docs.product_dir: produits/greet, one source repository given as a local path to the scratch greeter,ref: main), a skeletonproduits/greet/index.mdand an editorial override.product-docs/ton-et-style.md;--var catalog_path=catalog.yaml --var product_id=greet --var max_passes=1,--store-dirthe operator's workspace store,--sandbox none,--merge-into none, caps--max-cost-usd 1.5 --max-duration 10m, every LLM node onclaude_code/claude-opus-5(the GPT forfait is closed until 2026-09-20). The first campaign died at the 10-minute wall with three pages already committed; resumed with--max-duration 30m --max-cost-usd 3. - Result: $1.70 recorded (the cut attempt's spend is not), 20 min 10 s from launch to done. The campaign loaded the docs repository's own editorial rule before the bundle defaults, grounded every claim by reading the whole source AND running the CLI (help text quoted verbatim, the three refusals reproduced), wrote the pages in French for the non-technical reader — a glossary, a help page, the greeting journey, a blocking case (a composed name written without quotes) found by execution, not by reading — and reported
drift_remainingandunread_sourcesempty; the deterministic scope and lint gates passed,converged: true. Storage branchiterion/run/01a0afa6-aafb-7a59-a87f-1ea7948a9d00→e3490f3(four docs commits across the two attempts). - Value: the six prompts' paragraph breaks reach the models (proven in the run's
events.jsonlforcampaign_systemandcampaign_user, twice each; the MR and publish prompts were not rendered, both gates off), and the bot's grounding contract held on a source it could only read as a snapshot:catalog_ingestcopies sources without.git, the agent's firstgit logfailed with exit 128 and it fell back to reading files — by design. - Findings / misses: none bot-side.
- Engine hardening: none needed.
- Lessons for next run:
--max-duration 25mfor one full pass; the cheapest fixture is a catalog entry whoseurlis a local path; pass--merge-into noneagain on resume.
2026-09-04 — first run on the prod instance: the datalab URL brought back, then re-published by the bot (run 01a06baa)
- Status: validated on prod — first
product_docsrun ever on the cloud instance (every earlier run was local), scope ✓ lint ✓ converged, publish tail end to end,verify_publishhttp 200 from outside. - Versions: bot 1.0.0 (baked catalog) · iterion prod v3.100.0 runner / v3.100.1 server ·
claude_code/claude-opus-5on the team forfait. - Method: repo-targeted launch on
DNUM-SocialGouv/documentation_produits@demo/prody-sirena(the only branch carrying.prody/catalog/),max_passes=0,open_mr=false,publish=true,publish_base_url= the RELAUNCHED datalab service,publish_s3_bucket=devthejo,publish_tools_ref=v3.100.0. Team secretsonyxia_s3_*(renewed STS tokens) +forge_token(the PAT connection's managed secret — the docs repo has no repo integration, so the clone credential comes from the bot binding) bound toproduct-docs. - Result: 44 min, $25.5 — campaign $25.0 (one verification pass that fanned out 49 sub-agents in two modalities, page-first then source-first), publish $0.5 (105 s). 3 commits on 2 pages, all real: the fourth distribution and two national key figures of the indicators dashboards, a third filter that silently removes requests (
Inclure les EIG), the silent merge and the non-shared reopening of a request.is_product_bug: true(filed in the summary — the board MCP is unavailable in the sandboxed runner session, as for Doki). The site was rebuilt (21 pages) and mirrored; the truth gate polled the new host and gothttp 200, title present. - Delivery: with
open_mr=falsethe 3 commits are BANKED oniterion/run-01a06baa…in the DNUM repo (the engine's death/finish bank works on a foreign repo too) and nothing reached the draft PR #24 — a deliberate choice for a "verify + publish" run, but it means a human cherry-pick or a follow-up run withopen_mr=true.
What the run says
- The URL had died with the platform, not with the bot. On 09-02 the STS tokens expired and the datalab culled
prody-docs-serve; the bucket still held the site. Relaunching the service (Vscode-python, init script pinned atv3.100.0,SITES_BUCKET=devthejo, user port 5000) brought/sirena/back to 200 within a minute, BEFORE any run — a new release id, hence a new host. The launcher takes its whole config as URL parameters (init.personalInit=«…»,extraEnvVars[0].name=«…»,networking.user.enabled=true); the brackets must stay literal, a percent-encoded%5B0%5Dbecomes a bogus top-level key. - A manual launch runs on the launcher's OWN forfait first. The first attempt (
01a06b9f,POST /api/runswith the operator token) died in 6 s onusage cap: seven_day window at 97%— the operator's personal Claude connection, at its weekly ceiling, sits ABOVE the team key in the credential tiers. Relaunching through a one-shot schedule row (a system actor) resolved the team key and ran. Worth knowing before any manual prod launch on a shared team. - Scope notes are read by the campaign, not by the tail. The notes said "verification pass + publication"; the campaign agent took "publication" as its own job, mirrored the site to the bucket itself and probed the OLD hostname it found in the repo (404 — the culled service), then asked for an operator action the publish tail had already made moot. Harmless here, but a pass spent on the tail's work: keep scope notes about the DOCS.
- The verification pass is no longer a $5 pass. The campaign self-orchestrates (49 sub-agents) at the bot's default effort; the local recipe ran it at
high. On a converged corpus that is the price of breadth — the two findings Round 2 surfaced were in areas a page-first reader never opens. - Two credential leaks, both mine, both in the transcript: a full accessibility snapshot of the datalab launcher renders the helm values preview WITH the service's temporary credentials, and a value-stripping filter written for
key=valuelines printed thekey = valueones. The tokens were renewed and the bot only ever saw the new ones; the old ones expire on 09-11. Extract by filteredevaluate, never by snapshot, and redact by printing token COUNTS, never fields.
Lessons for next run
- The datalab is best effort by design (7-day STS, 7-day cull): the demo survives only as long as someone relaunches. Prody 1.1.0 makes the tail platform-agnostic (image +
deploy-target); the durable target is fabrique. - Launch prod dogfoods through a one-shot schedule row, delete it after the tick; keep
open_mr=truewhen the pass is allowed to write, or the commits stay on the bank branch.
2026-08-26 — first real product + first publication: Sirena live on the SSP Cloud datalab (run 01a03ed3)
- Status: validated — the whole chain ran once on a real product, including the opt-in publication tail shipped the same day (#533).
- Versions: bot 1.0.0 (publication tail on branch, merged as #533 the next morning) · iterion v3.64.1 (release binary) · skill
deploy-onyxia-sspcloudv0.1.0 → v0.1.3 during the session. - Method: local run from the DNUM docs repo (
DNUM-SocialGouv/documentation_produits, branchdemo/prody-sirena),claude_code/claude-opus-5,max_passes=0(one verification pass: the corpus had converged on 08-23/25, 22 pages),open_mr=true,publish=true,publish_base_url= the standing datalab service's user-port host,publish_s3_bucket=devthejo. Onyxia S3 STS credentials in the local sealed store (three file secrets). - Result: scope ✓, lint ✓ →
publish_gateready→ the publish agent built the site (GitBook blocks → MkDocs Material throughdeploy/gitbook_to_mkdocs.py), mirrored it tos3://devthejo/sites/sirenaand reporteddeployed: true→verify_publishhttp 200 from outside →surface_site_link. ≈ $17, 25 min. The six verification commits of the pass were cherry-picked onto the draft PR #24 in the DNUM repo (kept in draft: merging is the product owners' act). - Value: the DNUM deliverable — a business-readable Sirena documentation generated from the sources, verified, served on a URL the bot itself put live, with the RGPD finding (three retention periods documented, one implemented) kept in the promises ledger instead of aligned down.
- Findings / misses:
- Two runs before this one finished WITHOUT opening their PR because a file secret that was not mounted stays silent (
optional: true); fixed engine-side (#531), and the bot-probe recipe (atoolnode testing{{secrets.X.path}}) stays the way to check a mount. MC_HOSTis parsed as a URL: an STS secret carrying/silently truncated the authority. Percent-encode every part (skill v0.1.3).mc's table renders on failure too; only the exit code is truth (a| tail -1had swallowed it once).- The datalab bucket does not pre-exist (
mc mb --ignore-existingfirst),diffusion/is authenticated-read only (public exposure goes through a datalab service on the user port).
- Two runs before this one finished WITHOUT opening their PR because a file secret that was not mounted stays silent (
- Engine hardening: #531 (unmounted file secret fails loudly), #526 (sandbox default image falls back to
:latestbetween releases). - Lessons for next run:
- The platform is best effort by design: STS credentials expire in ~7 days and the datalab may delete the serve service on the same horizon. Both happened on 2026-09-02 — the URL 404'd by itself. A bot that publishes weekly needs a durable target (fabrique) or a rotation.
- A plugin-source manifest is not validated at registration: an unquoted
:inplugin.yamlbroke EVERY webhook launch of the instance for 2h20 (#536–#538). Validate the YAML before registering.
2026-08-25 — first dogfood, fixture product (run 01a03a6a)
- Status: validated (converged), with one real bot bug found and fixed.
- Versions: bot 1.0.0 · iterion
fd9c5e611(worktree build). - Method:
claude_code/claude-opus-5, two campaign passes. Fixture: a docs repo (/tmp/prody-dogfood/docs, empty product tree, one local editorial override) and one source repo (subvention-app: a grant request service — FR i18n catalog, two role-scoped route files, one form template, a.env). Catalogcatalog/demo.ymlnaming a single repo by localurl:. Vars:catalog_path=catalog/demo.yml product_id=demo max_passes=2 scratch_dir=/tmp/prody-dogfood/scratch. Flags:--merge-into none,--sandbox none(no container runtime on the host),open_mrleft false. - Result: converged on pass 2. 21 commits, one page per commit, all carrying both
Bot: product-docsandProduct-Docs-Sources: subvention-app@e7bb04f1. 8 pages: product home, glossary, two role hubs (espace-demandeur,espace-instructeur), two step pages each.page_lintgreen over all 8 (0 violations);scope_checkgreen;gate.converged = true. ≈$20.6 (13.6 + 7.0), ≈50 min wall clock. Commits landed oniterion/run/01a03a6a…(merge deliberately skipped). - Value: high, and specifically the value the bot was built for. From an EMPTY product directory the campaign produced a role-then-journey page set a business reader can follow, in French, grounded file by file: statuses from
fr.json, the SIRET/50 000 €/10 Mo constraints from the route validators, the "réponse sous 30 jours" promise from the mail body, the mandatory refusal motive from the refuse handler. Unprovable facts were marked[à confirmer]in-sentence rather than invented or dropped — pass 2 spent all 5 of its commits doing exactly that re-grounding.- The docs repo's own editorial rule was obeyed over the bundle default:
.product-docs/modele.mddemanded that hub pages open with## À qui s'adresse cette page, and both hubs do — while the product home (not a hub, per the bundle's own taxonomy) correctly does not. Editorial sovereignty (ADR-092) works end to end. - It found a genuine source defect and said so instead of documenting a promise the code does not keep:
instruction.jscomments that accepting "notifies the applicant", no mailer call exists, andfr.jsonhas no acceptance/refusal message body. Reported asis_product_bug: trueand filed on the board rather than papered over. - It refused to resolve an ambiguity it could not prove (the worklist reads
deposee, both decision handlers calltransition(id, 'instruction', …), andtransitionis defined in none of the cloned files): it documented both ends and marked the passage between them.
- The docs repo's own editorial rule was obeyed over the bundle default:
- Findings / misses:
- The redaction is honest but blind by construction: the inventory reported "1 secret-bearing file redacted" and the campaign flagged, unprompted, that it therefore could not know what functional area that file covered. Right behaviour; worth remembering that a product whose config carries functional truth will be under-documented by design.
catalog_ingestfailed LOUDLY and usefully three times before the run started (missing YAML parser path, unwritable scratch root) — each message named the cause and the remedy. No silent degradation observed.
- Engine / bot hardening: one real bug, found only because this was a live run. iterion mirrors the bundle's skills into
<workspace>/.claude/at run start and again on every resume;scope_checkcollected untracked paths fromgit statusand judged that engine-owned tree against the writeable set, so a pass that wrote 8 pages and 16 clean commits was failed on.claude/. Worse than a false positive: the agent then deleted the engine's own skill mirror to satisfy the gate. Fixed by excluding.claude/inscope_check(surgically — an untracked tree that is not the mirror still fails), with both halves pinned inbots/product_docs_gates_test.go. docs-refresh escapes the same class only by accident: its writeable set is "any.md", which happens to admit the mirrored skills. - Lessons for next run:
- Budget the campaign properly:
--max-cost-usd 15killed pass 1 atscope_checkafter the agent had already spent $13.57 and committed everything (the 90% hard limit blocks the next node). A single Prody pass on a one-repo product costs $7–14; budgetmax_passes × ~15or leave the bot's own 300 ceiling alone. Resuming with--max-cost-usd 45picked up exactly atscope_checkand lost nothing. iterion resumehas no--sandboxflag (onlyiterion rundoes) — useITERION_SANDBOX_DEFAULT=noneon a host with no container runtime.- Two passes is the natural shape on a bootstrap: pass 1 authors the page set, pass 2 re-grounds it.
max_passes=2was enough here; a multi-repo product will want the default 4. - The fixture is worth keeping: an empty product tree + one small role-shaped source repo exercises hub/step authoring, glossary,
[à confirmer]discipline and the editorial override in under an hour.
- Budget the campaign properly:
