Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,16 @@ body and hands them to the human (sessions cannot create Discussions). Only the
development flow — Mind prompt → `/start_dev` → `/create_issue` → one issue per
task → PR — opens issues here. Why: `PyAutoMind/policy/community_surface.md`.
<!-- repos_sync:filing:end -->

<!-- repos_sync:standards:begin -->
## Shared standards

Before changing a shared interface, consult the applicable
[organism standard](https://github.com/PyAutoLabs/PyAutoBrain/blob/main/docs/standards.md)
on demand, identify affected consumers, and validate their adoption. Change
generated guidance at its canonical source and regenerate.

For board changes, follow the applicable sizing, navigation and orchestration
standards and reuse Brain’s shared components. Keep domain data, prompt meaning
and approval boundaries with the board’s owner.
<!-- repos_sync:standards:end -->
9 changes: 8 additions & 1 deletion insight/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,17 @@ def render_html(views, now=None, campaign_data=None):
)
+ "<main>"
+ input_marker(views, data)
+ campaigns.render_html(data)
+ campaigns.render_html(
data,
work_links=[
{"label": "PyAutoInsight", "href": REPO_URL},
*({"label": v.instance.repo, "href": v.instance.github_url} for v in views),
],
)
+ "<p>Execution completion is separate from convergence and scientific acceptance. Cortex retains scientific conclusions. Evidence remains in project storage.</p>"
+ "".join(_detail(s, now) for s in views)
+ "</main><script>"
+ shared.JS
+ JS
+ "</script></body></html>\n"
)
Expand Down
11 changes: 5 additions & 6 deletions insight/campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def markdown(data: dict) -> str:
return "\n".join(rows)


def render_html(data: dict) -> str:
def render_html(data: dict, work_links=()) -> str:
def e(value):
return html.escape(str(value), quote=True)

Expand Down Expand Up @@ -201,11 +201,10 @@ def table(headers, rows):

return (
marker(data)
+ '<section class="checkin">'
+ theme().prompt_heading("insight")
+ f'<label for="checkin-prompt">Your check-in prompt</label><textarea id="checkin-prompt" rows="8">{e(PROMPT)}</textarea>'
'<button type="button" id="copy-checkin">Copy check-in prompt</button><span id="copy-status" role="status" aria-live="polite"></span>'
f'<p class="muted">Last check-in: {e(data.get("last_checkin") or "not recorded yet")}.</p></section>'
+ theme().orchestration_panel(
"insight", "", "", PROMPT, work_links=work_links, organ="insight"
)
+ f'<p class="muted">Last check-in: {e(data.get("last_checkin") or "not recorded yet")}.</p>'
'<h2 id="campaigns">Active campaigns</h2>'
+ table(
[
Expand Down
2 changes: 1 addition & 1 deletion insight/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ def theme():


CSS = "\nmain{padding-bottom:48px}\nh2,h3{color:var(--accent)}h2{border-bottom:1px solid var(--line);padding-bottom:4px;margin-top:32px}\na{color:var(--accent)}\n.lede{color:var(--muted)}\n.tablewrap{overflow-x:auto}\ntable{border-collapse:collapse;width:100%;font-size:14px}\nth,td{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}\ntd.num{text-align:right;white-space:nowrap}\n.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}\n.notions{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;margin:8px 0}\n.notions div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 12px}\n.notions b{display:block;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}\n.stats{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0}\n.stats div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px}\n.stats b{display:block;font-size:20px;color:var(--accent)}.stats span{font-size:12px;color:var(--muted)}\nbutton[data-copy]{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:left;\nbackground:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:6px;\npadding:3px 6px;cursor:pointer;word-break:break-all}\nbutton.copied{border-color:var(--ok)}\n.checkin textarea{display:block;width:100%;font:inherit;font-size:14px;line-height:1.5;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:12px;margin:8px 0;resize:vertical}\n#copy-checkin{background:var(--accent);color:var(--bg);border:0;border-radius:6px;padding:10px 16px;cursor:pointer;font-weight:600}\n#copy-status{margin-left:12px}\ncode{color:var(--accent);word-break:break-all}\n"
JS = "\nvar copyCheckin=document.getElementById('copy-checkin');\nif(copyCheckin){copyCheckin.addEventListener('click',async function(){\n var field=document.getElementById('checkin-prompt'), status=document.getElementById('copy-status');\n try {\n if(navigator.clipboard && window.isSecureContext){await navigator.clipboard.writeText(field.value)}\n else {field.focus();field.select();if(!document.execCommand('copy')){throw new Error('copy unavailable')}}\n status.textContent='Copied';\n } catch(error){field.focus();field.select();status.textContent='Select and copy the prompt above.'}\n});}\n\ndocument.querySelectorAll('button[data-copy]').forEach(function(b){\n b.addEventListener('click',function(){\n var t=b.getAttribute('data-copy');\n var done=function(){b.classList.add('copied');setTimeout(function(){b.classList.remove('copied')},1200)};\n if(navigator.clipboard){navigator.clipboard.writeText(t).then(done,function(){})}\n });\n});\ndocument.querySelectorAll('[data-age-from]').forEach(function(el){\n var t=Date.parse(el.getAttribute('data-age-from'));\n if(!isNaN(t)){el.textContent=' · '+Math.max(0,Math.floor((Date.now()-t)/864e5))+' d today';}\n});\n"
JS = "\ndocument.querySelectorAll('button[data-copy]').forEach(function(b){\n b.addEventListener('click',function(){\n var t=b.getAttribute('data-copy');\n var done=function(){b.classList.add('copied');setTimeout(function(){b.classList.remove('copied')},1200)};\n if(navigator.clipboard){navigator.clipboard.writeText(t).then(done,function(){})}\n });\n});\ndocument.querySelectorAll('[data-age-from]').forEach(function(el){\n var t=Date.parse(el.getAttribute('data-age-from'));\n if(!isNaN(t)){el.textContent=' · '+Math.max(0,Math.floor((Date.now()-t)/864e5))+' d today';}\n});\n"
2 changes: 1 addition & 1 deletion tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_controlroom_order_and_domain_labels(registry_file, fake_mind):
s = view(registry_file, fake_mind)
html = board.render_html([s], now="2026-10-04T00:00:00Z")
assert (
html.index('id="checkin-prompt"')
html.index('id="orchestration-insight-prompt"')
< html.index('id="campaigns"')
< html.index('id="tasks"')
< html.index('id="evidence"')
Expand Down
42 changes: 32 additions & 10 deletions tests/test_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import pytest
import yaml

from insight import campaigns
from insight.theme import JS
from insight import board, campaigns
from insight.theme import theme


def copied_ledger(tmp_path):
Expand Down Expand Up @@ -70,20 +70,42 @@ def test_closed_tasks_hidden_but_parked_stays_visible():


@pytest.mark.parametrize("mode", ["clipboard", "fallback", "rejected", "failed-fallback"])
def test_copy_button_copies_edited_text_or_selects_manual_fallback(mode):
def test_checkin_copies_complete_shared_preview_or_selects_manual_fallback(mode):
node = os.environ.get("CODEX_PRIMARY_RUNTIME_NODE") or shutil.which("node")
if not node:
pytest.fail("Node runtime required to verify actual copy behavior")
harness = r"""
const assert=require('node:assert/strict');
let callback,copied=null,focused=false,selected=false;
let field={value:'USER EDITED: focus seed 4',focus(){focused=true},select(){selected=true}};
let status={textContent:''};
let field={value:'',defaultValue:'BASE\n\nWork on GitHub:\n- Trusted: https://github.com/example/work',focus(){focused=true},select(){selected=true}};
let status={textContent:''},details={open:false},direction={value:'focus seed 4\nUnicode α'};
let mode=MODE;
global.document={getElementById(id){return id==='copy-checkin'?{addEventListener(_,cb){callback=cb}}:id==='checkin-prompt'?field:status},querySelectorAll(){return []},execCommand(command){assert.equal(command,'copy');if(mode==='failed-fallback')return false;copied=field.value;return true}};
global.window={isSecureContext:mode!=='fallback'&&mode!=='failed-fallback'};
Object.defineProperty(global,'navigator',{value:{clipboard:{async writeText(t){if(mode==='rejected')throw Error('denied');copied=t}}},configurable:true});
let panel={querySelector(s){return s==='[data-orchestration-prompt]'?field:s==='[data-orchestration-direction]'?direction:s==='[data-orchestration-preview]'?details:status}};
let button={closest(){return panel}};
global.document={addEventListener(event,cb){if(event==='click')callback=cb},execCommand(){if(mode==='failed-fallback'||mode==='rejected')return false;copied=field.value;return true}};
global.guardPrompt=()=>true;
Object.defineProperty(global,'navigator',{value:{clipboard:{async writeText(t){if(mode!=='clipboard')throw Error('denied');copied=t}}},configurable:true});
SCRIPT
(async()=>{await callback(); if(mode==='rejected'||mode==='failed-fallback'){assert.equal(copied,null);assert.ok(focused&&selected);assert.match(status.textContent,/Select and copy/)}else{assert.equal(copied,field.value);assert.equal(status.textContent,'Copied')}})().catch(e=>{console.error(e);process.exitCode=1});
""".replace("MODE", json.dumps(mode)).replace("SCRIPT", JS)
(async()=>{await callback({target:{closest(){return button}}});assert.ok(field.value.startsWith(field.defaultValue));assert.ok(field.value.endsWith(direction.value));if(mode==='rejected'||mode==='failed-fallback'){assert.equal(copied,null);assert.ok(focused&&selected&&details.open);assert.match(status.textContent,/copy it manually/)}else{assert.equal(copied,field.value);assert.match(status.textContent,/Prompt copied/);if(mode==='fallback')assert.ok(focused&&selected&&details.open)}})().catch(e=>{console.error(e);process.exitCode=1});
""".replace("MODE", json.dumps(mode)).replace("SCRIPT", theme().ORCHESTRATION_JS)
subprocess.run([node, "-e", harness], check=True, capture_output=True, text=True)


def test_shared_panel_keeps_owner_prompt_and_all_trusted_destinations():
import html
import re

links = [
{"label": "First project", "href": "https://github.com/example/first"},
{"label": "Second project", "href": "https://github.com/example/second"},
]
page = campaigns.render_html(campaigns.load(), work_links=links)
preview = html.unescape(
re.search(r'data-orchestration-prompt readonly rows="8">(.*?)</textarea>', page, re.S)[1]
)
assert preview.startswith(campaigns.PROMPT + "\n\nWork on GitHub:\n")
for link in links:
assert f"- {link['label']}: {link['href']}" in preview
assert page.count("data-orchestration-panel") == 1
assert "data-orchestration-direction" in page
assert "orchestrationSync" in board.render_html([])
Loading