bc-session-close

star 0

Use on procedure terminal step, explicit customer close, session end, or pending-state submit-now branch. Runs the 9-step close sequence — summary, save state, per-item observation review, drafter handoff for ready research-notes, contributions double-filtered, direct typed submission, defensive dossier check, next-time framing, cleanup, goodbye. Auto-spawns bc-path-drafter and bc-process-drafter sub-agents for ready research-notes.

Be-Civic By Be-Civic schedule Updated 6/5/2026

name: bc-session-close description: Use on procedure terminal step, explicit customer close, session end, or pending-state submit-now branch. Runs the 9-step close sequence — summary, save state, per-item observation review, drafter handoff for ready research-notes, contributions double-filtered, direct typed submission, defensive dossier check, next-time framing, cleanup, goodbye. Auto-spawns bc-path-drafter and bc-process-drafter sub-agents for ready research-notes.

Be Civic — Session Close

Two invocations:

  • Full close — procedure terminal step, explicit customer close, session timeout. Runs the 9 steps below in order.
  • Resume-submit — invoked from the PENDING_STATE surface at session open (preamble §4.3). Skip steps 1–2 and 7–9; jump to per-item review (step 3), drafter handoff (step 4), and submission (step 6). Used when the customer chose "handle now" on a deferred item from a prior session — the items live in ${SUBSTRATE_STATE}/sessions/<session_id>/pending-submissions.jsonl (a prior session's local-buffer fallback).

The customer-facing language for the observation buffer is list or notes, never "buffer."

Wire basics (read once)

All submissions are direct typed POSTs through the bundled wire.py over bashnot WebFetch, which is GET-only and cannot carry a request body (so it cannot do a single write). wire.py is the documented "provide a utility script" write path; it sends the request to the REST surface at https://becivic.be/api, handles auth internally, and retries once on a transient network failure. The per-item user review below IS the gate — it is harness behaviour, not an API call. Once the user approves an item, exactly one POST leaves the machine.

$BC_ROOT below is the resolved install root the preamble emitted as SUBSTRATE_ROOT: at session start (harness §3) — $CLAUDE_PLUGIN_ROOT is unset in the Cowork VM shell, so never use a bare ${SUBSTRATE_ROOT}/${CLAUDE_PLUGIN_ROOT} literal in a bash command; use the resolved $BC_ROOT.

  • Auth — handled inside wire.py. wire.py reads BECIVIC_HARNESS_KEY from ${SUBSTRATE_STATE}/.env itself and sends Authorization: Bearer <harness_key>; you never touch the key here and it is never echoed or logged. If the session is in anonymous-read mode (no key — user declined verification), no submissions are possible: wire.py would post anonymously and the worker 401s. So do not call it — tell the customer plainly that their notes can't be sent without verification, offer to verify (hand back to onboarding) or to hold the notes locally (step 6 fallback).
  • submission_id. Generate client-side before each POST: python3 "$BC_ROOT/scripts/gen_submission_id.py" <issue|validation|feedback|rating> → prints <iss|val|fbk|rat>_<uuidv7>. One id per submission; the worker echoes it back.
  • submitting_harness = the SUBMITTING_HARNESS value the preamble surfaces (form be-civic/<version>; also persisted at ${SUBSTRATE_STATE}/version.json). Use it verbatim — never hardcode a version, it tracks the plugin manifest. submitting_model = the model running this session with optional effort suffix (e.g. claude-opus-4-7/xhigh), per the preamble's model context.
  • NEVER send worker-set fields. The worker stamps and rejects-if-present: user_id, accepted_at, cohort_anchor, regex_passes, ner_status, cancel_token. Build envelopes from submitter fields only.
  • Accept response. wire.py prints http_status:, result: ok|error, the data: object (when present), and the full body:, and exits 0 on a 2xx. The accept body is 202 { "status": 202, "data": { "submission_id", "accepted_at", "cancel_token"[, "cohort_anchor"] } }. Persist cancel_token (and the submission_id + type) — it is the only handle for the 48-hour cancellation window and cannot be reissued if lost. Branch on the http_status: line first; a non-202 (result: error) with { "error": "<category>", ... } in the body means the item did not land (handle per step 6).
  • Cancellation (48h). A DELETE (also via wire.py): python3 "$BC_ROOT/scripts/wire.py" DELETE /api/submissions/<type>/<submission_id> --cancel-token <token> (the Bearer is read from .env inside the script; --cancel-token sets the X-Cancel-Token header), where <type>issue|validation|feedback|rating. Surface the cancel handle to the customer at goodbye (step 7).

The 9 steps

1. Summarise progress

One short paragraph in plain English. What you covered today, what's done, what's still open. Tone is warm and concrete, not a status report. Skip on resume-submit.

2. Save state

Update each procedure walked this session: write its visible progress at ${SUBSTRATE_DATA}/<procedure-slug>/progress.md (last step reached, what's pending, anything the user said worth holding) and refresh that procedure's entry status / updated_at in ${SUBSTRATE_STATE}/procedures.json. Skip on resume-submit.

3. Per-item observation review (consume the buffer)

Read this session's observation list at ${SUBSTRATE_STATE}/sessions/<session_id>/observations-buffer.jsonl (on resume-submit, read pending-submissions.jsonl instead). One JSON object per line, each an observation.v3-shaped item written by bc-path-traversal and bc-discovery as observations accumulated this session. Inline-commit Validations on path sources (target_type: path_source) were already POSTed at traversal time and are not in this buffer — do not re-submit them.

For each item:

  • Show it in plain English (rendered from the JSON, not the JSON itself).
  • AskUserQuestion: approve / edit / discard. (Two options + free-text fallback keeps the gate MECE.)
  • On edit: ask what to change, rewrite, re-run scripts/scrub-layer1.py against the rewritten version before it is eligible to send.
  • On discard: drop the line; do not re-surface.

Apply the CC BY 4.0 grant reminder once at the top of this step, not per item: "Anything you approve is shared anonymously under CC BY 4.0. You can cancel anything within 48 hours of submission — I'll give you the cancel codes after we send."

Approved items carry forward to step 6 for submission. The buffer file itself is deleted in step 8, only after every item is submitted, discarded, or written into pending-submissions.jsonl.

4. Drafter handoff (the new core of close)

Scan ${SUBSTRATE_DATA}/<procedure-slug>/memory/research-notes-*.md (the preamble surfaced these as PENDING_STATE: ready_to_draft) for files with frontmatter status: ready_to_draft. For each:

  • Surface to customer: "I have research-notes from [N] session(s) about [target]. Submit now, keep researching, or discard?"
  • Submit now: spawn the relevant drafter via the Agent tool. Dispatch in parallel when multiple distinct entities are ready; collect results and surface them to the user one at a time for review.
    • bc-process-drafter for process-shaped notes (model: opus for a new-Process proposal — judgment-heavy; model: sonnet only for a trivial amendment to an existing Process). If the drafter's Step 0 finds the notes are path-shaped, it hands off to bc-path-drafter itself.
    • bc-path-drafter for path-shaped notes (model: sonnet usually; model: opus for cross-region / cross-commune scope judgment).
    • Pass the research-notes path and the customer's profile snapshot.
  • The drafter returns a structured payload: { proposed_process_id | target_process_id (or path equivalent), label, canonical_markdown | body_diff, rationale, evidence, provenance: { research_notes_scrubbed } }. It also returns the Issue submission envelope it built (it does NOT submit — close owns the wire call):
    • New Process proposal → target_type: knowledge_graph, label: gap, evidence.proposed_process_id: <kebab-slug>.
    • Amend an existing Process → target_type: process, label: missing | bug | divergence, target_id: <process_id>.
    • Wholly-new Path → target_type: path, label: missing.
    • New / commune-specific Path source → target_type: path_source, label: missing | divergence, target_id: <path_id>:<source_id>.
  • Present the payload + research-notes to the customer for review.
  • On approve: submit the Issue envelope per step 6 (single direct POST to /api/issues). On a 202, rewrite the notes frontmatter to status: drafted and clear the matching discovery:* entry from profile.json.active_procedures.
  • On keep-researching: leave status ready_to_draft; the next session's pending-state scan picks it up.
  • On discard: rewrite frontmatter status: discarded.

5. Surface §8 Requests-for-contributions — filtered

For every procedure Process walked this session, read its body's [Requests for contributions] section (if present). Apply two filters before surfacing — never dump the full list on the customer:

  • Relevance filter. Only surface items the customer's session actually touched. If the Process has 5 contribution requests but this customer's path only exercised 2 sub-scenarios, surface only those 2.
  • Genuine-access filter. Only surface items the customer is actually positioned to help with. A request for "first-hand commune-staff judgment from Schaerbeek" is for a Schaerbeek customer, not a Ghent customer. A request about a sub-category the customer didn't qualify under is not for them.

Present the survivors (typically 0–2 items) as: "Things you've seen firsthand that would help others." Frame as contribution, not extraction. If zero survive, skip the section entirely — don't manufacture asks.

For each item the customer commits to, map to the right submission shape (concern/amendment-shaped items are all submitted as Issues, per the routing table below):

  • "I saw an extra step / a missing doc on this Process" → Issue, target_type: process, label: missing (or bug for an incorrect step).
  • "A citation / source link is dead" → Issue, target_type: process | path | path_source, label: rotted.
  • "It differed at my commune" → Issue, target_type: process | path_source, label: divergence, with evidence.scope + evidence.specifier (NIS5).
  • "This whole sub-procedure is missing from Be Civic" → route into bc-discovery for next session (becomes research-notes, then a drafter handoff at a future close), not a bare Issue now.

6. Submission — single direct typed POST, with local-buffer fallback

Submit each approved item — observations from step 3, drafter Issue envelopes from step 4, contribution Issues from step 5. One POST per item, no staging round-trip. Each POST goes through wire.py (per Wire basics above), piping the envelope on stdin so nothing sensitive hits the process table:

printf '%s' '<the JSON envelope>' \
  | python3 "$BC_ROOT/scripts/wire.py" POST /api/<issues|validations|feedback|ratings> --stdin

Build the envelope for the item's type — the path each one POSTs to:

  • IssuePOST /api/issues. Body: { schema_version, submission_id (iss_…), submitted_at (RFC3339 UTC), submitting_harness (the preamble's SUBMITTING_HARNESS), submitting_model, submission_contract_version, target_type (process|path|path_source|tool|provider|volatile_value|reference|resource|knowledge_graph), target_id, title (≤120, no newline), body (markdown ≤2000), label (bug|missing|rotted|divergence|gap), context { language_used, region?, commune_nis5? }, evidence { …per-target } }. Per-target evidence: graph entities + path_source{ evidence_date, evidence_source: customer-report|citation|corroboration, scope?, specifier? }; knowledge_graph{ proposed_process_id? }; volatile_value{ observed_value, evidence_date }; reference{ evidence_date, evidence_source }; resource{ evidence_date, observed_path_id? }.
  • ValidationPOST /api/validations. Body: { schema_version, submission_id (val_…), submitted_at, submitting_harness, submitting_model, submission_contract_version, target_type, target_id, outcome (positive|negative), signal_class }. No body/rationale field. (Most Validations are inline-committed at traversal time; a Validation only reaches close if it was buffered.)
  • FeedbackPOST /api/feedback. Body: { schema_version, submission_id (fbk_…), submitted_at, submitting_harness, submitting_model, submission_contract_version, topic? (bug|suggestion|praise|confusion|accessibility|other), pointer?, body (≤2000) }. No target_type.
  • RatingPOST /api/ratings. Body: { schema_version, submission_id (rat_…), submitted_at, submitting_harness, submitting_model, submission_contract_version, target_type (process|agent_protocol|session), target_id, score (1..5), would_be_5_stars? (when score ≤ 4) }.

On 202: parse data.{submission_id, accepted_at, cancel_token} and persist cancel_token + submission_id + type (carry into step 7). Mark the item submitted.

On a non-202 / error envelope (result: error with { "error": "<category>", … } in body:): do NOT silently retry.

  • A scrub / field rejection (e.g. worker_field_supplied_by_submitter, a scrub-detector category) names what tripped — tell the customer plainly which field, offer rewrite-or-drop, and re-submit only after they fix it.
  • A transport failure (wire.py exits non-zero with result: network — it already retried once internally — or result: blocked / exit 4 = blocked-by-allowlist, becivic.be unreachable in this sandbox) → local-buffer fallback (don't lose the contribution). Append the approved item to ${SUBSTRATE_STATE}/sessions/<session_id>/pending-submissions.jsonl (same JSONL line shape, plus a staged_at timestamp; Layer-1 scrub already ran at step 3 so resubmit goes straight to the POST). Tell the customer plainly: "I couldn't reach Be Civic right now — your contribution is saved locally and I'll try again next session." The next session's preamble surfaces it via PENDING_STATE: pending_submissions for the resume-submit branch.

If the preamble set SUBMIT_OBSERVATIONS_THIS_SESSION: no (scrub-rules couldn't be confirmed), do NOT submit. Tell the customer: "I'm holding back submissions this session — Be Civic's scrub rules couldn't be confirmed. We'll send next time," and write approved items to pending-submissions.jsonl instead.

7. Name what happens next time

One sentence per active item. "Next session we'll pick up at [step]." / "When you have [doc], come back." / "Cancel handle for what we just sent is in your notes; you have 48 hours." Skip on resume-submit.

8. Defensive dossier check, then cleanup

Defensive dossier check.

Before the goodbye, scan ${SUBSTRATE_STATE}/procedures.json. For any procedure whose status is completed (terminal / dossier-eligible) and whose visible folder ${SUBSTRATE_DATA}/<slug>/ holds no dossier artefact (nothing under ${SUBSTRATE_DATA}/<slug>/documents/dossier/), offer once: "You finished [procedure] but we never built the dossier you'd file — want me to compile it now?" On yes, hand off to bc-dossier-compilation for that procedure. On no, drop it — don't re-ask. Skip this check entirely on resume-submit.

Cleanup. Delete ${SUBSTRATE_STATE}/sessions/<session_id>/observations-buffer.jsonl once every item is submitted, discarded, or written into pending-submissions.jsonl. Leave the session directory for the orphan-buffer scan to handle on a hard close. Do not delete pending-submissions.jsonl — the preamble owns its lifecycle. Skip on resume-submit.

9. Goodbye

One sentence. Warm, specific to what the customer worked through. No "great chatting!" Don't preamble. Skip on resume-submit.

Portability — bc-export

If the customer asks "how do I back up my Be Civic data?" or "can I use this on another machine?", run the export script at session close (after cleanup in step 8):

python3 "$BC_ROOT/scripts/bc_export.py" --cowork --out ~/Desktop

($BC_ROOT is the resolved install root from Wire basics above — $CLAUDE_PLUGIN_ROOT is unset in the Cowork VM shell.)

The script bundles the project folder into a single bc-export-<timestamp>.tar.gz and prints the mandatory Identity warning. The harness key is gitignored (never in git history), but when a key is present the export carries it as a loose identity/env member — so the bundle is credential-bearing (treat it like a passport scan). On the destination machine the user runs (resolving the install root there the same way the harness does, since $BC_ROOT from this session won't carry over):

python3 "$BC_ROOT/scripts/bc_import.py" <bundle.tar.gz> --cowork --data-parent <parent>

then continues as a returning user — re-verifying via the onboarding flow only if the bundle carried no key (identity-preserving: re-verifying the same email restores the same identity). See skills/be-civic/SKILL.md §5 for import detection in the gate skill.

What this skill does NOT own

  • Generating canonical markdown from research-notes. The drafter subagent does that; close hands off, reviews, and owns the single wire POST.
  • Deciding what's an Issue vs a Validation vs a discovery. That classification is made upstream (by bc-path-traversal / bc-discovery as items land in the buffer). Close routes the already-classified items and maps concern/amendment-shaped contributions to the right Issue target_type + label.
  • Inline-commit Validations on path sources — those POST directly from bc-path-traversal at traversal time and never enter this skill's buffer.
  • Re-running scrub on items already at Layer-1. The worker runs Layer-2 at ingress. A Layer-1 re-run is needed only when an item is edited at step 3 — call scripts/scrub-layer1.py then.
Install via CLI
npx skills add https://github.com/Be-Civic/be-civic-plugin --skill bc-session-close
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator