diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2d71fdb..75be1d7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hermes-helmet", "displayName": "Hermes Helmet", - "version": "0.3.0", + "version": "0.4.0", "description": "First-officer skills for Hermes Helmet: setup, single-issue delivery, and dependent-issue coordination.", "author": { "name": "Machine Wisdom", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index fd161f3..e88f62d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "hermes-helmet", - "version": "0.3.0", + "version": "0.4.0", "description": "First-officer skills for Hermes Helmet: setup, single-issue delivery, and dependent-issue coordination.", "author": { "name": "Machine Wisdom", diff --git a/CHANGELOG.md b/CHANGELOG.md index a35fd0c..ef34b49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to Hermes Helmet are recorded here. The project follows ## Unreleased +### 0.4.0 + +- Captain’s Bridge now presents the Changes First layout: purpose and outcome + in prose, grouped work items, and details for handoffs, review, evidence and + before/after changes. Evidence opens the original referenced records, Back + restores selection, scroll and focus, and elapsed time and gaps are collapsed + on the overview. Explicit handoff and wakeup relations are recorded without + implying causation or stalls; Hermes steps and readable names are validated + against the selected chat. The panel paints its own foreground and + background and follows the host's light or dark theme, so headings, strong + text and buttons stay readable in a dark Codex panel. + ### 0.3.0 - Add Captain’s Bridge to the Codex plugin: a read-only panel for the invoking diff --git a/mcp/captains-bridge/test_view.cjs b/mcp/captains-bridge/test_view.cjs index f13cc4b..5ab190a 100644 --- a/mcp/captains-bridge/test_view.cjs +++ b/mcp/captains-bridge/test_view.cjs @@ -1,19 +1,48 @@ const fs=require('fs'),vm=require('vm'),assert=require('node:assert/strict'); -class Element{constructor(tag){this.tagName=tag;this.children=[];this.textContent='';this.hidden=false;this.style={setProperty(){}};}append(c){this.children.push(c)}prepend(c){this.children.unshift(c)}replaceChildren(...c){this.children=c}focus(){document.activeElement=this}} +class Element{constructor(tag){this.tagName=tag;this.children=[];this.textContent='';this.hidden=false;this.style={setProperty(){}};this.attrs={};}setAttribute(k,v){this.attrs[k]=v}append(c){this.children.push(c)}prepend(c){this.children.unshift(c)}replaceChildren(...c){this.children=c}focus(){document.activeElement=this}} const roots={app:new Element('main'),error:new Element('div'),connect:new Element('button'),connection:new Element('p')}; function find(e,id){return e.id===id?e:e.children.map(c=>find(c,id)).find(Boolean)} const document={getElementById:id=>roots[id]||Object.values(roots).map(e=>find(e,id)).find(Boolean),createElement:t=>new Element(t),documentElement:new Element('html'),activeElement:null}; -const window={scrollY:0,scrollTo(x,y){this.scrollY=y}},parent={postMessage(){}}; -const context=vm.createContext({document,window,parent,setTimeout,clearTimeout,addEventListener(){},requestAnimationFrame:f=>f(),console}); +const listeners=[];const window={scrollY:0,scrollTo(x,y){this.scrollY=y}},parent={postMessage(){}}; +const context=vm.createContext({document,window,parent,setTimeout,clearTimeout,addEventListener(t,f){if(t==='message')listeners.push(f)},requestAnimationFrame:f=>f(),console}); vm.runInContext(fs.readFileSync('view.html','utf8').match(/ diff --git a/mcp/captains-bridge/walkthrough.py b/mcp/captains-bridge/walkthrough.py index 0af95ed..e995398 100644 --- a/mcp/captains-bridge/walkthrough.py +++ b/mcp/captains-bridge/walkthrough.py @@ -4,6 +4,10 @@ import json import re +# Internal planning identifiers are not human-facing names. +INTERNAL_ID = re.compile(r'\b(?:t_[0-9a-f]{6,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b', re.I) +RELATION_KINDS = ('Handoff', 'Wakeup', 'Follow-up') + def fingerprint(data): evidence = {'records': data.get('records', []), 'turns': [ @@ -26,6 +30,12 @@ def refs(value): raise ValueError('Conclusions need existing record IDs from this bound chat.') return list(dict.fromkeys(value)) + def name(value, label, maximum): + text = prose(value, label, maximum) + if INTERNAL_ID.search(text): + raise ValueError(f'{label} must be a readable name, not an internal planning identifier.') + return text + result = {'objective': prose(account.get('objective'), 'Objective', 600), 'summary': prose(account.get('summary'), 'Summary', 1000), 'evidence': refs(account.get('evidence')), 'items': []} @@ -41,17 +51,26 @@ def refs(value): group = item.get('group') if group not in ('changed', 'unresolved', 'activity'): raise ValueError('Group must be changed, unresolved, or activity.') - out = {'id': key, 'group': group, 'title': prose(item.get('title'), 'Work title', 140), + out = {'id': key, 'group': group, 'title': name(item.get('title'), 'Work title', 140), 'summary': prose(item.get('summary'), 'Work summary', 600), 'detail': prose(item.get('detail'), 'Work explanation'), 'status': prose(item.get('status'), 'Status', 60), 'evidence': refs(item.get('evidence')), - 'steps': [], 'links': []} + 'steps': [], 'links': [], 'relations': []} for step in item.get('steps', [])[:16]: actor = step.get('actor') if actor not in ('Captain', 'First officer', 'Hermes', 'Other agent'): raise ValueError('Use the named actor roles.') - out['steps'].append({'actor': actor, 'label': prose(step.get('label'), 'Step', 140), - 'detail': prose(step.get('detail'), 'Step detail', 1000), 'evidence': refs(step.get('evidence'))}) + cited = refs(step.get('evidence')) + if actor == 'Hermes' and not any('hermes' in records[r]['text'].lower() for r in cited): + raise ValueError('Hermes activity must cite a record in this chat that explicitly mentions Hermes.') + out['steps'].append({'actor': actor, 'label': name(step.get('label'), 'Step', 140), + 'detail': prose(step.get('detail'), 'Step detail', 1000), 'evidence': cited}) + for rel in item.get('relations', [])[:8]: + kind = rel.get('kind') + if kind not in RELATION_KINDS: + raise ValueError('Relation kind must be Handoff, Wakeup, or Follow-up.') + out['relations'].append({'kind': kind, 'label': name(rel.get('label'), 'Relation', 140), + 'detail': prose(rel.get('detail'), 'Relation detail', 1000), 'evidence': refs(rel.get('evidence'))}) for field in ('reported', 'disposition'): if item.get(field): val = item[field] @@ -66,7 +85,7 @@ def refs(value): url = prose(link.get('url'), 'Source URL', 2000) if not re.match(r'^https://[^\s<>]+$', url) or not any(url in records[r]['text'] for r in source): raise ValueError('Links must be HTTPS URLs in their cited records.') - out['links'].append({'label': prose(link.get('label'), 'Link label', 160), 'url': url, 'evidence': source}) + out['links'].append({'label': name(link.get('label'), 'Link label', 160), 'url': url, 'evidence': source}) result['items'].append(out) result['explainedAt'] = data['readAt'] result['fingerprint'] = fingerprint(data) diff --git a/pyproject.toml b/pyproject.toml index bbd147e..01bb9ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hermes-helmet" -version = "0.3.0" +version = "0.4.0" description = "Open-source software factory for coding agents: delegation, review, and repair" readme = "README.md" requires-python = ">=3.11" diff --git a/skills/observe-chat/SKILL.md b/skills/observe-chat/SKILL.md index 44ad422..ba07dad 100644 --- a/skills/observe-chat/SKILL.md +++ b/skills/observe-chat/SKILL.md @@ -58,6 +58,12 @@ and what remains unresolved. `evidence`: record IDs supporting those statements. - `change`: optional `{before, after, explanation, evidence}` for a particular supported change, with a useful before/after representation. Do not invent a before state, a diff or runtime success. Plain comparisons are currently supported. +- `relations`: optional `{kind, label, detail, evidence}` with kind `Handoff`, + `Wakeup`, or `Follow-up`, only where the chat explicitly records the + asynchronous relationship (including across turns). Never infer one from order + or timing alone. Use `Hermes` as actor only when a cited record mentions Hermes. + Titles, labels and link names must be readable; planning IDs and chat UUIDs are + rejected. - `links`: optional `{label, url, evidence}`. Use descriptive issue/PR/review names. URLs must occur in the cited records; never invent a destination. diff --git a/src/hermes_helmet/__init__.py b/src/hermes_helmet/__init__.py index 6cc2e83..7b99fba 100644 --- a/src/hermes_helmet/__init__.py +++ b/src/hermes_helmet/__init__.py @@ -7,7 +7,7 @@ from hermes_helmet.authority import Policy, load_authority, load_policy, render_crew_contract -__version__ = "0.3.0" +__version__ = "0.4.0" __all__ = [ "Policy",