feat(query): pending queue briefings and session continuity - #574
Conversation
The read-only console shipped in PR #572 with dec.control-plane-programme unsigned, which Task 1 defines as the stop line; post-signature widening and the intake lane move to the new todo.console-signed-widening (gated on the maintainer signature, undeclarable per ruling 4), and ui-asset-refresh re-points at the two remaining webui-changing units.
Implements todo.pending-queue-briefing (campaign step 5): the pending queue renders the maintainer's rubric briefing in plain language, and a session opens with what is waiting and the recommended next action. - pending wire: ruling_summary, rubric (tier/unblocks/alignment/options), local-tier evidence receipts, changed_since_review; SCHEMA_VERSION 10. - receipt hash comparison is tri-state: a receipt with no comparable hash renders as unverified and never claims the text changed; any known-stale receipt keeps the changed marker (affects-linked reviews included). - cairn pending <id> renders one briefing on both the human and --json paths (CAIRN_COMMAND_FAILED on an unknown id); MCP PendingRequest exposes the optional id. - cairn context opens with Waiting on you: briefings plus the shared next-recommended selection; pending errors propagate instead of rendering an honest-empty queue; the human path honours --changes-dir. - webui pending channel shows the same briefing read-only with lane-scoped landmarks in the visual harness. - renderers split per the 300-line seam convention: cli/render/pending.rs and handlers/pending_evidence.rs with sibling test modules.
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe pending queue now supports decision-specific briefings. Query and CLI responses include ruling, rubric, review evidence, change status, and reopen commands. The context view exposes continuity data. The web UI expands pending rows into detailed briefings. The API schema version is 10. ChangesPending decision briefing flow
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/design-system/copy.toml`:
- Around line 426-428: Update the evidence-verdict-unknown value used by
render_pending_evidence to “not recorded” so evidence-receipt supplies the only
literal “verdict” prefix and the rendered fallback is not duplicated.
In `@meta/todos/todo.overharness-console-ux.md`:
- Line 3: Update the todo’s status and acceptance criteria consistently: either
change the retained criterion to explicitly allow the documented unsigned
fallback as an alternative, or remove status: done and keep the todo open until
the signed programme decision is recorded. Align the decision with the
acceptance-criteria text around the signed decision and fallback entries.
In `@src/query_api/handlers/pending_brief.rs`:
- Around line 62-73: Update first_ruling_paragraph so
first_body_paragraph(&lines) is used only when find_section finds no
ruling/decision heading; when a section is found, scope any fallback paragraph
search to lines[start..end] and do not fall back to unrelated document-level
prose.
In `@src/query_api/handlers/pending_evidence.rs`:
- Around line 80-98: The assemble loading path must produce deterministic
receipt ordering. Update the final stems or receipts construction in assemble so
the returned receipts Vec is sorted, while preserving the existing review
matching and PendingReceipt field population.
In `@src/ui_assets/channel-bar.js`:
- Around line 134-155: Update the evidence rendering block in the pending-detail
template to render the evidence section unconditionally, removing the outer
evidence truthiness guard. Keep the existing receipts.length conditional so
populated evidence shows the receipt list and absent or empty evidence shows the
pending-no-evidence fallback, while preserving the Evidence label.
- Around line 163-170: The pending ChannelItem lists lack stable keys, allowing
expanded state to move between decisions when rows reorder. Update every
pendingRows ChannelItem call site, including the console mapping, to pass
key={item.id}; keep the existing item prop and rendering behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 26ac701f-aea1-4a47-924a-edde369dd277
⛔ Files ignored due to path filters (17)
tests/snapshots/wire_format_snapshots__api_blueprint.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_dependents_app_api.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_depends_app_api.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_frontier_cycle_error.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_graph.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_lint.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_meta.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_contract.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_decisions.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_rationale.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_research.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_sources.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_node_app_api_todos.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_pending.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_roadmap.snapis excluded by!**/*.snaptests/snapshots/wire_format_snapshots__api_status.snapis excluded by!**/*.snap
📒 Files selected for processing (45)
docs/commands.mddocs/design-system/copy.tomldocs/integration-contract.mdharness/eval.mjsharness/fixtures/api/pendingharness/fixtures/assets/copy.jsonharness/lib/audit.mjsmap.jsonmeta/decisions/bootstrap-fixture-corpus-split.mdmeta/decisions/control-plane-programme.mdmeta/todos/todo.console-signed-widening.mdmeta/todos/todo.overharness-console-ux.mdmeta/todos/todo.pending-queue-briefing.mdmeta/todos/todo.ui-asset-refresh.mdschemas/PendingResponse.schema.jsonsrc/artefacts/registry/manifest.rssrc/artefacts/registry/manifest/review_coverage.rssrc/cli/mod.rssrc/cli/render/mod.rssrc/cli/render/pending.rssrc/cli/render/project.rssrc/cli/render/project/tests.rssrc/cli/render/session.rssrc/cli/render/session/tests.rssrc/mcp/mod.rssrc/query_api/handlers/context.rssrc/query_api/handlers/mod.rssrc/query_api/handlers/pending.rssrc/query_api/handlers/pending_brief.rssrc/query_api/handlers/pending_brief_live_tests.rssrc/query_api/handlers/pending_brief_tests.rssrc/query_api/handlers/pending_evidence.rssrc/query_api/handlers/pending_evidence_tests.rssrc/query_api/handlers/pending_rubric.rssrc/query_api/handlers/pending_tests.rssrc/query_api/handlers/project.rssrc/query_api/mod.rssrc/query_api/registry.rssrc/scanner/ratification/convergence.rssrc/ui/mod.rssrc/ui_assets/channel-bar.jssrc/ui_assets/style.csstests/graph_explorer.rstests/pending_queue.rstests/wire_format_snapshots.rs
| evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}" | ||
| evidence-reviewer-unknown = "reviewer unknown" | ||
| evidence-verdict-unknown = "verdict not recorded" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect how evidence-receipt placeholders are filled for the unknown-verdict case.
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown' --type=rust -B3 -A6Repository: cairn-framework/cairn
Length of output: 159
🏁 Script executed:
#!/bin/bash
set -u
echo "Tracked files around render/pending.rs:"
git ls-files | rg '(^|/)pending\.rs$|docs/design-system/copy\.toml|cairn\.blueprint' || true
echo
echo "Search evidence placeholders across repo:"
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown|{reviewer}|{verdict}' . || trueRepository: cairn-framework/cairn
Length of output: 698
🏁 Script executed:
#!/bin/bash
set -u
echo "Search evidence placeholders across repo (safe regex):"
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown' . || true
echo
rg -n '\{reviewer\}|\{verdict\}' . || true
echo
echo "Inspect src/cli/render/pending.rs outline and relevant sections:"
wc -l src/cli/render/pending.rs
ast-grep outline src/cli/render/pending.rs || true
rg -n 'evidence|receipt|reviewer|verdict|where_left|fill|replace' src/cli/render/pending.rs -C 4 || trueRepository: cairn-framework/cairn
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -u
echo "src/cli/render/pending.rs size:"
wc -l src/cli/render/pending.rs
echo
echo "Lines 1-260 of src/cli/render/pending.rs:"
sed -n '1,260p' src/cli/render/pending.rs | cat -n
echo
echo "Targeted substitution/rendering references in src/cli/render/pending.rs:"
rg -n 'evidence|receipt|where_left|render|replace|pending::|copy|templates|Evidence|Verdict|Reviewer' src/cli/render/pending.rs -C 3 || true
echo
echo "Parse copy entries into a deterministic substitution verifier (does not run repo code):"
python3 - <<'PY'
from pathlib import Path
import re
text = Path('docs/design-system/copy.toml').read_text()
template = None
reviewer = None
verdict = None
for line in text.splitlines():
line = line.strip()
if line.startswith('evidence-receipt = ') and template is None:
m = re.match(r'^evidence-receipt\s*=\s*"(.*)"$', line)
if m:
template = m.group(1).encode('unicode_escape').decode('ascii').encode().decode('unicode_escape')
if line == 'evidence-reviewer-unknown = "reviewer unknown"':
reviewer = "reviewer unknown"
if line == 'evidence-verdict-unknown = "verdict not recorded"':
verdict = "verdict not recorded"
print({"template": template, "reviewer": reviewer, "verdict": verdict})
if template and reviewer and verdict:
sentence = template.replace("{reviewer}", reviewer).replace("{verdict}", verdict)
print({"rendered": sentence, "duplicate_verdict": sentence.count("verdict verdict") > 0})
PYRepository: cairn-framework/cairn
Length of output: 13182
Remove the redundant “verdict” from the unknown verdict fallback.
render_pending_evidence replaces {verdict} directly with pending.evidence-verdict-unknown, and pending.evidence-receipt already contains the literal verdict prefix. This makes the fallback render as verdict verdict not recorded. Use not recorded for the fallback instead.
✏️ Proposed fix
-evidence-verdict-unknown = "verdict not recorded"
+evidence-verdict-unknown = "not recorded"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}" | |
| evidence-reviewer-unknown = "reviewer unknown" | |
| evidence-verdict-unknown = "verdict not recorded" | |
| evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}" | |
| evidence-reviewer-unknown = "reviewer unknown" | |
| evidence-verdict-unknown = "not recorded" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design-system/copy.toml` around lines 426 - 428, Update the
evidence-verdict-unknown value used by render_pending_evidence to “not recorded”
so evidence-receipt supplies the only literal “verdict” prefix and the rendered
fallback is not duplicated.
| --- | ||
| node: cairn.ui | ||
| status: in_progress | ||
| status: done |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Align the terminal status with the acceptance criteria.
status: done marks this work complete. The retained acceptance criterion at Line 49 still requires a signed programme decision, while Lines 61-66 record only the unsigned fallback. Define the fallback as an alternative acceptance criterion, or keep this todo open until the decision is signed.
Also applies to: 61-66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@meta/todos/todo.overharness-console-ux.md` at line 3, Update the todo’s
status and acceptance criteria consistently: either change the retained
criterion to explicitly allow the documented unsigned fallback as an
alternative, or remove status: done and keep the todo open until the signed
programme decision is recorded. Align the decision with the acceptance-criteria
text around the signed decision and fallback entries.
| fn first_ruling_paragraph(body: &str) -> Option<String> { | ||
| let lines: Vec<&str> = body.lines().collect(); | ||
| find_section(&lines, |level, heading| { | ||
| // Level 2+ only: an H1 title mentioning "decision" is the document | ||
| // name, never the ruling section. | ||
| level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision")) | ||
| }) | ||
| .and_then(|(start, end)| first_paragraph(&lines[start..end])) | ||
| .or_else(|| first_body_paragraph(&lines)) | ||
| .map(|paragraph| clean_markdown(¶graph)) | ||
| .filter(|paragraph| !paragraph.is_empty()) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scope the ruling-paragraph fallback to inside the found section.
first_ruling_paragraph falls back to first_body_paragraph(&lines) whenever first_paragraph returns None for the found "ruling"/"decision" section. first_body_paragraph searches the whole document from the top, not the found section. If the section's first line is itself a heading (no lead-in paragraph before a subheading), the function silently picks up unrelated prose from earlier in the document instead of the ruling text.
Only use the whole-document fallback when no "ruling"/"decision" heading is found at all.
🐛 Proposed fix to scope the fallback correctly
fn first_ruling_paragraph(body: &str) -> Option<String> {
let lines: Vec<&str> = body.lines().collect();
- find_section(&lines, |level, heading| {
+ let section = find_section(&lines, |level, heading| {
// Level 2+ only: an H1 title mentioning "decision" is the document
// name, never the ruling section.
level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision"))
- })
- .and_then(|(start, end)| first_paragraph(&lines[start..end]))
- .or_else(|| first_body_paragraph(&lines))
- .map(|paragraph| clean_markdown(¶graph))
- .filter(|paragraph| !paragraph.is_empty())
+ });
+ let paragraph = match section {
+ Some((start, end)) => first_paragraph(&lines[start..end]),
+ None => first_body_paragraph(&lines),
+ };
+ paragraph
+ .map(|paragraph| clean_markdown(¶graph))
+ .filter(|paragraph| !paragraph.is_empty())
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn first_ruling_paragraph(body: &str) -> Option<String> { | |
| let lines: Vec<&str> = body.lines().collect(); | |
| find_section(&lines, |level, heading| { | |
| // Level 2+ only: an H1 title mentioning "decision" is the document | |
| // name, never the ruling section. | |
| level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision")) | |
| }) | |
| .and_then(|(start, end)| first_paragraph(&lines[start..end])) | |
| .or_else(|| first_body_paragraph(&lines)) | |
| .map(|paragraph| clean_markdown(¶graph)) | |
| .filter(|paragraph| !paragraph.is_empty()) | |
| } | |
| fn first_ruling_paragraph(body: &str) -> Option<String> { | |
| let lines: Vec<&str> = body.lines().collect(); | |
| let section = find_section(&lines, |level, heading| { | |
| // Level 2+ only: an H1 title mentioning "decision" is the document | |
| // name, never the ruling section. | |
| level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision")) | |
| }); | |
| let paragraph = match section { | |
| Some((start, end)) => first_paragraph(&lines[start..end]), | |
| None => first_body_paragraph(&lines), | |
| }; | |
| paragraph | |
| .map(|paragraph| clean_markdown(¶graph)) | |
| .filter(|paragraph| !paragraph.is_empty()) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/query_api/handlers/pending_brief.rs` around lines 62 - 73, Update
first_ruling_paragraph so first_body_paragraph(&lines) is used only when
find_section finds no ruling/decision heading; when a section is found, scope
any fallback paragraph search to lines[start..end] and do not fall back to
unrelated document-level prose.
| let receipts = stems | ||
| .iter() | ||
| .map(|stem| { | ||
| let review = reviews.iter().find(|review| review_stem(review) == *stem); | ||
| let subject_hash_matches = match ( | ||
| review.and_then(|review| review.subject_hash.as_deref()), | ||
| current_subject_hash, | ||
| ) { | ||
| (Some(reviewed), Some(current)) => Some(reviewed == current), | ||
| _ => None, | ||
| }; | ||
| PendingReceipt { | ||
| stem: stem.clone(), | ||
| reviewer: review.map(|review| review.reviewer.clone()), | ||
| verdict: review.and_then(|review| first_verdict_line(&review.body)), | ||
| subject_hash_matches, | ||
| } | ||
| }) | ||
| .collect::<Vec<_>>(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether scan_result.artefacts.reviews is populated in a stable sorted order.
rg -n 'reviews\s*:\s*Vec<Review>|reviews\.sort|fn load_reviews|reviews\.push' -g '*.rs' src/artefacts src/scannerRepository: cairn-framework/cairn
Length of output: 291
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files | rg '(^|/)pending_evidence\.rs$|(^|/)pending\.rs$|artefacts/registry/types.rs$|artefacts/registry/kinds.rs$|cairn.blueprint' || true
echo "== outline pending_evidence.rs =="
ast-grep outline src/query_api/handlers/pending_evidence.rs --view expanded || true
echo "== relevant lines pending_evidence.rs =="
sed -n '1,160p' src/query_api/handlers/pending_evidence.rs
echo "== relevant types/kinds for Review =="
sed -n '300,370p' src/artefacts/registry/types.rs
sed -n '220,270p' src/artefacts/registry/kinds.rs
echo "== search all reviews construction/usages =="
rg -n 'reviews|Review|first_verdict_line|review_stem|artifact_sets|receipts|affects' -g '*.rs' src | sed -n '1,240p'Repository: cairn-framework/cairn
Length of output: 32835
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== scanner artifact loading =="
rg -n 'fn load|scan_result|ArtefactSet|reviews|decisions|affects' -g '*.rs' src/scanner src/artefacts/registry | sed -n '1,260p'
echo "== pending.rs evidence calls =="
sed -n '1,180p' src/query_api/handlers/pending.rs
echo "== review coverage helper =="
sed -n '1,80p' src/artefacts/registry/manifest/review_coverage.rs
rg -n 'load_reviews|find.*review|meta/reviews|rev.*sort|sort_by|DirEntry|WalkDir' -g '*.rs' src/artefacts src/scanner | sed -n '1,220p'Repository: cairn-framework/cairn
Length of output: 38704
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== artefact kinds implementation =="
sed -n '1,115p' src/artefacts/registry/kinds.rs
sed -n '203,255p' src/artefacts/registry/kinds.rs
echo "== registry load order implementation =="
sed -n '1,65p' src/artefacts/registry/mod.rs
sed -n '1,70p' src/artefacts/registry/io.rs
echo "== deterministic path-order behaviour probe =="
python3 - <<'PY'
import os, tempfile, pathlib
root = pathlib.Path(tempfile.mkdtemp())
root.joinpath("blueprint.toml").write_bytes(b'')
root.joinpath("meta/reviews").mkdir(parents=True)
paths = (
"a.md", "b.md", "m1/a.md", "m2/a.md", "z.md", "A.md"
)
# create filesystem directory entries in reverse order; os.scandir order is filesystem-dependent.
for name in reversed(paths):
p = root.joinpath("meta/reviews", name)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(b"")
files = tuple(p.name for p in os.scandir(root.joinpath("meta/reviews")) if p.name.endswith(".md"))
print("scandir_order=", files)
print("sorted_orders_match=", tuple(sorted(files)) == (files if False else None))
print("lexicographic=", tuple(sorted(paths)))
PYRepository: cairn-framework/cairn
Length of output: 10495
Sort the loading path before returning receipts.
Reviews are pushed in filesystem-entry order, and stems are then built from decision.receipts, decision.affects, then reviews. pending_response sorts the final decision rows, but assemble does not sort the resulting receipts Vec. Sort the final stems or receipts before returning to keep the wire payload deterministic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/query_api/handlers/pending_evidence.rs` around lines 80 - 98, The
assemble loading path must produce deterministic receipt ordering. Update the
final stems or receipts construction in assemble so the returned receipts Vec is
sorted, while preserving the existing review matching and PendingReceipt field
population.
| ${ | ||
| evidence | ||
| ? html`<section class="pending-detail-section pending-detail-evidence"> | ||
| <p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p> | ||
| ${ | ||
| receipts.length | ||
| ? html`<ul class="pending-detail-list"> | ||
| ${receipts.map( | ||
| (receipt) => html` | ||
| <li> | ||
| <span>${receipt.stem}</span> | ||
| <span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span> | ||
| <span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span> | ||
| <span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span> | ||
| </li> | ||
| `, | ||
| )} | ||
| </ul>` | ||
| : html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>` | ||
| } | ||
| </section>` | ||
| : null |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Render the empty evidence state when evidence is absent.
Lines 134-155 omit the full evidence section when item.evidence is absent. dec.control-plane-programme in harness/fixtures/api/pending has no evidence object. Its expanded row therefore shows neither the Evidence label nor the pending-no-evidence fallback.
Render the section unconditionally. Use receipts.length to select the receipt list or the empty state.
Proposed fix
- ${
- evidence
- ? html`<section class="pending-detail-section pending-detail-evidence">
+ <section class="pending-detail-section pending-detail-evidence">
<p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p>
${
receipts.length
...
: html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>`
}
- </section>`
- : null
- }
+ </section>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ${ | |
| evidence | |
| ? html`<section class="pending-detail-section pending-detail-evidence"> | |
| <p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p> | |
| ${ | |
| receipts.length | |
| ? html`<ul class="pending-detail-list"> | |
| ${receipts.map( | |
| (receipt) => html` | |
| <li> | |
| <span>${receipt.stem}</span> | |
| <span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span> | |
| <span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span> | |
| <span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span> | |
| </li> | |
| `, | |
| )} | |
| </ul>` | |
| : html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>` | |
| } | |
| </section>` | |
| : null | |
| <section class="pending-detail-section pending-detail-evidence"> | |
| <p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p> | |
| ${ | |
| receipts.length | |
| ? html`<ul class="pending-detail-list"> | |
| ${receipts.map( | |
| (receipt) => html` | |
| <li> | |
| <span>${receipt.stem}</span> | |
| <span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span> | |
| <span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span> | |
| <span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span> | |
| </li> | |
| `, | |
| )} | |
| </ul>` | |
| : html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>` | |
| } | |
| </section> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ui_assets/channel-bar.js` around lines 134 - 155, Update the evidence
rendering block in the pending-detail template to render the evidence section
unconditionally, removing the outer evidence truthiness guard. Keep the existing
receipts.length conditional so populated evidence shows the receipt list and
absent or empty evidence shows the pending-no-evidence fallback, while
preserving the Evidence label.
| function ChannelItem({ item, kind, onFocus }) { | ||
| const label = itemLabel(item, kind); | ||
| const nodeId = item?.node || (Array.isArray(item?.nodes) ? item.nodes[0] : undefined); | ||
| const showFocus = Boolean(nodeId); | ||
| const pending = kind === "pending"; | ||
| const [expanded, setExpanded] = useState(false); | ||
| const toggle = () => setExpanded((value) => !value); | ||
| const fullText = [label.title, label.body, label.meta].filter(Boolean).join(" · "); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file overview =="
wc -l src/ui_assets/channel-bar.js
echo
echo "== ChannelBar and ChannelItem contexts =="
sed -n '1,240p' src/ui_assets/channel-bar.js
echo
echo "== ChannelItem usages =="
rg -n "ChannelItem|ChannelBar|items\.map" src/ui_assets -S || true
echo "== git diff stat/name/status if available =="
git diff --stat -- src/ui_assets/channel-bar.js || true
git diff -- src/ui_assets/channel-bar.js | sed -n '1,220p' || trueRepository: cairn-framework/cairn
Length of output: 11148
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic React/Preact key behavior probe (conceptual via source) =="
python3 - <<'PY'
from pathlib import Path
p = Path("src/ui_assets/channel-bar.js")
text = p.read_text()
checks = {
"ChannelItem has useState expanded": 'const [expanded, setExpanded] = useState(false)' in text,
"ChannelItem has useId expanded": "expanded" in text and ("useId" in text or "useId(" in text) for _ in [1],
"ChannelBar maps no key": "items.map((item) => html`<${ChannelItem}" in text and "key=${item.id}" not in text,
"ChannelBar maps with key": "items.map((item) => html`<${ChannelItem} key=${item.id}" in text,
}
print(checks)
PYRepository: cairn-framework/cairn
Length of output: 502
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== pending rows construction in src/ui_assets/console.js =="
sed -n '1,130p' src/ui_assets/console.js
echo "== app ChannelBar call =="
sed -n '440,470p' src/ui_assets/app.js
echo "== package/framework snippets mentioning preact/react/jsx =="
fd -a 'package.json|README.md|*.blueprint|blueprint.*' . | sed 's#^\./##' | head -50
for f in package.json README.md docs/design-system; do
if [ -f "$f" ]; then echo "--- $f"; sed -n '1,220p' "$f"; fi
done
echo "== toolchain declarations =="
find . -maxdepth 2 -type f \( -name 'package.json' -o -name '*.lock' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'cairn.blueprint' \) -print | sort | while read -r f; do
echo "--- $f"
sed -n '1,220p' "$f"
doneRepository: cairn-framework/cairn
Length of output: 5395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== framework/tool declarations =="
git ls-files | grep -E '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json|cairn\.blueprint|\.blueprint|README\.md)$' | sort | readarray -t files
printf '%s\n' "${files[@]}" | while read -r f; do
echo "--- $f"
sed -n '1,260p' "$f"
done
echo "== HTML helper implementation if relevant =="
cat -n src/ui_assets/utils.js | sed -n '1,220p'
echo "== behavioral check: current ChannelItem call sites without key =="
python3 - <<'PY'
from pathlib import Path
import re
paths = ["src/ui_assets/channel-bar.js", "src/ui_assets/console.js", "src/ui_assets/app.js"]
for p in paths:
text = Path(p).read_text()
m = re.findall(r'<\$\{ChannelItem\}([^>]+)>', text)
print(p + ":", m)
print("has ChannelItem call with key parameter:", bool(re.search(r'<\$\{ChannelItem\}\s+key=', Path("src/ui_assets/channel-bar.js").read_text() + Path("src/ui_assets/console.js").read_text() + Path("src/ui_assets/app.js").read_text())))
PYRepository: cairn-framework/cairn
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files to inspect =="
git ls-files | grep -E '(^package\.json$|pnpm-lock\.yaml$|yarn\.lock$|package-lock\.json$|cairn\.blueprint$|\.blueprint$|README\.md$)' | sort
echo "== relevant declarations =="
find . -maxdepth 2 -type d \( -name .git -prune \) -o -type f \( -name package.json -o -name pn*lock*.yaml -o -name y*lock -o -name cairn.blueprint -o -name .blueprint -o -name README.md \) -print | sort | while read -r f; do
echo "--- $f"
sed -n '1,260p' "$f"
done
echo "== utils.js snippets =="
cat -n src/ui_assets/utils.js | sed -n '1,220p'
echo "== behavioral check: current ChannelItem call sites without key =="
python3 - <<'PY'
from pathlib import Path
import re
files = ["src/ui_assets/channel-bar.js", "src/ui_assets/console.js", "src/ui_assets/app.js"]
all = "".join(Path(f).read_text() for f in files)
print("ChannelItem calls:")
for m in re.findall(r'<\$\{ChannelItem\}([^>]+)>', all):
print(" ->", m.strip())
print("has ChannelItem call with key=", bool(re.search(r'<\$\{ChannelItem\}\s+key=', all)))
print("ChannelItem declaration has useState expanded:", "const [expanded, setExpanded] = useState(false)" in Path("src/ui_assets/channel-bar.js").read_text())
PYRepository: cairn-framework/cairn
Length of output: 40556
Use a stable key for each pending ChannelItem.
ChannelItem stores expanded state. The pending rows mapping passes item as a prop but not key, so if a pending refresh changes row order, a component instance can keep the expansion state of another decision. Add key=${item.id} on the ChannelItem call sites, including the console pendingRows mapping.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ui_assets/channel-bar.js` around lines 163 - 170, The pending ChannelItem
lists lack stable keys, allowing expanded state to move between decisions when
rows reorder. Update every pendingRows ChannelItem call site, including the
console mapping, to pass key={item.id}; keep the existing item prop and
rendering behavior unchanged.
* chore(todos): close the pending briefing unit (#574) * feat(query): surface reverse provenance edges on decisions
Campaign step 5:
todo.pending-queue-briefing. The pending queue now briefs the maintainer in place and a session opens where the last one stopped.Surfaces
PendingDecisiongainsruling_summary,rubric(tier/unblocks/alignment/options), local-tierevidencereceipts with tri-statesubject_hash_matches(required, nullable),changed_since_review,ruling_prompt, andreopen_command.cairn pending <id>renders one briefing on the human and--jsonpaths (unknown id fails withpending.not-found);cairn contextopens with Waiting on you (full briefings), Where work was left (in-progress todos, active changes, in-progress beads underdec.beads-task-layer's read-only authority), and the shared next-recommended selection; pending errors propagate instead of rendering an honest-empty queue.PendingRequestexposes optionalid; registry description names the briefing form.pendingPrompt/pendingRubric/pendingEvidence/pendingReopen) added to the visual harness.manifest::review_path_covered(canonicalise, strip root, slash-normalised for Windows, directory rules); dangling exactaffects:review pointers seed unverified rows; any comparable stale receipt keeps the changed marker while uncomparable hashes render "could not be checked" and never claim change.## Decision), logical rubric-bullet accumulation, terminator counting immune todec.foo/path tokens and closing quotes.Review
Two-lens sequence per repo convention, iterated to zero findings: reforge lens (14 findings, all applied), adversarial lens (REJECT cycles driving 20+ fixes: parser bugs proven on the committed fixture, evidence-path normalisation, continuity gaps, module splits per the 300-line seam rule), final reforge on the shipped tree (10 cleanup findings applied). Both lenses APPROVE
3bd3c9a.Deliberate boundaries (recorded per review)
next_actionwas split intoruling_prompt(chat-first instruction) +reopen_command(exact command) rather than carrying either alone; schema v10 is introduced by this PR so the rename is compatibility-free.Gates: 55 test suites, pre-archive, dogfood Decision pass, scan --strict, biome CI-exact, eval 16/16 scenarios ux_defect_score=0.