From b53e7e6aca49c702ca44b1b27ced7e7f9580b20e Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 21 Sep 2026 20:21:46 +0200 Subject: [PATCH] generate: a Norwegian run on a local model, and what it took to read it Every prompt and reply is now kept under /llm/. An empty reply is a retry, not a fatal error; a spec that never parses stops the run before the structure phase; --visitor-llm lets a different model play the visitor. The file-block parser finds markers anywhere in a line, accepts a short END and lets a new FILE close an open one, after a reply glued two files together with `<<>`. The reference gains a language section: copy in the intake's language, identifiers in ASCII. runs/rehab-entreprenor-no-2026-09-21: nemotron-3.5-lightning:30b-a3b did not produce a loadable site. Written up with the reason and the way forward: structure from needs.yaml deterministically, words from the model. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH --- generator/reference.md | 11 + intakes/rehab-entreprenor-no.yaml | 48 ++ .../rehab-entreprenor-no-2026-09-21/README.md | 30 ++ .../generator.log | 13 + .../needs.yaml | 82 ++++ .../replies/01-reply.md | 150 ++++++ .../replies/02-reply.md | 148 ++++++ .../replies/03-reply.md | 331 +++++++++++++ .../replies/04-reply.md | 84 ++++ .../replies/05-reply.md | 462 ++++++++++++++++++ .../replies/06-reply.md | 310 ++++++++++++ .../replies/07-reply.md | 181 +++++++ .../replies/08-reply.md | 93 ++++ .../replies/09-reply.md | 64 +++ .../replies/10-reply.md | 24 + .../verifier-last.log | 9 + src/main.rs | 95 +++- 17 files changed, 2123 insertions(+), 12 deletions(-) create mode 100644 intakes/rehab-entreprenor-no.yaml create mode 100644 runs/rehab-entreprenor-no-2026-09-21/README.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/generator.log create mode 100644 runs/rehab-entreprenor-no-2026-09-21/needs.yaml create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/01-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/02-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/03-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/04-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/05-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/06-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/07-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/08-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/09-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/replies/10-reply.md create mode 100644 runs/rehab-entreprenor-no-2026-09-21/verifier-last.log diff --git a/generator/reference.md b/generator/reference.md index b5afaf1..50f5f8d 100644 --- a/generator/reference.md +++ b/generator/reference.md @@ -158,6 +158,17 @@ title: lang: en ``` +## Language + +Everything a person reads is written in the intake's `language`: page +names, headings, descriptions, field labels, button labels, the +`who`/`wants` of each need, and the personas. Write it the way people +in that trade and country actually speak, not as a translation. +Everything a machine reads stays plain ASCII snake_case or kebab-case: +need ids, persona ids, bucket names, state names, field `name`s, file +names and paths. Letters such as æ, ø and å belong in the copy and +never in an identifier. Set `lang` in site.yaml to the intake's code. + ## Copy rules - Headings say what the visitor gets, in the visitor's words. diff --git a/intakes/rehab-entreprenor-no.yaml b/intakes/rehab-entreprenor-no.yaml new file mode 100644 index 0000000..489edb1 --- /dev/null +++ b/intakes/rehab-entreprenor-no.yaml @@ -0,0 +1,48 @@ +# En fiktiv norsk rehabiliteringsentreprenør, inspirert av hva +# Klingenberg Bygg driver med: tilstandsrapporter, prosjektering, +# byggesøknader og utførelse på eldre murgårder. Skrevet slik eieren +# ville sagt det, uten portal-ord. +business: Åsrud Bygg og Rehabilitering AS +contact: { name: Marte Åsrud, email: marte@asrudbygg.example } +language: "no" + +what_we_do: > + Vi er fjorten ansatte og jobber mest med eldre murgårder og + trehus. Vi lager tilstandsrapporter, prosjekterer rehabilitering, + sender byggesøknad med ansvarsrett, og utfører arbeidet med egne + tømrere og murere. Elektriker, rørlegger, stillas og blikkenslager + leier vi inn som underentreprenører. + +who_comes_to_us: > + Styreledere i borettslag og sameier, og private huseiere, som har + et problem med bygget og vil at noen skal se på det. Eiere som + allerede vet hva de vil bygge eller endre, og trenger tegninger, + søknad til kommunen og en pris. Håndverksfirmaer som vil stå på + lista vår over underentreprenører. Tømrere, murere og lærlinger som + lurer på om vi trenger folk. + +what_we_keep_track_of: > + Henvendelser: de kommer inn, vi avtaler og drar på befaring, vi + sender tilbud, og så vinner vi jobben eller taper den. Noen ganger + er det rett og slett ikke noe for oss, og da sier vi det med en gang. + Underentreprenører: de melder seg, vi sjekker organisasjonsnummer, + sentral godkjenning, forsikring og at folka har HMS-kort. Ofte + mangler det papirer og vi må etterlyse dem. Så blir de godkjent + eller avslått. + Jobbsøknader: vi leser dem, kaller inn noen til intervju, og + ansetter eller takker nei. + Avvik og uønskede hendelser på byggeplass: løse rekkverk, + nestenulykker, feil materialer levert. Noen melder fra, HMS-ansvarlig + bestemmer tiltak, og lukker saken når det er utbedret. + +who_is_involved: > + To prosjektledere tar seg av henvendelser, underentreprenører og + ansettelser. En HMS-ansvarlig tar alt som meldes fra byggeplass. + Mannskapet, både våre egne og underentreprenørenes folk, er de som + står på plassen og melder fra. De sitter med telefonen, har hansker + på, og fyller ikke ut lange skjemaer. + +what_slows_us_down: > + Henvendelser kommer på telefon, e-post og SMS og blir borte. Vi vet + aldri sikkert hvilke underentreprenører som har papirene i orden. + Avvik blir nevnt i lunsjen og glemt. diff --git a/runs/rehab-entreprenor-no-2026-09-21/README.md b/runs/rehab-entreprenor-no-2026-09-21/README.md new file mode 100644 index 0000000..a7c03a4 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/README.md @@ -0,0 +1,30 @@ +# Norwegian contractor, generated by a local model: failed + +Intake: `intakes/rehab-entreprenor-no.yaml`. Generator: +`nemotron-3.5-lightning:30b-a3b` on ergo's Ollama, six rounds per +phase, about 16 minutes. It never produced a site that loads. + +- The spec parsed on the fourth try, after nesting personas inside + each need once and returning no files at all twice. +- Structure went 11 failures, 1, then a YAML syntax error it could not + get past in four rounds: an end marker came out as + `<<>` with the next file glued to it, so two files were + written as one. The parser now tolerates that; the model did not + notice it when shown the file back. +- It copied the reference's English example into its desk page + (`bucket: leads`, "Quote sent") instead of writing its own, never + wrote `aggregates.yaml`, and one reply was 40 KB of empty code + fences (truncated here). +- The Norwegian is mixed: "Hva trenger du hjelp med?" is fine, + "kvalifisert mandskap" is Danish spelling, and it kept `å` in a state + name against the reference's rule. + +A Sonnet-class model did the English contractor in one round per +phase. What this run says is that a 3B-active model cannot hold the +whole content reference and write a dozen interlocking files at once, +in any language. The way to make small local models useful is to give +them less to do: generate `aggregates.yaml`, desks and followup pages +deterministically from `needs.yaml` (`stages`, `done_when`, +`handled_by` already say everything those files contain), and ask the +model only for words - the page question, headings, descriptions and +field labels, one page at a time. diff --git a/runs/rehab-entreprenor-no-2026-09-21/generator.log b/runs/rehab-entreprenor-no-2026-09-21/generator.log new file mode 100644 index 0000000..3ff9607 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/generator.log @@ -0,0 +1,13 @@ +needs round 1: FAIL: parsing needs.yaml: needs[0]: unknown field `personas`, expected one of `id`, `who`, `wants`, `enters`, `enters_as`, `via`, `lands_in`, `handled_by`, `done_when`, `max_steps`, `max_fields`, `planned`, `success`, `stages` at line 13 column 5 +needs round 2: the reply had no file blocks, asking again +needs round 3: the reply had no file blocks, asking again +needs round 4: needs.yaml parses, frozen +ignored "needs.yaml": outside what the generator may write +structure round 1: 11 FAIL line(s) +structure round 2: 1 FAIL line(s) +structure round 3: 0 FAIL line(s) +structure round 4: 0 FAIL line(s) +structure round 5: 0 FAIL line(s) +structure round 6: 0 FAIL line(s) +Error: structure never went green in 6 round(s); last verifier output is in run.log +GENERATOR_EXIT=1 in 944s diff --git a/runs/rehab-entreprenor-no-2026-09-21/needs.yaml b/runs/rehab-entreprenor-no-2026-09-21/needs.yaml new file mode 100644 index 0000000..1714759 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/needs.yaml @@ -0,0 +1,82 @@ +needs: + - id: leads + who: styreledere i borettslag og sameier, private huseiere + wants: at noen skal se på et problem med bygget + enters: / + lands_in: leads + handled_by: prosjektledere + done_when: [won, lost] + max_steps: 1 + max_fields: 4 + success: [won] + stages: [open, quoted, won, lost] + - id: subcontractors + who: underentreprenører (tømrere, murere, lærlinger) + wants: å bli godkjent som fast leverandør + enters: / + lands_in: subcontractors + handled_by: prosjektledere + done_when: [approved, rejected] + max_steps: 1 + max_fields: 3 + success: [approved] + stages: [open, approved, rejected] + - id: job_applications + who: personer som søker jobb + wants: å bli ansatt som tømrer, murer eller lærling + enters: / + lands_in: job_applications + handled_by: prosjektledere + done_when: [hired, rejected] + max_steps: 2 + max_fields: 6 + success: [hired] + stages: [open, interviewed, hired, rejected] + - id: incident_reports + who: HMS-ansvarlig, mandskap på byggeplass + wants: å rapportere avvik og uønskede hendelser + enters: / + lands_in: incident_reports + handled_by: HMS-ansvarlig + done_when: [closed] + max_steps: 1 + max_fields: 3 + success: [closed] + stages: [open, closed] + - id: internal_referral + who: prosjektledere + wants: anbefalinger av kvalifisert mandskap + enters: / + lands_in: internal_referral + handled_by: prosjektledere + done_when: [referred] + max_steps: 1 + max_fields: 2 + success: [referred] + stages: [open, referred] + +personas: + - id: hus_eier_med_problem + need: leads + text: > + Har et problem med huset mitt, enten det leaker, sitter losse eller trenger renovering. Trenger noen som ser på det og gir et tilbud. + - id: borettslag_styre + need: leads + text: > + Ansvarlig for fellesareal og byggestandard i borettslaget. Vil at en fagmann skal kartlegge tilstand og anbefale tiltak. + - id: underentreprenor + need: subcontractors + text: > + Har fagbrev og HKH-kort, men mangler alltid tid for å sende papirene inn. Søker om å bli fast leverandør på prosjekter. + - id: jobb_soker + need: job_applications + text: > + Lurer på om det finnes jobb som tømrer eller murer. Har fagbrev og lyst til å jobbe praktisk. + - id: hms_ansvarlig + need: incident_reports + text: > + Har sett noe på byggeplassen som ikke er greit: feil materialer, nestenulykke eller avvik fra planen. Må rapporteres rett opp og ned. + - id: prosjektleder_anbefaling + need: internal_referral + text: > + Kjenner på noen som skulle passa godt på laget vårt, enten det gjelder fagfolk eller underentreprenører. diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/01-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/01-reply.md new file mode 100644 index 0000000..9fc417c --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/01-reply.md @@ -0,0 +1,150 @@ +<<>> +needs: + - id: henvendelse-til-bygger + who: styreleder i borettslag eller sameier, eller privat huseier + wants: at noen skal se på et problem med bygget + enters: / + lands_in: henvendelser + handled_by: prosjektledere + done_when: [vunnet, tapt] + max_steps: 1 + max_fields: 4 + success: [vunnet] + stages: [open, avtale, tilbud, vunnet, tapt] + personas: + - id: styreleder-huseier + need: henvendelse-til-bygger + text: > + Vi har et problem med huset vårt og trenger at dere kommer og ser på det. Kan dere hjelpe oss med å finne ut av hva som er galt og hva det koster? + - id: huseier-ved-vedtak + need: henvendelse-til-bygger + text: > + Jeg vet nøyaktig hva jeg vil bygge eller endre. Treng tegninger, kommunesøknad og et fast prisoverslag. + - id: huseier-ukjent + need: henvendelse-til-bygger + text: > + Jeg har et problem med huset men vet ikke helt hva som er galt eller hva jeg trenger. + + - id: underentreprenor-anmeldelse + who: tømrer, murer eller lærling som lurer på om firmaet vårt trenger folk + wants: å bli kjent av oss og eventuelt få jobb + enters: / + lands_in: underentreprenører + handled_by: prosjektledere + done_when: [godkjent, avslått] + max_steps: 1 + max_fields: 3 + success: [godkjent] + stages: [open, sjekk, godkjent, avslått] + personas: + - id: tømrer-lurer + need: underentreprenor-anmeldelse + text: > + Lurer på om vi trenger flere hender på jobb, eller om dere har plass til meg. + - id: murer-sjekker-lista + need: underentreprenor-anmeldelse + text: > + Sjekker om dere tar inn flere underentreprenører i år. Har organisasjonsnummer og referanser klar. + - id: lærling-uklar + need: underentreprenor-anmeldelse + text: > + Er ikke sikker på om fagbransjen passer meg ennå, men vil gjerne bli kjent. + + - id: jobbansøker + who: person som søker jobb som tømrer, murer eller lærling + wants: å få jobb som håndverker + enters: / + lands_in: jobbansøkere + handled_by: prosjektledere + done_when: [ansatt, takk-nej] + max_steps: 2 + max_fields: 5 + success: [ansatt] + stages: [open, intervju, ansatt, takk-nej] + personas: + - id: jobbsoeker-klart + need: jobbansøker + text: > + Har klarte eksamen og har jobbsøkt en stund. Klar for intervju og påbør. + - id: jobbsoeker-uklar + need: jobbansøker + text: > + Er nysgjerrig på håndverkerfaget men har ikke fått bevis på ferdigheter ennå. + - id: jobbsoeker-etter-praktikk + need: jobbansøker + text: > + Har jobbet som praktikant og vil gjerne få fast stilling. + + - id: avvik-og-hendelse + who: HMS-ansvarlig eller ansatt på byggeplassen + wants: å rapportere avvik eller uønsket hendelse og få det utbedret + enters: / + lands_in: avvik + handled_by: hms-ansvarlig + done_when: [lukket] + max_steps: 1 + max_fields: 3 + success: [lukket] + stages: [meldt, utbedret, lukket] + personas: + - id: hms-ansvarlig + need: avvik-og-hendelse + text: > + Ansvarlig for helse, miljø og sikkerhet. Må sikre at alt rapporteres og settes rett. + - id:arbeider-raporterer + need: avvik-og-hendelse + text: > + Sitter på stedet, har sett noe skje eller fått i oppdrag av kollega. Har telefonen og hanskene på. + - id: koordinator-etteropp + need: avvik-og-hendelse + text: > + Mottar meldingen og sikrer at tiltak blir tatt. Lukker saken når det er utbedret. + + - id: underentreprenor-dokumentasjon + who: underentreprenør som har mottatt godkjennelse + wants: å få på plass papir og forsikring for å kunne jobbe trygt + enters: / + lands_in: underentreprenør-dok + handled_by: prosjektledere + done_when: [godkjent] + max_steps: 2 + max_fields: 4 + success: [godkjent] + stages: [innsendt, godkjent] + personas: + - id: underentreprenør-papirer + need: underentreprenør-dokumentasjon + text: > + Har fått jobb, men må få organisasjonsnummer, HMS-kort og forsikring på plass før første arbeidsdag. + - id: underentreprenør-forsikring + need: underentreprenør-dokumentasjon + text: > + Trenger å sjekke at forsikringspapirene er gyldige og at alt dokumentasjon er i orden. + - id: underentreprenør-koordinator + need: underentreprenør-dokumentasjon + text: > + Mottar ansøkningen og sjekker opp på at papirene samsvarer kravene. + + - id: jobbansøk-behandling + who: prosjektleder som tar intervju og avgjør ansettelse + wants: å finne rette person til teamet + enters: /desk + lands_in: jobbansøkere + handled_by: prosjektledere + done_when: [ansatt, takk-nej] + max_steps: 2 + max_fields: 4 + success: [ansatt] + stages: [open, intervju, ansatt, takk-nej] + enters_as: prosjektledere + personas: + - id: prosjektleder-intervjuer + need: jobbansøk-behandling + text: > + Tar intervjuer og avgjør hvem som passer teamet best. + - id: prosjektleder-avgjør + need: jobbansøk-behandling + text: > + Tar beslutningen om ansettelse videre til teamet. + +<<>> diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/02-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/02-reply.md new file mode 100644 index 0000000..d85d73d --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/02-reply.md @@ -0,0 +1,148 @@ +<<>> +needs: + - id: henvendelse-til-bygger + who: styreleder i borettslag eller sameier, eller privat huseier + wants: at noen skal se på et problem med bygget + enters: / + lands_in: henvendelser + handled_by: prosjektledere + done_when: [vunnet, tapt] + max_steps: 1 + max_fields: 4 + success: [vunnet] + stages: [open, avtale, tilbud, vunnet, tapt] + personas: + - id: styreleder-huseier + need: henvendelse-til-bygger + text: > + Vi har et problem med huset vårt og trenger at dere kommer og ser på det. Kan dere hjelpe oss med å finne ut av hva som er galt og hva det koster? + - id: huseier-ved-vedtak + need: henvendelse-til-bygger + text: > + Jeg vet nøyaktig hva jeg vil bygge eller endre. Treng tegninger, kommunesøknad og et fast prisoverslag. + - id: huseier-ukjent + need: henvendelse-til-bygger + text: > + Jeg har et problem med huset men vet ikke helt hva som er galt eller hva jeg trenger. + + - id: underentreprenor-anmeldelse + who: tømrer, murer eller lærling som lurer på om firmaet vårt trenger folk + wants: å bli kjent av oss og eventuelt få jobb + enters: / + lands_in: underentreprenører + handled_by: prosjektledere + done_when: [godkjent, avslått] + max_steps: 1 + max_fields: 3 + success: [godkjent] + stages: [open, sjekk, godkjent, avslått] + personas: + - id: tømrer-lurer + need: underentreprenor-anmeldelse + text: > + Lurer på om vi trenger flere hender på jobb, eller om dere har plass til meg. + - id: murer-sjekker-lista + need: underentreprenor-anmeldelse + text: > + Sjekker om dere tar inn flere underentreprenører i år. Har organisasjonsnummer og referanser klar. + - id: lærling-uklar + need: underentreprenor-anmeldelse + text: > + Er ikke sikker på om fagbransjen passer meg ennå, men vil gjerne bli kjent. + + - id: jobbansøker + who: person som søker jobb som tømrer, murer eller lærling + wants: å få jobb som håndverker + enters: / + lands_in: jobbansøkere + handled_by: prosjektledere + done_when: [ansatt, takk-nej] + max_steps: 2 + max_fields: 5 + success: [ansatt] + stages: [open, intervju, ansatt, takk-nej] + personas: + - id: jobbsoeker-klart + need: jobbansøker + text: > + Har klarte eksamen og har jobbsøkt en stund. Klar for intervju og påbør. + - id: jobbsoeker-uklar + need: jobbansøker + text: > + Er nysgjerrig på håndverkerfaget men har ikke fått bevis på ferdigheter ennå. + - id: jobbsoeker-etter-praktikk + need: jobbansøker + text: > + Har jobbet som praktikant og vil gjerne få fast stilling. + + - id: avvik-og-hendelse + who: HMS-ansvarlig eller ansatt på byggeplassen + wants: å rapportere avvik eller uønsket hendelse og få det utbedret + enters: / + lands_in: avvik + handled_by: hms-ansvarlig + done_when: [lukket] + max_steps: 1 + max_fields: 3 + success: [lukket] + stages: [meldt, utbedret, lukket] + personas: + - id: hms-ansvarlig + need: avvik-og-hendelse + text: > + Ansvarlig for helse, miljø og sikkerhet. Må sikre at alt rapporteres og settes rett. + - id: arbeider-raporterer + need: avvik-og-hendelse + text: > + Sitter på stedet, har sett noe skje eller fått i oppdrag av kollega. Har telefonen og hanskene på. + - id: koordinator-etteropp + need: avvik-og-hendelse + text: > + Mottar meldingen og sikrer at tiltak blir tatt. Lukker saken når det er utbedret. + + - id: underentreprenør-dokumentasjon + who: underentreprenør som har mottatt godkjennelse + wants: å få på plass papir og forsikring for å kunne jobbe trygt + enters: / + lands_in: underentreprenør-dok + handled_by: prosjektledere + done_when: [godkjent] + max_steps: 2 + max_fields: 4 + success: [godkjent] + stages: [innsendt, godkjent] + personas: + - id: underentreprenør-papirer + need: underentreprenør-dokumentasjon + text: > + Har fått jobb, men må få organisasjonsnummer, HMS-kort og forsikring på plass før første arbeidsdag. + - id: underentreprenør-forsikring + need: underentreprenør-dokumentasjon + text: > + Trenger å sjekke at forsikringspapirene er gyldige og at alt dokumentasjon er i orden. + - id: underentreprenør-koordinator + need: underentreprenør-dokumentasjon + text: > + Mottar ansøkningen og sjekker opp på at papirene samsvarer kravene. + + - id: jobbansøk-behandling + who: prosjektleder som tar intervju og avgjør ansettelse + wants: å finne rette person til teamet + enters: /desk + lands_in: jobbansøkere + handled_by: prosjektledere + done_when: [ansatt, takk-nej] + max_steps: 2 + max_fields: 4 + success: [ansatt] + stages: [open, intervju, ansatt, takk-nej] + enters_as: prosjektledere + personas: + - id: prosjektleder-intervjuer + need: jobbansøk-behandling + text: > + Tar intervjuer og avgjør hvem som passer teamet best. + - id: prosjektleder-avgjør + need: jobbansøk-behandling + text: > + Tar beslutningen om ansettelse videre til teamet. diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/03-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/03-reply.md new file mode 100644 index 0000000..0ac8da7 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/03-reply.md @@ -0,0 +1,331 @@ +<<>> +needs: + - id: henvendelse-til-byggr + who: styreleder i borettslag eller sameie, eller privat huseier + wants: at noen skal se på et problem med bygget + enters: / + lands_in: henvendelser + handled_by: prosjektledere + done_when: + - vunnet + - tapt + max_steps: 1 + max_fields: 4 + success: [vunnet] + stages: [open, avtale, tilbud, vunnet, tapt] + personas: + - id: styreleder-hus + need: henvendelse-til-byggr + text: > + Vi har et problem med huset vårt og trenger at dere kommer og ser på det. + - id: huseier-vedtatt + need: henvendelse-til-byggr + text: > + Jeg vet nøyaktig hva jeg vil bygge eller endre, og trenger tegninger, søknad til kommunen og en pris. + - id: håndverksfirma + need: henvendelse-til-byggr + text: > + Vi lurer på om Dere trenger folk på arbeidsplassen deres. + + - id: underentreprenor-anmeldelse + who: underentreprenør (tømrer, murer, lærling) + wants: å melde seg på listen over underentreprenører + enters: / + lands_in: underentreprenører + handled_by: prosjektledere + done_when: [godkjent, avslått] + max_steps: 1 + max_fields: 3 + success: [godkjent] + stages: [open, sjekk, godkjent, avslått] + personas: + - id: tømrer-anmeldelse + need: underentreprenor-anmeldelse + text: > + Jeg er tømrer og vil jobbe med Dere. + - id: murer-anmeldelse + need: underentreprenor-anmeldelse + text: > + Jeg er murer og har papirene i orden. + - id: lærling-uklar + need: underentreprenor-anmeldelse + text: > + Jeg er lærling og har ikke fått sertifisert alt enda. + + - id: jobbansøker + who: person som søker jobb som tømrer, murer eller lærling + wants: å bli ansatt som håndverker + enters: / + lands_in: jobbansøkninger + handled_by: prosjektledere + done_when: [ansatt, takk-nei] + max_steps: 2 + max_fields: 5 + success: [ansatt] + stages: [open, samtale, ansatt, takk-nei] + personas: + - id: jobb-søker-energisk + need: jobbansøker + text: > + Jeg er en energisk håndverker som lurer på om Dere har plasser ledige. + - id: jobb-søker-rolig + need: jobbansøker + text: > + Jeg jobber allerede men vil skifte arbeidsplass til en bedre atmosfære. + + - id: avvik-og-ulykke + who: HMS-ansvarlig på byggeplassen, eller ansatt/underentreprenør som melder fra + wants: å rapportere avvik eller uønsket hendelse + enters: / + lands_in: avvik + handled_by: hms-ansvarlig + done_when: [lukket] + max_steps: 1 + max_fields: 3 + success: [lukket] + stages: [open, utbedret, lukket] + personas: + - id: hms-ansvarlig-rapport + need: avvik-og-ulykke + text: > + Det skjedde en hendelse på byggeplassen som krever oppmærksomhet. + - id: tømrer-ukjent + need: avvik-og-ulykke + text: > + Noe gikk galt i jobbingen i dag, men det er løst nå. + - id: maskinoperatør-rolig + need: avvik-og-ulykke + text: > + Vi klarte å løse problemet selv uten at noe brøt. + +aggregates: + - bucket: henvendelser + initial: open + states: + open: { event: received } + avtale: { event: avtale } + tilbud: { event: tilbud } + vunnet: { event: vunnet } + tapt: { event: tapt } + transitions: + open: [avtale, tapt] + avtale: [tilbud, tapt] + tilbud: [vunnet, tapt] + - bucket: underentreprenører + initial: open + states: + open: { event: meldt } + sjekk: { event: sjekket } + godkjent: { event: godkjent } + avslått: { event: avslått } + transitions: + open: [sjekk, avslått] + sjekk: [godkjent, avslått] + - bucket: jobbansøkninger + initial: open + states: + open: { event: mottatt } + samtale: { event: samtale } + ansatt: { event: ansett } + takk-nei: { event: takk_nei } + transitions: + open: [samtale, takk-nei] + samtale: [ansatt, takk-nei] + - bucket: avvik + initial: open + states: + open: { event: meldt } + utbedret: { event: utbedret } + lukket: { event: lukket } + transitions: + open: [utbedret, lukket] + utbedret: [lukket] + +desk: + - group: prosjektledere + bucket: henvendelser + transitions: + open: [avtale, tapt] + avtale: [tilbud, tapt] + tilbud: [vunnet, tapt] + - group: prosjektledere + bucket: underentreprenører + transitions: + open: [sjekk, avslått] + sjekk: [godkjent, avslått] + - group: prosjektledere + bucket: jobbansøkninger + transitions: + open: [samtale, takk-nei] + samtale: [ansatt, takk-nei] + - group: hms-ansvarlig + bucket: avvik + transitions: + open: [utbedret, lukket] + utbedret: [lukket] +<<><<>> +needs: + - id: henvendelse-til-byggr + who: styreleder i borettslag eller sameie, eller privat huseier + wants: at noen skal se på et problem med bygget + enters: / + lands_in: henvendelser + handled_by: prosjektledere + done_when: + - vunnet + - tapt + max_steps: 1 + max_fields: 4 + success: [vunnet] + stages: [open, avtale, tilbud, vunnet, tapt] + personas: + - id: styreleder-hus + need: henvendelse-til-byggr + text: > + Vi har et problem med huset vårt og trenger at dere kommer og ser på det. + - id: huseier-vedtatt + need: henvendelse-til-byggr + text: > + Jeg vet nøyaktig hva jeg vil bygge eller endre, og trenger tegninger, søknad til kommunen og en pris. + - id: håndverksfirma + need: henvendelse-til-byggr + text: > + Vi lurer på om Dere trenger folk på arbeidsplassen deres. + + - id: underentreprenor-anmeldelse + who: underentreprenør (tømrer, murer, lærling) + wants: å melde seg på listen over underentreprenører + enters: / + lands_in: underentreprenører + handled_by: prosjektledere + done_when: [godkjent, avslått] + max_steps: 1 + max_fields: 3 + success: [godkjent] + stages: [open, sjekk, godkjent, avslått] + personas: + - id: tømrer-anmeldelse + need: underentreprenor-anmeldelse + text: > + Jeg er tømrer og vil jobbe med Dere. + - id: murer-anmeldelse + need: underentreprenor-anmeldelse + text: > + Jeg er murer og har papirene i orden. + - id: lærling-uklar + need: underentreprenor-anmeldelse + text: > + Jeg er lærling og har ikke fått sertifisert alt enda. + + - id: jobbansøker + who: person som søker jobb som tømrer, murer eller lærling + wants: å bli ansatt som håndverker + enters: / + lands_in: jobbansøkninger + handled_by: prosjektledere + done_when: [ansatt, takk-nei] + max_steps: 2 + max_fields: 5 + success: [ansatt] + stages: [open, samtale, ansatt, takk-nei] + personas: + - id: jobb-søker-energisk + need: jobbansøker + text: > + Jeg er en energisk håndverker som lurer på om Dere har plasser ledige. + - id: jobb-søker-rolig + need: jobbansøker + text: > + Jeg jobber allerede men vil skifte arbeidsplass til en bedre atmosfære. + + - id: avvik-og-ulykke + who: HMS-ansvarlig på byggeplassen, eller ansatt/underentreprenør som melder fra + wants: å rapportere avvik eller uønsket hendelse + enters: / + lands_in: avvik + handled_by: hms-ansvarlig + done_when: [lukket] + max_steps: 1 + max_fields: 3 + success: [lukket] + stages: [open, utbedret, lukket] + personas: + - id: hms-ansvarlig-rapport + need: avvik-og-ulykke + text: > + Det skjedde en hendelse på byggeplassen som krever oppmærksomhet. + - id: tømrer-ukjent + need: avvik-og-ulykke + text: > + Noe gikk galt i jobbingen i dag, men det er løst nå. + - id: maskinoperatør-rolig + need: avvik-og-ulykke + text: > + Vi klarte å løse problemet selv uten at noe brøt. + +aggregates: + - bucket: henvendelser + initial: open + states: + open: { event: received } + avtale: { event: avtale } + tilbud: { event: tilbud } + vunnet: { event: vunnet } + tapt: { event: tapt } + transitions: + open: [avtale, tapt] + avtale: [tilbud, tapt] + tilbud: [vunnet, tapt] + - bucket: underentreprenører + initial: open + states: + open: { event: meldt } + sjekk: { event: sjekket } + godkjent: { event: godkjent } + avslått: { event: avslått } + transitions: + open: [sjekk, avslått] + sjekk: [godkjent, avslått] + - bucket: jobbansøkninger + initial: open + states: + open: { event: mottatt } + samtale: { event: samtale } + ansatt: { event: ansett } + takk-nei: { event: takk_nei } + transitions: + open: [samtale, takk-nei] + samtale: [ansatt, takk-nei] + - bucket: avvik + initial: open + states: + open: { event: meldt } + utbedret: { event: utbedret } + lukket: { event: lukket } + transitions: + open: [utbedret, lukket] + utbedret: [lukket] + +desk: + - group: prosjektledere + bucket: henvendelser + transitions: + open: [avtale, tapt] + avtale: [tilbud, tapt] + tilbud: [vunnet, tapt] + - group: prosjektledere + bucket: underentreprenører + transitions: + open: [sjekk, avslått] + sjekk: [godkjent, avslått] + - group: prosjektledere + bucket: jobbansøkninger + transitions: + open: [samtale, takk-nei] + samtale: [ansatt, takk-nei] + - group: hms-ansvarlig + bucket: avvik + transitions: + open: [utbedret, lukket] + utbedret: [lukket] +<<> diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/04-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/04-reply.md new file mode 100644 index 0000000..5522834 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/04-reply.md @@ -0,0 +1,84 @@ +<<>> +needs: + - id: leads + who: styreledere i borettslag og sameier, private huseiere + wants: at noen skal se på et problem med bygget + enters: / + lands_in: leads + handled_by: prosjektledere + done_when: [won, lost] + max_steps: 1 + max_fields: 4 + success: [won] + stages: [open, quoted, won, lost] + - id: subcontractors + who: underentreprenører (tømrere, murere, lærlinger) + wants: å bli godkjent som fast leverandør + enters: / + lands_in: subcontractors + handled_by: prosjektledere + done_when: [approved, rejected] + max_steps: 1 + max_fields: 3 + success: [approved] + stages: [open, approved, rejected] + - id: job_applications + who: personer som søker jobb + wants: å bli ansatt som tømrer, murer eller lærling + enters: / + lands_in: job_applications + handled_by: prosjektledere + done_when: [hired, rejected] + max_steps: 2 + max_fields: 6 + success: [hired] + stages: [open, interviewed, hired, rejected] + - id: incident_reports + who: HMS-ansvarlig, mandskap på byggeplass + wants: å rapportere avvik og uønskede hendelser + enters: / + lands_in: incident_reports + handled_by: HMS-ansvarlig + done_when: [closed] + max_steps: 1 + max_fields: 3 + success: [closed] + stages: [open, closed] + - id: internal_referral + who: prosjektledere + wants: anbefalinger av kvalifisert mandskap + enters: / + lands_in: internal_referral + handled_by: prosjektledere + done_when: [referred] + max_steps: 1 + max_fields: 2 + success: [referred] + stages: [open, referred] + +personas: + - id: hus_eier_med_problem + need: leads + text: > + Har et problem med huset mitt, enten det leaker, sitter losse eller trenger renovering. Trenger noen som ser på det og gir et tilbud. + - id: borettslag_styre + need: leads + text: > + Ansvarlig for fellesareal og byggestandard i borettslaget. Vil at en fagmann skal kartlegge tilstand og anbefale tiltak. + - id: underentreprenor + need: subcontractors + text: > + Har fagbrev og HKH-kort, men mangler alltid tid for å sende papirene inn. Søker om å bli fast leverandør på prosjekter. + - id: jobb_soker + need: job_applications + text: > + Lurer på om det finnes jobb som tømrer eller murer. Har fagbrev og lyst til å jobbe praktisk. + - id: hms_ansvarlig + need: incident_reports + text: > + Har sett noe på byggeplassen som ikke er greit: feil materialer, nestenulykke eller avvik fra planen. Må rapporteres rett opp og ned. + - id: prosjektleder_anbefaling + need: internal_referral + text: > + Kjenner på noen som skulle passa godt på laget vårt, enten det gjelder fagfolk eller underentreprenører. +<<>> diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/05-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/05-reply.md new file mode 100644 index 0000000..22aa178 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/05-reply.md @@ -0,0 +1,462 @@ +<<>> +title: Åsrud Bygg og Rehabilitering AS +lang: no +<<>> + +<<>> +needs: + - id: leads + who: styreledere i borettslag og sameier, private huseiere + wants: at noen skal se på et problem med bygget + enters: / + lands_in: leads + handled_by: prosjektledere + done_when: [won, lost] + max_steps: 1 + max_fields: 4 + success: [won] + stages: [open, quoted, won, lost] + - id: subcontractors + who: underentreprenører (tømrere, murere, lærlinger) + wants: å bli godkjent som fast leverandør + enters: / + lands_in: subcontractors + handled_by: prosjektledere + done_when: [approved, rejected] + max_steps: 1 + max_fields: 3 + success: [approved] + stages: [open, approved, rejected] + - id: job_applications + who: personer som søker jobb + wants: å bli ansatt som tømrer, murer eller lærling + enters: / + lands_in: job_applications + handled_by: prosjektledere + done_when: [hired, rejected] + max_steps: 2 + max_fields: 6 + success: [hired] + stages: [open, interviewed, hired, rejected] + - id: incident_reports + who: HMS-ansvarlig, mandskap på byggeplass + wants: å rapportere avvik og uønskede hendelser + enters: / + lands_in: incident_reports + handled_by: HMS-ansvarlig + done_when: [closed] + max_steps: 1 + max_fields: 3 + success: [closed] + stages: [open, closed] + - id: internal_referral + who: prosjektledere + wants: anbefalinger av kvalifisert mandskap + enters: / + lands_in: internal_referral + handled_by: prosjektledere + done_when: [referred] + max_steps: 1 + max_fields: 2 + success: [referred] + stages: [open, referred] +<<>> + +<<>> +aggregates: + - bucket: leads + initial: open + states: + open: { event: received } + quoted: { event: quoted } + won: { event: won } + lost: { event: lost } + transitions: + open: [quoted, lost] + quoted: [won, lost] + - bucket: subcontractors + initial: open + states: + open: { event: received } + approved: { event: approved } + rejected: { event: rejected } + transitions: + open: [approved, rejected] + approved: [] + rejected: [] + - bucket: job_applications + initial: open + states: + open: { event: received } + interviewed: { event: interviewed } + hired: { event: hired } + rejected: { event: rejected } + transitions: + open: [interviewed] + interviewed: [hired, rejected] + hired: [] + rejected: [] + - bucket: incident_reports + initial: open + states: + open: { event: reported } + closed: { event: closed } + transitions: + open: [closed] + closed: [] + - bucket: internal_referral + initial: open + states: + open: { event: referred } + referred: { event: referred } + transitions: + open: [referred] + referred: [] +<< + +<<>> +name: Hva hjelper du på? +description: > + Vi hjelper med tilstandsrapporter, prosjektering, byggesøknad og jobb. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Melding om problem med bygget + description: > + action: /leads + consequence: [Send] + record_as: leads + - name: Melding om underentreprenør + description: > + action: /subcontractors + consequence: [Send] + record_as: subcontractors + - name: Søk jobb + description: > + action: /job_applications + consequence: [Send] + record_as: job_applications + - name: Melding om avvik på byggeplass + description: > + action: /incident_reports + consequence: [Send] + record_as: incident_reports + - name: Anbefaling av kompetanse + description: > + action: /internal_referral + consequence: [Send] + record_as: internal_referral +<< + +<<>> +name: Hva problemet gjelder +description: > + Fortell kort hva som trenger å gjøres med bygget ditt. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Send tilbud + description: > + action: /leads/quoted + consequence: [Send] + record_as: leads + features: + - name: E-post + description: > + requirements: + - { name: email, label: E-post, type: email } + - { name: problem, label: Hva er problemet, type: textarea } + - { name: urgency, label: Hvor snart, type: select, options: [I denne uken, I denne måneden, Ikke akutt] } + - { name: address, label: Adresse, optional: true } + - name: Avslå + description: > + action: /leads/lost + consequence: [Send] + record_as: leads +<< + +<<>> +name: Tilbud sendt +description: > + Vi sender et tilbud på arbeidet. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Aksepter tilbudet + description: > + action: /leads/won + consequence: [Godkjenn] + record_as: leads + - name: Trekke tilbudet + description: > + action: /leads/lost + consequence: [Avslå] + record_as: leads +<< + +<<>> +name: Melding om underentreprenør +description: > + Fortell kort hvem du er og hva du kan. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Godkjenn + description: > + action: /subcontractors/approved + consequence: [Godkjenn] + record_as: subcontractors + features: + - name: Organisasjonsnummer + description: > + requirements: + - { name: org_number, label: Organisasjonsnummer, type: text } + - { name: hms_kort, label: HMS-kort, type: select, options: [Har, Ikke har] } + - { name: insurance, label: Forsikring, type: select, options: [OK, Mangler] } + - { name: availability, label: Tilgjengelighet, type: select, options: [Normalt, Begrenset] } + - name: Avslå + description: > + action: /subcontractors/rejected + consequence: [Avslå] + record_as: subcontractors +<< + +<<>> +name: Godkjent som leverandør +description: > + Underentreprenøren er godkjent. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: subcontractors +<< + +<<>> +name: Ikke kvalifisert +description: > + Underentreprenøren blir ikke godkjent nå. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: subcontractors +<< + +<<>> +name: Stillingsansøkning +description: > + Fortell kort hvem du er og hva du kan. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Send påmelding + description: > + action: /job_applications/interviewed + consequence: [Send] + record_as: job_applications + features: + - name: Navn + description: > + requirements: + - { name: name, label: Navn, type: text } + - { name: phone, label: Telefon, type: tel } + - { name: email, label: E-post, type: email } + - { name: experience, label: Erfaring, type: textarea } + - name: Avslå påmelding + description: > + action: /job_applications/rejected + consequence: [Avslå] + record_as: job_applications +<< + +<<>> +name: Intervju +description: > + Vi kaller deg til intervju. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Ansett + description: > + action: /job_applications/hired + consequence: [Ansett] + record_as: job_applications + - name: Takk nei + description: > + action: /job_applications/rejected + consequence: [Avslå] + record_as: job_applications +<< + +<<>> +name: Ansett +description: > + Du blir ansatt som tømrer, murer eller lærling. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: job_applications +<< + +<<>> +name: Takk nei +description: > + Vi takker nei til din påmelding. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: job_applications +<< + +<<>> +name: Rapporter avvik +description: > + Fortell kort hva som har skjedd. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Lukk saken + description: > + action: /incident_reports/closed + consequence: [Lukk] + record_as: incident_reports + features: + - name: beskrivelse + description: > + requirements: + - { name: description, label: Hva skjedde, type: textarea } + - { name: avvik_type, label: Typen avvik, type: select, options: [Løse rekkverk, Nestenulykke, Feil materialer] } + - { name: rapportert_av, label: Hvem rapporterte, type: text } + - name: Avslå å rapportere + description: > + action: /incident_reports/closed + consequence: [Avslå] + record_as: incident_reports +<< + +<<>> +name: Saken er lukket +description: > + Avviket er løst og saken lukkes. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: incident_reports +<< + +<<>> +name: Anbefaling av kompetanse +description: > + Fortell kort hvem du anbefaler og hvorfor. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Send anbefaling + description: > + action: /internal_referral/referred + consequence: [Send] + record_as: internal_referral + features: + - name: Navn + description: > + requirements: + - { name: name, label: Navn, type: text } + - { name: role, label: Rolle, type: text } + - { name: contact, label: Kontakt, type: email } + - name: Avslå anbefaling + description: > + action: /internal_referral/referred + consequence: [Avslå] + record_as: internal_referral +<< + +<<>> +name: Anbefaling sendt +description: > + Anbefalingen er sendt videre. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Gå tilbake til hovedmeny + description: > + action: / + consequence: [Tilbake] + record_as: internal_referral +<< + +<<>> +qualifies: prosjektledere +<< + +<<>> +name: Hva kom inn? +description: > + Henvendelser og saker som trenger handling. +responsible: { name: Marte Åsrud, email: marte@asrudbygg.example } +alternatives: + - name: Leads + description: > + action: /desk + consequence: [Confirm] + record_as: leads + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: prosjektledere + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } + - name: Subcontractors + description: > + action: /desk + consequence: [Confirm] + record_as: subcontractors + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: subcontractors } + requires_group: prosjektledere + - name: Job applications + description: > + action: /desk + consequence: [Confirm] + record_as: job_applications + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: job_applications } + requires_group: prosjektledere + - name: Incident reports + description: > + action: /desk + consequence: [Confirm] + record_as: incident_reports + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: incident_reports } + requires_group: HMS-ansvarlig + - name: Internal referrals + description: > + action: /desk + consequence: [Confirm] + record_as: internal_referral + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: internal_referral } + requires_group: prosjektledere +<< diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/06-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/06-reply.md new file mode 100644 index 0000000..8a042cf --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/06-reply.md @@ -0,0 +1,310 @@ +<<>> +name: Hva trenger du hjelp med? +description: > + Velg hva du trenger hjelp med. +responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no } +alternatives: + - name: Meld inn et problem + description: > + Fortell oss om det er noe galt med bygget ditt. + action: /leads + consequence: [Send] + record_as: leads + features: + - name: Om bygget + description: > + Hva slags problem opplever du? + requirements: + - { name: problem, label: Hva er problemet, type: textarea } + - { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] } + - { name: contact, label: Kontaktinformasjon, type: email } + - name: Bliv godkjent som leverandør + description: > + Søk om å bli fast leverandør til oss. + action: /subcontractors + consequence: [Send] + record_as: subcontractors + features: + - name: Førstehåndsinformasjon + description: > + Hvem er du og hva kan du? + requirements: + - { name: company, label: Bedriftsnavn, type: text } + - { name: contact, label: Kontaktinformasjon, type: email } + - { name: experience, label: Erfaring, type: textarea } + - name: Søk jobb + description: > + Søk om jobb som tømrer, murer eller lærling. + action: /job_applications + consequence: [Send] + record_as: job_applications + features: + - name: Faglig profil + description: > + Hvilken stilling søker du? + requirements: + - { name: role, label: Stillingsnavn, type: select, options: [Tømrer, Murer, Lærling] } + - { name: contact, label: Kontaktinformasjon, type: email } + - { name: experience, label: Erfaring, type: textarea } + - name: Rapporter avvik + description: > + Rapporter uønskede hendelser eller avvik på byggeplassen. + action: /incident_reports + consequence: [Send] + record_as: incident_reports + features: + - name: Avviksinformasjon + description: > + Hva skjedde? + requirements: + - { name: incident, label: Hva skjedde, type: textarea } + - { name: severity, label: Alvorlighetsgrad, type: select, options: [Høyt, Medium, Lavt] } + - { name: reporter, label: Rapportert av, type: text } + - name: Anbefal kvalifisert mandskap + description: > + Anbefal noen vi bør snakke med. + action: /internal_referral + consequence: [Send] + record_as: internal_referral + features: + - name: Anbefaling + description: > + Hvem anbefaler du og hvorfor? + requirements: + - { name: person, label: Navn, type: text } + - { name: reason, label: Grunn, type: textarea } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> + +<<>> +name: Meld inn et problem +description: > + Fortell oss om det er noe galt med bygget ditt. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + description: > + Send inn meldingen. + action: /received + consequence: [Send] + record_as: leads + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: prosjektledere + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } +<< + +<<>> +name: Bliv godkjent som leverandør +description: > + Søk om å bli fast leverandør til oss. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + description: > + Send inn søknaden. + action: /received + consequence: [Send] + record_as: subcontractors + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: subcontractors } + requires_group: prosjektledere + transitions: + - { to: approved, label: Godkjent } + - { to: rejected, label: Ikke godkjent } +<< + +<< +name: Søk jobb +description: > + Søk om jobb som tømrer, murer eller lærling. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + description: > + Send inn søknaden. + action: /received + consequence: [Send] + record_as: job_applications + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: job_applications } + requires_group: prosjektledere + transitions: + - { to: interviewed, label: Interview requested } + - { to: rejected, label: Ikke relevant } + - { from: interviewed, to: hired, label: Hired } + - { from: interviewed, to: rejected, label: Rejected } +<< + +<< +name: Rapporter avvik +description: > + Rapporter uønskede hendelser eller avvik på byggeplassen. +responsible: { name: HMS-ansvarlig, contact: post@asrud.no } +alternatives: + - name: Send + description: > + Send inn rapporten. + action: /received + consequence: [Send] + record_as: incident_reports + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: incident_reports } + requires_group: HMS-ansvarlig + transitions: + - { to: closed, label: Closed } +<< + +<< +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + description: > + Send inn anbefalingen. + action: /received + consequence: [Send] + record_as: internal_referral + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: internal_referral } + requires_group: prosjektledere + transitions: + - { to: referred, label: Referred } +<< + +<<>> +qualifies: prosjektledere +<< + +<<>> +name: Hva kom inn? +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Leads + description: > + Behandl innkommende meldinger. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: prosjektledere + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } + - name: Subcontractors + description: > + Behandl leverandør-søknader. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: subcontractors } + requires_group: prosjektledere + transitions: + - { to: approved, label: Godkjent } + - { to: rejected, label: Ikke godkjent } + - name: Job applications + description: > + Behandl jobb-søknader. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: job_applications } + requires_group: prosjektledere + transitions: + - { to: interviewed, label: Interview requested } + - { to: rejected, label: Ikke relevant } + - { from: interviewed, to: hired, label: Hired } + - { from: interviewed, to: rejected, label: Rejected } + - name: Incident reports + description: > + Behandl avvik rapporter. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: incident_reports } + requires_group: HMS-ansvarlig + transitions: + - { to: closed, label: Closed } + - name: Internal referrals + description: > + Behandl anbefalinger. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: internal_referral } + requires_group: prosjektledere + transitions: + - { to: referred, label: Referred } +<< + +<<>> +name: Melding mottatt +description: > + Meldingen er mottatt. Hvem skal lese den og hva skjer nå? +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Gå tilbake + description: > + Gå tilbake til hovedmenyen. + action: / + consequence: [Gå tilbake] + followup: true +<< + +<<>> +qualifies: prosjektledere +<< + +<<>> +qualifies: prosjektledere +<< + +<<>> +qualifies: prosjektledere +<< + +<<>> +qualifies: HMS-ansvarlig +<< + +<<>> +qualifies: prosjektledere +<< diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/07-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/07-reply.md new file mode 100644 index 0000000..6661c02 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/07-reply.md @@ -0,0 +1,181 @@ +<<>> +name: Hva trenger du hjelp med? +description: > + Velg hva du trenger hjelp med. +responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no } +alternatives: + - name: Meld inn et problem + description: > + Fortell oss om det er noe galt med bygget ditt. + action: /questions/leads + consequence: [Send] + record_as: leads + features: + - name: Om bygget + description: > + Hva slags problem opplever du? + requirements: + - { name: problem, label: Hva er problemet, type: textarea } + - { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] } + - { name: contact, label: Kontaktinformasjon, type: email } + - name: Bliv godkjent som leverandør + description: > + Søk om å bli fast leverandør til oss. + action: /questions/subcontractors + consequence: [Send] + record_as: subcontractors + features: + - name: Førstehåndsinformasjon + description: > + Hvem er du og hva kan du? + requirements: + - { name: company, label: Bedriftsnavn, type: text } + - { name: contact, label: Kontaktinformasjon, type: email } + - { name: experience, label: Erfaring, type: textarea } + - name: Søk jobb + description: > + Søk om jobb som tømrer, murer eller lærling. + action: /questions/job_applications + consequence: [Send] + record_as: job_applications + features: + - name: Faglig profil + description: > + Hvilken stilling søker du? + requirements: + - { name: role, label: Stillingsnavn, type: select, options: [Tømrer, Murer, Lærling] } + - { name: contact, label: Kontaktinformasjon, type: email } + - { name: experience, label: Erfaring, type: textarea } + - name: Rapporter avvik + description: > + Rapporter uønskede hendelser eller avvik på byggeplassen. + action: /questions/incident_reports + consequence: [Send] + record_as: incident_reports + features: + - name: Avviksinformasjon + description: > + Hva skjedde? + requirements: + - { name: incident, label: Hva skjedde, type: textarea } + - { name: severity, label: Alvorlighetsgrad, type: select, options: [Høyt, Medium, Lavt] } + - { name: reporter, label: Rapportert av, type: text } + - name: Anbefal kvalifisert mandskap + description: > + Anbefal noen vi bør snakke med. + action: /questions/internal_referral + consequence: [Send] + record_as: internal_referral + features: + - name: Anbefaling + description: > + Hvem anbefaler du og hvorfor? + requirements: + - { name: person, label: Navn, type: text } + - { name: reason, label: Grunn, type: textarea } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> + +<<>> +name: Meld inn et problem +description: > + Fortell oss om det er noe galt med bygget ditt. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: leads + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: prosjektledere + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } +``` + +<<>> +name: Bliv godkjent som leverandør +description: > + Søk om å bli fast leverandør til oss. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: subcontractors + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: subcontractors } + requires_group: prosjektledere + transitions: + - { to: approved, label: Godkjent } + - { to: rejected, label: Ikke godkjent } +``` + +<<>> +name: Søk jobb +description: > + Søk om jobb som tømrer, murer eller lærling. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: job_applications + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: job_applications } + requires_group: prosjektledere + transitions: + - { to: interviewed, label: Interview } + - { to: rejected, label: Ikke den rette } + - { from: interviewed, to: hired, label: Hired } + - { from: interviewed, to: rejected, label: Ikke den rette } +``` + +<<> +name: Rapporter avvik +description: > + Rapporter uønskede hendelser eller avvik på byggeplassen. +responsible: { name: HMS-ansvarlig, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: incident_reports + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: incident_reports } + requires_group: HMS-ansvarlig + transitions: + - { to: closed, label: Lukket } +``` + +<<>> +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: internal_referral + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: internal_referral } + requires_group: prosjektledere + transitions: + - { to: referred, label: Anbefalt } +``` + +<<>> diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/08-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/08-reply.md new file mode 100644 index 0000000..19b8b46 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/08-reply.md @@ -0,0 +1,93 @@ +<<>> +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: internal_referral + features: + - name: Anbefaling + description: > + Hvem anbefaler du og hvorfor? + requirements: + - { name: person, label: Navn, type: text } + - { name: reason, label: Grunn, type: textarea } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> + +<<>> +qualifies: prosjektledere +<<> + +<<>> +name: What came in? +responsible: { name: ..., contact: ... } +alternatives: + - name: Leads + description: One line on what to do with them. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: project_managers + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } +``` + +<<>> + +Wait, I need to check the verifier output again. The error is about `questions/internal_referral.yaml` having a parsing error at line 17. Let me look at the file content that was provided. + +The file `questions/internal_referral.yaml` ends with: +``` + transitions: + - { to: referred, label: Anbefalt } +``` + +It seems like the file is truncated or has a syntax error. The YAML structure seems incomplete. There's no closing brackets for the `features` list or the `alternatives` list or the resource block. + +Looking at the original file that was rejected: +``` +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: internal_referral + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: internal_referral } + requires_group: prosjektledere + transitions: + - { to: referred, label: Anbefalt } +``` + +The file seems to be missing closing brackets. The `features` list has one item, but the list isn't closed. The `alternatives` list has one item, but isn't closed. The YAML document itself might not have the proper closing. + +Actually, looking more carefully, the error message says: "found character that cannot start any token at line 17 column 1, while scanning for the next token". This typically means there's an issue with indentation or unexpected characters. + +But wait, the user says: "The verifier rejected the repo below. Fix it." and provides the verifier output. The current files include `questions/internal_referral.yaml` which appears to be truncated or malformed. + +Let me also check the other files. There's `questions/desk/_section.yaml` and `questions/desk/index.yaml` but they seem to have issues too. The `questions/desk/index.yaml` has `responsible: { name: ..., contact: ... }` with ellipsis which might not be valid, and it references `project_managers` but the need says `handled_by: prosjektledere`. + +Actually, re-reading the task: "Return only the files you change, each complete. To remove a file, return it with the single line DELETE. needs.yaml is frozen; do not return it." + +So I need to fix the parsing error in `questions/internal_referral.yaml`. The error is at line 17 column 1. Looking at the file content, it seems like the file was cut off. I need to provide a complete valid YAML file. + +Let me construct the complete `questions/internal_referral.yaml` file based on the pattern from other files and the needs specification. + +From `needs.yaml`, the `internal_referral` need has: +- id: internal_referral +- who: \ No newline at end of file diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/09-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/09-reply.md new file mode 100644 index 0000000..5792c18 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/09-reply.md @@ -0,0 +1,64 @@ +<<>> +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: internal_referral + features: + - name: Anbefaling + description: > + Hvem anbefaler du og hvorfor? + requirements: + - { name: person, label: Navn, type: text } + - { name: reason, label: Grunn, type: textarea } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> + +<<>> +title: Åsrud Bygg og Rehabilitering AS +lang: no +<<>> + +<<>> +name: What came in? +responsible: { name: ..., contact: ... } +alternatives: + - name: Leads + description: One line on what to do with them. + action: /desk + consequence: [Confirm] + features: + - name: "" + description: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: project_managers + transitions: + - { to: quoted, label: Quote sent } + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } +<<><<>> +name: Hva trenger du hjelp med? +description: > + Velg hva du trenger hjelp med. +responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no } +alternatives: + - name: Meld inn et problem + description: > + Fortell oss om det er noe galt med bygget ditt. + action: /questions/leads + consequence: [Send] + record_as: leads + features: + - name: Om bygget + description: > + Hva slags problem opplever du? + requirements: + - { name: problem, label: Hva er problemet, type: textarea } + - { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> diff --git a/runs/rehab-entreprenor-no-2026-09-21/replies/10-reply.md b/runs/rehab-entreprenor-no-2026-09-21/replies/10-reply.md new file mode 100644 index 0000000..81e8f23 --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/replies/10-reply.md @@ -0,0 +1,24 @@ +<<>> +name: Anbefal kvalifisert mandskap +description: > + Anbefal noen vi bør snakke med. +responsible: { name: prosjektledere, contact: post@asrud.no } +alternatives: + - name: Send + consequence: [Send] + record_as: internal_referral + features: + - name: Anbefaling + description: > + Hvem anbefaler du og hvorfor? + requirements: + - { name: person, label: Navn, type: text } + - { name: reason, label: Grunn, type: textarea } + - { name: contact, label: Kontaktinformasjon, type: email } +<<>> + +<<>> +title: Åsrud Bygg og Rehabilitering AS +lang: no +<<>> +``` diff --git a/runs/rehab-entreprenor-no-2026-09-21/verifier-last.log b/runs/rehab-entreprenor-no-2026-09-21/verifier-last.log new file mode 100644 index 0000000..466ae0e --- /dev/null +++ b/runs/rehab-entreprenor-no-2026-09-21/verifier-last.log @@ -0,0 +1,9 @@ +needs: parsed in round 4 +structure round 1: ok=false fails=11 +structure round 2: ok=false fails=1 +structure round 3: ok=false fails=0 +structure round 4: ok=false fails=0 +structure round 5: ok=false fails=0 +structure round 6: ok=false fails=0 + +Error: parsing desk/index.yaml: could not find expected ':' at line 20 column 1, while scanning a simple key at line 19 column 1 diff --git a/src/main.rs b/src/main.rs index 163ccd2..6632de7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,9 @@ struct Opts { llm: String, rounds: usize, wording_rounds: usize, + /// The model that plays the visitor. A generator grading its own + /// wording agrees with itself, so this should be a different one. + visitor: Option, } fn main() -> anyhow::Result<()> { @@ -64,6 +67,8 @@ fn main() -> anyhow::Result<()> { let intake = std::fs::read_to_string(&opts.intake) .with_context(|| format!("reading {}", opts.intake.display()))?; std::fs::create_dir_all(opts.out.join("questions"))?; + std::fs::create_dir_all(opts.out.join("llm"))?; + LLM_LOG.set(opts.out.join("llm")).ok(); let mut log = Vec::new(); // Phase 1: the spec. @@ -72,7 +77,11 @@ fn main() -> anyhow::Result<()> { let prompt = format!( "{REFERENCE}\n\n# Task\n\nA business filled in this intake. Write ONLY `needs.yaml` for it: every need the intake implies, for the public and for each role inside the business, with states taken from how the business says things progress, and personas.\n\n## Intake\n\n{intake}\n{feedback}\n{FORMAT}" ); - let files = parse_files(&llm(&opts.llm, &prompt)?)?; + let Ok(files) = parse_files(&llm(&opts.llm, &prompt)?) else { + eprintln!("needs round {round}: the reply had no file blocks, asking again"); + feedback = "\n## Your last attempt\n\nIt contained no <<>> blocks at all. Return needs.yaml in exactly that format.\n".to_string(); + continue; + }; let Some(needs) = files.get("needs.yaml") else { feedback = "\n## Your last attempt\n\nIt contained no needs.yaml.\n".to_string(); continue; @@ -89,8 +98,12 @@ fn main() -> anyhow::Result<()> { } eprintln!("needs round {round}: needs.yaml parses, frozen"); log.push(format!("needs: parsed in round {round}")); + feedback.clear(); break; } + if !feedback.is_empty() { + bail!("needs.yaml never parsed in {} round(s); the replies are in {}/llm/", opts.rounds, opts.out.display()); + } let needs = std::fs::read_to_string(opts.out.join("needs.yaml")).context("no needs.yaml was produced")?; // Phase 2: structure. The page-writing model sees the needs and @@ -116,7 +129,10 @@ fn main() -> anyhow::Result<()> { dump(&opts.out)? ) }; - apply(&opts.out, parse_files(&llm(&opts.llm, &prompt)?)?)?; + match parse_files(&llm(&opts.llm, &prompt)?) { + Ok(files) => apply(&opts.out, files)?, + Err(_) => eprintln!("structure round {round}: the reply had no file blocks"), + } let (ok, r) = lint(&opts, &[])?; report = r; let fails = report.lines().filter(|l| l.starts_with("FAIL")).count(); @@ -143,7 +159,7 @@ fn main() -> anyhow::Result<()> { args.push("--skim".to_string()); } lint(&opts, &args)?; - let answers = answer_blind(&opts.llm, &std::fs::read_to_string(&tasks_path)?)?; + let answers = answer_blind(opts.visitor.as_deref().unwrap_or(&opts.llm), &std::fs::read_to_string(&tasks_path)?)?; let answers_path = opts.out.join(if skim { "answers-skim.jsonl" } else { "answers.jsonl" }); std::fs::write(&answers_path, answers)?; let mut args = vec!["--needs-score".to_string(), answers_path.display().to_string()]; @@ -198,6 +214,7 @@ fn parse_args() -> anyhow::Result { llm: "claude -p".to_string(), rounds: 4, wording_rounds: 2, + visitor: None, }; while let Some(arg) = args.next() { let mut value = || args.next().ok_or_else(|| anyhow!("{arg} needs a value")); @@ -208,6 +225,7 @@ fn parse_args() -> anyhow::Result { "--llm" => opts.llm = value()?, "--rounds" => opts.rounds = value()?.parse()?, "--wording-rounds" => opts.wording_rounds = value()?.parse()?, + "--visitor-llm" => opts.visitor = Some(value()?), other => bail!("unknown argument {other}"), } } @@ -218,8 +236,23 @@ fn parse_args() -> anyhow::Result { } /// Prompt on stdin, text on stdout. +/// Where `generate` keeps every prompt and reply (`/llm/`). A +/// model that returns nothing usable has to be read, not guessed at. +static LLM_LOG: std::sync::OnceLock = std::sync::OnceLock::new(); +static LLM_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + fn llm(cmd: &str, prompt: &str) -> anyhow::Result { - llm_with(cmd, prompt, &[]) + let reply = llm_with(cmd, prompt, &[]); + if let Some(dir) = LLM_LOG.get() { + let n = LLM_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + std::fs::write(dir.join(format!("{n:02}-prompt.md")), prompt).ok(); + let text = match &reply { + Ok(text) => text.clone(), + Err(e) => format!("ERROR: {e}"), + }; + std::fs::write(dir.join(format!("{n:02}-reply.md")), text).ok(); + } + reply } /// As `llm`, with hints for the backend in its environment (`SEED`, @@ -262,20 +295,45 @@ fn lint(opts: &Opts, extra: &[String]) -> anyhow::Result<(bool, String)> { /// `<<>> ... <<>>` blocks. Anything outside a block is /// ignored, so a model that adds a sentence does no harm. fn parse_files(text: &str) -> anyhow::Result> { + // Small models mangle the markers: `<<>` with a bracket short, + // a stray ``, the next `<< = None; + let mut close = |current: &mut Option<(String, String)>, files: &mut BTreeMap| { + if let Some((path, body)) = current.take() { + files.insert(path, body); + } + }; for line in text.lines() { - if let Some(path) = line.trim().strip_prefix("<<>>")) { - current = Some((path.trim().to_string(), String::new())); - } else if line.trim() == "<<>>" { - if let Some((path, body)) = current.take() { - files.insert(path, body); + let mut rest = line; + loop { + let (file_at, end_at) = (rest.find("<< { + close(&mut current, &mut files); + let after = &rest[f + "<<>>").unwrap_or(after.len()); + current = Some((after[..stop].trim().to_string(), String::new())); + rest = after.get(stop + 3..).unwrap_or(""); + } + (_, Some(e)) => { + close(&mut current, &mut files); + rest = &rest[e + "<< break, + } + } + // A line that carried a marker carries no content. + if std::ptr::eq(rest, line) { + if let Some((_, body)) = current.as_mut() { + body.push_str(line); + body.push('\n'); } - } else if let Some((_, body)) = current.as_mut() { - body.push_str(line); - body.push('\n'); } } + close(&mut current, &mut files); if files.is_empty() { bail!("the model returned no <<>> blocks"); } @@ -403,6 +461,19 @@ mod tests { assert!(parse_files("no blocks here").is_err()); } + #[test] + fn mangled_markers_from_a_small_model_still_separate_the_files() { + // Verbatim shape of a nemotron reply: a short END, a stray + // think tag, the next FILE glued on, and no END at the very end. + let files = parse_files( + "<<>>\nname: Desk?\n<<><<>>\nname: Hva trenger du?\n<<>>\ntitle: A\n", + ) + .unwrap(); + assert_eq!(files["questions/desk/index.yaml"], "name: Desk?\n"); + assert_eq!(files["questions/index.yaml"], "name: Hva trenger du?\n"); + assert_eq!(files["site.yaml"], "title: A\n"); + } + #[test] fn only_content_paths_may_be_written() { assert!(allowed("site.yaml"));