chamba 0.8.0 → 0.9.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.
Files changed (59) hide show
  1. package/README.md +7 -1
  2. package/dist/lib/chamba-yaml.js +4 -4
  3. package/package.json +1 -1
  4. package/templates/Dockerfile +3 -3
  5. package/templates/context/baseline.md +2 -2
  6. package/templates/context/web-pane-craft.md +1 -1
  7. package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
  8. package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
  9. package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
  10. package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
  11. package/templates/pane-apps/client/assets/files-BkmyervY.js +1 -0
  12. package/templates/pane-apps/client/assets/home-3SeGzySM.css +1 -0
  13. package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -0
  14. package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
  15. package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
  16. package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -0
  17. package/templates/pane-apps/client/assets/specs-Cabb15F0.js +13 -0
  18. package/templates/pane-apps/client/assets/useNow-BHFKFraS.js +1 -0
  19. package/templates/pane-apps/client/home/index.html +16 -0
  20. package/templates/pane-apps/client/reviews/index.html +6 -4
  21. package/templates/pane-apps/client/specs/index.html +4 -3
  22. package/templates/pane-apps/server/home.mjs +12 -0
  23. package/templates/pane-apps/server/reviews.mjs +19 -18
  24. package/templates/pane-apps/server/specs.mjs +4 -4
  25. package/templates/skills/dx-review/SKILL.md +16 -3
  26. package/templates/skills/dx-review/references/acts.md +86 -15
  27. package/templates/skills/dx-spec/SKILL.md +7 -3
  28. package/templates/skills/dx-spec/references/review-guide.md +2 -2
  29. package/templates/skills/dx-spec/references/spec-guide.md +1 -1
  30. package/templates/skills/dx-spec/references/stages.md +6 -2
  31. package/templates/skills/dx-spec-execute/SKILL.md +73 -7
  32. package/templates/startup.mjs +13 -13
  33. package/templates/tool-helper.sh +4 -2
  34. package/templates/webterm/README.md +34 -13
  35. package/templates/webterm/config.js +20 -0
  36. package/templates/webterm/public/app/alerts.js +92 -11
  37. package/templates/webterm/public/app/composer.js +37 -3
  38. package/templates/webterm/public/app/connection.js +14 -0
  39. package/templates/webterm/public/app/dom.js +1 -0
  40. package/templates/webterm/public/app/frames.js +1 -1
  41. package/templates/webterm/public/app/home-host.js +16 -0
  42. package/templates/webterm/public/app/main.js +4 -2
  43. package/templates/webterm/public/app/pane-shape.js +5 -3
  44. package/templates/webterm/public/app/pane-shell.js +45 -5
  45. package/templates/webterm/public/app/pane.js +8 -2
  46. package/templates/webterm/public/app/reviews-host.js +5 -1
  47. package/templates/webterm/public/app/tool-host.js +19 -3
  48. package/templates/webterm/public/app/workspace-color.js +1 -1
  49. package/templates/webterm/public/index.html +5 -1
  50. package/templates/webterm/public/styles.css +52 -0
  51. package/templates/webterm/tool-document.js +3 -2
  52. package/templates/webterm/tools/home.js +58 -0
  53. package/templates/webterm/tools/index.js +5 -4
  54. package/templates/webterm/tools/reviews.js +7 -3
  55. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +0 -12
  56. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +0 -1
  57. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +0 -1
  58. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +0 -153
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +0 -13
@@ -1,5 +1,5 @@
1
- import{closeSync as e,constants as t,existsSync as n,fstatSync as r,lstatSync as i,mkdirSync as a,openSync as o,readFileSync as s,readdirSync as c,realpathSync as l,renameSync as u,rmSync as d,statSync as f,watch as p,writeFileSync as m}from"node:fs";import{basename as h,dirname as g,isAbsolute as _,join as v,relative as ee,resolve as te,sep as ne}from"node:path";import{createHash as re}from"node:crypto";var y=`.specs`,ie=`reader-state.json`,ae=`user-feedback.json`,oe=`pending-annotations.json`,se=`user-feedback-assets`;function ce(){return{version:1,units:{},dismissed:[]}}function le(e){return{version:1,entry:e,rounds:[]}}function b(e){return{version:1,entry:e,annotations:[]}}var x=e=>typeof e==`object`&&!!e&&!Array.isArray(e),S=(e,t=``)=>typeof e==`string`?e:t;function ue(e){let t;try{t=JSON.parse(e||`null`)}catch{return ce()}if(!x(t)||t.version!==1)return ce();let n=ce();if(x(t.units))for(let[e,r]of Object.entries(t.units)){if(!x(r))continue;let t={};typeof r.readHash==`string`&&(t.readHash=r.readHash),typeof r.readAt==`string`&&(t.readAt=r.readAt),n.units[e]=t}return Array.isArray(t.dismissed)&&(n.dismissed=t.dismissed.filter(e=>typeof e==`string`)),n}function de(e,t){let n;try{n=JSON.parse(e||`null`)}catch{return b(t)}if(!x(n)||n.version!==1)return b(t);let r=[];if(Array.isArray(n.annotations))for(let e of n.annotations)!x(e)||typeof e.id!=`string`||typeof e.unit!=`string`||e.id===``||e.unit===``||r.push({id:e.id,unit:e.unit,file:S(e.file),heading:S(e.heading),kind:e.kind===`remove`?`remove`:`comment`,quote:S(e.quote),text:S(e.text)});return{version:1,entry:S(n.entry,t),annotations:r}}function fe(e,t){let n;try{n=JSON.parse(e||`null`)}catch{return le(t)}if(!x(n)||n.version!==1)return le(t);let r=[];if(Array.isArray(n.rounds))for(let e of n.rounds){if(!x(e))continue;let t=[];if(Array.isArray(e.comments))for(let n of e.comments){if(!x(n)||typeof n.unit!=`string`)continue;let e=[];if(Array.isArray(n.assets))for(let t of n.assets)!x(t)||typeof t.path!=`string`||e.push({path:t.path,name:S(t.name,t.path),type:S(t.type)});t.push({unit:n.unit,file:S(n.file),heading:S(n.heading),kind:n.kind===`remove`?`remove`:`comment`,quote:S(n.quote),text:S(n.text),assets:e})}r.push({submittedAt:S(e.submittedAt),comments:t})}return{version:1,entry:S(n.entry,t),rounds:r}}function C(e){return e.toLowerCase().replace(/`/g,``).replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||``}var pe=`state.json`,w=[{id:`exploration`,title:`High-level exploration`,group:`discovery`,about:`We think the problem through together first - directions to take, risks, and what is still unclear.`},{id:`interview`,title:`Interview`,group:`discovery`,about:`I ask you focused questions until nothing important is left open.`},{id:`research`,title:`Research`,group:`discovery`,about:`I look outside your code - a library, a protocol, an algorithm, a subject area - and write up what I find.`},{id:`codebase-analysis`,title:`Codebase analysis`,group:`discovery`,about:`I read the parts of your code this work touches and note what shapes the design.`},{id:`technical-specs`,title:`Technical specs`,group:`deliverables`,about:`A written specification another agent can build from without asking you anything.`},{id:`ui-mocks`,title:`UI mocks`,group:`deliverables`,about:`Mock screens you open in your browser and comment on before anything is built.`},{id:`execution-plan`,title:`Execution plan`,group:`deliverables`,about:`The work split into phases, each with a goal and a way to tell it is done.`},{id:`quality-review`,title:`Quality review`,group:`deliverables`,about:`Fresh helper agents review everything we wrote, and I fix what is plainly wrong and bring the judgment calls to you.`}],me=[`exploration`,`ui-mocks`];function T(e){return w.find(t=>t.id===e)??null}var he=[{id:`per-phase`,said:`a commit per phase`},{id:`single`,said:`a single commit at the end`},{id:`none`,said:`no commits`}],ge=[{id:`current`,said:`the current branch`},{id:`new`,said:`a new branch`}],_e=[{id:`none`,said:`none`},{id:`chosen`,said:`after chosen phases`},{id:`every`,said:`every phase`}];function ve(e,t){return{version:1,name:e,created:t,status:`specifying`,protocol:[],modes:{imagination:!1},wish:{imagination:!1},stageSettings:{research:null},activity:{said:``,at:``},awaiting:[],phases:[],runRecommendations:null,runDecisions:null,reviews:[],approvals:[],log:[]}}var E=e=>typeof e==`object`&&!!e&&!Array.isArray(e),D=(e,t=``)=>typeof e==`string`?e:t,O=(e,t,n)=>t.includes(e)?e:n,ye=[`specifying`,`ready`,`executing`,`complete`],be=[`waiting`,`running`,`done`],xe=[`gate`,`round`,`protocol`],Se=[`focused`,`named`,`broad`],Ce=he.map(e=>e.id),we=ge.map(e=>e.id),Te=_e.map(e=>e.id);function Ee(e){if(!E(e)||e.scope===void 0)return null;let t=O(e.scope,Se,`focused`);return{scope:t,subject:t===`named`?k(D(e.subject)):``}}function k(e,t=200){return e.replace(/\p{C}+/gu,` `).replace(/\s+/g,` `).trim().slice(0,t)}function De(e){if(!E(e))return null;let t=[];for(let n of Array.isArray(e.checkpoints)?e.checkpoints:[]){if(!E(n))continue;let e=Array.isArray(n.panel)?n.panel.filter(e=>typeof e==`string`).map(e=>k(e)):[];t.push({after:C(D(n.after)),why:k(D(n.why)),panel:e,chosen:n.chosen!==!1&&e.length>0})}return{commits:O(e.commits,Ce,`per-phase`),branch:O(e.branch,we,`current`),branchName:k(D(e.branchName)),pauses:O(e.pauses,Te,`none`),pausePhases:Array.isArray(e.pausePhases)?e.pausePhases.filter(e=>typeof e==`string`).map(e=>C(e)):[],checkpoints:t}}function Oe(e){if(!E(e))return null;let t=De(e);if(t===null)return null;let n={};if(E(e.pauseReasons))for(let[t,r]of Object.entries(e.pauseReasons)){if(typeof r!=`string`)continue;let e=k(r);e!==``&&(n[C(t)]=e)}return{...t,pauseReasons:n,noPauseReason:k(D(e.noPauseReason))}}function ke(e,t=``,n=``){let r;try{r=JSON.parse(e||`null`)}catch{return ve(t,n)}if(!E(r)||r.version!==1)return ve(t,n);let i=ve(D(r.name,t),D(r.created,n)),a=D(r.status);if(ye.includes(a)&&(i.status=a),Array.isArray(r.protocol))for(let e of r.protocol){if(!E(e)||!T(D(e.id)))continue;let t=D(e.status);i.protocol.push({id:D(e.id),status:be.includes(t)?t:`waiting`,artifacts:Array.isArray(e.artifacts)?e.artifacts.filter(e=>typeof e==`string`):[]})}if(E(r.modes)&&(i.modes.imagination=r.modes.imagination===!0),E(r.wish)&&(i.wish.imagination=r.wish.imagination===!0),E(r.stageSettings)&&(i.stageSettings={research:Ee(r.stageSettings.research)}),E(r.activity)&&(i.activity={said:D(r.activity.said),at:D(r.activity.at)}),Array.isArray(r.awaiting))for(let e of r.awaiting){if(!E(e)||!e.id)continue;let t=D(e.kind);i.awaiting.push({id:D(e.id),kind:xe.includes(t)?t:`round`,title:D(e.title),file:D(e.file),payload:D(e.payload),declaredAt:D(e.declaredAt)})}if(Array.isArray(r.phases))for(let e of r.phases){if(!E(e)||!e.id)continue;let t=D(e.status);i.phases.push({id:D(e.id),title:D(e.title),status:be.includes(t)?t:`waiting`})}i.runRecommendations=Oe(r.runRecommendations);let o=De(r.runDecisions);if(o!==null&&E(r.runDecisions)&&(i.runDecisions={...o,decidedAt:D(r.runDecisions.decidedAt)}),Array.isArray(r.reviews))for(let e of r.reviews)!E(e)||typeof e.round!=`number`||i.reviews.push({round:e.round,verdict:D(e.verdict),findings:typeof e.findings==`number`?e.findings:0,judgments:typeof e.judgments==`number`?e.judgments:0,at:D(e.at)});if(Array.isArray(r.approvals))for(let e of r.approvals)!E(e)||!e.id||i.approvals.push({id:D(e.id),at:D(e.at),outcome:D(e.outcome)});if(Array.isArray(r.log))for(let e of r.log)E(e)&&i.log.push({at:D(e.at),said:D(e.said)});return i}function Ae(e,t){let n={};try{let t=JSON.parse(e||`null`);E(t)&&(n=t)}catch{n={}}return`${JSON.stringify({...n,...t},null,2)}\n`}function je(e){let t=e?.code;return typeof t==`string`&&Me.has(t)?t:null}var Me=new Set([`path-refused`,`no-entry`,`no-file`,`bad-request`,`too-large`,`upstream-failed`,`unreadable-answer`]),A=class extends Error{requested;code=`path-refused`;constructor(e,t){super(`${e}: refused path ${t}`),this.requested=t,this.name=`PathRefused`}},Ne=class extends Error{requested;code=`no-entry`;constructor(e,t){super(`${e}: no directory at ${t}`),this.requested=t,this.name=`NoSuchEntry`}},Pe=class extends Error{requested;code=`no-file`;constructor(e,t,n=`not a file`){super(`${e}: ${n} - ${t}`),this.requested=t,this.name=`NoSuchFile`}},Fe=class extends Error{requested;size;code=`too-large`;constructor(e,t,n){super(`${e}: too large to serve - ${t}`),this.requested=t,this.size=n,this.name=`FileTooLarge`}};function Ie(e){switch(je(e)){case`path-refused`:return 403;case`no-entry`:case`no-file`:return 404;case`bad-request`:return 400;case`too-large`:return 413;case`upstream-failed`:case`unreadable-answer`:return 502}return Le(e)?404:500}function Le(e){let t=e?.code;return t===`ENOENT`||t===`ENOTDIR`}var Re=32*1024*1024,ze=new Set([`md`,`markdown`,`mdown`,`mkd`]),Be=new Set([`html`,`htm`]),Ve=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`svg`,`avif`,`bmp`,`ico`]),He=new Set(`txt text log json yaml yml toml ini cfg conf csv tsv xml css scss js jsx mjs cjs ts tsx py rb go rs java kt swift c h cpp hpp sh bash zsh fish sql graphql gql env diff patch lock gitignore editorconfig`.split(` `));function Ue(e){let t=e.slice(e.lastIndexOf(`/`)+1).slice(1).match(/\.([^.]+)$/);return t?t[1].toLowerCase():``}function We(e){return e.startsWith(`.`)}function Ge(e){let t=Ue(e);return ze.has(t)?`markdown`:Be.has(t)?`html`:Ve.has(t)?`image`:He.has(t)||!t?`text`:`binary`}var Ke=e=>/^[A-Z]/.test(e);function qe(e,t){let n=Ke(e);return n===Ke(t)?e.localeCompare(t,`en`):n?-1:1}function Je(e){let t=e,n=[];for(;;)try{return v(l(t),...n)}catch{let r=g(t);if(r===t)return e;n.unshift(h(t)),t=r}}function Ye(e){return Je(te(e))}function Xe(e){return e.split(`/`).some(e=>e!==``&&We(e))}function Ze(e,t,n,r){let i=n.replace(/^\/+/,``);if(_(n)||i.includes(`\0`))throw new A(e,n);let a=r===`require`;if(Xe(i)!==a)throw new A(e,n);let o=te(t,i);if(!et(t,o)||!et(t,Je(o))||Xe(ee(t,o).split(ne).join(`/`))!==a)throw new A(e,n);return o}function Qe(e,t,n){return Ze(e,t,n,`refuse`)}function $e(e,t,n){return Ze(e,t,n,`require`)}function et(e,t){return t===e||t.startsWith(e+ne)}function tt(e,t=`children`){if(t===`self`)return``;let n=e.split(`/`).filter(Boolean);return n.length<2?``:n[0]===`archive`?n.length>2?`archive/${n[1]}`:``:n[0]}var nt={md:`text/markdown; charset=utf-8`,markdown:`text/markdown; charset=utf-8`,html:`text/html; charset=utf-8`,htm:`text/html; charset=utf-8`,css:`text/css; charset=utf-8`,js:`text/javascript; charset=utf-8`,json:`application/json; charset=utf-8`,svg:`image/svg+xml`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,webp:`image/webp`,avif:`image/avif`,bmp:`image/bmp`,ico:`image/x-icon`,pdf:`application/pdf`,txt:`text/plain; charset=utf-8`};function rt(e){return nt[Ue(e)]??`application/octet-stream`}var it=Re;function at(e,t,n){return ot(e,Qe(e,t,n),n)}function ot(n,i,a){let c=o(i,t.O_RDONLY|t.O_NOFOLLOW);try{let e=r(c);if(!e.isFile())throw new Pe(n,a);if(e.size>it)throw new Fe(n,a,e.size);return{bytes:s(c),mediaType:rt(a),size:e.size,modified:new Date(e.mtimeMs).toISOString()}}finally{e(c)}}var st=`.tmp-`;function ct(e){return e.slice(e.lastIndexOf(`/`)+1).includes(st)}function j(e,t){a(g(e),{recursive:!0});let n=`${e}${st}${process.pid}-${Math.random().toString(36).slice(2)}`;try{m(n,t),u(n,e)}finally{d(n,{force:!0})}}function lt(e,t,n,r){j($e(e,t,n),r)}function ut(e,t,n){let r=$e(e,t,n);try{return s(r,`utf8`)}catch{return null}}function dt(e){try{let t=i(e);return{directory:t.isDirectory(),id:t.ino}}catch{return{directory:!1,id:0}}}function ft(e,t,r={}){let i=r.entries??`children`,a=r.coalesce??`per-path`,o=r.debounceMs??120,s=r.shared??(()=>!1),l=new Map,u=null,d=new Map,f=!1,m=t=>t===``?e:`${e}/${t}`,h=()=>{u=null;let e=[...l.values()];l.clear();for(let n of e)try{t(n)}catch{}},g=e=>{if(e===``||ct(e)||Xe(e)&&!s(e))return;let t=tt(e,i);if(a===`per-entry`)l.set(t,{entry:t,path:t,kind:`changed`});else{let r=n(m(e))?`changed`:`gone`;l.set(e,{entry:t,path:e,kind:r})}u||=setTimeout(h,o)},_=e=>{for(let[t,n]of d)(t===e||t.startsWith(`${e}/`))&&(n.watcher.close(),d.delete(t))},v=e=>{if(f||d.has(e))return;let t;try{t=p(m(e),(t,r)=>{if(!r)return;let i=r.toString();if(i===``)return;let a=e===``?i:`${e}/${i}`;if(i===e.split(`/`).at(-1)&&!n(m(a)))return;let o=d.get(a),s=dt(m(a));if(o&&s.directory){if(o.id===s.id)return;_(a),v(a),g(a);return}g(a),o?_(a):s.directory&&v(a)})}catch{return}t.on(`error`,()=>_(e)),d.set(e,{watcher:t,id:dt(m(e)).id});for(let t of ee(e))v(t)},ee=e=>{try{return c(m(e),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(t=>e===``?t.name:`${e}/${t.name}`)}catch{return[]}};return v(``),{refollow(){f||v(``)},close(){f=!0,u&&clearTimeout(u),u=null;for(let e of d.values())e.watcher.close();d.clear()}}}var pt=`archive`;function mt(e){let t=e=>String(e).padStart(2,`0`);return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}`}function ht(e,t){let n=e.split(`/`).pop()??e;return`${pt}/${mt(t)}-${n}`}var M=`specs`;function N(e,t){return Qe(M,e,t)}function P(e,t){return $e(M,e,t)}var F=`archive`;function gt(e){let t;try{t=c(e,{withFileTypes:!0})}catch{return[]}return t.filter(e=>!We(e.name)).sort((e,t)=>qe(e.name,t.name))}var _t=4*1024*1024;function vt(e,t,n){if(!(t===`markdown`||t===`text`)&&!(n>_t))try{return re(`sha1`).update(s(e)).digest(`hex`).slice(0,16)}catch{return}}function yt(e,t,n=!0,r=!1){let i=gt(e),a=[],o=0;for(let n of i){if(!n.isFile()&&!n.isDirectory()||n.isDirectory())continue;let i=t?`${t}/${n.name}`:n.name,s;try{s=f(v(e,n.name))}catch{continue}o=Math.max(o,s.mtimeMs);let c=Ge(n.name);a.push({path:i,name:n.name,dir:t,kind:c,size:s.size,modified:new Date(s.mtimeMs).toISOString(),hash:r?vt(v(e,n.name),c,s.size):void 0})}if(n)for(let n of i){if(!n.isDirectory())continue;let i=t?`${t}/${n.name}`:n.name,s=yt(v(e,n.name),i,!0,r);a.push(...s.files),o=Math.max(o,s.modified)}return{files:a,modified:o}}function bt(e,t,n){let r=t?v(e,t):e,i=yt(r,``,n),a=i.modified;if(!a)try{a=f(r).mtimeMs}catch{a=0}return{path:t,name:h(t||e),files:i.files.length,modified:new Date(a).toISOString(),archived:I(t),status:St(e,t),awaiting:Ct(e,t)}}function xt(e,t){if(!t)return null;try{return ke(s(v(e,t,y,pe),`utf8`),h(t))}catch{return null}}function St(e,t){return xt(e,t)?.status??null}function Ct(e,t){return xt(e,t)?.awaiting.length??0}function wt(e,t=`children`){let n=h(e);if(t===`self`)return{root:e,rootName:n,mode:t,entries:[bt(e,``,!0)]};let r=gt(e),i=[];for(let t of r)!t.isDirectory()||t.name===F||i.push(bt(e,t.name,!0));if(r.some(e=>e.isDirectory()&&e.name===F))for(let t of gt(v(e,F)).reverse())t.isDirectory()&&i.push(bt(e,`${F}/${t.name}`,!0));return{root:e,rootName:n,mode:t,entries:i}}function I(e){return e===F||e.startsWith(`${F}/`)}function Tt(e,t,n=`children`){let r=t?N(e,t):e,i;try{i=f(r)}catch{throw new Ne(M,t)}if(!i.isDirectory())throw new Ne(M,t);return{path:t,name:h(t||e),archived:I(t),files:yt(r,``,t!==``||n===`self`,!0).files}}function Et(e){let t=new Date(e);if(!/^\d{4}-\d{2}-\d{2}/.test(e)||Number.isNaN(t.getTime()))return``;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())}`}function Dt(e){let t=[`# ${e.name||`A piece of work`}`,``],n=Et(e.created);if(n&&t.push(`- Created: ${n}`),t.push(`- Status: ${e.status}`),e.modes.imagination&&t.push(`- Mode: imagination`),e.protocol.length>0){t.push(``,`## Protocol`,``);for(let n of e.protocol){let e=T(n.id),r=n.status===`running`?` (running)`:``;t.push(`- [${n.status===`done`?`x`:` `}] ${e?.title??n.id}${r}`)}}if(e.log.length>0){t.push(``,`## Log`,``);for(let n of e.log){let e=Et(n.at);t.push(e?`- ${e}: ${n.said}`:`- ${n.said}`)}}return`${t.join(`
2
- `)}\n`}var Ot=e=>typeof e==`object`&&!!e&&!Array.isArray(e),L=(e,t=``)=>typeof e==`string`?e:t,kt=[`approved`,`changes`,`answered`];function At(e){if(!Ot(e))return null;let t=L(e.outcome);if(!kt.includes(t))return null;let n=[];for(let t of Array.isArray(e.questions)?e.questions:[])!Ot(t)||typeof t.question!=`string`||n.push({question:t.question,chosen:Array.isArray(t.chosen)?t.chosen.filter(e=>typeof e==`string`):[],said:L(t.said),answered:t.answered===!0});return{outcome:t,said:L(e.said),questions:n,accepted:e.accepted===!0,answeredAt:L(e.answeredAt)}}function R(e){return e?`${e}/${y}`:y}function z(e,t,n){lt(M,e,t,n)}function B(e,t){return ut(M,e,t)}function jt(e,t){return Nt(e,t),ue(B(e,`${R(t)}/${ie}`))}function Mt(e,t,n){V(e,t),z(e,`${R(t)}/${ie}`,`${JSON.stringify(n,null,2)}\n`)}function Nt(e,t){if(!n(t?N(e,t):e))throw new Pe(M,t,`no entry`)}function V(e,t){if(Nt(e,t),I(t))throw new A(M,t)}function Pt(e,t){return V(e,t),de(B(e,`${R(t)}/${oe}`),t).annotations}function Ft(e,t,n){V(e,t);let r={version:1,entry:t,annotations:n};z(e,`${R(t)}/${oe}`,`${JSON.stringify(r,null,2)}\n`)}function It(e,t){return`${e}-${t.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}`}function Lt(e,t){if(!n(v(e,t)))return t;let r=t.lastIndexOf(`.`),i=r>0?t.slice(0,r):t,a=r>0?t.slice(r):``,o=2;for(;n(v(e,`${i}-${o}${a}`));)o++;return`${i}-${o}${a}`}function Rt(e,t){return fe(B(e,`${R(t)}/${ae}`),t).rounds}function zt(e,t,n,r=new Date){V(e,t);let i=R(t),o=`${i}/${se}`,s=P(e,o),c=r.toISOString().slice(0,19).replace(`T`,`-`).replace(/:/g,``),l=[];for(let t of n.comments){if(!t.unit)continue;let n=t.kind===`remove`?`remove`:`comment`;if(n===`comment`&&!t.text.trim()&&t.assets.length===0)continue;let r=[];for(let n of t.assets){a(s,{recursive:!0});let t=Lt(s,It(c,n.name));z(e,`${o}/${t}`,Buffer.from(n.data,`base64`)),r.push({path:`${se}/${t}`,name:n.name,type:n.type})}l.push({unit:t.unit,file:t.file,heading:t.heading,kind:n,quote:t.quote??``,text:t.text,assets:r})}let u=`${i}/${ae}`,d=fe(B(e,u),t);d.version=1,d.entry=t,d.rounds.push({submittedAt:r.toISOString(),comments:l}),z(e,u,`${JSON.stringify(d,null,2)}\n`);try{Bt(e,t,n.queued??[])}catch{}return{path:u,round:d.rounds.length}}function Bt(e,t,n){if(n.length===0)return;let r=new Set(n),i=Pt(e,t),a=i.filter(e=>!r.has(e.id));a.length!==i.length&&Ft(e,t,a)}var H=class extends Error{code=`bad-request`;constructor(e){super(`specs: ${e}`),this.name=`Refused`}},U=`rounds`,Vt=`intake.md`,Ht=`intake-assets`;function Ut(e){return`${R(e)}/${pe}`}function Wt(e,t){try{return s(P(e,t),`utf8`)}catch{return null}}function W(e,t,n=`children`){G(e,t,n);let r=Wt(e,Ut(t));return{state:ke(r,t===``?h(e):t.split(`/`).pop()??t),raw:r}}var Gt=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function Kt(e,t,r){let i=r.trim();if(i===``)throw new H(`a decision card names the artifact it is about`);let a=N(e,t?`${t}/${i}`:i);if(!n(a)||!f(a).isFile())throw new H(`there is no ${i} in this piece of work`)}function G(e,t,n=`children`){let r=t.trim();if(!r&&n!==`self`)throw new H(`which piece of work? name one`);if(n!==`self`&&!qt(r))throw new H(`there is no piece of work called "${r}"`);let i=!1;try{i=f(r?N(e,r):e).isDirectory()}catch{i=!1}if(!i)throw new H(`there is no piece of work called "${r}"`)}function qt(e){let t=e.split(`/`);return t.some(e=>e===``)?!1:t.length===1?t[0]!==pt:t.length===2&&t[0]===`archive`}function K(e,t,n,r){if(I(t))throw new H(`${t} is signed off, so nothing is written into it`);z(e,Ut(t),Ae(r,n)),j(N(e,`${t}/README.md`),Dt(n))}function q(e,t,n){e.log.push({at:n,said:t})}function Jt(e){return e.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}function Yt(e,t){let r=C(t)||`spec`;if(!n(N(e,r)))return r;let i=2;for(;n(N(e,`${r}-${i}`));)i++;return`${r}-${i}`}function Xt(e,t,n=new Date){let r=t.name.trim();if(!r)throw new H(`an intake needs a working name`);if(!t.text.trim()&&t.attachments.length===0)throw new H(`an intake needs something in it`);let i=Yt(e,r);a(N(e,i),{recursive:!0});let o=[];for(let n of t.attachments){let t=`${Ht}/${Jt(n.name)}`;a(N(e,`${i}/${Ht}`),{recursive:!0}),j(N(e,`${i}/${t}`),Buffer.from(n.data,`base64`)),o.push(t)}let s=[`# ${r}`,``,t.text.trim(),``];if(o.length>0){s.push(`## Attached`,``);for(let e of o)s.push(`- [${e.split(`/`).pop()}](${e})`);s.push(``)}j(N(e,`${i}/${Vt}`),s.join(`
3
- `));let c=n.toISOString(),l=ke(null,r,c);return l.wish={imagination:t.wish.imagination===!0},q(l,`filed`,c),K(e,i,l,null),{entry:i,path:`${i}/${Vt}`}}var Zt=`${U}/protocol.json`;function Qt(e,t,n,r=new Date){G(e,t);let i=new Map;for(let e of n.stages){if(!T(e.id))throw new H(`no stage named ${e.id}`);i.set(e.id,e)}let a=w.map(e=>({id:e.id,recommended:i.get(e.id)?.recommended===!0,why:i.get(e.id)?.why??``})),o=r.toISOString();z(e,`${R(t)}/${Zt}`,`${JSON.stringify({stages:a,note:n.note,offeredAt:o},null,2)}\n`);let s={id:`protocol`,kind:`protocol`,title:`Confirm the steps for this work`,file:``,payload:Zt,declaredAt:o},{state:c,raw:l}=W(e,t);return c.awaiting=[...c.awaiting.filter(e=>e.id!==s.id),s],K(e,t,c,l),s}function $t(e,t,n,r=new Date){let i=new Set(n.stages);for(let e of i)if(!T(e))throw new H(`no stage named ${e}`);let{state:a,raw:o}=W(e,t),s=new Map(a.protocol.map(e=>[e.id,e])),c=a.approvals.some(e=>e.id===`protocol`),l={imagination:a.modes.imagination,settings:a.stageSettings},u=e=>i.has(e)||s.get(e)?.status===`done`||s.get(e)?.status===`running`;a.protocol=w.filter(e=>u(e.id)).map(e=>s.get(e.id)??{id:e.id,status:`waiting`,artifacts:[]});let d=new Set(a.protocol.map(e=>e.id)),f=n.imagination??l.imagination;a.modes.imagination=f&&a.protocol.some(e=>me.includes(e.id)),a.stageSettings={research:d.has(`research`)?n.settings.research??l.settings.research:null},a.awaiting=a.awaiting.filter(e=>e.kind!==`protocol`);let p={amended:c,added:[...d].filter(e=>!s.has(e)),removed:[...s.keys()].filter(e=>!d.has(e)),changed:!1,said:``};p.changed=p.added.length>0||p.removed.length>0||a.modes.imagination!==l.imagination||JSON.stringify(a.stageSettings)!==JSON.stringify(l.settings),p.said=en(p,d.size);let m=r.toISOString();return(!c||p.changed)&&(a.approvals.push({id:`protocol`,at:m,outcome:c?`amended`:`confirmed`}),q(a,c?`protocol amended - ${p.said}`:`protocol confirmed${a.modes.imagination?` - imagination mode`:``}`,m),n.note.trim()&&q(a,`said with the protocol: ${n.note.trim()}`,m)),K(e,t,a,o),{state:a,change:p}}function en(e,t){let n=e=>e.map(e=>T(e)?.title.toLowerCase()??e).join(`, `);if(!e.amended)return`${t} ${t===1?`step`:`steps`}`;let r=[];return e.added.length>0&&r.push(`added ${n(e.added)}`),e.removed.length>0&&r.push(`dropped ${n(e.removed)}`),r.length>0?r.join(`, `):e.changed?`the same steps, set up differently`:`the steps are as they were`}function tn(e,t,n){let{state:r,raw:i}=W(e,t);return r.runRecommendations=n,K(e,t,r,i),r}function nn(e,t,n,r=new Date){let{state:i,raw:a}=W(e,t),o=r.toISOString();return i.runDecisions={...n,decidedAt:o},q(i,`the run is set up - ${rn(i.runDecisions)}`,o),K(e,t,i,a),i}function rn(e){let t=he.find(t=>t.id===e.commits)?.said??e.commits,n=e.branch===`new`?`on ${e.branchName||`a new branch`}`:`here`,r=e.checkpoints.filter(e=>e.chosen).length;return`${t}, ${n}, ${r} review ${r===1?`checkpoint`:`checkpoints`}`}function an(e,t,n,r=new Date){let{state:i,raw:a}=W(e,t),o=r.toISOString();if(n.kind===`activity`)i.activity={said:n.said,at:o};else if(n.kind===`stage`){let e=T(n.stage);if(!e)throw new H(`no stage named ${n.stage}`);let t=i.protocol.find(e=>e.id===n.stage);if(!t)throw new H(`${n.stage} is not in this protocol`);t.status=n.status,n.artifacts&&(t.artifacts=[...new Set([...t.artifacts,...n.artifacts])]),n.status===`done`&&q(i,`${e.title.toLowerCase()} complete`,o),n.status===`running`&&(i.activity={said:e.title,at:o})}else if(n.kind===`phase`){let e=C(n.phase);if(!e)throw new H(`a phase needs a name`);let t=i.phases.find(t=>t.id===e),r=n.title??t?.title??n.phase;t?(t.status=n.status,t.title=r):i.phases.push({id:e,title:r,status:n.status}),n.status===`done`&&q(i,`${r} done`,o),n.status===`running`&&(i.activity={said:r,at:o})}else if(n.kind===`plan`){let e=new Set,t=[];for(let r of n.phases){let n=C(r.phase);if(!n||e.has(n))continue;e.add(n);let a=i.phases.find(e=>e.id===n);t.push({id:n,title:r.title||a?.title||r.phase,status:a?.status??`waiting`})}let r=i.phases.filter(t=>!e.has(t.id));i.phases=t,r.length>0&&q(i,`the plan no longer holds ${r.map(e=>e.title).join(`, `)}`,o)}else n.kind===`status`?(i.status=n.status,q(i,`status: ${n.status}`,o)):q(i,n.said,o);return K(e,t,i,a),i}function on(e,t,n,r=new Date){G(e,t);let i=C(n.id);if(!i)throw new H(`an awaiting item needs an id`);Gt(n.payload)&&n.payload.kind===`decision`&&Kt(e,t,n.file);let a=`${U}/${i}.json`,o=r.toISOString();z(e,`${R(t)}/${a}`,`${JSON.stringify({id:i,kind:n.kind,title:n.title,file:n.file,declaredAt:o,payload:n.payload},null,2)}\n`);let s={id:i,kind:n.kind,title:n.title,file:n.file,payload:a,declaredAt:o},{state:c,raw:l}=W(e,t);if(sn(s,n.payload))for(let n of c.awaiting)n.id===i||n.file!==s.file||sn(n,cn(e,t,n.id))&&fn(e,t,c,n,`superseded by ${i}`,o);return c.awaiting=[...c.awaiting.filter(e=>e.id!==i),s],K(e,t,c,l),s}function sn(e,t){return e.kind!==`gate`||!e.file?!1:!(Gt(t)&&t.kind===`decision`)}function cn(e,t,n){try{return J(e,t,n).payload}catch{return null}}function J(e,t,n){let r=C(n);if(!r)throw new H(`a round has an id`);let i=Wt(e,`${R(t)}/${U}/${r}.json`);if(i===null)throw new H(`no round named ${n}`);try{let e=JSON.parse(i);if(typeof e!=`object`||!e||Array.isArray(e))throw new H(`the round named ${n} could not be read`);return e}catch{throw new H(`the round named ${n} could not be read`)}}function ln(e,t,n){return J(e,t,n).answer??null}function un(e,t,n,r,i=new Date){let a=C(n),o=J(e,t,a);if(At(o.answer)!==null)throw new H(`${n} was already answered`);if(o.withdrawn!=null)throw new H(`${n} was withdrawn`);let s=i.toISOString(),c={...r,answeredAt:s},l=`${R(t)}/${U}/${a}.json`;z(e,l,`${JSON.stringify({...o,answer:c},null,2)}\n`);let{state:u,raw:d}=W(e,t),f=u.awaiting.find(e=>e.id===a);return u.awaiting=u.awaiting.filter(e=>e.id!==a),u.approvals.push({id:a,at:s,outcome:r.outcome}),q(u,`${f?.title||a}: ${pn[r.outcome]}`,s),K(e,t,u,d),{path:l,outcome:r.outcome}}function dn(e,t,n,r=new Date){let i=C(n.id);if(!i)throw new H(`a withdrawal names the ask it takes back`);let a=n.reason.trim();if(!a)throw new H(`a withdrawal says why the ask is no longer open`);let{state:o,raw:s}=W(e,t),c=o.awaiting.find(e=>e.id===i);if(!c)throw new H(`${n.id} is not waiting for an answer`);return fn(e,t,o,c,a,r.toISOString()),K(e,t,o,s),{path:`${R(t)}/${U}/${i}.json`,id:i}}function fn(e,t,n,r,i,a){let o=`${R(t)}/${U}/${r.id}.json`,s=null;try{s=J(e,t,r.id)}catch{s=null}if(s!==null){let t={...s,withdrawn:{at:a,reason:i}};z(e,o,`${JSON.stringify(t,null,2)}\n`)}n.awaiting=n.awaiting.filter(e=>e.id!==r.id),q(n,`${r.title||r.id}: withdrawn - ${i}`,a)}var pn={approved:`approved`,changes:`changes asked for`,answered:`answered`};function mn(e,t,n,r=new Date){let i=r.toISOString(),{state:a,raw:o}=W(e,t),s={...n,at:i};return a.reviews=[...a.reviews.filter(e=>e.round!==s.round),s].sort((e,t)=>e.round-t.round),q(a,`review round ${s.round}: ${s.verdict}`,i),K(e,t,a,o),a}var hn=`spec/SPEC.md`;function gn(e){try{return{at:e,before:s(e,`utf8`)}}catch{return{at:e,before:null}}}function _n(e){for(let t of e)t.before===null?d(t.at,{force:!0}):j(t.at,t.before)}function vn(e,t){if(/^Completed:/m.test(e))return null;let n=e.split(`
1
+ import{closeSync as e,constants as t,existsSync as n,fstatSync as r,lstatSync as i,mkdirSync as a,openSync as o,readFileSync as s,readdirSync as c,realpathSync as l,renameSync as u,rmSync as d,statSync as f,watch as p,writeFileSync as m}from"node:fs";import{basename as h,dirname as g,isAbsolute as _,join as v,relative as y,resolve as b,sep as ee}from"node:path";import{createHash as te}from"node:crypto";var x=`.specs`,ne=`reader-state.json`,re=`user-feedback.json`,ie=`pending-annotations.json`,ae=`user-feedback-assets`;function oe(){return{version:1,units:{},dismissed:[]}}function se(e){return{version:1,entry:e,rounds:[]}}function ce(e){return{version:1,entry:e,annotations:[]}}var S=e=>typeof e==`object`&&!!e&&!Array.isArray(e),C=(e,t=``)=>typeof e==`string`?e:t;function le(e){let t;try{t=JSON.parse(e||`null`)}catch{return oe()}if(!S(t)||t.version!==1)return oe();let n=oe();if(S(t.units))for(let[e,r]of Object.entries(t.units)){if(!S(r))continue;let t={};typeof r.readHash==`string`&&(t.readHash=r.readHash),typeof r.readAt==`string`&&(t.readAt=r.readAt),n.units[e]=t}return Array.isArray(t.dismissed)&&(n.dismissed=t.dismissed.filter(e=>typeof e==`string`)),n}function ue(e,t){let n;try{n=JSON.parse(e||`null`)}catch{return ce(t)}if(!S(n)||n.version!==1)return ce(t);let r=[];if(Array.isArray(n.annotations))for(let e of n.annotations)!S(e)||typeof e.id!=`string`||typeof e.unit!=`string`||e.id===``||e.unit===``||r.push({id:e.id,unit:e.unit,file:C(e.file),heading:C(e.heading),kind:e.kind===`remove`?`remove`:`comment`,quote:C(e.quote),text:C(e.text)});return{version:1,entry:C(n.entry,t),annotations:r}}function de(e,t){let n;try{n=JSON.parse(e||`null`)}catch{return se(t)}if(!S(n)||n.version!==1)return se(t);let r=[];if(Array.isArray(n.rounds))for(let e of n.rounds){if(!S(e))continue;let t=[];if(Array.isArray(e.comments))for(let n of e.comments){if(!S(n)||typeof n.unit!=`string`)continue;let e=[];if(Array.isArray(n.assets))for(let t of n.assets)!S(t)||typeof t.path!=`string`||e.push({path:t.path,name:C(t.name,t.path),type:C(t.type)});t.push({unit:n.unit,file:C(n.file),heading:C(n.heading),kind:n.kind===`remove`?`remove`:`comment`,quote:C(n.quote),text:C(n.text),assets:e})}r.push({submittedAt:C(e.submittedAt),comments:t})}return{version:1,entry:C(n.entry,t),rounds:r}}function w(e){return e.toLowerCase().replace(/`/g,``).replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||``}function fe(e,t){return e.kind===`checkpoint`&&t.kind===`checkpoint`?e.after===t.after:e.kind===t.kind}function pe(e){if(me(e)){if(e.kind===`execution`)return{kind:`execution`};if(e.kind===`checkpoint`&&typeof e.after==`string`&&e.after!==``)return{kind:`checkpoint`,after:e.after}}return{kind:`spec`}}var me=e=>typeof e==`object`&&!!e&&!Array.isArray(e),T=(e,t=``)=>typeof e==`string`?e:t,he=[`approved`,`changes`,`answered`];function ge(e){if(!me(e))return null;let t=T(e.outcome);if(!he.includes(t))return null;let n=[];for(let t of Array.isArray(e.questions)?e.questions:[])!me(t)||typeof t.question!=`string`||n.push({question:t.question,chosen:Array.isArray(t.chosen)?t.chosen.filter(e=>typeof e==`string`):[],said:T(t.said),answered:t.answered===!0});return{outcome:t,said:T(e.said),questions:n,accepted:e.accepted===!0,answeredAt:T(e.answeredAt)}}var _e=`state.json`,E=[{id:`exploration`,title:`High-level exploration`,group:`discovery`,about:`We think the problem through together first - directions to take, risks, and what is still unclear.`},{id:`interview`,title:`Interview`,group:`discovery`,about:`I ask you focused questions until nothing important is left open.`},{id:`research`,title:`Research`,group:`discovery`,about:`I look outside your code - a library, a protocol, an algorithm, a subject area - and write up what I find.`},{id:`codebase-analysis`,title:`Codebase analysis`,group:`discovery`,about:`I read the parts of your code this work touches and note what shapes the design.`},{id:`technical-specs`,title:`Technical specs`,group:`deliverables`,about:`A written specification another agent can build from without asking you anything.`},{id:`ui-mocks`,title:`UI mocks`,group:`deliverables`,about:`Mock screens you open in your browser and comment on before anything is built.`},{id:`execution-plan`,title:`Execution plan`,group:`deliverables`,about:`The work split into phases, each with a goal and a way to tell it is done.`},{id:`quality-review`,title:`Specs quality review`,group:`deliverables`,about:`Fresh helper agents review everything we wrote, and I fix what is plainly wrong and bring the judgment calls to you.`}],ve=[`exploration`,`ui-mocks`];function D(e){return E.find(t=>t.id===e)??null}function ye(e){return[...e].sort((e,t)=>e.startedAt===t.startedAt?0:e.startedAt?t.startedAt?e.startedAt<t.startedAt?-1:1:-1:1)}var be=`final`;function xe(e,t){if(e===`final`)return be;let n=t.find(t=>t.id===e);return n===void 0?null:n.title}var Se=[{id:`per-phase`,said:`a commit per phase`},{id:`single`,said:`a single commit at the end`},{id:`none`,said:`no commits`}],Ce=[{id:`current`,said:`the current branch`},{id:`new`,said:`a new branch`}],we=[{id:`none`,said:`none`},{id:`chosen`,said:`after chosen phases`},{id:`every`,said:`every phase`}];function Te(e,t){return{version:1,name:e,created:t,status:`specifying`,protocol:[],modes:{imagination:!1},wish:{imagination:!1},stageSettings:{research:null},activity:{said:``,at:``},awaiting:[],phases:[],runRecommendations:null,runDecisions:null,reviews:[],approvals:[],log:[]}}var O=e=>typeof e==`object`&&!!e&&!Array.isArray(e),k=(e,t=``)=>typeof e==`string`?e:t,A=(e,t,n)=>t.includes(e)?e:n,Ee=[`specifying`,`ready`,`executing`,`complete`],De=[`waiting`,`running`,`done`],Oe=[`gate`,`round`,`protocol`],ke=[`focused`,`named`,`broad`],Ae=Se.map(e=>e.id),je=Ce.map(e=>e.id),Me=we.map(e=>e.id);function Ne(e){if(!O(e)||e.scope===void 0)return null;let t=A(e.scope,ke,`focused`);return{scope:t,subject:t===`named`?j(k(e.subject)):``}}function j(e,t=200){return e.replace(/\p{C}+/gu,` `).replace(/\s+/g,` `).trim().slice(0,t)}function Pe(e){if(!O(e))return null;let t=[];for(let n of Array.isArray(e.checkpoints)?e.checkpoints:[]){if(!O(n))continue;let e=Array.isArray(n.panel)?n.panel.filter(e=>typeof e==`string`).map(e=>j(e)):[];t.push({after:w(k(n.after)),why:j(k(n.why)),panel:e,chosen:n.chosen!==!1&&e.length>0})}return{commits:A(e.commits,Ae,`per-phase`),branch:A(e.branch,je,`current`),branchName:j(k(e.branchName)),pauses:A(e.pauses,Me,`none`),pausePhases:Array.isArray(e.pausePhases)?e.pausePhases.filter(e=>typeof e==`string`).map(e=>w(e)):[],checkpoints:t}}function Fe(e){if(!O(e))return null;let t=Pe(e);if(t===null)return null;let n={};if(O(e.pauseReasons))for(let[t,r]of Object.entries(e.pauseReasons)){if(typeof r!=`string`)continue;let e=j(r);e!==``&&(n[w(t)]=e)}return{...t,pauseReasons:n,noPauseReason:j(k(e.noPauseReason))}}function Ie(e,t=``,n=``){let r;try{r=JSON.parse(e||`null`)}catch{return Te(t,n)}if(!O(r)||r.version!==1)return Te(t,n);let i=Te(k(r.name,t),k(r.created,n)),a=k(r.status);if(Ee.includes(a)&&(i.status=a),Array.isArray(r.protocol))for(let e of r.protocol){if(!O(e)||!D(k(e.id)))continue;let t=k(e.status);i.protocol.push({id:k(e.id),status:De.includes(t)?t:`waiting`,artifacts:Array.isArray(e.artifacts)?e.artifacts.filter(e=>typeof e==`string`):[],startedAt:k(e.startedAt)})}if(O(r.modes)&&(i.modes.imagination=r.modes.imagination===!0),O(r.wish)&&(i.wish.imagination=r.wish.imagination===!0),O(r.stageSettings)&&(i.stageSettings={research:Ne(r.stageSettings.research)}),O(r.activity)&&(i.activity={said:k(r.activity.said),at:k(r.activity.at)}),Array.isArray(r.awaiting))for(let e of r.awaiting){if(!O(e)||!e.id)continue;let t=k(e.kind);i.awaiting.push({id:k(e.id),kind:Oe.includes(t)?t:`round`,title:k(e.title),file:k(e.file),payload:k(e.payload),declaredAt:k(e.declaredAt)})}if(Array.isArray(r.phases))for(let e of r.phases){if(!O(e)||!e.id)continue;let t=k(e.status);i.phases.push({id:k(e.id),title:k(e.title),status:De.includes(t)?t:`waiting`})}i.runRecommendations=Fe(r.runRecommendations);let o=Pe(r.runDecisions);if(o!==null&&O(r.runDecisions)&&(i.runDecisions={...o,decidedAt:k(r.runDecisions.decidedAt)}),Array.isArray(r.reviews))for(let e of r.reviews)!O(e)||typeof e.round!=`number`||i.reviews.push({round:e.round,verdict:k(e.verdict),findings:typeof e.findings==`number`?e.findings:0,judgments:typeof e.judgments==`number`?e.judgments:0,source:pe(e.source),at:k(e.at)});if(Array.isArray(r.approvals))for(let e of r.approvals)!O(e)||!e.id||i.approvals.push({id:k(e.id),at:k(e.at),outcome:k(e.outcome)});if(Array.isArray(r.log))for(let e of r.log)O(e)&&i.log.push({at:k(e.at),said:k(e.said)});return i}function Le(e,t){let n={};try{let t=JSON.parse(e||`null`);O(t)&&(n=t)}catch{n={}}return`${JSON.stringify({...n,...t},null,2)}\n`}function Re(e){let t=e?.code;return typeof t==`string`&&ze.has(t)?t:null}var ze=new Set([`path-refused`,`no-entry`,`no-file`,`bad-request`,`too-large`,`upstream-failed`,`unreadable-answer`]),M=class extends Error{requested;code=`path-refused`;constructor(e,t){super(`${e}: refused path ${t}`),this.requested=t,this.name=`PathRefused`}},Be=class extends Error{requested;code=`no-entry`;constructor(e,t){super(`${e}: no directory at ${t}`),this.requested=t,this.name=`NoSuchEntry`}},Ve=class extends Error{requested;code=`no-file`;constructor(e,t,n=`not a file`){super(`${e}: ${n} - ${t}`),this.requested=t,this.name=`NoSuchFile`}},He=class extends Error{requested;size;code=`too-large`;constructor(e,t,n){super(`${e}: too large to serve - ${t}`),this.requested=t,this.size=n,this.name=`FileTooLarge`}};function Ue(e){switch(Re(e)){case`path-refused`:return 403;case`no-entry`:case`no-file`:return 404;case`bad-request`:return 400;case`too-large`:return 413;case`upstream-failed`:case`unreadable-answer`:return 502}return We(e)?404:500}function We(e){let t=e?.code;return t===`ENOENT`||t===`ENOTDIR`}var Ge=32*1024*1024,Ke=new Set([`md`,`markdown`,`mdown`,`mkd`]),qe=new Set([`html`,`htm`]),Je=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`svg`,`avif`,`bmp`,`ico`]),Ye=new Set(`txt text log json yaml yml toml ini cfg conf csv tsv xml css scss js jsx mjs cjs ts tsx py rb go rs java kt swift c h cpp hpp sh bash zsh fish sql graphql gql env diff patch lock gitignore editorconfig`.split(` `));function Xe(e){let t=e.slice(e.lastIndexOf(`/`)+1).slice(1).match(/\.([^.]+)$/);return t?t[1].toLowerCase():``}function Ze(e){return e.startsWith(`.`)}function Qe(e){let t=Xe(e);return Ke.has(t)?`markdown`:qe.has(t)?`html`:Je.has(t)?`image`:Ye.has(t)||!t?`text`:`binary`}var $e=e=>/^[A-Z]/.test(e);function et(e,t){let n=$e(e);return n===$e(t)?e.localeCompare(t,`en`):n?-1:1}function tt(e){let t=e,n=[];for(;;)try{return v(l(t),...n)}catch{let r=g(t);if(r===t)return e;n.unshift(h(t)),t=r}}function nt(e){return tt(b(e))}function rt(e){return e.split(`/`).some(e=>e!==``&&Ze(e))}function it(e,t,n,r){let i=n.replace(/^\/+/,``);if(_(n)||i.includes(`\0`))throw new M(e,n);let a=r===`require`;if(rt(i)!==a)throw new M(e,n);let o=tt(b(t)),s=b(o,i);if(!st(o,s)||!st(o,tt(s))||rt(y(o,s).split(ee).join(`/`))!==a)throw new M(e,n);return s}function at(e,t,n){return it(e,t,n,`refuse`)}function ot(e,t,n){return it(e,t,n,`require`)}function st(e,t){return t===e||t.startsWith(e+ee)}function ct(e,t=`children`){if(t===`self`)return``;let n=e.split(`/`).filter(Boolean);return n.length<2?``:n[0]===`archive`?n.length>2?`archive/${n[1]}`:``:n[0]}var lt={md:`text/markdown; charset=utf-8`,markdown:`text/markdown; charset=utf-8`,html:`text/html; charset=utf-8`,htm:`text/html; charset=utf-8`,css:`text/css; charset=utf-8`,js:`text/javascript; charset=utf-8`,json:`application/json; charset=utf-8`,svg:`image/svg+xml`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,webp:`image/webp`,avif:`image/avif`,bmp:`image/bmp`,ico:`image/x-icon`,pdf:`application/pdf`,txt:`text/plain; charset=utf-8`};function ut(e){return lt[Xe(e)]??`application/octet-stream`}var dt=Ge;function ft(e,t,n){return pt(e,at(e,t,n),n)}function pt(n,i,a){let c=o(i,t.O_RDONLY|t.O_NOFOLLOW);try{let e=r(c);if(!e.isFile())throw new Ve(n,a);if(e.size>dt)throw new He(n,a,e.size);return{bytes:s(c),mediaType:ut(a),size:e.size,modified:new Date(e.mtimeMs).toISOString()}}finally{e(c)}}var mt=`.tmp-`;function ht(e){return e.slice(e.lastIndexOf(`/`)+1).includes(mt)}function N(e,t){a(g(e),{recursive:!0});let n=`${e}${mt}${process.pid}-${Math.random().toString(36).slice(2)}`;try{m(n,t),u(n,e)}finally{d(n,{force:!0})}}function gt(e,t,n,r){N(ot(e,t,n),r)}function _t(e,t,n){let r=ot(e,t,n);try{return s(r,`utf8`)}catch{return null}}function vt(e){try{let t=i(e);return{directory:t.isDirectory(),id:t.ino}}catch{return{directory:!1,id:0}}}function yt(e,t,r={}){let i=r.entries??`children`,a=r.coalesce??`per-path`,o=r.debounceMs??120,s=r.shared??(()=>!1),l=new Map,u=null,d=new Map,f=!1,m=t=>t===``?e:`${e}/${t}`,g=()=>{u=null;let e=[...l.values()];l.clear();for(let n of e)try{t(n)}catch{}},_=e=>{if(e===``||ht(e)||rt(e)&&!s(e))return;let t=ct(e,i);if(a===`per-entry`)l.set(t,{entry:t,path:t,kind:`changed`});else{let r=n(m(e))?`changed`:`gone`;l.set(e,{entry:t,path:e,kind:r})}u||=setTimeout(g,o)},v=e=>{for(let[t,n]of d)(t===e||t.startsWith(`${e}/`))&&(n.watcher.close(),d.delete(t))},y=t=>{if(f||d.has(t))return;let r;try{r=p(m(t),(r,i)=>{if(!i)return;let a=i.toString();if(a===``)return;let o=t===``?a:`${t}/${a}`;if(a===(t===``?h(e):t.split(`/`).at(-1))&&!n(m(o)))return;let s=d.get(o),c=vt(m(o));if(s&&c.directory){if(s.id===c.id)return;v(o),y(o),_(o);return}_(o),s?v(o):c.directory&&y(o)})}catch{return}r.on(`error`,()=>v(t)),d.set(t,{watcher:r,id:vt(m(t)).id});for(let e of b(t))y(e)},b=e=>{try{return c(m(e),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(t=>e===``?t.name:`${e}/${t.name}`)}catch{return[]}};return y(``),{refollow(){f||y(``)},close(){f=!0,u&&clearTimeout(u),u=null;for(let e of d.values())e.watcher.close();d.clear()}}}var bt=`archive`;function xt(e){let t=e=>String(e).padStart(2,`0`);return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}`}function St(e,t){let n=e.split(`/`).pop()??e;return`${bt}/${xt(t)}-${n}`}var P=`specs`;function F(e,t){return at(P,e,t)}function I(e,t){return ot(P,e,t)}var L=`archive`;function Ct(e){let t;try{t=c(e,{withFileTypes:!0})}catch{return[]}return t.filter(e=>!Ze(e.name)).sort((e,t)=>et(e.name,t.name))}var wt=4*1024*1024;function Tt(e,t,n){if(!(t===`markdown`||t===`text`)&&!(n>wt))try{return te(`sha1`).update(s(e)).digest(`hex`).slice(0,16)}catch{return}}function Et(e,t,n=!0,r=!1){let i=Ct(e),a=[],o=0;for(let n of i){if(!n.isFile()&&!n.isDirectory()||n.isDirectory())continue;let i=t?`${t}/${n.name}`:n.name,s;try{s=f(v(e,n.name))}catch{continue}o=Math.max(o,s.mtimeMs);let c=Qe(n.name);a.push({path:i,name:n.name,dir:t,kind:c,size:s.size,modified:new Date(s.mtimeMs).toISOString(),hash:r?Tt(v(e,n.name),c,s.size):void 0})}if(n)for(let n of i){if(!n.isDirectory())continue;let i=t?`${t}/${n.name}`:n.name,s=Et(v(e,n.name),i,!0,r);a.push(...s.files),o=Math.max(o,s.modified)}return{files:a,modified:o}}function Dt(e,t,n){let r=t?v(e,t):e,i=Et(r,``,n),a=i.modified;if(!a)try{a=f(r).mtimeMs}catch{a=0}return{path:t,name:h(t||e),files:i.files.length,modified:new Date(a).toISOString(),archived:R(t),status:kt(e,t),awaiting:At(e,t)}}function Ot(e,t){if(!t)return null;try{return Ie(s(v(e,t,x,_e),`utf8`),h(t))}catch{return null}}function kt(e,t){return Ot(e,t)?.status??null}function At(e,t){return Ot(e,t)?.awaiting.length??0}function jt(e,t=`children`){let n=h(e);if(t===`self`)return{root:e,rootName:n,mode:t,entries:[Dt(e,``,!0)]};let r=Ct(e),i=[];for(let t of r)!t.isDirectory()||t.name===L||i.push(Dt(e,t.name,!0));if(r.some(e=>e.isDirectory()&&e.name===L))for(let t of Ct(v(e,L)).reverse())t.isDirectory()&&i.push(Dt(e,`${L}/${t.name}`,!0));return{root:e,rootName:n,mode:t,entries:i}}function R(e){return e===L||e.startsWith(`${L}/`)}function Mt(e,t,n=`children`){let r=t?F(e,t):e,i;try{i=f(r)}catch{throw new Be(P,t)}if(!i.isDirectory())throw new Be(P,t);return{path:t,name:h(t||e),archived:R(t),files:Et(r,``,t!==``||n===`self`,!0).files}}function Nt(e){let t=new Date(e);if(!/^\d{4}-\d{2}-\d{2}/.test(e)||Number.isNaN(t.getTime()))return``;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())}`}function Pt(e){let t=[`# ${e.name||`A piece of work`}`,``],n=Nt(e.created);if(n&&t.push(`- Created: ${n}`),t.push(`- Status: ${e.status}`),e.modes.imagination&&t.push(`- Mode: imagination`),e.protocol.length>0){t.push(``,`## Protocol`,``);for(let n of ye(e.protocol)){let e=D(n.id),r=n.status===`running`?` (running)`:``;t.push(`- [${n.status===`done`?`x`:` `}] ${e?.title??n.id}${r}`)}}if(e.log.length>0){t.push(``,`## Log`,``);for(let n of e.log){let e=Nt(n.at);t.push(e?`- ${e}: ${n.said}`:`- ${n.said}`)}}return`${t.join(`
2
+ `)}\n`}function z(e){return e?`${e}/${x}`:x}function B(e,t,n){gt(P,e,t,n)}function V(e,t){return _t(P,e,t)}function Ft(e,t){return Lt(e,t),le(V(e,`${z(t)}/${ne}`))}function It(e,t,n){H(e,t),B(e,`${z(t)}/${ne}`,`${JSON.stringify(n,null,2)}\n`)}function Lt(e,t){if(!n(t?F(e,t):e))throw new Ve(P,t,`no entry`)}function H(e,t){if(Lt(e,t),R(t))throw new M(P,t)}function Rt(e,t){return H(e,t),ue(V(e,`${z(t)}/${ie}`),t).annotations}function zt(e,t,n){H(e,t);let r={version:1,entry:t,annotations:n};B(e,`${z(t)}/${ie}`,`${JSON.stringify(r,null,2)}\n`)}function Bt(e,t){return`${e}-${t.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}`}function Vt(e,t){if(!n(v(e,t)))return t;let r=t.lastIndexOf(`.`),i=r>0?t.slice(0,r):t,a=r>0?t.slice(r):``,o=2;for(;n(v(e,`${i}-${o}${a}`));)o++;return`${i}-${o}${a}`}function Ht(e,t){return de(V(e,`${z(t)}/${re}`),t).rounds}function Ut(e,t,n,r=new Date){H(e,t);let i=z(t),o=`${i}/${ae}`,s=I(e,o),c=r.toISOString().slice(0,19).replace(`T`,`-`).replace(/:/g,``),l=[];for(let t of n.comments){if(!t.unit)continue;let n=t.kind===`remove`?`remove`:`comment`;if(n===`comment`&&!t.text.trim()&&t.assets.length===0)continue;let r=[];for(let n of t.assets){a(s,{recursive:!0});let t=Vt(s,Bt(c,n.name));B(e,`${o}/${t}`,Buffer.from(n.data,`base64`)),r.push({path:`${ae}/${t}`,name:n.name,type:n.type})}l.push({unit:t.unit,file:t.file,heading:t.heading,kind:n,quote:t.quote??``,text:t.text,assets:r})}let u=`${i}/${re}`,d=de(V(e,u),t);d.version=1,d.entry=t,d.rounds.push({submittedAt:r.toISOString(),comments:l}),B(e,u,`${JSON.stringify(d,null,2)}\n`);try{Wt(e,t,n.queued??[])}catch{}return{path:u,round:d.rounds.length}}function Wt(e,t,n){if(n.length===0)return;let r=new Set(n),i=Rt(e,t),a=i.filter(e=>!r.has(e.id));a.length!==i.length&&zt(e,t,a)}var U=class extends Error{code=`bad-request`;constructor(e){super(`specs: ${e}`),this.name=`Refused`}},W=`rounds`,Gt=`intake.md`,Kt=`intake-assets`;function qt(e){return`${z(e)}/${_e}`}function Jt(e,t){try{return s(I(e,t),`utf8`)}catch{return null}}function G(e,t,n=`children`){Zt(e,t,n);let r=Jt(e,qt(t));return{state:Ie(r,t===``?h(e):t.split(`/`).pop()??t),raw:r}}var Yt=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function Xt(e,t,r){let i=r.trim();if(i===``)throw new U(`a decision card names the artifact it is about`);let a=F(e,t?`${t}/${i}`:i);if(!n(a)||!f(a).isFile())throw new U(`there is no ${i} in this piece of work`)}function Zt(e,t,n=`children`){let r=t.trim();if(!r&&n!==`self`)throw new U(`which piece of work? name one`);if(n!==`self`&&!Qt(r))throw new U(`there is no piece of work called "${r}"`);let i=!1;try{i=f(r?F(e,r):e).isDirectory()}catch{i=!1}if(!i)throw new U(`there is no piece of work called "${r}"`)}function Qt(e){let t=e.split(`/`);return t.some(e=>e===``)?!1:t.length===1?t[0]!==bt:t.length===2&&t[0]===`archive`}function K(e,t,n,r){if(R(t))throw new U(`${t} is signed off, so nothing is written into it`);B(e,qt(t),Le(r,n)),N(F(e,`${t}/README.md`),Pt(n))}function q(e,t,n){e.log.push({at:n,said:t})}function $t(e){return e.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}function en(e,t){let r=w(t)||`spec`;if(!n(F(e,r)))return r;let i=2;for(;n(F(e,`${r}-${i}`));)i++;return`${r}-${i}`}function tn(e,t,n=new Date){let r=t.name.trim();if(!r)throw new U(`an intake needs a working name`);if(!t.text.trim()&&t.attachments.length===0)throw new U(`an intake needs something in it`);let i=en(e,r);a(F(e,i),{recursive:!0});let o=[];for(let n of t.attachments){let t=`${Kt}/${$t(n.name)}`;a(F(e,`${i}/${Kt}`),{recursive:!0}),N(F(e,`${i}/${t}`),Buffer.from(n.data,`base64`)),o.push(t)}let s=[`# ${r}`,``,t.text.trim(),``];if(o.length>0){s.push(`## Attached`,``);for(let e of o)s.push(`- [${e.split(`/`).pop()}](${e})`);s.push(``)}N(F(e,`${i}/${Gt}`),s.join(`
3
+ `));let c=n.toISOString(),l=Ie(null,r,c);return l.wish={imagination:t.wish.imagination===!0},q(l,`filed`,c),K(e,i,l,null),{entry:i,path:`${i}/${Gt}`}}var nn=`${W}/protocol.json`;function rn(e,t,n,r=new Date){Zt(e,t);let i=new Map;for(let e of n.stages){if(!D(e.id))throw new U(`no stage named ${e.id}`);i.set(e.id,e)}let a=E.map(e=>({id:e.id,recommended:i.get(e.id)?.recommended===!0,why:i.get(e.id)?.why??``})),o=r.toISOString();B(e,`${z(t)}/${nn}`,`${JSON.stringify({stages:a,note:n.note,offeredAt:o},null,2)}\n`);let s={id:`protocol`,kind:`protocol`,title:`Confirm the steps for this work`,file:``,payload:nn,declaredAt:o},{state:c,raw:l}=G(e,t);return c.awaiting=[...c.awaiting.filter(e=>e.id!==s.id),s],K(e,t,c,l),s}function an(e,t,n,r=new Date){let i=new Set(n.stages);for(let e of i)if(!D(e))throw new U(`no stage named ${e}`);let{state:a,raw:o}=G(e,t),s=new Map(a.protocol.map(e=>[e.id,e])),c=a.approvals.some(e=>e.id===`protocol`),l={imagination:a.modes.imagination,settings:a.stageSettings},u=e=>i.has(e)||s.get(e)?.status===`done`||s.get(e)?.status===`running`;a.protocol=E.filter(e=>u(e.id)).map(e=>s.get(e.id)??{id:e.id,status:`waiting`,artifacts:[],startedAt:``});let d=new Set(a.protocol.map(e=>e.id)),f=n.imagination??l.imagination;a.modes.imagination=f&&a.protocol.some(e=>ve.includes(e.id)),a.stageSettings={research:d.has(`research`)?n.settings.research??l.settings.research:null},a.awaiting=a.awaiting.filter(e=>e.kind!==`protocol`);let p={amended:c,added:[...d].filter(e=>!s.has(e)),removed:[...s.keys()].filter(e=>!d.has(e)),changed:!1,said:``};p.changed=p.added.length>0||p.removed.length>0||a.modes.imagination!==l.imagination||JSON.stringify(a.stageSettings)!==JSON.stringify(l.settings),p.said=on(p,d.size);let m=r.toISOString();return(!c||p.changed)&&(a.approvals.push({id:`protocol`,at:m,outcome:c?`amended`:`confirmed`}),q(a,c?`protocol amended - ${p.said}`:`protocol confirmed${a.modes.imagination?` - imagination mode`:``}`,m),n.note.trim()&&q(a,`said with the protocol: ${n.note.trim()}`,m)),K(e,t,a,o),{state:a,change:p}}function on(e,t){let n=e=>e.map(e=>D(e)?.title.toLowerCase()??e).join(`, `);if(!e.amended)return`${t} ${t===1?`step`:`steps`}`;let r=[];return e.added.length>0&&r.push(`added ${n(e.added)}`),e.removed.length>0&&r.push(`dropped ${n(e.removed)}`),r.length>0?r.join(`, `):e.changed?`the same steps, set up differently`:`the steps are as they were`}function sn(e,t,n){let{state:r,raw:i}=G(e,t);return r.runRecommendations=n,K(e,t,r,i),r}function cn(e,t,n,r=new Date){let{state:i,raw:a}=G(e,t),o=r.toISOString();return i.runDecisions={...n,decidedAt:o},q(i,`the run is set up - ${ln(i.runDecisions)}`,o),K(e,t,i,a),i}function ln(e){let t=Se.find(t=>t.id===e.commits)?.said??e.commits,n=e.branch===`new`?`on ${e.branchName||`a new branch`}`:`here`,r=e.checkpoints.filter(e=>e.chosen).length;return`${t}, ${n}, ${r} review ${r===1?`checkpoint`:`checkpoints`}`}function un(e,t,n,r=new Date){let{state:i,raw:a}=G(e,t),o=r.toISOString();if(n.kind===`activity`)i.activity={said:n.said,at:o};else if(n.kind===`stage`){let e=D(n.stage);if(!e)throw new U(`no stage named ${n.stage}`);let t=i.protocol.find(e=>e.id===n.stage);if(!t)throw new U(`${n.stage} is not in this protocol`);t.status=n.status,!t.startedAt&&n.status!==`waiting`&&(t.startedAt=o),n.artifacts&&(t.artifacts=[...new Set([...t.artifacts,...n.artifacts])]),n.status===`done`&&q(i,`${e.title.toLowerCase()} complete`,o),n.status===`running`&&(i.activity={said:e.title,at:o})}else if(n.kind===`phase`){let e=w(n.phase);if(!e)throw new U(`a phase needs a name`);let t=i.phases.find(t=>t.id===e);if(!t&&i.phases.length>0)throw new U(`this plan holds no phase ${e}; it holds ${i.phases.map(e=>e.id).join(`, `)}. Post the plan's phase list with \`{"kind":"plan"}\` where the plan changed, or report one of those.`);let r=n.title??t?.title??n.phase;t?(t.status=n.status,t.title=r):i.phases.push({id:e,title:r,status:n.status}),n.status===`done`&&q(i,`${r} done`,o),n.status===`running`&&(i.activity={said:r,at:o})}else if(n.kind===`plan`){let e=new Set,t=[];for(let r of n.phases){let n=w(r.phase);if(!n||e.has(n))continue;e.add(n);let a=i.phases.find(e=>e.id===n);t.push({id:n,title:r.title||a?.title||r.phase,status:a?.status??`waiting`})}let r=i.phases.filter(t=>!e.has(t.id));i.phases=t,r.length>0&&q(i,`the plan no longer holds ${r.map(e=>e.title).join(`, `)}`,o)}else n.kind===`status`?(i.status=n.status,q(i,`status: ${n.status}`,o)):q(i,n.said,o);return K(e,t,i,a),i}function dn(e,t,n,r=new Date){Zt(e,t);let i=w(n.id);if(!i)throw new U(`an awaiting item needs an id`);Yt(n.payload)&&n.payload.kind===`decision`&&Xt(e,t,n.file);let a=`${W}/${i}.json`,o=r.toISOString();B(e,`${z(t)}/${a}`,`${JSON.stringify({id:i,kind:n.kind,title:n.title,file:n.file,declaredAt:o,payload:n.payload},null,2)}\n`);let s={id:i,kind:n.kind,title:n.title,file:n.file,payload:a,declaredAt:o},{state:c,raw:l}=G(e,t);if(fn(s,n.payload))for(let n of c.awaiting)n.id===i||n.file!==s.file||fn(n,pn(e,t,n.id))&&_n(e,t,c,n,`superseded by ${i}`,o);return c.awaiting=[...c.awaiting.filter(e=>e.id!==i),s],K(e,t,c,l),s}function fn(e,t){return e.kind!==`gate`||!e.file?!1:!(Yt(t)&&t.kind===`decision`)}function pn(e,t,n){try{return J(e,t,n).payload}catch{return null}}function J(e,t,n){let r=w(n);if(!r)throw new U(`a round has an id`);let i=Jt(e,`${z(t)}/${W}/${r}.json`);if(i===null)throw new U(`no round named ${n}`);try{let e=JSON.parse(i);if(typeof e!=`object`||!e||Array.isArray(e))throw new U(`the round named ${n} could not be read`);return e}catch{throw new U(`the round named ${n} could not be read`)}}function mn(e,t,n){return J(e,t,n).answer??null}function hn(e,t,n,r,i=new Date){let a=w(n),o=J(e,t,a);if(ge(o.answer)!==null)throw new U(`${n} was already answered`);if(o.withdrawn!=null)throw new U(`${n} was withdrawn`);let s=i.toISOString(),c={...r,answeredAt:s},l=`${z(t)}/${W}/${a}.json`;B(e,l,`${JSON.stringify({...o,answer:c},null,2)}\n`);let{state:u,raw:d}=G(e,t),f=u.awaiting.find(e=>e.id===a);return u.awaiting=u.awaiting.filter(e=>e.id!==a),u.approvals.push({id:a,at:s,outcome:r.outcome}),q(u,`${f?.title||a}: ${vn[r.outcome]}`,s),K(e,t,u,d),{path:l,outcome:r.outcome}}function gn(e,t,n,r=new Date){let i=w(n.id);if(!i)throw new U(`a withdrawal names the ask it takes back`);let a=n.reason.trim();if(!a)throw new U(`a withdrawal says why the ask is no longer open`);let{state:o,raw:s}=G(e,t),c=o.awaiting.find(e=>e.id===i);if(!c)throw new U(`${n.id} is not waiting for an answer`);return _n(e,t,o,c,a,r.toISOString()),K(e,t,o,s),{path:`${z(t)}/${W}/${i}.json`,id:i}}function _n(e,t,n,r,i,a){let o=`${z(t)}/${W}/${r.id}.json`,s=null;try{s=J(e,t,r.id)}catch{s=null}if(s!==null){let t={...s,withdrawn:{at:a,reason:i}};B(e,o,`${JSON.stringify(t,null,2)}\n`)}n.awaiting=n.awaiting.filter(e=>e.id!==r.id),q(n,`${r.title||r.id}: withdrawn - ${i}`,a)}var vn={approved:`approved`,changes:`changes asked for`,answered:`answered`};function yn(e){let t=new Set((e.runDecisions?.checkpoints??[]).filter(e=>e.chosen).map(e=>e.after)),n=e.phases.filter(e=>e.id!==`final`&&t.has(e.id)).map(e=>e.id);return t.has(`final`)&&n.push(be),n}function bn(e,t){if(t.length===0)return!1;if(e===`final`)return t.every(e=>e.status===`done`);let n=t.findIndex(t=>t.id===e);return n<0||!t.slice(0,n+1).every(e=>e.status===`done`)?!1:t.slice(n+1).every(e=>e.status===`waiting`)}function xn(e,t=null){if(e.protocol.some(e=>e.id===`quality-review`&&e.status===`running`)||e.runDecisions===null)return{kind:`spec`};let n=yn(e).filter(t=>bn(t,e.phases)),r=t=>e.reviews.filter(e=>e.source.kind===`checkpoint`&&e.source.after===t),i=n[n.length-1];if(t!==null){let n=[{kind:`execution`}];i!==void 0&&n.push({kind:`checkpoint`,after:i});let r=[...e.reviews].reverse().find(e=>e.round===t&&n.some(t=>fe(t,e.source)));if(r!==void 0)return r.source}let a=n.find(e=>r(e).length===0);return a===void 0?i===void 0||i===`final`?{kind:`execution`}:{kind:`checkpoint`,after:i}:{kind:`checkpoint`,after:a}}function Sn(e,t,n){return`${e.kind===`spec`?`specs review`:`execution review`} round ${n}${e.kind===`checkpoint`?e.after===`final`?` after the last phase`:` after ${xe(e.after,t)||e.after}`:``}`}function Cn(e,t,n,r=new Date){let i=r.toISOString(),{state:a,raw:o}=G(e,t),s=xn(a,n.round),c=a.reviews.filter(e=>fe(e.source,s)),l=n.round===null?-1:a.reviews.findIndex(e=>fe(e.source,s)&&e.round===n.round),u=l<0?Math.max(0,...c.map(e=>e.round))+1:n.round,d={round:u,verdict:n.verdict,findings:n.findings,judgments:n.judgments,source:s,at:i};return a.reviews=l<0?[...a.reviews,d]:a.reviews.map((e,t)=>t===l?d:e),q(a,`${Sn(s,a.phases,u)}: ${d.verdict}`,i),K(e,t,a,o),a}var wn=`spec/SPEC.md`;function Tn(e){try{return{at:e,before:s(e,`utf8`)}}catch{return{at:e,before:null}}}function En(e){for(let t of e)t.before===null?d(t.at,{force:!0}):N(t.at,t.before)}function Dn(e,t){if(/^Completed:/m.test(e))return null;let n=e.split(`
4
4
  `),r=n.findIndex(e=>e.startsWith(`# `));return r===-1?`Completed: ${t}\n\n${e}`:(n.splice(r+1,0,``,`Completed: ${t}`),n.join(`
5
- `))}function yn(e,t,r=`children`,i=new Date){if(r===`self`)throw new H(`this pane serves one piece of work, so there is no root to file it under`);let o=t.trim();if(!o)throw new H(`which piece of work? name one`);if(I(o))throw new H(`this piece of work is already archived`);let s=!1;try{s=f(N(e,o)).isDirectory()}catch{s=!1}if(!s)throw new H(`there is no piece of work called "${o}"`);let c=mt(i),l=ht(o,i),d=N(e,l);if(n(d))throw new H(`${l} is already there`);let{state:p,raw:m}=W(e,o,r),h=gn(N(e,`${o}/${hn}`)),_=[gn(N(e,`${o}/README.md`)),h,gn(P(e,`${o}/${y}/${pe}`))];try{if(p.status=`complete`,p.log.push({at:i.toISOString(),said:`signed off and archived`}),K(e,o,p,m),h.before!==null){let e=vn(h.before,c);e!==null&&j(h.at,e)}a(g(d),{recursive:!0}),u(N(e,o),d)}catch(e){throw _n(_),e instanceof H?e:new H(`it could not be filed away: ${e.message}`)}return{entry:l,path:`${l}/README.md`}}var Y=class extends Error{code=`bad-request`;constructor(e){super(`specs: ${e}`),this.name=`BadRequest`}},bn=new Set([Vt,`README.md`]);function xn(e,t){let n=t.split(`/`),r=n.pop()??``;return n.length===1&&n[0]&&bn.has(r)?N(e,t):P(e,t)}var X=(e,t)=>{if(typeof e!=`object`||!e||Array.isArray(e))throw new Y(`${t} is an object`);return e},Z=(e,t=``)=>typeof e==`string`?e:t,Q=e=>Array.isArray(e)?e:[],Sn=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function Cn(e){let t=X(e,`an intake`),n=[];for(let e of Q(t.attachments)){let t=X(e,`an attachment`);if(typeof t.data!=`string`||typeof t.name!=`string`)throw new Y(`an attachment carries a name and base64 data`);n.push({name:t.name,type:Z(t.type),data:t.data})}return{name:Z(t.name),text:Z(t.text),attachments:n,wish:wn(t.wish)}}function wn(e){return{imagination:(typeof e==`object`&&e?e:{}).imagination===!0}}function Tn(e){let t=X(e,`a proposal`),n=[];for(let e of Q(t.stages)){let t=X(e,`a proposed stage`);if(typeof t.id!=`string`)throw new Y(`a proposed stage names one`);n.push({id:t.id,recommended:t.recommended===!0,why:Z(t.why)})}return{stages:n,note:Z(t.note)}}function En(e){let t=X(e,`a confirmation`),n=t.settings===void 0?{}:X(t.settings,`the settings`);return{stages:Q(t.stages).filter(e=>typeof e==`string`),imagination:t.imagination===void 0||t.imagination===null?null:t.imagination===!0,note:Z(t.note),settings:{research:Ee(n.research)}}}function Dn(e){let t=X(e,`a post`),n=Z(t.kind);if(n===`activity`||n===`note`)return{kind:n,said:Z(t.said)};if(n===`status`){let e=Z(t.status);if(!In.includes(e))throw new Y(`a status is one of ${In.join(`, `)}`);return{kind:n,status:e}}if(n===`phase`){let e=Z(t.status);if(!$.includes(e))throw new Y(`a phase status is one of ${$.join(`, `)}`);let r=Z(t.title);return{kind:n,phase:Z(t.phase),status:e,...r?{title:r}:{}}}if(n===`plan`){let e=Q(t.phases).filter(e=>typeof e==`object`&&!!e).map(e=>({phase:Z(e.phase),title:Z(e.title)})).filter(e=>e.phase!==``);if(e.length===0)throw new Y(`a plan post names at least one phase`);return{kind:n,phases:e}}if(n===`stage`){let e=Z(t.status);if(!$.includes(e))throw new Y(`a stage status is one of ${$.join(`, `)}`);let r=Q(t.artifacts).filter(e=>typeof e==`string`);return{kind:n,stage:Z(t.stage),status:e,artifacts:r}}throw new Y(`a post is an activity, a stage, a phase, a status or a note`)}function On(e){let t=X(e,`an answer`),n=Z(t.outcome);if(!An.includes(n))throw new Y(`an outcome is one of ${An.join(`, `)}`);let r=[];for(let e of Q(t.questions)){let t=X(e,`an answered question`);if(typeof t.question!=`string`)throw new Y(`an answer names its question`);let n=Q(t.chosen).filter(e=>typeof e==`string`),i=Z(t.said);r.push({question:t.question,chosen:n,said:i,answered:t.answered===!0})}return{outcome:n,said:Z(t.said),questions:r,accepted:t.accepted===!0,answeredAt:``}}function kn(e){let t=X(e,`a review round`);if(typeof t.round!=`number`)throw new Y(`a review round is numbered`);return{round:t.round,verdict:Z(t.verdict),findings:typeof t.findings==`number`?t.findings:0,judgments:typeof t.judgments==`number`?t.judgments:0,at:``}}var An=[`approved`,`changes`,`answered`];function jn(e){let t=De(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function Mn(e){let t=Oe(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function Nn(e){let t=X(e,`a withdrawal`);return{id:Z(t.id),reason:Z(t.reason)}}function Pn(e){let t=X(e,`a declaration`),n=Z(t.kind);if(n!==`gate`&&n!==`round`)throw new Y(`a declaration is a gate or a round`);let r={id:Z(t.id),kind:n,title:Z(t.title),file:Z(t.file),payload:t.payload??null};return Fn(r),r}function Fn(e){let t=e.payload;if(!Sn(t)||t.kind!==`decision`)return;if(e.file===``)throw new Y("a decision card names the artifact it is about in `file`");if(Z(t.heading)===``)throw new Y("a decision card names the section it is about in `heading`");let n=Array.isArray(t.questions)?t.questions:[];if(n.length!==1)throw new Y("a decision card asks exactly one question in `questions`");let r=n[0];if((Sn(r)&&Array.isArray(r.options)?r.options:[]).length<2)throw new Y(`a decision card offers two options at least`)}var In=[`specifying`,`ready`,`executing`,`complete`],$=[`waiting`,`running`,`done`],Ln=[ae,oe];function Rn(e){return Ln.some(t=>e.endsWith(`/${y}/${t}`))}function zn(e){let t=Ye(e.root),r=e.entries??`children`,i=e.onChange,a=i?ft(t,i,{shared:Rn,entries:r,coalesce:`per-path`}):null;return{root:t,dashboard:()=>wt(t,r),entry:e=>Tt(t,e,r),state:e=>jt(t,e),writeState(e,n){Mt(t,e,ue(typeof n==`string`?n:JSON.stringify(n)))},pending:e=>({...b(e),annotations:Pt(t,e)}),writePending(e,n){Ft(t,e,de(typeof n==`string`?n:JSON.stringify(n),e).annotations)},feedback(e,n){let r=n,i=r?.comments;if(!Array.isArray(i))throw new Y(`a submission is a list of comments`);let a=Q(r?.queued).filter(e=>typeof e==`string`);return zt(t,e,{comments:i,queued:a})},filed:e=>Rt(t,e),toolPath(e){let r=xn(t,e);if(!n(r))throw new Pe(M,e);return r},stages:()=>w,specState:e=>W(t,e,r).state,archive:e=>yn(t,e,r),intake:e=>Xt(t,Cn(e)),propose:(e,n)=>Qt(t,e,Tn(n)),confirm:(e,n)=>$t(t,e,En(n)),recommend:(e,n)=>tn(t,e,Mn(n)),decide:(e,n)=>nn(t,e,jn(n)),post:(e,n)=>an(t,e,Dn(n)),declare:(e,n)=>on(t,e,Pn(n)),withdraw:(e,n)=>dn(t,e,Nn(n)),answers:(e,n)=>ln(t,e,n),answer:(e,n,r)=>un(t,e,n,On(r)),review:(e,n)=>mn(t,e,kn(n)),round:(e,n)=>J(t,e,n),file:e=>at(M,t,e),close:()=>a?.close()}}export{Y as BadRequest,zn as createSpecsModule,Ie as statusFor};
5
+ `))}function On(e,t,r=`children`,i=new Date){if(r===`self`)throw new U(`this pane serves one piece of work, so there is no root to file it under`);let o=t.trim();if(!o)throw new U(`which piece of work? name one`);if(R(o))throw new U(`this piece of work is already archived`);let s=!1;try{s=f(F(e,o)).isDirectory()}catch{s=!1}if(!s)throw new U(`there is no piece of work called "${o}"`);let c=xt(i),l=St(o,i),d=F(e,l);if(n(d))throw new U(`${l} is already there`);let{state:p,raw:m}=G(e,o,r),h=Tn(F(e,`${o}/${wn}`)),_=[Tn(F(e,`${o}/README.md`)),h,Tn(I(e,`${o}/${x}/${_e}`))];try{if(p.status=`complete`,p.log.push({at:i.toISOString(),said:`signed off and archived`}),K(e,o,p,m),h.before!==null){let e=Dn(h.before,c);e!==null&&N(h.at,e)}a(g(d),{recursive:!0}),u(F(e,o),d)}catch(e){throw En(_),e instanceof U?e:new U(`it could not be filed away: ${e.message}`)}return{entry:l,path:`${l}/README.md`}}var Y=class extends Error{code=`bad-request`;constructor(e){super(`specs: ${e}`),this.name=`BadRequest`}},kn=new Set([Gt,`README.md`]);function An(e,t){let n=t.split(`/`),r=n.pop()??``;return n.length===1&&n[0]&&kn.has(r)?F(e,t):I(e,t)}var X=(e,t)=>{if(typeof e!=`object`||!e||Array.isArray(e))throw new Y(`${t} is an object`);return e},Z=(e,t=``)=>typeof e==`string`?e:t,Q=e=>Array.isArray(e)?e:[],jn=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function Mn(e){let t=X(e,`an intake`),n=[];for(let e of Q(t.attachments)){let t=X(e,`an attachment`);if(typeof t.data!=`string`||typeof t.name!=`string`)throw new Y(`an attachment carries a name and base64 data`);n.push({name:t.name,type:Z(t.type),data:t.data})}return{name:Z(t.name),text:Z(t.text),attachments:n,wish:Nn(t.wish)}}function Nn(e){return{imagination:(typeof e==`object`&&e?e:{}).imagination===!0}}function Pn(e){let t=X(e,`a proposal`),n=[];for(let e of Q(t.stages)){let t=X(e,`a proposed stage`);if(typeof t.id!=`string`)throw new Y(`a proposed stage names one`);n.push({id:t.id,recommended:t.recommended===!0,why:Z(t.why)})}return{stages:n,note:Z(t.note)}}function Fn(e){let t=X(e,`a confirmation`),n=t.settings===void 0?{}:X(t.settings,`the settings`);return{stages:Q(t.stages).filter(e=>typeof e==`string`),imagination:t.imagination===void 0||t.imagination===null?null:t.imagination===!0,note:Z(t.note),settings:{research:Ne(n.research)}}}function In(e){let t=X(e,`a post`),n=Z(t.kind);if(n===`activity`||n===`note`)return{kind:n,said:Z(t.said)};if(n===`status`){let e=Z(t.status);if(!Gn.includes(e))throw new Y(`a status is one of ${Gn.join(`, `)}`);return{kind:n,status:e}}if(n===`phase`){let e=Z(t.status);if(!$.includes(e))throw new Y(`a phase status is one of ${$.join(`, `)}`);let r=Z(t.title);return{kind:n,phase:Z(t.phase),status:e,...r?{title:r}:{}}}if(n===`plan`){let e=Q(t.phases).filter(e=>typeof e==`object`&&!!e).map(e=>({phase:Z(e.phase),title:Z(e.title)})).filter(e=>e.phase!==``);if(e.length===0)throw new Y(`a plan post names at least one phase`);return{kind:n,phases:e}}if(n===`stage`){let e=Z(t.status);if(!$.includes(e))throw new Y(`a stage status is one of ${$.join(`, `)}`);let r=Q(t.artifacts).filter(e=>typeof e==`string`);return{kind:n,stage:Z(t.stage),status:e,artifacts:r}}throw new Y(`a post is an activity, a stage, a phase, a status or a note`)}function Ln(e){let t=X(e,`an answer`),n=Z(t.outcome);if(!zn.includes(n))throw new Y(`an outcome is one of ${zn.join(`, `)}`);let r=[];for(let e of Q(t.questions)){let t=X(e,`an answered question`);if(typeof t.question!=`string`)throw new Y(`an answer names its question`);let n=Q(t.chosen).filter(e=>typeof e==`string`),i=Z(t.said);r.push({question:t.question,chosen:n,said:i,answered:t.answered===!0})}return{outcome:n,said:Z(t.said),questions:r,accepted:t.accepted===!0,answeredAt:``}}function Rn(e){let t=X(e,`a review round`);return{round:typeof t.round==`number`?t.round:null,verdict:Z(t.verdict),findings:typeof t.findings==`number`?t.findings:0,judgments:typeof t.judgments==`number`?t.judgments:0}}var zn=[`approved`,`changes`,`answered`];function Bn(e){let t=Pe(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function Vn(e){let t=Fe(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function Hn(e){let t=X(e,`a withdrawal`);return{id:Z(t.id),reason:Z(t.reason)}}function Un(e){let t=X(e,`a declaration`),n=Z(t.kind);if(n!==`gate`&&n!==`round`)throw new Y(`a declaration is a gate or a round`);let r={id:Z(t.id),kind:n,title:Z(t.title),file:Z(t.file),payload:t.payload??null};return Wn(r),r}function Wn(e){let t=e.payload;if(!jn(t)||t.kind!==`decision`)return;if(e.file===``)throw new Y("a decision card names the artifact it is about in `file`");if(Z(t.heading)===``)throw new Y("a decision card names the section it is about in `heading`");let n=Array.isArray(t.questions)?t.questions:[];if(n.length!==1)throw new Y("a decision card asks exactly one question in `questions`");let r=n[0];if((jn(r)&&Array.isArray(r.options)?r.options:[]).length<2)throw new Y(`a decision card offers two options at least`)}var Gn=[`specifying`,`ready`,`executing`,`complete`],$=[`waiting`,`running`,`done`],Kn=[re,ie];function qn(e){return Kn.some(t=>e.endsWith(`/${x}/${t}`))}function Jn(e){let t=nt(e.root),r=e.entries??`children`,i=e.onChange,a=i?yt(t,i,{shared:qn,entries:r,coalesce:`per-path`}):null;return{root:t,dashboard:()=>jt(t,r),entry:e=>Mt(t,e,r),state:e=>Ft(t,e),writeState(e,n){It(t,e,le(typeof n==`string`?n:JSON.stringify(n)))},pending:e=>({...ce(e),annotations:Rt(t,e)}),writePending(e,n){zt(t,e,ue(typeof n==`string`?n:JSON.stringify(n),e).annotations)},feedback(e,n){let r=n,i=r?.comments;if(!Array.isArray(i))throw new Y(`a submission is a list of comments`);let a=Q(r?.queued).filter(e=>typeof e==`string`);return Ut(t,e,{comments:i,queued:a})},filed:e=>Ht(t,e),toolPath(e){let r=An(t,e);if(!n(r))throw new Ve(P,e);return r},stages:()=>E,specState:e=>G(t,e,r).state,archive:e=>On(t,e,r),intake:e=>tn(t,Mn(e)),propose:(e,n)=>rn(t,e,Pn(n)),confirm:(e,n)=>an(t,e,Fn(n)),recommend:(e,n)=>sn(t,e,Vn(n)),decide:(e,n)=>cn(t,e,Bn(n)),post:(e,n)=>un(t,e,In(n)),declare:(e,n)=>dn(t,e,Un(n)),withdraw:(e,n)=>gn(t,e,Hn(n)),answers:(e,n)=>mn(t,e,n),answer:(e,n,r)=>hn(t,e,n,Ln(r)),review:(e,n)=>Cn(t,e,Rn(n)),round:(e,n)=>J(t,e,n),file:e=>ft(P,t,e),close:()=>a?.close()}}export{Y as BadRequest,Jn as createSpecsModule,Ue as statusFor};
@@ -55,6 +55,7 @@ The answer holds what you need and nothing you have to work out:
55
55
  | --- | --- |
56
56
  | `awaits` | `{ side, act, reason }` - who holds the review, which act is asked, and why, in one line |
57
57
  | `act` | `prepare`, `process`, `sync`, or null |
58
+ | `instruction` | What the reviewer asked for in their own words, and empty where they asked nothing |
58
59
  | `side` | `agent` while the review is yours, `you` while it is the reviewer's |
59
60
  | `status` | `reviewing` while there is work; `approved`, `closed` or `archived` where there is none |
60
61
  | `baseline` | The changeset: the `fingerprint`, the totals, and every file with the id and header of each of its hunks |
@@ -78,16 +79,18 @@ A review that came back to the reviewer a moment ago answers `side: you`, and th
78
79
 
79
80
  ### The three acts, in one line each
80
81
 
81
- - **`prepare`** - the review has a baseline and no grouping. Group every hunk into a few pieces of work, give each group a narrative, and write the notes: `info` where an explanation helps the reviewer, `attention` where a passage needs a human eye, `issue` where you are certain something is wrong.
82
+ - **`prepare`** - the review has a baseline and no grouping. Group every hunk into a few pieces of work, give each group a narrative, and write the notes: `info` where an explanation helps the reviewer, `attention` where a passage needs a human eye, `issue` where you are certain something is wrong. Give each note the lines it is about, and omit them only for a note about the hunk as a whole. Where `instruction` holds words, follow them, because the reviewer asked for this grouping in them; a reset erases the grouping, so this act finds none to keep and groups the whole baseline again.
82
83
  - **`process`** - the reviewer sent a batch. Do what each item asks, and reply in the conversation each item opened.
83
84
  - **`sync`** - the baseline moved, because the reviewer synced the review, stopped you, or followed a merge. Group what is new, and revise what the new code made wrong.
84
85
 
85
86
  ## What you post
86
87
 
87
- Five verbs, and they are the whole of what you may do to a review:
88
+ Seven verbs, and they are the whole of what you may do to a review:
88
89
 
89
90
  ```
90
- echo '<json>' | reviews metadata "<review>" the grouping and the notes for this baseline
91
+ echo '<json>' | reviews metadata "<review>" the whole grouping and its notes, replacing what is there
92
+ echo '<json>' | reviews group "<review>" one group and the notes on its hunks: { fingerprint, group, notes? }
93
+ echo '<json>' | reviews ungroup "<review>" remove one group: { fingerprint, id }
91
94
  echo '<json>' | reviews reply "<review>" one reply in one conversation: { thread, text }
92
95
  echo '<json>' | reviews activity "<review>" what you are doing right now: { said, groups? }
93
96
  echo '<json>' | reviews log "<review>" one dated line for the activity log: { said }
@@ -97,6 +100,16 @@ reviews done "<review>" your act is finished
97
100
  A payload is JSON on stdin, and the answer is JSON on stdout.
98
101
  `references/acts.md` holds the shape of each payload.
99
102
 
103
+ **Posting from more than one place.**
104
+ How you do the reading is yours to decide: alone, or with workers that each read a part of the change.
105
+ The verbs are made so that either way is safe, and this is how they behave.
106
+
107
+ - `metadata` replaces the whole grouping and every note. It is for one writer that holds the whole picture. Two writers that post it in turn each erase the other's work.
108
+ - `group` adds one group, or replaces the group with the same `id` in place, and replaces the notes on that group's hunks only. It is safe to post from several workers at once: the tool takes the posts one at a time, and no post can remove another worker's group.
109
+ - Say how many groups are coming in `activity { groups }` as soon as you know it. `done` holds you to that number.
110
+ - `done` is refused while a hunk is in no group, and while fewer groups are posted than you said were coming. The refusal names what is missing.
111
+ - One caller ends the act. The agent that took the act reads its own words back, as the read-back step of `prepare` says, and posts `done` once. A worker reads, posts its group, and never posts `done`.
112
+
100
113
  **Post `activity` as you go.**
101
114
  It is one short sentence in the present tense - "Grouping the parser changes", "Working on the second item" - and the tab shows it with the time you posted it.
102
115
  A review that has said nothing for ten minutes is drawn as stale, so a long act posts as it moves rather than once at the end.
@@ -11,6 +11,11 @@ The reviewer has filed a review and is waiting to read it.
11
11
  Until you post a grouping the screen shows every hunk under "Not grouped yet", which is a wall of diff with no story in it.
12
12
  So this act is the one the reviewer waits on longest, and it is the one that decides how the review reads.
13
13
 
14
+ The state's `instruction` holds what the reviewer asked for, and it is empty where they asked nothing.
15
+ Read it before you group, and follow it: those are the reviewer's own words about this grouping, and they come before your own judgment on the same point.
16
+ This act reaches you a second time where the reviewer reset the review, and a reset erases the grouping and its notes, so there is no earlier grouping to keep.
17
+ Group the whole baseline again.
18
+
14
19
  **1. Read the change before you group it.**
15
20
  The state's `baseline` holds every file and, for each one, the id and header of each of its hunks.
16
21
  Read the code itself as well - the files of the workspace are there, and the review is of the tree you are in.
@@ -22,18 +27,33 @@ Three to seven groups reads well for most changesets.
22
27
  One group of forty hunks says nothing, and twenty groups of two are the file list again.
23
28
 
24
29
  - A hunk may sit in several groups where it belongs to both.
25
- - A hunk in no group lands under "Ungrouped", which is the tool's own group. It is for the case where you missed one, and a review that shows it is a review you left half grouped.
30
+ - A hunk in no group is shown under "Ungrouped", which is the tool's own group, until you group it. `done` is refused while one is there.
26
31
  - `ungrouped` in the post's answer is that list. Read it, and post again with the hunks it names in the groups they belong to.
27
32
 
28
- **3. Give each group a narrative and its facts.**
29
- The narrative is a short paragraph in plain language: what this piece of work does, and why the change is what it is.
30
- The reviewer reads it before the hunks, so it says the thing the diff cannot - the intent.
33
+ **3. Name each group, give it a narrative, and give it its facts.**
34
+
35
+ The name is a label, and the reviewer reads it in a narrow column beside the code.
36
+ Use two to four words that say which piece of work this is.
37
+
38
+ - "Discount rules" is a name.
39
+ - "The discount rules move out of the cart into the one place that holds them" is a sentence, and it is wrong here. A sentence belongs in the narrative.
40
+
41
+ The tool refuses a name with nothing in it, and one too long to draw as a label.
42
+ The refusal says how long a name may be, so shorten it and post again.
43
+
44
+ The narrative is a paragraph for a reader who has not seen the code: what the change in this group is, and what to look for while reading it.
45
+ Write it in plain words.
46
+ A file name, a count of lines and the name of a function are not the story: a file name belongs in the facts, and what is true of a passage belongs in a note on its lines.
47
+
48
+ - "The cart worked out its own discounts, and each caller repeated the rules. The rules live in one place now, and the cart asks it for a price. Read it for what changed while the rules moved: two for one now applies before the member discount." is a narrative.
49
+ - "Changes discount.ts, cart.ts and price.ts. Adds applyDiscount() and takes 40 lines out of the cart." is the file list again, and it is wrong here. The diff says all of that already.
50
+
31
51
  The facts are a few short lines of what is true and checkable: "adds one table column", "no call site outside the cart", "the old path stays for one release".
32
52
  Facts are facts. Anything that needs "I think" is not one.
33
53
 
34
54
  **4. Write the notes.**
35
55
  You read every line of this change, and the reviewer has not.
36
- The notes are what you learned while you read, beside the code, so that the human review is quick and misses nothing.
56
+ The notes are what you learned while you read, drawn in the code the reviewer reads, so that the human review is quick and misses nothing.
37
57
  A note is plain text of one kind, on one hunk or on a line range inside it.
38
58
 
39
59
  - `info` - an explanation that helps the reviewer understand the code: what a passage does, why the change is the way it is, what it mirrors elsewhere. "This mirrors the rule in `pricing.ts`." Write one wherever an explanation would save the reviewer a search or a guess.
@@ -43,13 +63,17 @@ A note is plain text of one kind, on one hunk or on a line range inside it.
43
63
  The screen counts the open `issue` and `attention` notes on each group and in the status bar, so the reviewer sees where the eyes are needed before they read.
44
64
  The reviewer can dismiss a note, ask you to fix an issue, or open a conversation on it.
45
65
  One note per thing you have to say, and the kind that fits it.
46
- A note on every hunk is a rail nobody reads, but a hunk that needs an explanation and has none is a question the reviewer has to ask you.
66
+ A note on every hunk is noise nobody reads, but a hunk that needs an explanation and has none is a question the reviewer has to ask you.
47
67
 
48
68
  **Do not fix anything in this act.**
49
69
  You found the issue, and the reviewer decides what to do with it: the fix is an item in the batch they send, and `process` is where you do it.
50
70
 
51
71
  **5. Post it.**
52
72
 
73
+ There are two ways to post a grouping, and the state reads the same after either.
74
+
75
+ The whole grouping in one post, for one writer that holds the whole picture:
76
+
53
77
  ```
54
78
  echo '{
55
79
  "fingerprint": "sha256:...",
@@ -68,18 +92,55 @@ echo '{
68
92
  }' | reviews metadata "<review>"
69
93
  ```
70
94
 
95
+ One group at a time, which is the way to post when more than one worker reads the change:
96
+
97
+ ```
98
+ echo '{
99
+ "fingerprint": "sha256:...",
100
+ "group": { "id": "discount-rules", "name": "Discount rules",
101
+ "narrative": "The rules move out of the cart and into one place ...",
102
+ "facts": ["adds one table column", "no call site outside the cart"],
103
+ "hunks": ["src/cart/discount.ts#9f3a...#1", "src/cart/discount.ts#9f3a...#2"] },
104
+ "notes": [
105
+ { "hunk": "src/cart/discount.ts#9f3a...#1", "kind": "attention", "lines": [44, 46],
106
+ "text": "This drops the guard the caller used to hold." }
107
+ ]
108
+ }' | reviews group "<review>"
109
+ ```
110
+
71
111
  `fingerprint` is the baseline's own, from the state you read.
72
112
  `id` is yours to choose and is the name the tool refers to the group by; `name` is what the reviewer reads.
73
113
  `kind` is `info`, `attention` or `issue`.
74
114
  `lines` are new-side line numbers inside the hunk, and a note without them is about the whole hunk.
75
115
  The screen draws one bar in the gutter from the smallest number to the largest, so two numbers name a range.
116
+ Give `lines` when the note is about a passage, and omit them only when the note is about the hunk as a whole.
117
+ The screen puts a note directly above the first line it names.
118
+ The screen draws a note about the whole hunk under the file bar, at the top of the card.
119
+ In a hunk of sixty lines, the reviewer must search for the passage a note with no lines is about.
76
120
 
77
- **A post replaces the whole grouping.**
78
- That is what makes a group appear as it becomes ready: post the groups you have, and post them all again with the next one added.
79
- The screen then shows three groups, then four.
121
+ **`metadata` replaces the whole grouping.**
80
122
  A post with no group empties the grouping, which is what it says.
81
-
82
- **6. Say what you are doing, and finish.**
123
+ Two writers that post it in turn each erase the other's work, so it is for one writer only.
124
+
125
+ **`group` adds or replaces one group.**
126
+ A group with the `id` of one that is there replaces it in place, and a new `id` is appended, so a group appears on the screen as it becomes ready.
127
+ The notes of the post replace the notes on that group's hunks, and a note on a hunk outside the group is refused.
128
+ Notes on other hunks stay.
129
+ Where a hunk is in two groups, the notes on it are those of the later post.
130
+ Several workers may post `group` at once: the tool takes the posts one at a time, and none of them can remove another's group.
131
+ `ungroup` with `{ fingerprint, id }` removes one group, and the notes on its hunks stay.
132
+
133
+ Every one of these answers with `ungrouped`, the hunks no group holds.
134
+ Read it, and post the hunks it names in the groups they belong to.
135
+
136
+ **6. Read your words back.**
137
+ Read every group name and every narrative you posted against the two rules of step 3, and post again with `group` each group whose words do not follow them.
138
+ A `group` post replaces the notes on that group's hunks, so post the group again with its notes and not with its words alone.
139
+ The step is here because those words are often written by several workers, or in one pass long before the act ends, so no other step reads them: what the reviewer gets is what this step leaves.
140
+ Two things to look for - a name that is a sentence rather than a label, and a narrative that lists files, counts of lines or names of functions rather than telling the story.
141
+ Read the grouping and the notes as a whole in the same pass, so the act ends on a review that is what you meant.
142
+
143
+ **7. Say what you are doing, and finish.**
83
144
  Post `activity` as you work through it - "Grouping the parser changes, 3 of 5 ready" - and `done` when the grouping is complete and every note is in.
84
145
 
85
146
  **Say how many groups there will be, in `groups`.**
@@ -88,9 +149,16 @@ Post `activity` as you work through it - "Grouping the parser changes, 3 of 5 re
88
149
  echo '{ "said": "Grouping the parser changes, 3 of 5 ready", "groups": 5 }' | reviews activity "<review>"
89
150
  ```
90
151
 
91
- You are the only one who knows it: the tool holds the groups you posted, and the tab draws one card waiting for each group you promised and has not arrived, so the reviewer sees how much is still coming.
152
+ You are the only one who knows it: the tool holds the groups you posted, and the tab draws one waiting row in the rail for each group you promised and has not posted, so the reviewer sees how much is still coming.
92
153
  Post it once you know the number, and post the sentence alone after that - the count stands until you post another, and it is cleared when the review goes back to the reviewer.
93
154
 
155
+ **`done` holds you to a whole grouping.**
156
+ It is refused while a hunk is in no group, and while fewer groups are posted than the count you gave.
157
+ The refusal names the hunks, or the count, so you know what is missing.
158
+ Group what it names, or say the new count with `activity { groups }`, and post `done` again.
159
+ Where workers read the change for you, one caller posts `done`: the agent that took the act, after step 6 and never before it.
160
+ A worker never posts `done`.
161
+
94
162
  ## process - the reviewer sent a batch
95
163
 
96
164
  `send` in the state is the newest batch: when it was sent, the baseline it was written against, and its `items`.
@@ -143,11 +211,14 @@ A hunk the reviewer had read whose content changed comes back unread with a "cha
143
211
  So this act is the difference, and not the review again:
144
212
 
145
213
  1. **Group what is new.** `ungrouped` names the hunks no group holds. Most of them are new hunks, and each one belongs in a group that exists or in one more group.
146
- 2. **Revise what the new code made wrong.** A narrative that describes code that is no longer there is worse than none, because the reviewer reads it as current. Read your own narratives against the new baseline and rewrite the ones that moved.
147
- 3. **Read your notes again.** The tool dropped the notes of every hunk whose content changed, and kept the rest. Write again only the notes that are still true of the new code, and write notes for what is new. An issue you fixed gets no note: the rail shows the code as it is, not what it was.
214
+ 2. **Revise what the new code made wrong, and read your words back.** A narrative that describes code that is no longer there is worse than none, because the reviewer reads it as current. Read your own narratives against the new baseline and rewrite the ones that moved. Read the names and the narratives against the two rules of `prepare` step 3 in the same pass, because those words are often written by several workers, or long before the act ends, so no other step reads them: a name that is a sentence rather than a label, and a narrative that lists files, counts of lines or names of functions rather than telling the story, are the two to look for.
215
+ 3. **Read your notes again.** The tool dropped the notes of every hunk whose content changed, and kept the rest. Write again only the notes that are still true of the new code, and write notes for what is new. An issue you fixed gets no note: the notes describe the code as it is, not as it was.
148
216
  4. **Answer what is open.** A conversation the reviewer is waiting on is one that is open and whose last message is theirs. Answer it if the new code answers it.
149
217
 
150
- Post the whole grouping again, with the new baseline's `fingerprint`, and `done` when it is in.
218
+ Post the groups that changed with `group`, or the whole grouping again with `metadata`, against the new baseline's `fingerprint`.
219
+ A `group` post replaces the notes on that group's hunks, so post each group with the notes it is to keep.
220
+ Then post `done` when it is in.
221
+ `done` holds to the same rules as in `prepare`: it is refused while a hunk is in no group, and while a promised group is missing.
151
222
 
152
223
  ## The no-act case
153
224
 
@@ -134,7 +134,7 @@ echo '<json>' | specs recommend <spec> recommend how the run is shaped, for t
134
134
  echo '<json>' | specs await <spec> declare a gate, a round or a decision card
135
135
  echo '<json>' | specs withdraw <spec> take back an ask nobody needs to answer
136
136
  echo '<json>' | specs post <spec> say what is happening
137
- echo '<json>' | specs review <spec> post one round of quality review
137
+ echo '<json>' | specs review <spec> post one round of review
138
138
  echo '{"id":"<id>"}' | specs answers <spec> read back what the user answered
139
139
  ```
140
140
 
@@ -262,6 +262,8 @@ Withdrawing is your act, not the user's, so it adds nothing to the approvals.
262
262
  ```json
263
263
  { "kind": "activity", "said": "Reading the checkout code" }
264
264
  { "kind": "stage", "stage": "codebase-analysis", "status": "done", "artifacts": ["spec/SPEC.md"] }
265
+ { "kind": "plan", "phases": [{ "phase": "phase-01-the-state", "title": "The state" }] }
266
+ { "kind": "phase", "phase": "phase-01-the-state", "title": "The state", "status": "done" }
265
267
  { "kind": "status", "status": "ready" }
266
268
  { "kind": "note", "said": "one dated line for the log" }
267
269
  ```
@@ -272,6 +274,7 @@ The board shows it while it is current, and stops the moment anything else is po
272
274
  So post one for a step that is under way, and post the next thing when that step ends.
273
275
 
274
276
  Post a stage as `running` when it starts, and as `done` with its artifacts when it ends.
277
+ The plan's phase list and a phase's own report belong to a run rather than to the specifying, and `dx-spec-execute` is the skill that posts them.
275
278
 
276
279
  **Reading an answer back:**
277
280
 
@@ -370,7 +373,8 @@ The tab serves one root, and each piece of work is one directory under it.
370
373
  research/RESEARCH.md written by the research stage
371
374
  mocks/<mock-id>.html one standalone page per screen
372
375
  plan/PLAN.md the execution plan, plus one file per phase during the run
373
- review/REVIEW.md the quality review report
376
+ review/REVIEW.md the specs quality review report
377
+ run-report.md what the run did, written by `/dx-spec-execute` at the end of it
374
378
  archive/
375
379
  YYYY-MM-DD-<slug>/ work that is finished and signed off
376
380
  ```
@@ -471,7 +475,7 @@ Rough rules for what to recommend:
471
475
  - A new user-facing feature: exploration, interview, codebase analysis, technical specs, UI mocks, execution plan.
472
476
  - Work in a subject you or the user do not know well: add research.
473
477
  - Pure thinking, or one decision to make: exploration, maybe research, and no plan.
474
- - Large or risky work: add the quality review. Many phases, several stages feeding one spec, or anything that touches security, data or money.
478
+ - Large or risky work: add the specs quality review. Many phases, several stages feeding one spec, or anything that touches security, data or money.
475
479
 
476
480
  **The wish.**
477
481
  `wish.imagination` in the state says the intake asked for imagination mode.
@@ -1,4 +1,4 @@
1
- # The quality review
1
+ # The specs quality review
2
2
 
3
3
  This file holds the reviewer briefs, the detection lenses, the severities, and the report format.
4
4
  The stage itself is defined in `stages.md`.
@@ -81,7 +81,7 @@ Never resolve it by softening the principle.
81
81
  `review/REVIEW.md`:
82
82
 
83
83
  ```markdown
84
- # Quality review - <title of the work>
84
+ # Specs quality review - <title of the work>
85
85
 
86
86
  - Run: YYYY-MM-DD
87
87
  - Mode: fresh-context subagents | inline - the session that wrote the spec
@@ -30,7 +30,7 @@ To clear a marker, ask the user. The answer replaces the marker in `SPEC.md`, in
30
30
  A doubt anywhere outside these stages does not wait: ask it in the next round.
31
31
  The finalize gate does not pass while a marker stays open.
32
32
 
33
- **Requirements carry IDs and acceptance criteria** when the protocol includes the execution plan stage or the quality review stage.
33
+ **Requirements carry IDs and acceptance criteria** when the protocol includes the execution plan stage or the specs quality review stage.
34
34
  With neither stage, write the requirements in plain language instead.
35
35
  Number each ID in writing order, as `FR-01`, `FR-02`, and so on.
36
36
  Never renumber an ID after you write it, so a phase or a finding that names one still points at the same requirement.
@@ -425,7 +425,7 @@ On changes: revise the plan, post the phases again where they moved, recommend t
425
425
  Loop until the user approves it.
426
426
  The rule over that last part is **Keep what derives from a change true**, in `SKILL.md`.
427
427
 
428
- ## Quality review
428
+ ## Specs quality review
429
429
 
430
430
  This is the last stage before the coherence rewrite and finalize.
431
431
  It reviews everything the spec produced - `spec/SPEC.md`, the research, the plan, and the mocks the spec lists - and whether those documents agree with each other.
@@ -457,9 +457,13 @@ It never downgrades quietly.
457
457
  Post each round of review as it lands, so the board shows it:
458
458
 
459
459
  ```
460
- echo '{"round":1,"verdict":"holds, two for you","findings":9,"judgments":2}' | specs review <spec>
460
+ echo '{"verdict":"holds, two for you","findings":9,"judgments":2}' | specs review <spec>
461
461
  ```
462
462
 
463
+ The number and the source are the tool's own.
464
+ It reads where the work stands when the round lands, decides which review the round belongs to - the spec's own review, the checkpoint the run is at, or the execution with no place, which is where a round posted at no checkpoint lands - and numbers the round within that review, so the spec's round 1 and a checkpoint's round 1 are two rounds.
465
+ Carry a `round` in the payload for one purpose only: to replace a round of the same review that you posted before, which is how a round is corrected.
466
+
463
467
  **Handling the findings.**
464
468
  Merge the findings, and remove the duplicates.
465
469
  Dismiss a finding you disagree with, and say why in the report.