runlist 0.96.0 → 0.97.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/README.md +170 -43
- package/assets/app/app.css +189 -3
- package/assets/app/app.mjs +49 -31
- package/assets/app/document-create.mjs +3 -2
- package/assets/app/document-lifecycle.mjs +17 -8
- package/assets/app/document-stages.mjs +3 -1
- package/assets/app/document-views.mjs +23 -1
- package/assets/app/editor-navigation.mjs +16 -23
- package/assets/app/filing-navigation.mjs +18 -16
- package/assets/app/git-changes.mjs +11 -2
- package/assets/app/index.html +34 -23
- package/assets/app/library-navigation.mjs +29 -16
- package/assets/app/record-navigation.mjs +68 -23
- package/assets/app/recovery-center.mjs +8 -7
- package/assets/app/settings.mjs +48 -10
- package/assets/app/template-editor.mjs +22 -12
- package/assets/app/transport.mjs +1 -2
- package/bin/runlist.mjs +64 -15
- package/package.json +1 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/runlist-codex/skills/runlist/SKILL.md +3 -1
- package/src/app.mjs +9 -4
- package/src/baton.mjs +49 -11
- package/src/brief.mjs +145 -0
- package/src/commands.mjs +4 -3
- package/src/config.mjs +16 -26
- package/src/doctor.mjs +15 -0
- package/src/extractors.mjs +7 -14
- package/src/flags.mjs +5 -3
- package/src/hud.mjs +32 -63
- package/src/index.mjs +2 -0
- package/src/install.mjs +6 -4
- package/src/installation.mjs +117 -0
- package/src/new.mjs +27 -7
- package/src/prompt-handoff.mjs +132 -0
- package/src/prompts.mjs +112 -16
- package/src/section.mjs +46 -11
- package/src/show.mjs +132 -16
- package/src/template-store.mjs +16 -5
- package/src/update.mjs +7 -28
- package/src/util.mjs +7 -0
|
@@ -21,58 +21,103 @@ export function nativeContent(record) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function recordNavigation({state,$,api,open,showLibrary,checkout,preferences=()=>({records:'pending'}),notice,create=async()=>{}}) {
|
|
24
|
-
const root=$('records-home');let kind='flags',selected=null,offset=0,sequence=0,timer,filters={q:'',status:'pending',severity:'',scope:''},pending=null;
|
|
24
|
+
const root=$('records-home'),drafts=new Map();let kind='flags',selected=null,selectedCheckout=null,offset=0,sequence=0,timer,filters={q:'',status:'pending',severity:'',scope:''},pending=null,draft=null,storageError='',storageBlocked=false;
|
|
25
25
|
const title=()=>kind==='flags'?'Flags':'Decisions';
|
|
26
|
-
const recoveryKey=()=>`runlist:record-action:${
|
|
26
|
+
const recoveryKey=()=>`runlist:record-action:${selectedCheckout}:${kind}:${selected?.key}`;
|
|
27
|
+
const draftKey=()=>`runlist:record-draft:${selectedCheckout}:${kind}:${selected?.key}`;
|
|
28
|
+
const formValue=()=>({action:$('record-action').value,note:$('record-note').value,choice:$('record-choice').value});
|
|
29
|
+
const storageMessage='Draft not stored. Keep this window open and retry.';
|
|
30
|
+
function draftStatus(){
|
|
31
|
+
if(!$('record-draft-status'))return;
|
|
32
|
+
const stale=draft&&draft.revision!==selected.revision;
|
|
33
|
+
$('record-draft-status').textContent=storageError||(stale?'Record changed since this draft. Check the current record before continuing.':draft?'Draft':'');
|
|
34
|
+
$('record-draft-status').className=storageError||stale?'inline-note':'muted';
|
|
35
|
+
$('record-draft-current').hidden=!stale||!!pending||!selected.editable;
|
|
36
|
+
$('record-draft-discard').hidden=!draft||!!pending;
|
|
37
|
+
}
|
|
38
|
+
function retainForm(){
|
|
39
|
+
if(!selected||pending||!$('record-action-form')||$('record-action-form').hidden)return;
|
|
40
|
+
draft={...draft,version:1,form:formValue(),revision:draft?.revision??selected.revision,path:selected.path,at:Date.now()};drafts.set(draftKey(),draft);
|
|
41
|
+
try{if(storageBlocked)throw new Error(storageError);localStorage.setItem(draftKey(),JSON.stringify(draft));storageError='';}catch{storageError=storageMessage;}
|
|
42
|
+
draftStatus();
|
|
43
|
+
if(storageError)throw new Error(storageError);
|
|
44
|
+
}
|
|
45
|
+
function beforeLeave(){if(state.mode!=='records'||root.hidden)return;if(selected&&!pending&&$('record-action-form')&&!$('record-action-form').hidden&&(draft||$('record-note').value||$('record-choice').value))retainForm();}
|
|
46
|
+
function restoreForm(form){
|
|
47
|
+
if(!$('record-action-form')||!form)return;
|
|
48
|
+
for(const [id,value] of [['record-action',form.action],['record-choice',form.choice]]){const field=$(id);if(field.tagName==='SELECT'&&value&&!Array.from(field.options).some(o=>o.value===value)){const option=new Option(`${value} (unavailable)`,value);field.append(option);}field.value=value??'';}
|
|
49
|
+
$('record-note').value=form.note??'';updateChoice();
|
|
50
|
+
}
|
|
51
|
+
function updateChoice(){const ruled=$('record-action').value==='ruled';$('record-choice-label').hidden=!ruled;$('record-choice').required=ruled;}
|
|
52
|
+
function changed(){try{retainForm();}catch{}}
|
|
53
|
+
|
|
27
54
|
const url=key=>{const u=new URL(location.href);u.searchParams.set('view',kind);if(key)u.searchParams.set('record',key);else u.searchParams.delete('record');history.replaceState(null,'',u);};
|
|
28
55
|
const run=fn=>async()=>{try{await fn();}catch(error){notice(error.message,true);}};
|
|
29
56
|
function deactivate(){for(const k of ['flags','decisions'])$('library-'+k).setAttribute('aria-pressed','false');}
|
|
30
57
|
function activate(){state.mode='records';$('empty').hidden=true;$('document').hidden=true;root.hidden=false;for(const k of ['flags','decisions'])$('library-'+k).setAttribute('aria-pressed',String(kind===k));for(const k of ['all','hubs','plans','documents'])$('library-'+k).setAttribute('aria-pressed','false');$('doc-title').textContent=title();}
|
|
31
58
|
function setCounts(counts){for(const k of ['flags','decisions'])$('library-'+k+'-count').textContent=counts[k].toLocaleString();}
|
|
32
59
|
async function counts(){const result=await api('records?kind=flags&status=pending');setCounts(result.counts);}
|
|
33
|
-
async function show(next,key){await showLibrary();kind=next;selected=null;offset=0;filters={q:'',status:preferences().records??'pending',severity:'',scope:''};activate();url(key);if(key)await detail(key);else await load();}
|
|
60
|
+
async function show(next,key){beforeLeave();await showLibrary();kind=next;selected=null;offset=0;filters={q:'',status:preferences().records??'pending',severity:'',scope:''};activate();url(key);if(key)await detail(key);else await load();}
|
|
61
|
+
function clearResults(message,{error,retry}={}){
|
|
62
|
+
if(!$('record-results'))return;
|
|
63
|
+
const status=document.createElement('p');status.className='library-no-results';status.textContent=error?`${message}: ${error.message}`:message;status.setAttribute('role',error?'alert':'status');
|
|
64
|
+
$('record-results').replaceChildren(status);$('record-range').textContent=message;$('record-unavailable').textContent='';
|
|
65
|
+
$('record-previous').disabled=true;$('record-next').disabled=true;
|
|
66
|
+
if(retry){const button=document.createElement('button');button.id='record-retry';button.className='quiet';button.textContent='Retry';button.onclick=run(()=>load(false,false));$('record-results').append(button);}
|
|
67
|
+
}
|
|
68
|
+
function searchChanged(){clearTimeout(timer);sequence++;clearResults('Loading records…');$('record-results').setAttribute('aria-busy','false');}
|
|
34
69
|
function shell(){root.innerHTML=`<div class="library-heading"><div><h1>${title()}</h1><p>${kind==='flags'?'Findings to triage, with original evidence and a history of what happened.':'Questions and outcomes across plans and registers, connected to their source.'}</p></div><div class="record-heading-actions"><button id="records-new" class="primary">New ${kind==='flags'?'flag':'decision'}</button><button id="records-refresh" class="quiet small">↻ Refresh</button></div></div><div class="record-controls"><label class="sr-only" for="record-search">Search ${title()}</label><input id="record-search" class="search" type="search" placeholder="Search ID, question, source…" value="${esc(filters.q)}"><label>Status<select id="record-status"><option value="pending">Needs attention</option><option value="">All statuses</option>${(kind==='flags'?['open','closed','resolved']:['open','held','ruled','closed','unknown']).map(s=>`<option value="${s}">${s==='closed'&&kind==='flags'?'Rejected':s[0].toUpperCase()+s.slice(1)}</option>`).join('')}</select></label>${kind==='flags'?'<label>Severity<select id="record-severity"><option value="">All severities</option><option>problem</option><option>warn</option><option>info</option></select></label>':''}<label>Plan or hub<select id="record-scope"><option value="">All sources</option></select></label></div><div class="library-pagination"><span id="record-range" role="status">Loading records…</span><button id="record-previous" class="quiet small">← Previous</button><button id="record-next" class="quiet small">Next →</button></div><nav id="record-results" aria-label="${title()} list" aria-busy="true"></nav><p id="record-unavailable" class="muted"></p>`;
|
|
35
70
|
$('record-status').value=filters.status;if(kind==='flags')$('record-severity').value=filters.severity;
|
|
36
|
-
$('record-search').oninput=()=>{filters.q=$('record-search').value;
|
|
37
|
-
for(const [id,key] of [['record-status','status'],['record-severity','severity'],['record-scope','scope']])if($(id))$(id).onchange=()=>{filters[key]=$(id).value;offset=0;run(()=>load(false,false))();};
|
|
71
|
+
$('record-search').oninput=()=>{filters.q=$('record-search').value;searchChanged();timer=setTimeout(()=>{offset=0;run(()=>load(false,false))();},180);};
|
|
72
|
+
for(const [id,key] of [['record-status','status'],['record-severity','severity'],['record-scope','scope']])if($(id))$(id).onchange=()=>{filters[key]=$(id).value;offset=0;searchChanged();run(()=>load(false,false))();};
|
|
38
73
|
$('records-new').onclick=run(()=>create(kind==='flags'?'flag':'decision'));$('records-refresh').onclick=run(()=>load(true,false));$('record-previous').onclick=run(()=>{offset=Math.max(0,offset-50);return load(false,false);});$('record-next').onclick=run(()=>{offset+=50;return load(false,false);});
|
|
39
74
|
}
|
|
40
|
-
async function load(refresh=false,rebuild=true){if(state.busy)throw new Error('Wait for the current save before navigating.');selected=null;pending=null;if(rebuild)shell();activate();url();const own=++sequence;$('record-results').setAttribute('aria-busy','true')
|
|
75
|
+
async function load(refresh=false,rebuild=true){beforeLeave();if(state.busy)throw new Error('Wait for the current save before navigating.');selected=null;pending=null;if(rebuild)shell();activate();url();const own=++sequence;clearResults('Loading records…');$('record-results').setAttribute('aria-busy','true');
|
|
41
76
|
try{const params=new URLSearchParams({kind,...filters,offset:String(offset)});if(refresh)params.set('refresh','1');const result=await api(`records?${params}`);if(own!==sequence||state.mode!=='records'||selected)return;setCounts(result.counts);offset=result.offset;
|
|
42
77
|
$('record-scope').replaceChildren(new Option('All sources',''),...result.scopes.map(s=>new Option(s.title,s.path)));$('record-scope').value=filters.scope;
|
|
43
78
|
$('record-range').textContent=result.total?`${offset+1}–${offset+result.records.length} of ${result.total.toLocaleString()}`:'0 records';$('record-previous').disabled=!offset;$('record-next').disabled=!result.hasMore;
|
|
44
79
|
$('record-results').replaceChildren(...result.records.map(row=>{const b=document.createElement('button');b.className='record-row quiet';b.innerHTML=`<span class="record-row-label"><strong>${esc(row.title||row.id||'Unnamed decision')}</strong><small>${esc(row.path)}${row.line?`:${row.line}`:''}</small></span><span class="record-row-meta"><span class="record-id">${esc(row.id??'No ID')}</span><span class="status-badge">${esc(row.status)}</span>${row.severity?`<span class="severity ${esc(row.severity)}">${esc(row.severity)}</span>`:''}${row.triage==='accepted'?'<small>Accepted</small>':''}</span>`;b.onclick=run(()=>detail(row.key));return b;}));
|
|
45
80
|
if(!result.records.length)$('record-results').innerHTML='<p class="library-no-results">No records match these filters. Choose All statuses or try another search.</p>';
|
|
46
|
-
$('record-unavailable').textContent=result.unavailable?`${result.unavailable} unavailable sources. Refresh to retry.`:'';
|
|
47
|
-
}catch(error){if(own===sequence
|
|
81
|
+
$('record-unavailable').textContent=result.unavailable?`${result.unavailable} unavailable sources. Refresh to retry.`:'';
|
|
82
|
+
}catch(error){if(own===sequence&&state.mode==='records'&&!selected)clearResults(filters.q?'Search unavailable':'Records unavailable',{error,retry:true});}finally{if(own===sequence&&$('record-results'))$('record-results').setAttribute('aria-busy','false');}
|
|
48
83
|
}
|
|
49
84
|
function connectionHtml(links){return links.length?`<div class="record-connections">${links.map((link,i)=>`<button class="quiet small" data-connection="${i}" title="${esc(link.path)}">${esc(link.title)} <span class="muted">${esc(link.kind==='hub'?'Hub':link.type??'Doc')}</span></button>`).join('')}</div>`:'<p class="muted">No linked documents were found in this library.</p>';}
|
|
50
85
|
function historyHtml(history){return history?.length?`<ol class="record-history">${[...history].reverse().map(h=>`<li><strong>${esc(h.event??h.kind??'Recorded')}</strong> <span class="muted">${esc(h.by?.name??h.by?.label??h.by?.id??'')} · ${esc(h.at??'')}</span><p>${esc(h.note??h.reason??'')}</p></li>`).join('')}</ol>`:'<p class="muted">No later actions have been recorded.</p>';}
|
|
51
|
-
async function detail(key,{allowBusy=false}={}){if(state.busy&&!allowBusy)throw new Error('Wait for the current save before navigating.');const own=++sequence,wantedKind=kind;clearTimeout(timer);
|
|
86
|
+
async function detail(key,{allowBusy=false,committed=false}={}){if(!committed)beforeLeave();if(state.busy&&!allowBusy)throw new Error('Wait for the current save before navigating.');const own=++sequence,wantedKind=kind,wantedCheckout=checkout();clearTimeout(timer);const row=await api(`records?${new URLSearchParams({kind,key})}`);if(own!==sequence||kind!==wantedKind||checkout()!==wantedCheckout||state.mode!=='records')return;selected=row;selectedCheckout=wantedCheckout;pending=null;draft=null;storageError='';storageBlocked=false;
|
|
87
|
+
const unreadable=()=>{storageBlocked=true;storageError='Retained record data could not be read. Keep this window open and check local storage.';};
|
|
88
|
+
try{pending=JSON.parse(localStorage.getItem(recoveryKey())??'null');if(pending&&(!pending.request||!['native/action','flags/triage','save'].includes(pending.route)||typeof pending.request.operationId!=='string'))throw new Error('Invalid review');}catch{pending=null;unreadable();}
|
|
89
|
+
try{draft=drafts.get(draftKey())??JSON.parse(localStorage.getItem(draftKey())??'null');if(draft&&(!draft.form||typeof draft.form.note!=='string'||typeof draft.form.action!=='string'||typeof draft.form.choice!=='string'))throw new Error('Invalid draft');}catch{draft=drafts.get(draftKey())??null;unreadable();}
|
|
90
|
+
activate();url(key);renderDetail();window.scrollTo(0,0);
|
|
91
|
+
}
|
|
52
92
|
function renderDetail(){const r=selected;
|
|
53
93
|
let content;
|
|
54
94
|
if(r.native)content=nativeContent(r)+`<article class="markdown record-body">${markdownHtml(r.body)}</article><details class="record-structured"><summary>Structured record details</summary><pre>${esc(JSON.stringify(r.data??{},null,2))}</pre></details>`;
|
|
55
95
|
else if(kind==='flags')content=`<div class="record-evidence"><section><h2>Original observation</h2><p class="muted">${esc(r.original.file)}${r.original.line?`:${r.original.line}`:''} · ${esc(r.original.at)}</p><pre>${esc(r.original.quote||'File-level finding; no quote was recorded.')}</pre><p class="muted">${esc(r.original.revisionReason)}</p></section><section><h2>Current evidence <span class="status-badge">${esc(r.current.status)}</span></h2><p class="muted">${r.current.line?`Line ${r.current.line}`:'Location unavailable'}</p><pre>${esc(r.current.quote??'Evidence is unavailable or ambiguous.')}</pre><p class="muted">The original observation stays in the record.</p></section></div><h2>History</h2>${historyHtml(r.history)}`;
|
|
56
96
|
else content=decisionContent(r.body)+`${r.missing?.length?`<p class="inline-note">Record gaps: ${esc(r.missing.join(', '))}. Open the source to fill them in.</p>`:''}${r.blocks?.length?`<h2>Blocking work</h2><ul>${r.blocks.map(b=>`<li>${esc(typeof b==='string'?b:b.text)}${b.doc?` <small class="muted">${esc(b.doc)}:${b.line}</small>`:''}</li>`).join('')}</ul>`:''}${r.peers?.length?`<h2>Related records</h2>${r.peers.map(p=>`<details><summary>${esc(p.id)} · ${esc(p.status)} · ${esc(p.path)}</summary><article class="markdown">${markdownHtml(p.body)}</article></details>`).join('')}`:''}`;
|
|
57
|
-
root.innerHTML=`<div class="record-back"><button id="records-back" class="quiet small">← ${title()}</button><button id="record-reload" class="quiet small">↻ Reload record</button></div><div class="record-heading"><div class="record-eyebrow">${esc(r.id??'Unnamed decision')} · <span class="status-badge">${esc(r.status)}</span> ${r.severity?`· ${esc(r.severity)}`:''}</div><h1>${esc(r.title)}</h1><p class="muted">${esc(r.path)}${r.line?`:${r.line}`:''}</p></div><section class="record-links"><h2>Connected work</h2>${connectionHtml(r.connections??[])}</section>${content}<section class="record-action"><h2>${kind==='flags'?'Triage this flag':'Record an outcome'}</h2>${r.editable?`<form id="record-action-form"><div class="record-action-fields"><label>${kind==='flags'?'Action':'Disposition'}<select id="record-action">${(kind==='flags'?(r.native&&r.status!=='open'?['reopen']:['accept','reject','resolve']):['held','ruled','closed','open']).map(v=>`<option value="${v}">${v[0].toUpperCase()+v.slice(1)}</option>`).join('')}</select></label><label id="record-choice-label" hidden>Selected answer${r.native&&kind==='decisions'?`<select id="record-choice"><option value="">Choose an existing alternative</option>${(r.data?.options??[]).map(o=>`<option value="${esc(o.id)}">${esc(o.label)}</option>`).join('')}</select>`:'<input id="record-choice" maxlength="4000" placeholder="Which answer was chosen?">'}</label></div><label>Reason<textarea id="record-note" required maxlength="4000" rows="3" placeholder="Why is this the right next step?"></textarea></label><
|
|
97
|
+
root.innerHTML=`<div class="record-back"><button id="records-back" class="quiet small">← ${title()}</button><button id="record-reload" class="quiet small">↻ Reload record</button></div><div class="record-heading"><div class="record-eyebrow">${esc(r.id??'Unnamed decision')} · <span class="status-badge">${esc(r.status)}</span> ${r.severity?`· ${esc(r.severity)}`:''}</div><h1>${esc(r.title)}</h1><p class="muted">${esc(r.path)}${r.line?`:${r.line}`:''}</p></div><section class="record-links"><h2>Connected work</h2>${connectionHtml(r.connections??[])}</section>${content}<section class="record-action"><h2>${kind==='flags'?'Triage this flag':'Record an outcome'}</h2>${r.editable?`<form id="record-action-form"><div class="record-action-fields"><label>${kind==='flags'?'Action':'Disposition'}<select id="record-action">${(kind==='flags'?(r.native&&r.status!=='open'?['reopen']:['accept','reject','resolve']):['held','ruled','closed','open']).map(v=>`<option value="${v}">${v[0].toUpperCase()+v.slice(1)}</option>`).join('')}</select></label><label id="record-choice-label" hidden>Selected answer${r.native&&kind==='decisions'?`<select id="record-choice"><option value="">Choose an existing alternative</option>${(r.data?.options??[]).map(o=>`<option value="${esc(o.id)}">${esc(o.label)}</option>`).join('')}</select>`:'<input id="record-choice" maxlength="4000" placeholder="Which answer was chosen?">'}</label></div><label>Reason<textarea id="record-note" required maxlength="4000" rows="3" placeholder="Why is this the right next step?"></textarea></label><button id="record-review-button" class="primary">Review ${kind==='flags'?'triage':'outcome'}</button></form>`:`<p class="muted">${r.readOnlyReason?esc(r.readOnlyReason):r.native?'Repair the native record diagnostics before recording an action.':kind==='flags'?'This flag has already been closed. Its observation and history are retained.':'This record is read only or has no stable ID. Open its source to inspect or complete it.'}</p>`}<p id="record-draft-status" class="muted" role="status"></p><div class="record-review-actions"><button id="record-draft-current" class="quiet small" hidden>Use current record</button><button id="record-draft-discard" class="quiet small" hidden>Discard draft</button></div><div id="record-review" hidden></div></section>`;
|
|
58
98
|
$('records-back').onclick=run(()=>load());$('record-reload').onclick=run(async()=>{await api(`records?kind=${kind}&refresh=1`);await detail(r.key);});
|
|
59
|
-
for(const b of root.querySelectorAll('[data-connection]'))b.onclick=run(()=>open(r.connections[Number(b.dataset.connection)].path));
|
|
60
|
-
if(r.editable){$('record-action').onchange=()=>{const
|
|
61
|
-
if(
|
|
99
|
+
for(const b of root.querySelectorAll('[data-connection]'))b.onclick=run(()=>{beforeLeave();return open(r.connections[Number(b.dataset.connection)].path);});
|
|
100
|
+
if(r.editable){$('record-action').onchange=()=>{updateChoice();changed();};for(const id of ['record-note','record-choice']){$(id).oninput=changed;$(id).onchange=changed;}$('record-action-form').onsubmit=e=>{e.preventDefault();return run(review)();};restoreForm(draft?.form);}
|
|
101
|
+
else if(draft){const retained=document.createElement('pre');retained.className='record-body';retained.textContent=`Draft · ${draft.form.action}${draft.form.choice?` · ${draft.form.choice}`:''}\n${draft.form.note}`;$('record-draft-status').before(retained);}
|
|
102
|
+
$('record-draft-current').onclick=run(async()=>{if(storageBlocked)throw new Error(storageError);draft.revision=selected.revision;retainForm();});
|
|
103
|
+
$('record-draft-discard').onclick=run(async()=>{if(storageBlocked)throw new Error(storageError);try{localStorage.removeItem(draftKey());}catch{throw new Error('Draft could not be discarded. Try again.');}drafts.delete(draftKey());draft=null;storageError='';renderDetail();});
|
|
104
|
+
draftStatus();if(pending)renderReview();
|
|
62
105
|
}
|
|
63
|
-
async function review(){if(state.busy)return;const action=$('record-action').value,note=$('record-note').value,choice=$('record-choice').value;
|
|
64
|
-
if(
|
|
65
|
-
else if(kind==='flags')
|
|
66
|
-
else {const preview=await api('decisions/preview',{key:
|
|
106
|
+
async function review(){if(state.busy)return;if(selectedCheckout!==checkout())throw new Error('Open this record in its original checkout.');if(storageBlocked)throw new Error(storageError);retainForm();if(draft.revision!==selected.revision)throw new Error('Check the changed record, then choose Use current record.');const allowed=kind==='flags'?(selected.native&&selected.status!=='open'?['reopen']:['accept','reject','resolve']):['held','ruled','closed','open'];if(!allowed.includes($('record-action').value))throw new Error('Choose an available action.');if(selected.native&&$('record-action').value==='ruled'&&!(selected.data?.options??[]).some(o=>o.id===$('record-choice').value))throw new Error('Choose an available alternative.');const action=$('record-action').value,note=$('record-note').value,choice=$('record-choice').value,own=sequence,wantedCheckout=checkout(),record=selected;let reviewed;
|
|
107
|
+
if(record.native){const preview=await api('native/preview',{path:record.path,expectedRevision:record.revision,operationId:crypto.randomUUID(),action,note,optionId:choice});reviewed={form:{action,note,choice},route:'native/action',request:preview.request,before:preview.before,label:`Record ${action}`,note};}
|
|
108
|
+
else if(kind==='flags')reviewed={form:{action,note,choice},route:'flags/triage',request:{key:record.key,action,note,expectedRevision:record.revision,operationId:crypto.randomUUID()},label:`${action[0].toUpperCase()+action.slice(1)} ${record.id}`,note};
|
|
109
|
+
else {const preview=await api('decisions/preview',{key:record.key,expectedRevision:record.revision,disposition:action,note,choice});reviewed={form:{action,note,choice},route:'save',request:{path:preview.path,source:preview.source,expectedRevision:preview.expectedRevision,operationId:crypto.randomUUID()},before:preview.before,label:`Record ${action} for ${record.id}`,note};}
|
|
67
110
|
// Retain exactly the reviewed request before any write, including its UUID.
|
|
68
|
-
|
|
111
|
+
if(own!==sequence||wantedCheckout!==checkout()||state.mode!=='records')return;pending={...reviewed,path:record.path,at:Date.now()};
|
|
112
|
+
try{localStorage.setItem(recoveryKey(),JSON.stringify(pending));}catch{pending=null;storageError='Review not stored. Keep this window open and retry.';draftStatus();throw new Error(storageError);}renderReview();
|
|
69
113
|
}
|
|
70
|
-
function renderReview(){if($('record-action-form'))$('record-action-form').hidden=true;$('record-review').hidden=false;$('record-review').innerHTML=`<h3>${esc(pending.label)}</h3><p>${esc(pending.note)}</p>${pending.before?`<div class="diff">${diffHtml(pending.before,pending.request.source)}</div>`:'
|
|
71
|
-
async function confirm(){if(state.busy)return;state.busy=true;$('record-confirm').disabled=true;$('record-change').disabled=true;
|
|
72
|
-
try{await api(pending.route,pending.request);localStorage.removeItem(recoveryKey());pending=null;await
|
|
73
|
-
catch(error){if($('record-action-feedback'))$('record-action-feedback').textContent=`${error.message}
|
|
114
|
+
function renderReview(){draftStatus();if($('record-action-form'))$('record-action-form').hidden=true;$('record-review').hidden=false;$('record-review').innerHTML=`<h3>${esc(pending.label)}</h3><p>${esc(pending.note)}</p>${pending.before?`<div class="diff">${diffHtml(pending.before,pending.request.source)}</div>`:''}<div class="record-review-actions"><button id="record-confirm" class="primary">Confirm ${kind==='flags'?'triage':'outcome'}</button><button id="record-change" class="quiet">Back to form</button></div><p id="record-action-feedback" role="status"></p>`;$('record-confirm').onclick=run(confirm);$('record-change').onclick=run(async()=>{const form=pending.form??draft?.form;if(!selected.editable)throw new Error('The record is read only. Keep this reviewed request for inspection.');if(storageBlocked)throw new Error(storageError);const restored={version:1,form,revision:pending.request.expectedRevision??draft?.revision??selected.revision,path:selected.path,at:Date.now()};try{localStorage.setItem(draftKey(),JSON.stringify(restored));localStorage.removeItem(recoveryKey());}catch{throw new Error('The review is retained. Local storage must be available before returning to the form.');}draft=restored;drafts.set(draftKey(),draft);pending=null;$('record-review').hidden=true;$('record-action-form').hidden=false;restoreForm(form);draftStatus();});}
|
|
115
|
+
async function confirm(){if(state.busy||!pending)return;state.busy=true;$('record-confirm').disabled=true;$('record-change').disabled=true;
|
|
116
|
+
try{if(selectedCheckout!==checkout())throw new Error('Open this review in its original checkout.');await api(pending.route,pending.request);try{localStorage.removeItem(draftKey());localStorage.removeItem(recoveryKey());}catch{throw new Error('Saved, but retained local data could not be cleared. Retry safely with the same request.');}drafts.delete(draftKey());draft=null;pending=null;try{await detail(selected.key,{allowBusy:true,committed:true});await counts();}catch(error){notice(`Saved; refresh unavailable. ${error.message}`,true);}}
|
|
117
|
+
catch(error){if($('record-action-feedback'))$('record-action-feedback').textContent=`${error.message} Reviewed request retained; retry when ready.`;throw error;}
|
|
74
118
|
finally{state.busy=false;if($('record-confirm'))$('record-confirm').disabled=false;if($('record-change'))$('record-change').disabled=false;}
|
|
75
119
|
}
|
|
76
120
|
for(const k of ['flags','decisions'])$('library-'+k).onclick=run(()=>show(k));
|
|
77
|
-
|
|
121
|
+
window.addEventListener('beforeunload',event=>{try{beforeLeave();}catch{event.preventDefault();event.returnValue='';}});
|
|
122
|
+
return {show,counts,deactivate,beforeLeave};
|
|
78
123
|
}
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
export function recoveryCenter({$,api,checkout,beforeNavigate,resume,notice}) {
|
|
2
2
|
const dialog=$('recovery-dialog');let rows=[],offset=0,hasMore=false,sequence=0;
|
|
3
3
|
function localItems(){
|
|
4
|
-
const items=[];let unreadable=0;
|
|
4
|
+
const items=[],reviewed=new Set();let unreadable=0;
|
|
5
5
|
try { for(let i=0;i<localStorage.length;i++) {
|
|
6
|
-
const key=localStorage.key(i),prefix=`runlist:recovery:${checkout()}:`,lifecycle=`runlist:lifecycle:${checkout()}:`,record=`runlist:record-action:${checkout()}:`,yardstick=`runlist:yardstick:${checkout()}:`;
|
|
7
|
-
if(!key.startsWith(prefix)&&!key.startsWith(lifecycle)&&!key.startsWith(record)&&!key.startsWith(yardstick)&&key!==`runlist:create:${checkout()}`)continue;
|
|
6
|
+
const key=localStorage.key(i),prefix=`runlist:recovery:${checkout()}:`,lifecycle=`runlist:lifecycle:${checkout()}:`,record=`runlist:record-action:${checkout()}:`,recordDraft=`runlist:record-draft:${checkout()}:`,yardstick=`runlist:yardstick:${checkout()}:`;
|
|
7
|
+
if(!key.startsWith(prefix)&&!key.startsWith(lifecycle)&&!key.startsWith(record)&&!key.startsWith(recordDraft)&&!key.startsWith(yardstick)&&key!==`runlist:create:${checkout()}`)continue;
|
|
8
8
|
try {
|
|
9
9
|
const value=JSON.parse(localStorage.getItem(key));if(!value)continue;
|
|
10
10
|
if(key.startsWith(prefix)&&typeof value.path==='string'&&typeof value.source==='string'&&typeof value.baseSource==='string'&&(value.source!==value.baseSource||value.pending))items.push({kind:'local-draft',path:value.path,location:key,draftId:value.draft?.id,operationId:value.pending?.operationId,at:new Date(value.at).toISOString(),stale:!!value.conflicted,state:value.pending?'interrupted':value.conflicted?'conflict':'draft',available:true});
|
|
11
11
|
else if(key.startsWith(yardstick))items.push({kind:'yardstick',path:value.path,operationId:value.operationId,at:'',available:true,state:value.state??'retained'});
|
|
12
12
|
else if(key.startsWith(lifecycle))items.push({kind:'lifecycle',path:value.path,operationId:value.operationId,at:'',available:true,state:'retained'});
|
|
13
|
-
else if(key.startsWith(record)){const suffix=key.slice(record.length),colon=suffix.indexOf(':');items.push({kind:'record',recordKind:suffix.slice(0,colon),recordKey:suffix.slice(colon+1),path:value.request
|
|
13
|
+
else if(key.startsWith(record)){if(!value.request||!['native/action','flags/triage','save'].includes(value.route)||typeof value.request.operationId!=='string')throw new Error('Invalid review');const suffix=key.slice(record.length),colon=suffix.indexOf(':');reviewed.add(key);items.push({kind:'record',recordKind:suffix.slice(0,colon),recordKey:suffix.slice(colon+1),location:key,path:value.path??value.request.path??value.label??'Flag or decision',operationId:value.request.operationId,at:value.at?new Date(value.at).toISOString():'',available:true,state:'retained'});}
|
|
14
|
+
else if(key.startsWith(recordDraft)&&value.form&&typeof value.form.note==='string'){const suffix=key.slice(recordDraft.length),colon=suffix.indexOf(':');items.push({kind:'record',reviewLocation:record+suffix,recordKind:suffix.slice(0,colon),recordKey:suffix.slice(colon+1),location:key,path:value.path??'Flag or decision',at:new Date(value.at).toISOString(),available:true,state:'draft'});}
|
|
14
15
|
else if(key===`runlist:create:${checkout()}`&&(value.form?.title||value.form?.body||value.review))items.push({kind:'creation',operationId:value.review?.operationId,path:value.review?.path??`${value.form?.folder??''}/${value.form?.filename??'New document'}`,state:value.review?.state??'draft',at:new Date(value.at).toISOString(),available:true});
|
|
15
16
|
}catch{unreadable++;}
|
|
16
17
|
}
|
|
17
18
|
} catch {unreadable++;}
|
|
18
|
-
return {items,unreadable};
|
|
19
|
+
return {items:items.filter(row=>!reviewed.has(row.reviewLocation)),unreadable};
|
|
19
20
|
}
|
|
20
|
-
const label=row=>row.kind==='git-commit'?`Local Git commit · ${row.state}`:row.kind==='creation'?'New document draft':row.kind==='yardstick'?`Product goal assessment · ${row.state}`:row.kind==='lifecycle'?`Status change · ${row.state}`:row.kind==='record'?'Flag
|
|
21
|
+
const label=row=>row.kind==='git-commit'?`Local Git commit · ${row.state}`:row.kind==='creation'?'New document draft':row.kind==='yardstick'?`Product goal assessment · ${row.state}`:row.kind==='lifecycle'?`Status change · ${row.state}`:row.kind==='record'?`${row.recordKind==='flags'?'Flag':'Decision'} ${row.state==='draft'?'draft':'review'}`:row.kind==='operation'?'Interrupted save · inspect outcome':row.operationId||row.pendingOperation?'Interrupted save · draft preserved locally':row.stale?'File changed · compare draft':'Draft preserved locally';
|
|
21
22
|
function render(){
|
|
22
23
|
const query=$('recovery-filter').value.toLowerCase(),visible=rows.filter(row=>`${row.path} ${label(row)}`.toLowerCase().includes(query));
|
|
23
24
|
$('recovery-items').replaceChildren(...visible.map(row=>{
|
|
24
25
|
const section=document.createElement('section');section.className='recovery-item';
|
|
25
26
|
const info=document.createElement('div'),title=document.createElement('strong'),detail=document.createElement('p'),date=document.createElement('small');title.textContent=row.path;detail.textContent=label(row);date.textContent=row.at?new Date(row.at).toLocaleString():'';info.append(title,detail,date);
|
|
26
|
-
const button=document.createElement('button');button.className='quiet';button.textContent=!row.available?'File unavailable':row.kind==='creation'?'Resume creation':row.kind==='git-commit'||row.kind==='yardstick'||row.kind==='lifecycle'||row.kind==='operation'?'Inspect operation':row.kind==='record'?'Resume review':'Resume draft';button.disabled=!row.available;
|
|
27
|
+
const button=document.createElement('button');button.className='quiet';button.textContent=!row.available?'File unavailable':row.kind==='creation'?'Resume creation':row.kind==='git-commit'||row.kind==='yardstick'||row.kind==='lifecycle'||row.kind==='operation'?'Inspect operation':row.kind==='record'&&row.state!=='draft'?'Resume review':'Resume draft';button.disabled=!row.available;
|
|
27
28
|
button.onclick=async()=>{try{dialog.close();await resume(row);}catch(error){notice(error.message,true);}};section.append(info,button);return section;
|
|
28
29
|
}));
|
|
29
30
|
if(!visible.length){const message=document.createElement('p');message.className='library-no-results';message.textContent=query?'No recovery items match this filter.':'No unfinished work in this checkout.';$('recovery-items').append(message);}
|
package/assets/app/settings.mjs
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
import {desktop} from './transport.mjs';
|
|
2
2
|
import {escapeHtml as esc} from './shared.mjs';
|
|
3
|
-
export function appSettings({$,api,checkout,library
|
|
4
|
-
const key=
|
|
3
|
+
export function appSettings({$,api,checkout,library}){
|
|
4
|
+
const key=owner=>`runlist:preferences:${owner}`;
|
|
5
5
|
const defaults={sort:'updated',archived:false,records:'pending'};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
let sequence=0;
|
|
7
|
+
function readPreferences(owner){try{const p=JSON.parse(localStorage.getItem(key(owner))??'{}');return {value:{sort:['updated','title','path'].includes(p.sort)?p.sort:'updated',archived:p.archived===true,records:p.records===''?'':'pending'}};}catch(error){return {value:{...defaults},error};}}
|
|
8
|
+
const read=()=>readPreferences(checkout()).value;
|
|
9
|
+
const detail=error=>error?.message||'Please try again.';
|
|
10
|
+
async function show(){
|
|
11
|
+
const own=++sequence,owner=checkout(),prefs=readPreferences(owner),current=()=>own===sequence&&owner===checkout()&&$('settings-dialog').open;
|
|
12
|
+
$('settings-checkout').textContent=$('checkout').textContent;
|
|
13
|
+
$('settings-content').innerHTML=`<section class="settings-section"><h3>Preferences</h3><form id="settings-preferences"><div class="settings-fields"><label>Appearance<select id="settings-theme"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label><label>Default document order<select id="settings-sort"><option value="updated">Recently updated</option><option value="title">Title</option><option value="path">File path</option></select></label><label>Default records view<select id="settings-records"><option value="pending">Needs attention</option><option value="">All statuses</option></select></label><label class="settings-check"><input id="settings-archived" type="checkbox"> Include archived documents by default</label></div><button class="primary">Save preferences</button><p id="settings-feedback" role="status"></p></form></section><section class="settings-section"><h3>Templates</h3><button id="settings-templates" class="quiet">Open templates</button></section><details id="settings-advanced" class="settings-section"><summary>Advanced</summary><p class="muted">Preferences stay ${desktop?'on this computer':'in this browser'} for this checkout.</p><section id="settings-semantic-section" class="settings-section"></section><section id="settings-repository-section" class="settings-section"></section></details>`;
|
|
14
|
+
const feedback=$('settings-feedback'),form=$('settings-preferences'),save=form.querySelector('button');
|
|
15
|
+
$('settings-theme').value=$('theme-mode').value;$('settings-sort').value=prefs.value.sort;$('settings-archived').checked=prefs.value.archived;$('settings-records').value=prefs.value.records;
|
|
16
|
+
if(prefs.error)feedback.textContent=`Saved preferences could not be read: ${detail(prefs.error)} Defaults are shown. Try saving again.`;
|
|
17
|
+
form.onsubmit=async event=>{
|
|
18
|
+
event.preventDefault();if(!current()||save.disabled)return;
|
|
19
|
+
const preferences={sort:$('settings-sort').value,archived:$('settings-archived').checked,records:$('settings-records').value},theme=$('settings-theme').value;
|
|
20
|
+
save.disabled=true;feedback.textContent='';
|
|
21
|
+
try{localStorage.setItem(key(owner),JSON.stringify(preferences));}
|
|
22
|
+
catch(error){feedback.textContent=`Preferences could not be saved: ${detail(error)} Try again.`;save.disabled=false;return;}
|
|
23
|
+
let appearanceError;
|
|
24
|
+
try{localStorage.setItem('runlist:appearance',theme);$('theme-mode').value=theme;$('theme-mode').dispatchEvent(new Event('change'));}catch(error){appearanceError=error;}
|
|
25
|
+
let refreshError;try{await library.preferences(preferences);}catch(error){refreshError=error;}
|
|
26
|
+
if(current()){
|
|
27
|
+
feedback.textContent=[appearanceError?`Document preferences saved. Appearance could not be saved: ${detail(appearanceError)} Try again.`:'Preferences saved.',refreshError?`Library could not refresh: ${detail(refreshError)} Use Refresh in the library.`:''].filter(Boolean).join(' ');
|
|
28
|
+
save.disabled=false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
$('settings-templates').onclick=()=>{sequence++;$('settings-dialog').close();$('open-templates').click();};
|
|
32
|
+
$('settings-dialog').showModal();
|
|
33
|
+
async function optional(sectionId,title,route,render){
|
|
34
|
+
const section=$(sectionId);section.innerHTML=`<h3>${title}</h3><p role="status">Loading…</p>`;
|
|
35
|
+
try{const value=await api(route);if(current())render(section,value);}
|
|
36
|
+
catch(error){if(!current())return;section.innerHTML=`<h3>${title}</h3><p id="${sectionId}-feedback" role="status">Could not load: ${esc(detail(error))}</p><button id="${sectionId}-retry" class="quiet">Retry</button>`;section.querySelector('button').onclick=()=>optional(sectionId,title,route,render);}
|
|
37
|
+
}
|
|
38
|
+
const tool=optional('settings-semantic-section','Semantic search · experimental','semantic/settings',(section,tool)=>{
|
|
39
|
+
section.innerHTML=`<h3>Semantic search · experimental</h3><p class="muted">${esc(tool.message)} Leave paths blank for automatic discovery.</p><form id="settings-semantic"><div class="settings-fields"><label>External Node executable<input id="settings-semantic-node" autocomplete="off" placeholder="Absolute path to node"></label><label>gmax entry file<input id="settings-semantic-entry" autocomplete="off" placeholder="Absolute path to grepmax/dist/bin.js"></label></div><button class="quiet">Save tool paths</button><p id="settings-semantic-feedback" role="status"></p></form>`;
|
|
40
|
+
const node=$('settings-semantic-node'),entry=$('settings-semantic-entry'),form=$('settings-semantic'),feedback=$('settings-semantic-feedback'),save=form.querySelector('button');node.value=tool.nodePath??'';entry.value=tool.entryPath??'';
|
|
41
|
+
form.onsubmit=async event=>{event.preventDefault();if(!current()||save.disabled)return;save.disabled=true;feedback.textContent='';try{const saved=await api('semantic/settings',{nodePath:node.value.trim(),entryPath:entry.value.trim()});if(current())feedback.textContent=saved.message;}catch(error){if(current())feedback.textContent=`Tool paths could not be saved: ${detail(error)} Try again.`;}finally{if(current())save.disabled=false;}};
|
|
42
|
+
});
|
|
43
|
+
const repository=optional('settings-repository-section','Checkout rules','settings',(section,info)=>{
|
|
44
|
+
if(info.checkout)$('settings-checkout').textContent=info.checkout;
|
|
45
|
+
section.innerHTML=`<h3>Checkout rules</h3><p class="muted">${esc(info.configFile??'Built-in defaults')}</p><dl class="settings-paths"><dt>Document roots</dt><dd>${info.roots.map(p=>`<code>${esc(p)}</code>`).join(' ')}</dd><dt>Excluded folders</dt><dd>${info.exclusions.map(p=>`<code>${esc(p)}</code>`).join(' ')}</dd></dl>${info.statuses.typeOrder.map(type=>`<details class="settings-rules"><summary>${esc(type)} · ${info.statuses.byType[type].length} statuses</summary><div class="settings-table"><table><thead><tr><th>Status</th><th>Context</th><th>Behavior</th></tr></thead><tbody>${info.statuses.byType[type].map(s=>`<tr><td>${esc(s.name)}</td><td>${esc(s.context)}</td><td>${esc([s.startable?'Can start':null,s.terminal?'Terminal':null,s.archive?'Archives':null,s.filed?`Files in ${s.filed}`:null,s.staleDays?`Stale after ${s.staleDays} days`:null].filter(Boolean).join(' · ')||'Ordinary')}</td></tr>`).join('')}</tbody></table></div></details>`).join('')}<h3>Where changes live</h3><dl class="settings-paths">${Object.entries(info.sharing).map(([name,value])=>`<dt>${esc(name)}</dt><dd>${esc(value)}</dd>`).join('')}</dl>`;
|
|
46
|
+
});
|
|
47
|
+
await Promise.all([tool,repository]);
|
|
13
48
|
}
|
|
14
|
-
$('open-settings').onclick=
|
|
49
|
+
$('open-settings').onclick=show;
|
|
50
|
+
$('settings-close').onclick=()=>{sequence++;$('settings-dialog').close();};
|
|
51
|
+
$('settings-dialog').addEventListener('cancel',()=>sequence++);
|
|
52
|
+
return {read};
|
|
15
53
|
}
|
|
@@ -4,20 +4,28 @@ export function templateEditor({api,$,checkout,notice}) {
|
|
|
4
4
|
const drafts=new Map();
|
|
5
5
|
const key=name=>`runlist:template-draft:${checkout()}:${name}`;
|
|
6
6
|
const current=()=>drafts.get(selected.name);
|
|
7
|
-
function keep() {if(!selected?.editable)return;try{localStorage.setItem(key(selected.name),JSON.stringify(current()));}catch{feedback('Template draft could not be saved in local editor storage. Keep this dialog open.',true);}}
|
|
8
|
-
function feedback(text,error=false){$('template-feedback').textContent=text;$('template-feedback').classList.toggle('error',error);}
|
|
7
|
+
function keep() {if(!selected?.editable)return;try{localStorage.setItem(key(selected.name),JSON.stringify(current()));return true;}catch{feedback('Template draft could not be saved in local editor storage. Keep this dialog open.',true);return false;}}
|
|
8
|
+
function feedback(text,error=false){$('template-feedback').hidden=!text;$('template-feedback').textContent=text;$('template-feedback').classList.toggle('error',error);}
|
|
9
9
|
function dirty(){const d=current();return selected?.editable&&(d.source!==d.base||d.reset);}
|
|
10
10
|
function render() {
|
|
11
11
|
if(!selected)return;
|
|
12
|
-
const d=current();$('template-name').textContent=selected.name;$('template-origin').textContent=selected.origin;$('template-description').textContent=selected.description;
|
|
12
|
+
const d=current(), handoff=selected.kind==='handoff';$('template-name').textContent=selected.label??selected.name;$('template-origin').textContent=selected.origin;$('template-description').textContent=selected.description;
|
|
13
13
|
$('template-config-note').hidden=!selected.configuredSource;$('template-config').hidden=!selected.configuredSource;$('template-config-source').textContent=selected.configuredSource??'';
|
|
14
|
+
$('template-config-label').textContent=selected.configLabel??'Repository JavaScript source';
|
|
15
|
+
$('template-config-note').textContent=selected.configNote??'Saving here replaces the configured template with this Markdown.';
|
|
16
|
+
$('template-tokens').textContent=handoff
|
|
17
|
+
? selected.name==='handoff:subagent'?'Keep parent_prompt: {{parent_prompt}} in frontmatter. The CLI fills it from --parent or RUNLIST_PARENT_PROMPT.':'No placeholders required. Keep only the context the next session needs.'
|
|
18
|
+
:'Placeholders: {{title}}, {{status}}, {{date}}, {{body}}, {{version}}. Keep the metadata fields and body placeholder.';
|
|
19
|
+
$('template-save-note').textContent=handoff
|
|
20
|
+
?'Used for future handoff drafts. Saved in runlist.templates.json.'
|
|
21
|
+
:'Used for new documents. Saved in runlist.templates.json.';
|
|
14
22
|
$('template-source').value=d.source;$('template-source').readOnly=!selected.editable;$('template-source').hidden=preview;
|
|
15
23
|
$('template-preview').hidden=!preview;
|
|
16
24
|
$('template-preview-tab').disabled=!selected.editable;
|
|
17
25
|
$('template-edit-tab').setAttribute('aria-pressed',String(!preview));$('template-preview-tab').setAttribute('aria-pressed',String(preview));
|
|
18
|
-
const example=d.source.replace(/\{\{(title|status|date|body|version)\}\}/g,(_,token)=>({title:'Example document',status:selected.name==='prompt'?'pending':selected.name==='plan'?'planned':'active',date:new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),body:'Authored content goes here.',version:catalog.version})[token]);
|
|
26
|
+
const example=d.source.replace(/\{\{(title|status|date|body|version|parent_prompt)\}\}/g,(_,token)=>({title:'Example document',status:selected.name==='prompt'?'pending':selected.name==='plan'?'planned':'active',date:new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),body:'Authored content goes here.',version:catalog.version,parent_prompt:'resume-example-session'})[token]);
|
|
19
27
|
$('template-preview').innerHTML=markdownHtml(example);
|
|
20
|
-
$('template-review-button').disabled=!dirty()||busy;$('template-reset').hidden=!selected.editable||(!selected.overridden
|
|
28
|
+
$('template-review-button').disabled=!dirty()||busy;$('template-reset').hidden=!selected.editable||(!selected.overridden&&(!selected.configuredSource||handoff));
|
|
21
29
|
$('template-discard').disabled=!dirty()||busy;$('template-tokens').hidden=!selected.editable;
|
|
22
30
|
$('template-review-button').hidden=!selected.editable;$('template-save').hidden=true;$('template-review').hidden=true;
|
|
23
31
|
$('template-save').disabled=busy;$('template-source').disabled=busy;
|
|
@@ -30,18 +38,20 @@ export function templateEditor({api,$,checkout,notice}) {
|
|
|
30
38
|
let saved;try{saved=JSON.parse(localStorage.getItem(key(template.name)));}catch{}
|
|
31
39
|
drafts.set(template.name,saved&&typeof saved.source==='string'&&typeof saved.base==='string'&&('revision'in saved)?saved:{source:template.source,base:template.source,revision:catalog.revision,reset:false});
|
|
32
40
|
}
|
|
33
|
-
|
|
41
|
+
// Saving a different template changes the store revision, not this base.
|
|
42
|
+
if(current().base===template.source)current().revision=catalog.revision;
|
|
43
|
+
render();feedback(!template.editable?'Edit this JavaScript template in runlist.config.mjs.':dirty()?'Unsaved changes':'');
|
|
34
44
|
}
|
|
35
|
-
function list(){ $('template-list').replaceChildren(...catalog.templates.map(template=>{const button=document.createElement('button');button.className='quiet';button.dataset.name=template.name;button.textContent=template.name;button.onclick=()=>select(template);return button;})); }
|
|
45
|
+
function list(){ $('template-list').replaceChildren(...catalog.templates.map(template=>{const button=document.createElement('button');button.className='quiet';button.dataset.name=template.name;button.textContent=template.label??template.name;button.onclick=()=>select(template);return button;})); }
|
|
36
46
|
$('open-templates').onclick=async()=>{try{catalog=await api('templates');list();select(catalog.templates.find(t=>t.name===selected?.name)??catalog.templates.find(t=>t.name==='plan')??catalog.templates[0]);$('template-dialog').showModal();}catch(error){notice(error.message,true);}};
|
|
37
47
|
$('close-templates').onclick=()=>{$('template-dialog').close();};
|
|
38
48
|
$('template-dialog').addEventListener('cancel',event=>{if(busy)event.preventDefault();});
|
|
39
|
-
$('template-source').oninput=()=>{const d=current();d.source=$('template-source').value;d.reset=false;keep();$('template-save').hidden=true;$('template-review').hidden=true;$('template-review-button').disabled=!dirty();feedback('
|
|
49
|
+
$('template-source').oninput=()=>{const d=current();d.source=$('template-source').value;d.reset=false;const kept=keep();$('template-save').hidden=true;$('template-review').hidden=true;$('template-review-button').hidden=false;$('template-review-button').disabled=!dirty();$('template-discard').disabled=!dirty();if(kept)feedback(dirty()?'Unsaved changes':'');};
|
|
40
50
|
$('template-edit-tab').onclick=()=>{preview=false;render();};$('template-preview-tab').onclick=()=>{preview=true;render();};
|
|
41
|
-
$('template-review-button').onclick=()=>{const d=current();$('template-diff').innerHTML=diffHtml(d.base,d.source);$('template-review').hidden=false;$('template-review').open=true;$('template-save').hidden=false;$('template-save').disabled=!dirty();feedback(d.reset?'
|
|
42
|
-
$('template-reset').onclick=()=>{const d=current();d.source=selected.defaultSource;d.reset=true;keep();render();feedback('Review and save to
|
|
51
|
+
$('template-review-button').onclick=()=>{const d=current();$('template-diff').innerHTML=diffHtml(d.base,d.source);$('template-review').hidden=false;$('template-review').open=true;$('template-save').hidden=false;$('template-save').disabled=!dirty();$('template-review-button').hidden=true;feedback(d.reset?'Saving restores the original template.':'');};
|
|
52
|
+
$('template-reset').onclick=()=>{const d=current();d.source=selected.defaultSource;d.reset=true;const kept=keep();render();if(kept)feedback('Review and save to restore the original template.');};
|
|
43
53
|
$('template-discard').onclick=()=>{drafts.set(selected.name,{source:selected.source,base:selected.source,revision:catalog.revision,reset:false});localStorage.removeItem(key(selected.name));render();feedback('Template draft discarded.');};
|
|
44
|
-
$('template-reload').onclick=async()=>{try{catalog=await api('templates');const name=selected.name;selected=catalog.templates.find(t=>t.name===name);const d=current();d.base=selected.source;d.revision=catalog.revision;keep();list();render();$('template-reload').hidden=true;feedback('Saved template reloaded. Your draft is kept
|
|
45
|
-
$('template-save').onclick=async()=>{if(busy||!dirty())return;const name=selected.name,d=current();busy=true;$('template-save').disabled=true;$('template-dialog').querySelector('.template-layout').inert=true;$('close-templates').disabled=true;try{catalog=await api('templates/save',{name,source:d.source,expectedRevision:d.revision,reset:d.reset});selected=catalog.templates.find(t=>t.name===name);drafts.delete(name);localStorage.removeItem(key(name));busy=false;list();select(selected);feedback('Template saved
|
|
54
|
+
$('template-reload').onclick=async()=>{try{catalog=await api('templates');const name=selected.name;selected=catalog.templates.find(t=>t.name===name);const d=current();d.base=selected.source;d.revision=catalog.revision;keep();list();render();$('template-reload').hidden=true;feedback('Saved template reloaded. Your draft is kept.');}catch(error){feedback(error.message,true);}};
|
|
55
|
+
$('template-save').onclick=async()=>{if(busy||!dirty())return;const name=selected.name,d=current();busy=true;$('template-save').disabled=true;$('template-dialog').querySelector('.template-layout').inert=true;$('close-templates').disabled=true;try{catalog=await api('templates/save',{name,source:d.source,expectedRevision:d.revision,reset:d.reset});selected=catalog.templates.find(t=>t.name===name);drafts.delete(name);localStorage.removeItem(key(name));busy=false;list();select(selected);feedback('Template saved.');}catch(error){feedback(error.message,true);keep();if(error.code==='template-conflict')$('template-reload').hidden=false;}finally{busy=false;$('template-dialog').querySelector('.template-layout').inert=false;$('template-save').disabled=false;$('close-templates').disabled=false;}};
|
|
46
56
|
document.addEventListener('keydown',event=>{if($('template-dialog').open&&(event.metaKey||event.ctrlKey)&&event.key==='s'){event.preventDefault();$('template-review-button').click();}});
|
|
47
57
|
}
|
package/assets/app/transport.mjs
CHANGED
|
@@ -21,8 +21,7 @@ export async function mountDesktop({beforeLeave,bootstrap,notice}) {
|
|
|
21
21
|
const $=id=>document.getElementById(id);
|
|
22
22
|
$('connect-panel').hidden=true;$('desktop-welcome').hidden=false;$('desktop-open').hidden=false;
|
|
23
23
|
$('disconnect').textContent='Close folder';
|
|
24
|
-
|
|
25
|
-
const footer=document.querySelector('.sidebar-footer');footer.replaceChildren(document.createTextNode('Local files · This computer only'),document.createElement('br'));const label=document.createElement('span');label.textContent='Save here. Review local commits in Changes.';footer.append(label);
|
|
24
|
+
document.querySelector('.sidebar-footer').textContent='';
|
|
26
25
|
async function recent(){const state=await invoke('desktop_status');const root=$('desktop-recents');root.replaceChildren();for(const item of state.recent){const button=document.createElement('button');button.className='quiet recent-checkout';button.textContent=item.path;button.onclick=()=>choose(item.id);root.append(button);}return state;}
|
|
27
26
|
let moving=false;
|
|
28
27
|
async function choose(id){if(moving)return;moving=true;try{
|
package/bin/runlist.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { resolveConfig } from '../src/config.mjs';
|
|
6
|
+
import { resolveConfig, locateConfig } from '../src/config.mjs';
|
|
7
7
|
import { readPlanStage } from '../src/stages.mjs';
|
|
8
8
|
import { scopeFilingCoverage } from '../src/filing.mjs';
|
|
9
9
|
import { die, warn, levenshtein, isArchivedPath, toRepoPath } from '../src/util.mjs';
|
|
@@ -285,7 +285,7 @@ Analyze:
|
|
|
285
285
|
record <file> <action> --note <reason> Act on a native flag or decision
|
|
286
286
|
decision <file> <id> --disposition <state> --note <reason> Record an outcome
|
|
287
287
|
decisions [doc|id] [--all|--check] Open and held decisions, read from the corpus
|
|
288
|
-
show <file...> [--
|
|
288
|
+
show <file...> [--section <h>] One document's card, outline and open boxes with line numbers; one section
|
|
289
289
|
xref <file...> | --check [--flag] Cross-references: what names it, its decisions, its cited code
|
|
290
290
|
|
|
291
291
|
Validate & Fix:
|
|
@@ -893,6 +893,8 @@ Modes:
|
|
|
893
893
|
when a verb says "No authoritative session identity",
|
|
894
894
|
or on any host runlist has never been tried on.
|
|
895
895
|
--session --json Machine-readable identity + host-integration state.
|
|
896
|
+
--prompts Read-only parent-link findings: linked, inferred, missing, archived,
|
|
897
|
+
ambiguous or invalid. Supports --json; never consumes or repairs.
|
|
896
898
|
--statuses Read-only diagnostic: detect overloaded status
|
|
897
899
|
buckets where one status holds plans pursuing
|
|
898
900
|
multiple distinct unstuck-actions. Suggests how
|
|
@@ -1295,6 +1297,8 @@ Subcommands:
|
|
|
1295
1297
|
show <file-or-slug> Read-only peek: print the body WITHOUT consuming
|
|
1296
1298
|
(triage). \`peek\` is an alias.
|
|
1297
1299
|
archive <file-or-slug> Archive a prompt without printing its body
|
|
1300
|
+
link <child...> --parent <resume-name>
|
|
1301
|
+
Attach existing children without replacing their handoffs
|
|
1298
1302
|
new <slug> [body] Create a new prompt (alias for
|
|
1299
1303
|
\`runlist new prompt <slug> [body]\`)
|
|
1300
1304
|
|
|
@@ -1308,6 +1312,14 @@ Pass \`--no-claim\` to read and archive the prompt without starting the plan.
|
|
|
1308
1312
|
|
|
1309
1313
|
Default prompt statuses: pending, archived.
|
|
1310
1314
|
|
|
1315
|
+
The default list shows [+ N subagent prompts] on their resume-* parent's line.
|
|
1316
|
+
Use --verbose to expand their names and targets.
|
|
1317
|
+
Record the parent with \`baton --subagent <task> --parent resume-<name>\`;
|
|
1318
|
+
legacy child names sharing the parent's name stem also group. Missing-parent
|
|
1319
|
+
children share a separate count. Grouping does not change which prompt is consumed next.
|
|
1320
|
+
Showing or consuming a parent also lists its pending children for inspection;
|
|
1321
|
+
the children remain pending until explicitly consumed or archived.
|
|
1322
|
+
|
|
1311
1323
|
Examples:
|
|
1312
1324
|
runlist prompts # pending prompts (default)
|
|
1313
1325
|
runlist prompts list --verbose # one row per prompt + target plan ref
|
|
@@ -1350,6 +1362,8 @@ Options:
|
|
|
1350
1362
|
--message / --body Inline body (one-liners; prefer @path or stdin)
|
|
1351
1363
|
--force Recover another session's plan (explicit path required)
|
|
1352
1364
|
--subagent Save subagent-<name> instead of the parent default resume-<name>
|
|
1365
|
+
--parent <resume-name> Group this --subagent handoff under its parent in prompts listings
|
|
1366
|
+
--template Print a resume draft (or --subagent draft), using saved Markdown settings
|
|
1353
1367
|
--replace Replace exactly one pending handoff; archive its prior text
|
|
1354
1368
|
--done Report a plan handoff done once next_step changed after the prompt was saved
|
|
1355
1369
|
--json Structured repository/session/generated file result
|
|
@@ -1362,9 +1376,12 @@ Repo-specific commit hint (runlist.config.mjs):
|
|
|
1362
1376
|
The function returns argv; baton shell-quotes each argument before printing.
|
|
1363
1377
|
|
|
1364
1378
|
Examples:
|
|
1379
|
+
runlist baton --template > /tmp/resume.md
|
|
1365
1380
|
runlist baton @/tmp/draft.md
|
|
1366
1381
|
runlist baton checkout-fixes @/tmp/draft.md
|
|
1367
1382
|
runlist baton --subagent totals-review @/tmp/draft.md
|
|
1383
|
+
runlist baton --subagent totals-review --parent resume-session-name @/tmp/draft.md
|
|
1384
|
+
runlist baton --subagent --template --parent resume-session-name > /tmp/child.md
|
|
1368
1385
|
runlist baton docs/plans/auth.md @/tmp/draft.md
|
|
1369
1386
|
runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
|
|
1370
1387
|
runlist baton --status paused --note "blocked on review" @/tmp/d.md
|
|
@@ -1396,6 +1413,15 @@ than releasing the parent plan. A child owning its own plan can hand it off.
|
|
|
1396
1413
|
Matching prefixes are kept once; conflicting or nested prefixes are refused.
|
|
1397
1414
|
Existing prompts keep their names. --subagent --replace refuses a parent-named
|
|
1398
1415
|
handoff rather than reclassifying it.
|
|
1416
|
+
Use --parent resume-<name> to record the owning parent prompt (it may be saved
|
|
1417
|
+
later). Prompt listings show linked child counts on that parent's line; legacy child
|
|
1418
|
+
names sharing its name stem also group. Consumption order is unchanged.
|
|
1419
|
+
The child draft template covers Task, State, Next, Evidence and Boundaries.
|
|
1420
|
+
Its parent_prompt field travels with the draft. A launcher can instead pass
|
|
1421
|
+
RUNLIST_PARENT_PROMPT to the child; explicit --parent wins and replacement
|
|
1422
|
+
preserves a retained link. An unlinked child save warns with a repair command.
|
|
1423
|
+
Keep the full record at a durable repo-relative path, with commits and checked
|
|
1424
|
+
results. On completion, say what the parent must record and what remains.
|
|
1399
1425
|
|
|
1400
1426
|
Slug mode (no plan involved) saves resume-<slug> by default without a status change or
|
|
1401
1427
|
commit. A bare word that names a plan is treated as that plan.
|
|
@@ -1545,18 +1571,32 @@ remain owned by their CLI sessions; coordinate/release them before a human save.
|
|
|
1545
1571
|
Metadata and lifecycle changes continue through the CLI. Ctrl+C stops the app.
|
|
1546
1572
|
--dry-run previews startup without opening a server or writing editor state.
|
|
1547
1573
|
`,
|
|
1548
|
-
show: `runlist show <file...> —
|
|
1549
|
-
|
|
1550
|
-
runlist show <file> title, status, next step, blockers,
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1574
|
+
show: `runlist show <file...> — a document in a few KB, read-only
|
|
1575
|
+
|
|
1576
|
+
runlist show <file> card (title, status, next step, blockers, related
|
|
1577
|
+
plans and docs, body links), then the outline and
|
|
1578
|
+
open boxes with line numbers, then the other
|
|
1579
|
+
frontmatter fields by name
|
|
1580
|
+
runlist show <file> --section "<heading>"
|
|
1581
|
+
that section, every line numbered, to the next
|
|
1582
|
+
heading at the same or a higher level
|
|
1583
|
+
--from <line> continue a section read that was cut
|
|
1584
|
+
--all every open box, not the first 30
|
|
1585
|
+
--card the card only
|
|
1586
|
+
--frontmatter the frontmatter as written, long values collapsed
|
|
1587
|
+
--json the same as data; a section read is
|
|
1588
|
+
{ path, query, heading, from, to, next, rows, others }
|
|
1589
|
+
|
|
1590
|
+
Read a plan or long doc this way, then one section at a time, never whole. A
|
|
1591
|
+
heading matches case-insensitively: exact first, then as a prefix, then as a
|
|
1592
|
+
substring; other matches are named. A section read stops at 500 lines or
|
|
1593
|
+
60 KB and prints the command that continues it. Headings and boxes inside code
|
|
1594
|
+
fences are ignored. Several files at once get a shorter outline each.
|
|
1595
|
+
|
|
1596
|
+
A file is a path, filename or slug, as for \`use\`, and any Markdown file in
|
|
1597
|
+
the repo works. Nothing is claimed or written. With --json a file that is not
|
|
1598
|
+
found is { path, error } rather than an exit; a heading that matches nothing
|
|
1599
|
+
exits 1 and lists the headings.`,
|
|
1560
1600
|
|
|
1561
1601
|
model: `runlist model — the local model behind summaries and lint
|
|
1562
1602
|
|
|
@@ -1909,7 +1949,7 @@ async function main() {
|
|
|
1909
1949
|
const doctorSubMode = command === 'doctor' && (
|
|
1910
1950
|
args.includes('--statuses') || args.includes('--migrate-template')
|
|
1911
1951
|
|| args.includes('--migrate-prompts') || args.includes('--frontmatter-fix')
|
|
1912
|
-
|| args.includes('--project')
|
|
1952
|
+
|| args.includes('--project') || args.includes('--prompts')
|
|
1913
1953
|
);
|
|
1914
1954
|
const doctorExplicitApply = args.includes('--apply') || args.includes('--yes');
|
|
1915
1955
|
const effectiveDryRun = dryRun || (command === 'doctor' && !doctorSubMode && !doctorExplicitApply);
|
|
@@ -1948,6 +1988,15 @@ async function main() {
|
|
|
1948
1988
|
return;
|
|
1949
1989
|
}
|
|
1950
1990
|
|
|
1991
|
+
if (command === 'baton' && restArgs.includes('--template')) {
|
|
1992
|
+
_suppressObservability = true;
|
|
1993
|
+
requireCommandPolicy(command, dispatchPolicy);
|
|
1994
|
+
try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
|
|
1995
|
+
const { runBaton } = await import('../src/baton.mjs');
|
|
1996
|
+
await runBaton(restArgs, locateConfig(process.cwd(), explicitConfig), { dryRun });
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1951
2000
|
if (command === 'desktop') {
|
|
1952
2001
|
requireCommandPolicy(command, dispatchPolicy);
|
|
1953
2002
|
try { restArgs=validateCommandArgs(command,restArgs); } catch(err) { die(err.message); }
|
package/package.json
CHANGED