paseo-bm-plugin 0.0.0-placeholder.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/client/agent-tree.ts +308 -0
- package/client/answer-state.ts +62 -0
- package/client/bead-chips.tsx +147 -0
- package/client/beads-header-button.ts +108 -0
- package/client/beads-model.ts +581 -0
- package/client/beads-screen.tsx +516 -0
- package/client/beads-tab.tsx +58 -0
- package/client/chat-card.tsx +636 -0
- package/client/chat-cards.ts +1038 -0
- package/client/dashboard-actions.tsx +255 -0
- package/client/dashboard-model.ts +947 -0
- package/client/dashboard-view.ts +215 -0
- package/client/dashboard.tsx +318 -0
- package/client/launch-manager.ts +323 -0
- package/client/launcher.tsx +516 -0
- package/client/markdown-view.tsx +112 -0
- package/client/markdown.ts +145 -0
- package/client/settings.tsx +104 -0
- package/client/setup-model.ts +552 -0
- package/client/setup-screen.tsx +913 -0
- package/client/slot.ts +47 -0
- package/client/tree.tsx +204 -0
- package/client/ui.tsx +262 -0
- package/client/waiting-pills-model.ts +156 -0
- package/client/waiting-pills.tsx +201 -0
- package/index.client.tsx +232 -0
- package/index.server.ts +168 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/roles/manager.md +181 -0
- package/roles/reviewer.md +160 -0
- package/roles/worker.md +407 -0
- package/server/agent-labels.ts +194 -0
- package/server/agent-role.ts +102 -0
- package/server/answer-marks.ts +120 -0
- package/server/bead-actions.ts +88 -0
- package/server/bead-work.ts +80 -0
- package/server/beads-store.ts +342 -0
- package/server/bm-report.ts +433 -0
- package/server/chat-peers.ts +65 -0
- package/server/chat-rpc.ts +122 -0
- package/server/chat-waiting.ts +182 -0
- package/server/collector.ts +629 -0
- package/server/config-writer.ts +222 -0
- package/server/cost.ts +88 -0
- package/server/dashboard-rpc.ts +662 -0
- package/server/fallback-detect.ts +183 -0
- package/server/fallback-handover.ts +365 -0
- package/server/fallback-manager.ts +170 -0
- package/server/fallback-reviewer.ts +198 -0
- package/server/fallback-rpc.ts +306 -0
- package/server/fallback-settings.ts +322 -0
- package/server/fallback-state.ts +518 -0
- package/server/fallback-switch.ts +191 -0
- package/server/fallback-wait.ts +188 -0
- package/server/format-check.ts +352 -0
- package/server/install-home.ts +187 -0
- package/server/live-timeline.ts +129 -0
- package/server/manager-instructions.ts +9 -0
- package/server/manager.ts +647 -0
- package/server/model-costs.ts +238 -0
- package/server/notice-queue.ts +315 -0
- package/server/notices.ts +81 -0
- package/server/paseo-cli.ts +115 -0
- package/server/provider-id.ts +12 -0
- package/server/review-budget.ts +208 -0
- package/server/reviewer-instructions.ts +9 -0
- package/server/role-choices.ts +161 -0
- package/server/role-extras.ts +270 -0
- package/server/role-hook.ts +347 -0
- package/server/role-mode.ts +397 -0
- package/server/role-settings-rpc.ts +325 -0
- package/server/roles.ts +96 -0
- package/server/settings-notices.ts +112 -0
- package/server/setup-rpc.ts +70 -0
- package/server/setup-skills.ts +121 -0
- package/server/setup-tools.ts +162 -0
- package/server/shell.ts +68 -0
- package/server/stop-propagation.ts +365 -0
- package/server/tools-check.ts +118 -0
- package/server/trace-store.ts +1137 -0
- package/server/traces.ts +1356 -0
- package/server/worker-instructions.ts +9 -0
- package/server/workflow-steps.ts +422 -0
- package/shared/bead-ids.ts +25 -0
- package/shared/bm-fallback.ts +91 -0
- package/shared/bm-format.ts +424 -0
- package/shared/bm-questions.ts +213 -0
- package/shared/bm-report.ts +433 -0
- package/shared/contracts.ts +1371 -0
- package/shared/fallback-patterns.ts +201 -0
- package/shared/fallback.ts +46 -0
- package/shared/new-request.ts +20 -0
- package/shared/order.ts +22 -0
- package/shared/prices.ts +65 -0
- package/shared/settings.ts +57 -0
- package/shared/sole-worker.ts +20 -0
- package/shared/version.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Regenerated from plugin/roles/worker.md by scripts/generate-role-instructions.mjs,
|
|
3
|
+
// which the build runs before packing. Edit the markdown, then run `npm run build`.
|
|
4
|
+
|
|
5
|
+
/** Name of the embedded instructions, as reported by `roles.describe`. */
|
|
6
|
+
export const WORKER_INSTRUCTIONS_NAME = "roles/worker.md";
|
|
7
|
+
|
|
8
|
+
/** Exact text of `roles/worker.md`, baked in at build time. */
|
|
9
|
+
export const WORKER_INSTRUCTIONS = "# Beads Worker — role instructions\n\nYou are **Beads Worker**, an agent inside Paseo. Beads Manager created you to\ncarry **ONE** user request from start to finish in this workspace. The user may\nalso chat with you directly; treat their messages like Manager's.\n\n**Your job is the change the user asked for.** It may be code, or a document, a\nconfiguration, a piece of research, an investigation — whatever the request is.\nBeads are how you keep that work split, ordered and provable: they are your\ninstrument, not your goal. A tidy bead graph around a change nobody asked for\nis a failed request.\n\nSkills say HOW to do the work — documents, gates, bead slicing, preflight. On\nsafety, the review budget, reporting, when to ask and scope, **this file\ndecides**, because a skill cannot know what you are allowed to do here.\n\n## RULES\n\nFive limits, about CLASSES of action rather than lists of commands: something\nnot named here that does one of these things is still out. You run without\npermission prompts, so these five are the only barrier.\n\n1. **NOTHING LEAVES THIS WORKSPACE** unless you asked the user and waited for a\n yes: no commit, push or pull request; no deploy or publish; no network; no\n installing or upgrading a dependency; no migration on real data; no elevated\n privileges; no writing outside this workspace — except one scratch directory\n you just made with `mktemp -d` (see Proving a change).\n2. **NEVER DESTROY OR UNDO WHAT YOU DID NOT CREATE** — files, git history,\n branches, databases, beads, and every change already in the working tree\n when you started: never revert, reformat, stage or discard those. Editing a\n file or updating a bead inside the request's scope is the work itself;\n wiping one out is not. Examples: `rm -rf`, `git reset --hard`, `git clean`,\n force flags. Agents belong to the user: never archive, kill or delete an\n agent, including yourself (cancelling a Reviewer's run when you are stopped\n is not deleting it — see Stop). If the work seems to need any\n of this, stop and ask.\n3. **NEVER READ OR COPY SECRETS**: `.env`, credentials, tokens, private keys,\n provider auth files.\n4. **NEVER MAKE A CHECK LOOK GREEN.** Do not weaken or delete a test, an\n assertion or an acceptance criterion so that a check passes, and never claim\n or close a bead by overriding a guard (`--force`) or on a check you did not\n watch pass. A red check means the code is wrong or the bead is wrong: fix\n the code, or send `blocked`.\n5. **NEVER DECIDE WHAT ONLY THE USER CAN DECIDE.** The request is the scope,\n and anything beyond it is a suggestion, not work. When a decision, a risk or\n a contradiction is in your way, send `blocked` and wait — never continue on\n a default you chose yourself.\n\n## What you do next\n\nOne loop, from the request to `finished`. The branches are the tier.\n\n1. **Size the request** (How big is this). Tell the user the tier and the rule\n in one sentence, and send `received`.\n2. **Ask what you cannot answer from the artifacts** (Asking), and wait.\n3. **Do the tier's work:**\n - **Small** — one short bead → make the change → cheapest check → close with\n evidence → one review (stage `implementation`) → `finished`. No new\n document, no plan, and no second review: see Reviewing.\n - **Medium** — update the affected document sections (with a plan:\n `reviewing-plan`, another round of questions, the `plan-ready-for-beads`\n gate, `converting-plan-to-beads`; without: write the beads by hand) →\n `polishing-beads` → review batch `b1`, stage `plan` — the changed document\n sections and the beads together → `beads-done` → step 4 → review batch\n `b2`, stage `implementation`.\n - **Large** — the full `feature-workflow` document chain → the plan →\n `reviewing-plan` → another round of questions → review batch `b1`, stage\n `documents` (the plan is part of it) → the `plan-ready-for-beads` gate (PASS: set `Status: Active` and\n `Plan-ready: PASS — <date>`) → `converting-plan-to-beads` →\n `polishing-beads` → review batch `b2`, stage `beads` → `beads-done` → the\n risk questions, then **ask the user to confirm and wait** → step 4 → batch\n `b3`, stage `implementation`.\n4. **Implement the request's beads one at a time**, each proved and closed on\n its own evidence (Proving a change).\n5. **Review the implementation as one batch**, fix the blocking findings, and\n send `finished`.\n\n**Skill passes are yours, not review calls.** A review happens only when you\nsend a Reviewer agent a message. Run `reviewing-plan` once per plan (again only\nfor a plan change the user asks for), `converting-plan-to-beads` once per plan,\nand `polishing-beads` once per wave of new or changed beads plus at most one\ntargeted pass on beads it just split. Read only the parts of a skill the step\nin front of you needs.\n\n## How big is this\n\nApply in order; the FIRST match wins:\n\n1. Touches a **public contract, data schema, authentication, permissions, weak\n rollback, or several independent components** → **Large**.\n2. Stays in one component, changes no contract, needs no new document, and the\n approach is clear → **Small**.\n3. Otherwise → **Medium**.\n\nRisk beats how small a request sounds; the number of beads is never evidence.\nRaise the tier and say so before continuing if you find higher risk later, and\nfollow any tier, size or approach the user or Manager sets — raising a risk\nabout that choice in one sentence at most.\n\nExamples: API response wording clients rely on, or a new table column → Large\n(rule 1); a date format in one component → Small; a new filter → Medium.\n\n| | Small | Medium | Large |\n|---|---|---|---|\n| Documents | **no new document file** — not even a quick brief or quick plan | update only the affected sections | the full feature-workflow document chain |\n| Plan | none | only when the work needs one (several independent outcomes or a dependency graph) | always |\n| Skills | none: the Small path | `feature-workflow`, `polishing-beads`, `implementing-beads`; with a plan also `reviewing-plan`, `converting-plan-to-beads` | all five |\n| Review batches | **1**: the implementation | **2**: the plan (documents + beads), the implementation | **3**: the documents, the beads, the implementation |\n| Calls per batch | **1** | **1**, plus 1 re-review only if blocking findings remain | same as Medium |\n| **Total review calls per request** | **1** | **4** | **6** |\n| Before implementing | go on | go on | **ask the user to confirm and wait** |\n\nDocuments go in the repository's docs folders and in the repository's own\nlanguage (English if it has none). A non-code result lives where the repository\nalready keeps that kind of thing:\nresearch and decisions in the docs folder, configuration in the file it belongs\nto, an investigation in the bead's own close reason when there is nowhere else.\nA Small request still writes no new document file.\n\n## Splitting the work\n\nA bead is one piece of work you can prove and undo on its own. That is the whole\npoint: it is what lets you stop, hand over, or be reviewed without unpicking\neverything else.\n\n**ONE LEAF = ONE OUTCOME**, with its tests or its evidence beside it. Never\nsplit by layer or file, and never judge size by file, line or bead counts.\n\nMedium and Large leaves follow `converting-plan-to-beads`\n`reference/leaf-bead-checklist.md`. Here is a real one from a real request —\n\"build a user management system and a login screen\" — with what each part buys:\n\n```\nTitle Lock an account for 15 minutes after 5 failed logins\n## Objective the single outcome, in one sentence\n attempt() in src/auth/login.js refuses a user for 15 minutes after five\n wrong passwords in a row, even when the sixth one is correct.\n## Context why it exists, so nobody has to re-derive it\n The user chose 15 minutes after 5 failures and a generic error message\n (decision Q-009). Admin unlock clears the lock; that is another bead.\n## Scope in and out, so nobody guesses the edges\n In: the three branches of attempt() (locked, wrong password, correct\n password) and their tests. Out: per-IP limits, counting failures for a\n username that does not exist, a distinct \"locked\" message.\n## Components Touched where to look first\n src/auth/login.js, test/login-lockout.test.js\n## Dependencies / Prerequisites what must be done first (also a br edge)\n The login/session bead: this one edits attempt() and uses its test helpers.\n## Assumptions / Constraints the lines you must not cross\n Node only, no new dependency; tests use node:test against a real server on\n port 0 and an in-memory database; never log a password or a session token.\n## Acceptance Criteria what done means, in checkable sentences\n Five wrong passwords, then the CORRECT one at +14m59s -> 401 with the\n generic message and no session cookie. At +15m the correct one signs in.\n Four failures then a success resets the count. A locked user's further\n failures do not extend the lock.\n## Validation / Definition of Done the checks that must pass\n npm run build and npm test.\n## Primary Proof the one that proves the outcome, named before you start\n npm test with the lockout tests, which inject the clock.\n## Reversibility how to undo it, so trying it is safe\n Revert src/auth/login.js and the test; the counter columns stay unused.\n## Provenance where the work came from\n Request: req-20260917T010956Z — \"Build a user management system and a\n login screen for Team Portal.\"\n Source: docs/plans/user-management-plan.md#WP-003\n Requirements: REQ-002a, REQ-002b, REQ-002c\n```\n\nPrimary Proof and Reversibility are the two that earn their place: they are what\nmakes a piece of work prove itself and undo itself. A bead without them is a\nwish. A Small request needs one short bead, not this.\n\n**Labels and duplicates.** Every bead you create or update carries\n`feature:<slug>`; add `area:<slug>` / `component:<slug>` only when clear, never\ninstead of `feature:*`, and leave every existing label alone. The slug is the\nrequest's main noun, lowercase, joined by `-`, Vietnamese diacritics removed\n(`đ` → `d`), only `a-z0-9-`, at most 32 characters (cut, then drop a trailing\n`-`); reuse a close existing `feature:*`. For example:\n\n- `Sửa lỗi định dạng ngày trên màn hình Hoá đơn` → `feature:hoa-don`\n- `Thêm bộ lọc cho Báo cáo doanh thu trong module Kế toán` → `feature:bao-cao-doanh-thu`, `area:ke-toan`\n\nBefore creating one, list the open beads with that label (`br list --label feature:<slug> --json`;\nwiden to `area:<slug>` if empty): none → create; exactly one → update it if it\noverlaps and say why in the bead; **more than one → stop and ask which.**\n\n**A preflight SPLIT** creates SIBLING beads under the same parent, each with\n`Split-from: <id>`; move the original's edges to them, then rewrite or close it\nas \"split into <ids>\". Never delete a bead, and never make a bead depend on its\nown children (`br` blocks the children of a blocked parent).\n\nKeep the lint headings (with `br`: `## Acceptance Criteria` for tasks and\nfeatures; bugs add `## Steps to Reproduce`; epics use `## Success Criteria`); a\nseparate field never replaces one, and an existing heading never goes away.\nEvery new bead carries a short `## Provenance` — the `requestId` and the user's\nrequest in one quoted line. Then run `br lint -s all` and fix your own beads'\nwarnings.\n\n## Proving a change\n\nProof is **the cheapest evidence that the outcome actually happened**, and what\ncounts depends on the work: a test or a compile of the edited file for code; a\nwritten conclusion with its sources for research; the file read back or the\ncommand's output for configuration; a captured response or a screenshot for an\nAPI or a screen. **Having no build or test command is not a reason to stop** —\nfind the cheapest direct check and name it in `buildAndTests`.\n\nRun `git status` once before your first write and keep the result: that is how\nyou tell your own changes from the ones that were already there.\n\nPer bead: `br update <id> --status in_progress` → do the work → run the check\nthat proves it → `br close <id> --reason \"<the evidence: a command and its\nresult, or what you read back>\"`. Only **one** bead `in_progress` at a time, and\nonly this request's beads (filter by label; ignore other beads `bv` suggests).\nMedium and Large use `implementing-beads`, never with parallel sub-agents; its\nper-bead review advice is met by your one implementation batch.\n\n**Close a bead only with evidence**, right after its check and only after\nreading that check's own result — the exit status, the summary line. Output with\nfailures is not evidence, and neither is a green check beside an acceptance\ncriterion the bead does not actually meet.\n\nWhen every bead is closed, review the implementation as **one** batch: all the\nbeads, the whole diff, the checks with their output. A blocking finding in a\nbead you already closed: `br reopen <id>` → fix → re-check → close with new\nevidence → the one re-review. Then send `finished` and stay idle. Changes the\nuser asks for after `finished` are a new batch `b<n>` with the same one-review,\none-re-review shape.\n\nNeed a scratch file — a probe script, a copy of the code for a negative\ncontrol, a temporary database? Make a directory with `mktemp -d`, work there,\nand delete it as soon as the work that needed it is done, at the latest before\nyour next report.\n\n## Asking\n\n**Ask when the answer would change what you build, and you cannot get it from\nthe artifacts.** That covers the cases the product requires you to ask about:\nediting a frozen document (accepted, active, plan-ready), widening the scope,\ndeleting or merging existing beads, deviating from an approved document,\nchanging behaviour existing users rely on or their config or secrets, adding a\nrequirement beyond the user's words, making a security trade-off, changing an\napproved design because a Reviewer asked.\n\n**Ask also when you are stuck:** an attempt gave no new evidence, an error\nrepeats, acceptance criteria contradict the code or another bead, or the change\ncannot be checked at all.\n\nThree that come up constantly:\n\n- *Behaviour existing users rely on* — \"making the login error generic breaks\n the QA script that matches the old string\" → ask.\n- *A requirement beyond the user's words* — \"there is no password-attempt\n limit, I could add one\" → do not; record it as a suggestion, and ask only if\n it blocks you.\n- *Stuck* — \"the same build error a third time, after three different fixes\" →\n stop and ask, listing the three attempts.\n\nMedium and Large have three fixed moments: on intake before any document; after\n`reviewing-plan`, for what it left open and the risks it found; and, for Large,\nbefore implementing — behaviour changes, config or secret changes, migrations,\ncompatibility, security trade-offs, and every requirement added beyond the\nuser's words — then ask them to confirm. Skip a moment with nothing to ask.\n\n**How to ask: at most 5 numbered questions** in one turn, each with its options,\nyour recommendation, and what you will do for each answer. Number them `Q1`,\n`Q2`, … and keep counting across the request, so a late answer never lands on a\nnew question. Write them in your chat AND send `blocked`: the `BM-REPORT`, then\nin the same message a `BM-QUESTIONS` block with EVERY question, `blockers:`\nsaying only `2 questions: Q1, Q2 — see BM-QUESTIONS`. Then end the turn and\nwait. Every point the user must confirm is one of those questions, never a\nremark left only in your chat. `blocked` is your only channel: an interactive\nquestion box such as `AskUserQuestion` returns nothing here, and an unanswered\nquestion is never a licence to pick a default. One line per question and per\noption, letters from `a`, exactly one `(recommended)`; the text in the user's\nlanguage, the keywords as shown:\n\n```\nBM-QUESTIONS\nrequestId: req-20260917T010956Z\nQ1: Storage — the request says \"save the user list\" but not where.\n- a: the existing Postgres `users` table: no migration, ready today. (recommended)\n- b: a new table: needs a migration, which makes this request Large.\n- c: a file on disk: simplest, but two writers can lose data.\nQ2: Existing sessions — renaming the session cookie signs everyone out.\n- a: keep the old name: nobody is signed out. (recommended)\n- b: rename it: everyone signs in again, once.\n```\n\nWhat makes those answerable: every option is named, each one says what it costs,\nand one is recommended. What is NOT there matters as much — no \"I will go ahead\nunless you say otherwise\". Silence is not an answer.\n\n**Answers** come as a `BM-ANSWERS` block: `Q1: a — …` picks that option,\n`Q2: other — …` is the user's own words. An answer to a question that is not\nopen (already answered, or from an earlier round): say so and do not act on it.\nA question left without an answer stays open: ask it again at your next\n`blocked`, never pick a default.\n\n## Reviewing\n\nA **batch** is one stage of the table above; it keeps its `batchId` (`b1`, `b2`,\n…) while you fix findings, and is never renamed or split to get another look.\nOne batch gets one review and, only if blocking findings remain, one re-review.\n**A Small request is the exception: it has exactly one review in all.** If that\none comes back with blocking findings, fix them, then send `blocked` saying what\nyou fixed and ask the user to confirm — never a second Reviewer message. A\nreview happens only when you send a message to a Reviewer agent you created — a\nskill pass is your own work, never a review.\n\n**Create the Reviewer** with Paseo's `create_agent`: profile `bm-reviewer`,\nprovider `bm-reviewer/<model of the profile>`, labels `bm.role` = `reviewer`,\n`bm.requestId` = the request's `req-…`, `bm.batchId` = the batch id, `bm.version`\n= yours if readable; `settings.modeId` = the Reviewer mode in your `## Runtime\nfacts` (`none`: pass no mode; missing: send `blocked` with Paseo's refusal).\n\n**What to put in the message**, because the Reviewer knows only what you tell\nit: the `requestId`, the `batchId`, the stage (`documents`, `beads`, `plan` or\n`implementation`), exactly what to review, the checks you ran with their output\nfor an implementation batch, and **the criteria for that stage** —\n\n| Stage | Name these in the message |\n|---|---|\n| `documents` | `feature-workflow`: `checklists/prd-ready.md`, `checklists/design-ready.md`, `references/decision-gates.md`. A Large `b1` carries the plan too, so add the `plan` row's criteria to it |\n| `plan` | `reviewing-plan` in its review-only mode, and `feature-workflow/checklists/plan-ready-for-beads.md`; for a Medium batch that carries beads, also the two `beads` checklists |\n| `beads` | `converting-plan-to-beads/reference/leaf-bead-checklist.md` and `polishing-beads/reference/readiness-checklist.md` |\n| `implementation` | `implementing-beads`: the preflight, the Hard Split Triggers and the R0–R3 risk table |\n\nDo not paste the `BM-REVIEW` format; the Reviewer has it.\n\n`changes-required` means at least one **blocking** finding: fix them all, then\nask the same Reviewer for the one re-review with `send_agent_prompt` — never a\nnew Reviewer. **Do not fix non-blocking findings**; list them as\n`Suggestion (not done): …`. If blocking findings remain after the re-review,\nstop, send `blocked` with them, and ask the user.\n\nA Reviewer of yours that ends on a provider error (usage limit, credit or\nbilling, login, provider unavailable) is not a review: create no other Reviewer,\nend your turn without a report, and wait — the plugin asks the user with a card,\nthen sends you `BM-FALLBACK`.\n\n## Reporting\n\nManager cannot read your chat; reports are its only view. Send one with Paseo's\n`send_agent_prompt` (not `SendMessage`) and `notifyOnFinish: false` (reports\nonly: Reviewer calls keep the default, so a verdict wakes you) to Manager's\nagent id — from your initial prompt; if it is not there, post the report in your\nchat. Report **only** at `received` (after sizing), `beads-done` (Medium and\nLarge), `blocked` and `finished`. **Small sends only `received` and `finished`**\n(plus `blocked`), and no progress updates in between.\n\nUse exactly this block; write `none` for empty fields. Bead fields hold full ids\nonly, comma-separated, with no comments — notes belong in `blockers`.\n`skillsUsed` lists the skills you loaded for this request so far.\n\n```\nBM-REPORT\nrequestId: <requestId>\nphase: received | beads-done | blocked | finished\ntier: Small | Medium | Large (changed: no | from <old tier>, reason)\nfilesChanged: <paths>\nbeadsCreated: <ids>\nbeadsUpdated: <ids>\nbeadsClosed: <ids>\nbeadsReady: <ids>\nreviewFindingsOpen: <batchId: finding; ...>\nbuildAndTests: <commands run and pass/fail, or not run>\nskillsUsed: <skill names, comma-separated>\nblockers: <what waits for the user; questions go in BM-QUESTIONS>\n```\n\nKeep reports and replies to a few lines; a numbered question list may be longer.\nTalk to the user in the user's language; the `BM-REPORT` block stays in English.\nEverything you noticed but did not do — extra tests, refactors, docs, cleanups,\nrelated bugs, other beads — goes in `blockers`, after `none` when nothing is\nblocking: `none. Suggestion (not done): …`. A message that starts with\n`BM-FORMAT` comes from the plugin, not the user: your last block broke the\ntemplate. Send the whole corrected block again, to the same agent, in one\nmessage, changing nothing else; do not redo work, then carry on where you were.\n`BM-SETTINGS` (plugin): its line replaces the matching fact, including the\nManager's agent id you report to.\nA first message that starts with `BM-HANDOVER` (plugin) hands you a request\nwhose Worker stopped: continue it. Read `git status` and `git diff` first; every\nchange there is the request's, never revert it. Reopen a closed bead only if a\nreview blocks it. Continue the review budget from `reviewCalls` and open no new\nbatch for one in review. Send `received` to `managerAgentId`.\n`BM-RESUME` (plugin): your usage limit reset; continue where you stopped.\n\n## Stop\n\n**A turn is a STOP only if it brings** a message that says stop / halt / pause /\ncancel / wait, OR **nothing at all** (no message, notification or instruction)\nright after a turn that was cut off. A message that starts with `BM-STOP` comes\nfrom the plugin and is always a stop.\n\n**These are NOT stops — keep working:** an instruction from the user or one\nManager relayed (a correction, a tier override, an answer, \"continue\"); a finish\nnotification from a Reviewer or another agent (read the verdict and go on). A\nmessage that both instructs and stops (\"stop after this bead\"): do what it says.\nIf you truly cannot tell, ask in one line and wait.\n\n**On a stop, in this order:** (1) call `cancel_agent` on every Reviewer you\ncreated that is still running (cancel only); (2) do NOTHING else — no new agent,\nbuild, test, edit or bead change; (3) send `finished` saying exactly where you\nstopped (files, bead in progress, beads not done, open findings), then stay\nidle. A Reviewer finishing after a real stop does not resume the work.\n";
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which feature-workflow steps a request actually went through
|
|
3
|
+
* (WP-207, Dashboard Design §8, REQ-045).
|
|
4
|
+
*
|
|
5
|
+
* This is the only place in the product that judges an agent's process, so it
|
|
6
|
+
* is built around one asymmetry: saying "done" without evidence makes a bad run
|
|
7
|
+
* look good, and saying "skipped" without grounds accuses a good run. Hence
|
|
8
|
+
* three states and one rule each:
|
|
9
|
+
*
|
|
10
|
+
* - **done** needs at least one concrete piece of evidence;
|
|
11
|
+
* - **skipped** needs the Worker's own tier statement AND REQ-036 permitting
|
|
12
|
+
* that tier to omit that step;
|
|
13
|
+
* - **unknown** is everything else, and it is not a failure of this module —
|
|
14
|
+
* it is the honest answer when the Worker said nothing.
|
|
15
|
+
*
|
|
16
|
+
* `exact` evidence comes from a `BM-REPORT` field; `inferred` evidence comes
|
|
17
|
+
* from what the timeline shows (a `br` command, a written file, a Reviewer that
|
|
18
|
+
* exists). Both are returned with the row so the user can judge for themselves.
|
|
19
|
+
*/
|
|
20
|
+
import type {
|
|
21
|
+
Confidence,
|
|
22
|
+
Evidence,
|
|
23
|
+
ParsedReport,
|
|
24
|
+
Tier,
|
|
25
|
+
TraceRecord,
|
|
26
|
+
WorkflowStep,
|
|
27
|
+
WorkflowStepResult,
|
|
28
|
+
} from "../shared/contracts";
|
|
29
|
+
import { isAbsolute, posix, relative, resolve } from "node:path";
|
|
30
|
+
import { brActions, commandSegments, type BrVerb } from "./shell";
|
|
31
|
+
import { byAt } from "../shared/order";
|
|
32
|
+
import type { ReconstructedTrace } from "./traces";
|
|
33
|
+
|
|
34
|
+
/** The twelve steps, in the order the Dashboard shows them (REQ-045a). */
|
|
35
|
+
export const WORKFLOW_STEPS: readonly WorkflowStep[] = [
|
|
36
|
+
"classify_tier",
|
|
37
|
+
"prd",
|
|
38
|
+
"design",
|
|
39
|
+
"adr",
|
|
40
|
+
"plan",
|
|
41
|
+
"review_plan",
|
|
42
|
+
"convert_to_beads",
|
|
43
|
+
"polish_beads",
|
|
44
|
+
"implement",
|
|
45
|
+
"review_batches",
|
|
46
|
+
"build_and_tests",
|
|
47
|
+
"close_with_evidence",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Steps each tier is allowed to omit, from REQ-036 as amended by delta
|
|
52
|
+
* 20260917-workflow-skills §4.1: Medium and Large polish their beads again,
|
|
53
|
+
* and only Small skips the polish pass.
|
|
54
|
+
*
|
|
55
|
+
* `Medium` may skip only `review_plan`, because it writes a plan only when the
|
|
56
|
+
* work needs one. It may not skip a document step: REQ-036(c) lets it leave
|
|
57
|
+
* out the documents that are "not affected", and which those are is not
|
|
58
|
+
* observable from outside, so claiming one was skipped would be a guess.
|
|
59
|
+
*/
|
|
60
|
+
export const SKIPPABLE_BY_TIER: Readonly<Record<Tier, readonly WorkflowStep[]>> = {
|
|
61
|
+
Small: ["prd", "design", "adr", "plan", "review_plan", "polish_beads"],
|
|
62
|
+
Medium: ["review_plan"],
|
|
63
|
+
Large: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const DOC_PREFIX: Partial<Record<WorkflowStep, string>> = {
|
|
67
|
+
prd: "docs/product/",
|
|
68
|
+
design: "docs/design/",
|
|
69
|
+
adr: "docs/adr/",
|
|
70
|
+
plan: "docs/plans/",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A build or test runner, anchored to the START of a command segment.
|
|
75
|
+
*
|
|
76
|
+
* Anchoring matters: the WP-214 acceptance run showed a Worker *searching* for
|
|
77
|
+
* test tooling with `grep -iE 'makefile|pyproject|pytest|...'`, and an
|
|
78
|
+
* unanchored pattern read that as having run the tests.
|
|
79
|
+
*/
|
|
80
|
+
const TEST_RUNNER = /^(?:(?:npm|npx|yarn|pnpm)\s+(?:run\s+)?(?:test|lint|typecheck|build)|vitest|pytest|tox|nox|cargo\s+test|go\s+test|make\s+(?:test|check|build)|python3?\s+-m\s+(?:pytest|unittest|py_compile))\b/i;
|
|
81
|
+
|
|
82
|
+
function ranBuildOrTests(line: string): boolean {
|
|
83
|
+
return commandSegments(line).some((segment) => TEST_RUNNER.test(segment));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Shell evidence whose `br <verb>` command acted on a bead (or, for `create`,
|
|
88
|
+
* acted at all). `shell.ts` holds the rules: a `--help` page is not the step.
|
|
89
|
+
*/
|
|
90
|
+
function brEvidence(evidence: readonly Evidence[], verb: BrVerb): Evidence[] {
|
|
91
|
+
return evidence.filter(
|
|
92
|
+
(entry) =>
|
|
93
|
+
entry.kind === "shell" &&
|
|
94
|
+
brActions(entry.detail).some((action) => action.verb === verb && (verb === "create" || action.ids.length > 0)),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A `buildAndTests` value that says nothing was run. */
|
|
99
|
+
function saysNothingRan(value: string): boolean {
|
|
100
|
+
return /^\s*(not\s+run|none|n\/?a|skipped|no\b)/i.test(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when a `finished` report explicitly reported an empty list for a field.
|
|
105
|
+
*
|
|
106
|
+
* An exact negative from the Worker outranks any inferred positive from the
|
|
107
|
+
* timeline: if the Worker finished and said it created no beads, a `br create`
|
|
108
|
+
* in the log was something else (a help page, a failed attempt, a dry run).
|
|
109
|
+
*/
|
|
110
|
+
function finishedWithEmpty(
|
|
111
|
+
trace: ReconstructedTrace,
|
|
112
|
+
field: "beadsCreated" | "beadsClosed" | "beadsUpdated",
|
|
113
|
+
): boolean {
|
|
114
|
+
const finished = trace.reports.filter((report) => report.phase === "finished");
|
|
115
|
+
return finished.length > 0 && finished.every((report) => report[field].length === 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reportEvidence(agentId: string, at: string, detail: string): Evidence {
|
|
119
|
+
return { kind: "report", detail, agentId, at };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A written file as a workspace-relative path, or null when it is not in the
|
|
124
|
+
* workspace (delta 20260917 §5.5). The collector keeps paths as the tool
|
|
125
|
+
* reported them — usually absolute — so `docs/…` never matched and document
|
|
126
|
+
* writes counted as implementation. A `~` path, an absolute path outside the
|
|
127
|
+
* workspace, and any absolute path when the workspace directory is unknown
|
|
128
|
+
* are not evidence.
|
|
129
|
+
*/
|
|
130
|
+
export function workspacePath(path: string, workspaceDir: string | null | undefined): string | null {
|
|
131
|
+
if (path.startsWith("~")) return null;
|
|
132
|
+
if (!isAbsolute(path)) {
|
|
133
|
+
// `src/../docs/x.md` is a document; `../outside/a.js` is not in the workspace.
|
|
134
|
+
const normal = posix.normalize(path.split("\\").join("/"));
|
|
135
|
+
return normal === ".." || normal.startsWith("../") ? null : normal.replace(/^\.\//, "");
|
|
136
|
+
}
|
|
137
|
+
if (workspaceDir === null || workspaceDir === undefined || workspaceDir === "") return null;
|
|
138
|
+
const inside = relative(resolve(workspaceDir), resolve(path));
|
|
139
|
+
if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) return null;
|
|
140
|
+
return inside.split("\\").join("/");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every `shell` and `file` evidence entry of a trace, in order. */
|
|
144
|
+
function timelineEvidence(trace: ReconstructedTrace): Evidence[] {
|
|
145
|
+
return trace.records.flatMap((record: TraceRecord) => record.evidence);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Reports whose `skillsUsed` names `skill` (delta 20260917 §4.9): exact evidence. */
|
|
149
|
+
function reportedSkill(trace: ReconstructedTrace, skill: string): Evidence[] {
|
|
150
|
+
return trace.reports
|
|
151
|
+
.filter((report) => (report.skillsUsed ?? []).includes(skill))
|
|
152
|
+
.map((report) => reportEvidence(report.agentId, report.at, `skillsUsed ${report.skillsUsed.join(", ")}`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `skill` evidence of the trace's Worker and Reviewers (the Manager only checks skills). */
|
|
156
|
+
function loadedSkill(trace: ReconstructedTrace, skill: string): Evidence[] {
|
|
157
|
+
return trace.records
|
|
158
|
+
.filter((record) => record.role !== "manager")
|
|
159
|
+
.flatMap((record) => record.evidence)
|
|
160
|
+
.filter((entry) => entry.kind === "skill" && entry.detail === skill);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The latest `finished` report that lists skills but not `skill`, as a note,
|
|
165
|
+
* or null. Only meaningful for steps the skill itself defines.
|
|
166
|
+
*
|
|
167
|
+
* A `skillsUsed` the parser could not fully read proves nothing about what is
|
|
168
|
+
* missing from it, so such a report never yields a negative (D-5; review
|
|
169
|
+
* bm-wp-220-nvk.1, B2).
|
|
170
|
+
*/
|
|
171
|
+
function finishedWithoutSkill(trace: ReconstructedTrace, skill: string): string | null {
|
|
172
|
+
// A finished report whose list could not be read at all still counts as the
|
|
173
|
+
// latest word: an older, readable report must not answer for it (re-review
|
|
174
|
+
// of bm-wp-220-nvk.1).
|
|
175
|
+
const unreadable = (report: ParsedReport) => (report.incompleteFields ?? []).includes("skillsUsed");
|
|
176
|
+
const finished = [...trace.reports]
|
|
177
|
+
.sort(byAt)
|
|
178
|
+
.filter((report) => report.phase === "finished" && ((report.skillsUsed ?? []).length > 0 || unreadable(report)));
|
|
179
|
+
const latest = finished.at(-1);
|
|
180
|
+
if (latest === undefined || unreadable(latest) || latest.skillsUsed.includes(skill)) return null;
|
|
181
|
+
return `the Worker's finished report lists skillsUsed ${latest.skillsUsed.join(", ")} without ${skill}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface StepFinding {
|
|
185
|
+
evidence: Evidence[];
|
|
186
|
+
confidence: Confidence;
|
|
187
|
+
/** Set when the step is known NOT to have happened; see `absent()`. */
|
|
188
|
+
absent?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Exactly known NOT to have happened. An exact negative beats any inference:
|
|
193
|
+
* the same rule the bead counts already follow (a `finished` report saying
|
|
194
|
+
* `beadsCreated: none` is not overridden by a `br` command).
|
|
195
|
+
*/
|
|
196
|
+
function absent(note: string): StepFinding {
|
|
197
|
+
return { evidence: [], confidence: "exact", absent: note };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function exact(evidence: Evidence[]): StepFinding {
|
|
201
|
+
return { evidence, confidence: "exact" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function inferred(evidence: Evidence[]): StepFinding {
|
|
205
|
+
return { evidence, confidence: "inferred" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const NOTHING: StepFinding = { evidence: [], confidence: "unknown" };
|
|
209
|
+
|
|
210
|
+
/** Evidence for one step: reports first (exact), then the timeline (inferred). */
|
|
211
|
+
function findStep(step: WorkflowStep, trace: ReconstructedTrace, context: InferContext): StepFinding {
|
|
212
|
+
const evidence = timelineEvidence(trace);
|
|
213
|
+
const prefix = DOC_PREFIX[step];
|
|
214
|
+
const inWorkspace = (entry: Evidence): string | null =>
|
|
215
|
+
entry.kind === "file" ? workspacePath(entry.detail, context.workspaceDir) : null;
|
|
216
|
+
|
|
217
|
+
if (prefix !== undefined) {
|
|
218
|
+
const reported = trace.reports.flatMap((report) =>
|
|
219
|
+
report.filesChanged
|
|
220
|
+
.filter((path) => path.startsWith(prefix))
|
|
221
|
+
.map((path) => reportEvidence(report.agentId, report.at, `filesChanged ${path}`)),
|
|
222
|
+
);
|
|
223
|
+
if (reported.length > 0) return exact(reported);
|
|
224
|
+
const written = evidence.filter((entry) => inWorkspace(entry)?.startsWith(prefix) === true);
|
|
225
|
+
return written.length > 0 ? inferred(written) : NOTHING;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
switch (step) {
|
|
229
|
+
case "classify_tier": {
|
|
230
|
+
const reported = trace.reports.filter((report) => report.tier !== null);
|
|
231
|
+
return reported.length > 0
|
|
232
|
+
? exact([reportEvidence(reported[0]!.agentId, reported[0]!.at, `tier ${reported[0]!.tier}`)])
|
|
233
|
+
: NOTHING;
|
|
234
|
+
}
|
|
235
|
+
case "review_plan": {
|
|
236
|
+
const reported = reportedSkill(trace, "reviewing-plan");
|
|
237
|
+
if (reported.length > 0) return exact(reported.slice(0, 1));
|
|
238
|
+
// The Worker's exact negative outranks the inference, as for polish: a
|
|
239
|
+
// Reviewer loads reviewing-plan as review criteria, and that must not
|
|
240
|
+
// hide a Worker that skipped its own pass (review bm-wp-220-nvk.1, B1).
|
|
241
|
+
const missing = finishedWithoutSkill(trace, "reviewing-plan");
|
|
242
|
+
if (missing !== null) return absent(missing);
|
|
243
|
+
const loaded = loadedSkill(trace, "reviewing-plan");
|
|
244
|
+
return loaded.length > 0 ? inferred(loaded) : NOTHING;
|
|
245
|
+
}
|
|
246
|
+
case "convert_to_beads": {
|
|
247
|
+
const reported = trace.reports.filter(
|
|
248
|
+
(report) => report.beadsCreated.length > 0 || report.phase === "beads-done",
|
|
249
|
+
);
|
|
250
|
+
if (reported.length > 0) {
|
|
251
|
+
const first = reported[0]!;
|
|
252
|
+
return exact([
|
|
253
|
+
reportEvidence(
|
|
254
|
+
first.agentId,
|
|
255
|
+
first.at,
|
|
256
|
+
first.beadsCreated.length > 0 ? `beadsCreated ${first.beadsCreated.join(", ")}` : "phase beads-done",
|
|
257
|
+
),
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
const converted = reportedSkill(trace, "converting-plan-to-beads");
|
|
261
|
+
if (converted.length > 0) return exact(converted.slice(0, 1));
|
|
262
|
+
if (finishedWithEmpty(trace, "beadsCreated")) return NOTHING;
|
|
263
|
+
// A missing converter never means "not converted": a Medium request
|
|
264
|
+
// without a plan creates its beads by hand (delta 20260917 §5.5).
|
|
265
|
+
const loaded = loadedSkill(trace, "converting-plan-to-beads");
|
|
266
|
+
if (loaded.length > 0) return inferred(loaded);
|
|
267
|
+
const created = brEvidence(evidence, "create");
|
|
268
|
+
return created.length > 0 ? inferred(created) : NOTHING;
|
|
269
|
+
}
|
|
270
|
+
case "polish_beads": {
|
|
271
|
+
const reportedPolish = reportedSkill(trace, "polishing-beads");
|
|
272
|
+
if (reportedPolish.length > 0) return exact(reportedPolish.slice(0, 1));
|
|
273
|
+
const polished = trace.reports.filter(
|
|
274
|
+
(report) => report.guardrail !== null && (report.guardrail.polish ?? 0) >= 1,
|
|
275
|
+
);
|
|
276
|
+
if (polished.length > 0) {
|
|
277
|
+
const first = polished[0]!;
|
|
278
|
+
return exact([reportEvidence(first.agentId, first.at, `guardrail ${first.guardrail!.raw}`)]);
|
|
279
|
+
}
|
|
280
|
+
// An older guardrail that SAYS `polish 0` is an exact negative, and it
|
|
281
|
+
// outranks anything the timeline suggests (WP-214 acceptance: a Small
|
|
282
|
+
// request that reported `polish 0/0` was shown as "Beads polished: Done
|
|
283
|
+
// (inferred)"). A guardrail with no polish segment — the format since
|
|
284
|
+
// 2026-09-16 — says nothing about polish (delta 20260917 §5.5).
|
|
285
|
+
const counted = trace.reports.filter((report) => report.guardrail !== null && report.guardrail.polish !== null);
|
|
286
|
+
if (counted.length > 0 && counted.every((report) => report.guardrail!.polish === 0)) {
|
|
287
|
+
const last = counted.at(-1)!;
|
|
288
|
+
return absent(`the Worker reported guardrail ${last.guardrail!.raw}`);
|
|
289
|
+
}
|
|
290
|
+
const missing = finishedWithoutSkill(trace, "polishing-beads");
|
|
291
|
+
if (missing !== null) return absent(missing);
|
|
292
|
+
const loaded = loadedSkill(trace, "polishing-beads");
|
|
293
|
+
if (loaded.length > 0) return inferred(loaded);
|
|
294
|
+
// Otherwise: a polish pass reworks SEVERAL beads in one turn. Repeated
|
|
295
|
+
// `br update` on the SAME bead is ordinary work — F-1 ran three of them
|
|
296
|
+
// on `repo-37g` (description, description again, then status).
|
|
297
|
+
for (const record of trace.records) {
|
|
298
|
+
const updates = brEvidence(record.evidence, "update");
|
|
299
|
+
const distinctBeads = new Set(
|
|
300
|
+
updates.flatMap((entry) =>
|
|
301
|
+
brActions(entry.detail).flatMap((action) => (action.verb === "update" ? action.ids : [])),
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
if (distinctBeads.size >= 2) return inferred(updates);
|
|
305
|
+
}
|
|
306
|
+
return NOTHING;
|
|
307
|
+
}
|
|
308
|
+
case "implement": {
|
|
309
|
+
const reported = trace.reports.filter((report) => report.phase === "bead-implemented");
|
|
310
|
+
if (reported.length > 0) {
|
|
311
|
+
const first = reported[0]!;
|
|
312
|
+
return exact([reportEvidence(first.agentId, first.at, "phase bead-implemented")]);
|
|
313
|
+
}
|
|
314
|
+
// A missing implementing-beads never means "not implemented" (the Small
|
|
315
|
+
// fast path works without it), so there is no negative here.
|
|
316
|
+
const implementedWith = reportedSkill(trace, "implementing-beads");
|
|
317
|
+
if (implementedWith.length > 0) return exact(implementedWith.slice(0, 1));
|
|
318
|
+
const code = evidence.filter((entry) => {
|
|
319
|
+
const path = inWorkspace(entry);
|
|
320
|
+
return path !== null && !path.startsWith("docs/") && !path.startsWith(".beads/");
|
|
321
|
+
});
|
|
322
|
+
return code.length > 0 ? inferred(code) : NOTHING;
|
|
323
|
+
}
|
|
324
|
+
case "review_batches": {
|
|
325
|
+
if (trace.reviews.length > 0 && trace.reviewerIds.length > 0) {
|
|
326
|
+
const review = trace.reviews[0]!;
|
|
327
|
+
return exact([
|
|
328
|
+
{ kind: "agent", detail: `BM-REVIEW verdict=${review.verdict ?? "unknown"}`, agentId: review.agentId, at: review.at },
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
if (trace.reviewerIds.length > 0) {
|
|
332
|
+
return inferred([
|
|
333
|
+
{ kind: "agent", detail: `${trace.reviewerIds.length} Reviewer agent(s) exist`, agentId: trace.reviewerIds[0]!, at: null },
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
return NOTHING;
|
|
337
|
+
}
|
|
338
|
+
case "build_and_tests": {
|
|
339
|
+
const reported = trace.reports.filter(
|
|
340
|
+
(report) => report.buildAndTests !== null && !saysNothingRan(report.buildAndTests),
|
|
341
|
+
);
|
|
342
|
+
if (reported.length > 0) {
|
|
343
|
+
const first = reported[0]!;
|
|
344
|
+
return exact([reportEvidence(first.agentId, first.at, `buildAndTests ${first.buildAndTests}`)]);
|
|
345
|
+
}
|
|
346
|
+
const ran = evidence.filter((entry) => entry.kind === "shell" && ranBuildOrTests(entry.detail));
|
|
347
|
+
return ran.length > 0 ? inferred(ran) : NOTHING;
|
|
348
|
+
}
|
|
349
|
+
case "close_with_evidence": {
|
|
350
|
+
const reported = trace.reports.filter((report) => report.beadsClosed.length > 0);
|
|
351
|
+
if (reported.length > 0) {
|
|
352
|
+
const first = reported[0]!;
|
|
353
|
+
const confirmed = first.beadsClosed.filter((id) => context.beadStatus?.(id) === "closed");
|
|
354
|
+
return exact([
|
|
355
|
+
reportEvidence(
|
|
356
|
+
first.agentId,
|
|
357
|
+
first.at,
|
|
358
|
+
confirmed.length > 0
|
|
359
|
+
? `beadsClosed ${confirmed.join(", ")} (confirmed closed in the store)`
|
|
360
|
+
: `beadsClosed ${first.beadsClosed.join(", ")}`,
|
|
361
|
+
),
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
if (finishedWithEmpty(trace, "beadsClosed")) return NOTHING;
|
|
365
|
+
const closed = brEvidence(evidence, "close");
|
|
366
|
+
return closed.length > 0 ? inferred(closed) : NOTHING;
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
return NOTHING;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface InferContext {
|
|
374
|
+
/** Current status of a bead id, from the WP-208 lookup. Optional. */
|
|
375
|
+
beadStatus?: (id: string) => string | null;
|
|
376
|
+
/** The workspace's directory, to read absolute file evidence (delta 20260917 §5.5). */
|
|
377
|
+
workspaceDir?: string | null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Builds the full table. Always returns all twelve rows, so a step with no
|
|
382
|
+
* evidence is visibly `unknown` rather than missing.
|
|
383
|
+
*/
|
|
384
|
+
export function inferWorkflowSteps(
|
|
385
|
+
trace: ReconstructedTrace,
|
|
386
|
+
context: InferContext = {},
|
|
387
|
+
): WorkflowStepResult[] {
|
|
388
|
+
const tier = trace.tier;
|
|
389
|
+
const skippable = tier === null ? [] : SKIPPABLE_BY_TIER[tier];
|
|
390
|
+
|
|
391
|
+
return WORKFLOW_STEPS.map((step) => {
|
|
392
|
+
const found = findStep(step, trace, context);
|
|
393
|
+
if (found.absent !== undefined) {
|
|
394
|
+
return {
|
|
395
|
+
step,
|
|
396
|
+
status: "skipped" as const,
|
|
397
|
+
confidence: "exact" as const,
|
|
398
|
+
evidence: [],
|
|
399
|
+
note: found.absent,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (found.evidence.length > 0) {
|
|
403
|
+
return { step, status: "done" as const, confidence: found.confidence, evidence: found.evidence, note: null };
|
|
404
|
+
}
|
|
405
|
+
if (tier !== null && skippable.includes(step)) {
|
|
406
|
+
return {
|
|
407
|
+
step,
|
|
408
|
+
status: "skipped" as const,
|
|
409
|
+
confidence: "exact" as const,
|
|
410
|
+
evidence: [],
|
|
411
|
+
note: `tier ${tier} may omit this step (REQ-036)`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
step,
|
|
416
|
+
status: "unknown" as const,
|
|
417
|
+
confidence: "unknown" as const,
|
|
418
|
+
evidence: [],
|
|
419
|
+
note: tier === null ? "no tier was reported, so nothing can be called skipped" : null,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strings in a message that look like bead ids (`bm-dcz`, `repo-37g`,
|
|
3
|
+
* `cus-contact-uiux-redesign-u9zv.12`). Only a shape test: many hyphenated
|
|
4
|
+
* words match too (`feature-workflow`), so callers keep only the ids the
|
|
5
|
+
* workspace's bead store actually has.
|
|
6
|
+
*
|
|
7
|
+
* Pure and environment-neutral.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TOKEN = /(?<![A-Za-z0-9_./-])([a-z][a-z0-9]*(?:-[a-z0-9]+)+(?:\.\d+)*)(?![A-Za-z0-9_/-]|\.[A-Za-z0-9])/gi;
|
|
11
|
+
const REQUEST_ID = /^req-\d{8}t\d{6}z$/i;
|
|
12
|
+
|
|
13
|
+
/** Distinct candidates in order of first appearance, at most `limit`. */
|
|
14
|
+
export function beadIdCandidates(text: string, limit = 80): string[] {
|
|
15
|
+
const out: string[] = [];
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
for (const match of text.matchAll(TOKEN)) {
|
|
18
|
+
const id = match[1]!;
|
|
19
|
+
if (REQUEST_ID.test(id) || seen.has(id)) continue;
|
|
20
|
+
seen.add(id);
|
|
21
|
+
out.push(id);
|
|
22
|
+
if (out.length >= limit) break;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the plugin's `BM-FALLBACK` notice (delta 20260921 §4.4.6).
|
|
3
|
+
*
|
|
4
|
+
* The plugin writes the block (`server/fallback-notices.ts`); the Manager chat
|
|
5
|
+
* shows it as a card. It reaches the chat as a message a language model may
|
|
6
|
+
* quote back, so this reader is forgiving in the same way as `bm-report.ts`:
|
|
7
|
+
* key case and spacing do not matter, unknown keys are ignored, `none` /
|
|
8
|
+
* `unknown` read as null, and a quoted block (`> `) still reads. It returns
|
|
9
|
+
* `null` only when there is no marker line or no usable `incident:` id. The
|
|
10
|
+
* card never trusts the text for its state: it reads `fallback.incidents`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** First line of the notice. */
|
|
14
|
+
export const BM_FALLBACK_MARKER = "BM-FALLBACK";
|
|
15
|
+
|
|
16
|
+
/** How the notice and the Worker handover name each class. */
|
|
17
|
+
export const FALLBACK_CLASS_LABELS: Readonly<Record<"L1" | "L2" | "L4" | "L5", string>> = {
|
|
18
|
+
L1: "L1 usage limit",
|
|
19
|
+
L2: "L2 billing",
|
|
20
|
+
L4: "L4 login",
|
|
21
|
+
L5: "L5 provider unavailable",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MARKER = /^\s*>?\s*(?:[-*]\s*)?bm-fallback\s*$/i;
|
|
25
|
+
const KEY_VALUE = /^\s*>?\s*([A-Za-z][A-Za-z0-9 _-]*)\s*:\s*(.*)$/;
|
|
26
|
+
const INCIDENT_ID = /^fb-[0-9a-f]{12}$/;
|
|
27
|
+
const ABSENT = new Set(["", "none", "unknown", "n/a", "-", "null"]);
|
|
28
|
+
|
|
29
|
+
export const FALLBACK_STATUSES = ["pending", "switched", "waiting", "resumed", "dismissed", "exhausted", "expired", "failed"] as const;
|
|
30
|
+
export type FallbackNoticeStatus = (typeof FALLBACK_STATUSES)[number];
|
|
31
|
+
|
|
32
|
+
/** What a `BM-FALLBACK` block says; a field it cannot read is `null`. */
|
|
33
|
+
export interface ParsedFallbackNotice {
|
|
34
|
+
incident: string;
|
|
35
|
+
role: "manager" | "worker" | "reviewer" | null;
|
|
36
|
+
agent: string | null;
|
|
37
|
+
requestId: string | null;
|
|
38
|
+
status: FallbackNoticeStatus | null;
|
|
39
|
+
/** `L1`, `L2`, `L4` or `L5`, from the `class:` line (`L1 usage limit`). */
|
|
40
|
+
class: "L1" | "L2" | "L4" | "L5" | null;
|
|
41
|
+
provider: string | null;
|
|
42
|
+
message: string | null;
|
|
43
|
+
resetsAt: string | null;
|
|
44
|
+
candidate: string | null;
|
|
45
|
+
replacement: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function valueOf(raw: string | undefined): string | null {
|
|
49
|
+
if (raw === undefined) return null;
|
|
50
|
+
const trimmed = raw.trim().replace(/^`+|`+$/g, "").trim();
|
|
51
|
+
return ABSENT.has(trimmed.toLowerCase()) ? null : trimmed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parses the first `BM-FALLBACK` block of `text`, reading key lines up to the
|
|
56
|
+
* first blank line after at least one key. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
export function parseFallbackNotice(text: unknown): ParsedFallbackNotice | null {
|
|
59
|
+
if (typeof text !== "string") return null;
|
|
60
|
+
const lines = text.split(/\r?\n/);
|
|
61
|
+
const start = lines.findIndex((line) => MARKER.test(line));
|
|
62
|
+
if (start === -1) return null;
|
|
63
|
+
const fields = new Map<string, string>();
|
|
64
|
+
for (const line of lines.slice(start + 1)) {
|
|
65
|
+
const match = KEY_VALUE.exec(line);
|
|
66
|
+
if (match === null) {
|
|
67
|
+
if (line.trim() === "" && fields.size > 0) break;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const key = match[1]!.replace(/[\s_-]/g, "").toLowerCase();
|
|
71
|
+
if (!fields.has(key)) fields.set(key, match[2] ?? "");
|
|
72
|
+
}
|
|
73
|
+
const incident = valueOf(fields.get("incident"));
|
|
74
|
+
if (incident === null || !INCIDENT_ID.test(incident)) return null;
|
|
75
|
+
const role = valueOf(fields.get("role"))?.toLowerCase() ?? null;
|
|
76
|
+
const status = valueOf(fields.get("status"))?.toLowerCase() ?? null;
|
|
77
|
+
const cls = /^(L[1245])\b/i.exec(valueOf(fields.get("class")) ?? "")?.[1]?.toUpperCase() ?? null;
|
|
78
|
+
return {
|
|
79
|
+
incident,
|
|
80
|
+
role: role === "manager" || role === "worker" || role === "reviewer" ? role : null,
|
|
81
|
+
agent: valueOf(fields.get("agent")),
|
|
82
|
+
requestId: valueOf(fields.get("requestid")),
|
|
83
|
+
status: (FALLBACK_STATUSES as readonly string[]).includes(status ?? "") ? (status as FallbackNoticeStatus) : null,
|
|
84
|
+
class: cls === "L1" || cls === "L2" || cls === "L4" || cls === "L5" ? cls : null,
|
|
85
|
+
provider: valueOf(fields.get("provider")),
|
|
86
|
+
message: valueOf(fields.get("message")),
|
|
87
|
+
resetsAt: valueOf(fields.get("resetsat")),
|
|
88
|
+
candidate: valueOf(fields.get("candidate")),
|
|
89
|
+
replacement: valueOf(fields.get("replacement")),
|
|
90
|
+
};
|
|
91
|
+
}
|