diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 5ac07b4b35..4e149ae66d 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -30,10 +30,10 @@ Read the spec through the t27 MCP server, which is already installed in Claude C gHashTag/t27 checkout instead. Say which one you used. From the source, take: `LENS_NAMES`, `LENS_QUESTIONS`, `STAGE_NAMES`, the four `*_WORDS` -vocabularies, the five `*_BEES` lists, `MAX_ACTIVE_BEES`, and the functions `intake_verdict`, -`map_verdict`, `initial_status`, `route`, `dispatch_allowed`, `required_evidence`, -`evidence_accepted`, `verify`, `counts_as_progress`, `needs_human_approval`, `action_allowed`, -`after_action`. Evaluate them as written; they are small and total. +vocabularies and `KEYWORD_MATCH`, the five `*_BEES` lists, `MAX_ACTIVE_BEES`, `GAP_BEE`, and the +functions `intake_verdict`, `map_verdict`, `initial_status`, `ground`, `route`, `dispatch_allowed`, +`bee_target`, `required_evidence`, `evidence_accepted`, `verify`, `counts_as_progress`, +`needs_human_approval`, `action_allowed`, `after_action`. Evaluate them as written; they are small and total. Everything in `DETERMINISTIC_STEPS` is decided by those functions, never by your judgement. You write only what `LLM_WRITES` names. @@ -49,8 +49,8 @@ Apply `intake_verdict(has_topic, source_count)`: ## 2. Route -Count whole-word, case-insensitive hits of the brief against each `*_WORDS` list. Show the four -counts, then apply `route(content, engineering, research, sales)`. Name the domain and the Bee list +Count the brief's hits against each `*_WORDS` list exactly as `KEYWORD_MATCH` says. Show the four +counts and the words that hit, then apply `route(content, engineering, research, sales)`. Name the domain and the Bee list it allows. Do not override the result; if it looks wrong, the vocabulary in the spec is what needs a PR. @@ -64,14 +64,21 @@ the sources. A claim with no source is not written; it becomes a gap. Print a table of 27 rows, lens-major (`step_lens`, `step_stage`): step, lens, stage, lesson title, source, status. -- For each lesson, `grounded` means at least one source supports it. -- Status from `initial_status(grounded, first_grounded)`. A GAP row says **GAP: needs a source on - ** and nothing else. Never invent filler to make the map look complete. +- For each lesson, `grounded` means at least one source from intake supports it. Sources you find + yourself are not intake; they arrive through the Bee task in step 5 and `ground`. +- A GAP row keeps its lesson title (titles are in `LLM_WRITES`); its source cell is empty. +- Status from `initial_status(grounded, first_grounded)`; `first_grounded` is the grounded lesson + with the lowest step. A GAP row's status cell says **GAP: needs a source on ** and nothing + else. Never invent filler to make the map look complete. - Count grounded lessons per lens and report `map_verdict(...)`. ## 5. One Bee task -Pick the first OPEN lesson. Check `dispatch_allowed(active_bees)`. Choose one capability from the +Apply `bee_target(open_lessons)`. `TARGET_OPEN_LESSON`: take the first OPEN lesson. +`TARGET_FIRST_GAP` (nothing is OPEN, as when intake was `INTAKE_NO_SOURCES`): take the first GAP, the +capability is `GAP_BEE`, and the task is to find a source for it; once one is attached, apply +`ground`. `active_bees` counts the Bees of this journey still working; at the start it is 0. Check +`dispatch_allowed(active_bees)`. Choose one capability from the domain's Bee list and write one task for it: - goal (one sentence), input (the lesson and its sources), output (the artifact), @@ -97,7 +104,9 @@ approved)`. - Publish, send to a client, any payment, anything destructive: ask the user in chat and wait for a clear yes. A yes covers that one action only. -- Publishing or sending needs a VERIFIED item. A draft is never published. +- Publishing needs a VERIFIED item. A draft is never published. Sending to a client also takes a + DRAFT, because a client approval is the only evidence that verifies a sales or content DELIVER + lesson; the draft stays a DRAFT (`after_action`) until that approval comes back. ## 8. Keep the three layers apart diff --git a/specs/t27f/topic_engine.t27 b/specs/t27f/topic_engine.t27 index 3e0a85f0ca..0dac0e6a25 100644 --- a/specs/t27f/topic_engine.t27 +++ b/specs/t27f/topic_engine.t27 @@ -63,7 +63,9 @@ pub const DOMAIN_SALES : u8 = 4; pub const DOMAIN_NAMES : str = "general,content,engineering,research,sales"; /// Keyword vocabularies, comma separated. The host counts how many words of the brief appear in each -/// list (whole words, case-insensitive) and passes the four counts to route(). +/// list and passes the four counts to route(). How a word is matched is KEYWORD_MATCH, not a guess: +/// "List a villa" does not hit "listing", and "sea-view" is the two tokens "sea" and "view". +pub const KEYWORD_MATCH : str = "lowercase the brief; split it on every character that is not a-z or 0-9; a token is a hit when it equals a list word exactly; no stems, no plurals; a word repeated in the brief counts each time"; pub const CONTENT_WORDS : str = "blog,post,campaign,reel,video,channel,audience,content,newsletter"; pub const ENGINEERING_WORDS : str = "fpga,rtl,verilog,compiler,spec,code,bug,test,firmware,api"; pub const RESEARCH_WORDS : str = "paper,research,experiment,dataset,proof,preprint,result,benchmark,gf16"; @@ -80,6 +82,13 @@ pub const SALES_BEES : str = "researcher,copywriter,listing-builder"; pub const MAX_ACTIVE_BEES : u8 = 3; pub const BEES_PER_STEP : u8 = 1; +/// What the one Bee works on. While a lesson is OPEN, that lesson. When none is (every lesson is a +/// GAP, as with a brief that brought no source), the first GAP, and the task is to find a source for +/// it: GAP_BEE, which every *_BEES list above carries, judged by required_evidence(STAGE_LEARN, ...). +pub const TARGET_OPEN_LESSON : u8 = 0; +pub const TARGET_FIRST_GAP : u8 = 1; +pub const GAP_BEE : str = "researcher"; + /// Evidence kinds a Bee or a person can attach to a draft. pub const EVIDENCE_NONE : u8 = 0; pub const EVIDENCE_SOURCE_LINK : u8 = 1; @@ -161,7 +170,8 @@ pub fn map_verdict(grounded_structure: u8, grounded_process: u8, grounded_value: return MAP_PARTIAL; } -/// The status a lesson starts in. Only the first grounded lesson of the map is open. +/// The status a lesson starts in. Only the first grounded lesson of the map is open: the grounded +/// lesson with the lowest step, in the lens-major order of step_lens. pub fn initial_status(grounded: bool, first_grounded: bool) -> u8 { if (grounded == false) { return STATUS_GAP; @@ -201,6 +211,22 @@ pub fn dispatch_allowed(active_bees: u8) -> bool { return active_bees < MAX_ACTIVE_BEES; } +/// Where the one Bee task goes, from how many lessons are OPEN right now. +pub fn bee_target(open_lessons: u8) -> u8 { + if (open_lessons == 0) { + return TARGET_FIRST_GAP; + } + return TARGET_OPEN_LESSON; +} + +/// A GAP that has gained a source starts over as a grounded lesson; every other status stays. +pub fn ground(status: u8, first_grounded: bool) -> u8 { + if (status == STATUS_GAP) { + return initial_status(true, first_grounded); + } + return status; +} + /// The evidence a stage asks for first, per domain. pub fn required_evidence(stage: u8, domain: u8) -> u8 { if (stage == STAGE_LEARN) { @@ -276,8 +302,10 @@ pub fn needs_human_approval(action: u8) -> bool { return action >= ACTION_PUBLISH; } -/// Whether an action may run now. High-impact actions need approval; publishing and sending to a -/// client need a VERIFIED item as well. +/// Whether an action may run now. High-impact actions need approval. Publishing needs a VERIFIED +/// item. Sending to a client takes a VERIFIED item, or a DRAFT sent so the client can approve it: +/// for sales and content, DELIVER is verified only by EVIDENCE_CLIENT_APPROVAL, and a client cannot +/// approve what never reached them. pub fn action_allowed(status: u8, action: u8, approved: bool) -> bool { if (action == ACTION_DRAFT_ONLY) { return status >= STATUS_OPEN && status <= STATUS_PUBLISHED; @@ -288,15 +316,19 @@ pub fn action_allowed(status: u8, action: u8, approved: bool) -> bool { if (approved == false) { return false; } - if (action == ACTION_PUBLISH || action == ACTION_SEND_TO_CLIENT) { + if (action == ACTION_PUBLISH) { return status == STATUS_VERIFIED; } + if (action == ACTION_SEND_TO_CLIENT) { + return status == STATUS_DRAFT || status == STATUS_VERIFIED; + } return action <= ACTION_DESTRUCTIVE; } -/// The status after an action: a VERIFIED item that is published or sent becomes PUBLISHED. +/// The status after an action: a VERIFIED item that is published or sent becomes PUBLISHED. A DRAFT +/// sent for the client's approval stays a DRAFT until that approval comes back as evidence. pub fn after_action(status: u8, action: u8, approved: bool) -> u8 { - if (action_allowed(status, action, approved)) { + if (action_allowed(status, action, approved) && status == STATUS_VERIFIED) { if (action == ACTION_PUBLISH || action == ACTION_SEND_TO_CLIENT) { return STATUS_PUBLISHED; } @@ -353,9 +385,9 @@ test dispatch_is_capped { } test demo_villa_sale_brief { - // "List a sea-view villa for a client and draft the sale post": villa, client, sale, listing -> 4 - // sales hits; post -> 1 content hit - var domain : u8 = route(1, 0, 0, 4); + // "List a sea-view villa for a client and draft the sale post", counted by KEYWORD_MATCH: villa, + // client, sale -> 3 sales hits ("list" is not "listing"); post -> 1 content hit + var domain : u8 = route(1, 0, 0, 3); assert domain == DOMAIN_SALES; assert required_evidence(STAGE_LEARN, domain) == EVIDENCE_SOURCE_LINK; assert required_evidence(STAGE_PRACTICE, domain) == EVIDENCE_REVIEW; @@ -366,6 +398,40 @@ test demo_villa_sale_brief { // sending to the client still needs a human approval at send time assert action_allowed(STATUS_VERIFIED, ACTION_SEND_TO_CLIENT, false) == false; assert after_action(STATUS_VERIFIED, ACTION_SEND_TO_CLIENT, true) == STATUS_PUBLISHED; + // the brief brought no source: all 27 lessons are GAPs, nothing is OPEN, so the one Bee task is + // to find a source for the first GAP, at LEARN + assert intake_verdict(true, 0) == INTAKE_NO_SOURCES; + assert initial_status(false, true) == STATUS_GAP; + assert map_verdict(0, 0, 0) == MAP_NEED_SOURCES; + assert bee_target(0) == TARGET_FIRST_GAP; + assert step_stage(0) == STAGE_LEARN; + assert required_evidence(step_stage(0), domain) == EVIDENCE_SOURCE_LINK; +} + +test client_approves_a_draft_it_was_sent { + // the sale post is a DELIVER draft; only the client's yes verifies it, so it must be sendable + // as a draft, with the user's approval, and stay a draft until that yes comes back + var domain : u8 = DOMAIN_SALES; + assert action_allowed(STATUS_DRAFT, ACTION_SEND_TO_CLIENT, false) == false; + assert action_allowed(STATUS_DRAFT, ACTION_SEND_TO_CLIENT, true); + assert after_action(STATUS_DRAFT, ACTION_SEND_TO_CLIENT, true) == STATUS_DRAFT; + assert verify(STATUS_DRAFT, STAGE_DELIVER, domain, EVIDENCE_CLIENT_APPROVAL) == STATUS_VERIFIED; + // a draft is still never published, and nothing below a draft is sent + assert action_allowed(STATUS_DRAFT, ACTION_PUBLISH, true) == false; + assert action_allowed(STATUS_OPEN, ACTION_SEND_TO_CLIENT, true) == false; + assert action_allowed(STATUS_GAP, ACTION_SEND_TO_CLIENT, true) == false; + assert action_allowed(STATUS_PUBLISHED, ACTION_SEND_TO_CLIENT, true) == false; +} + +test bee_goes_to_the_open_lesson_or_the_first_gap { + assert bee_target(0) == TARGET_FIRST_GAP; + assert bee_target(1) == TARGET_OPEN_LESSON; + assert bee_target(LESSON_COUNT) == TARGET_OPEN_LESSON; + // a gap that gains a source becomes a grounded lesson: OPEN if it is the first, else LOCKED + assert ground(STATUS_GAP, true) == STATUS_OPEN; + assert ground(STATUS_GAP, false) == STATUS_LOCKED; + assert ground(STATUS_DRAFT, true) == STATUS_DRAFT; + assert ground(STATUS_VERIFIED, false) == STATUS_VERIFIED; } test demo_gf16_research_topic {