chamba 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/commands/dev.js +11 -11
- package/dist/commands/menu.js +1 -1
- package/dist/commands/settings.js +9 -6
- package/dist/lib/awake-holder.js +3 -0
- package/dist/lib/awake-marker.js +4 -0
- package/dist/lib/awake.js +1 -0
- package/dist/lib/chamba-yaml.js +4 -4
- package/dist/lib/constants.js +1 -1
- package/dist/lib/global-config.js +3 -3
- package/dist/lib/skills.js +2 -2
- package/package.json +1 -1
- package/templates/Dockerfile +3 -3
- package/templates/context/baseline.md +2 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-CguEsvNp.js +12 -0
- package/templates/pane-apps/client/assets/api-Cyoc3E6K.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
- package/templates/pane-apps/client/assets/home-CLFnmg7u.css +1 -0
- package/templates/pane-apps/client/assets/home-CrhBkqDQ.js +1 -0
- package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/reviews-Bq8EwmG-.js +153 -0
- package/templates/pane-apps/client/assets/reviews-DzHtA35Q.css +1 -0
- package/templates/pane-apps/client/assets/search-DgFnnu-J.js +1 -0
- package/templates/pane-apps/client/assets/specs-JvdNFtXR.js +13 -0
- package/templates/pane-apps/client/assets/useNow-Ww-Gffnk.js +1 -0
- package/templates/pane-apps/client/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +6 -4
- package/templates/pane-apps/client/specs/index.html +4 -3
- package/templates/pane-apps/server/home.mjs +11 -0
- package/templates/pane-apps/server/reviews.mjs +19 -18
- package/templates/pane-apps/server/specs.mjs +4 -4
- package/templates/skills/dx-review/SKILL.md +16 -3
- package/templates/skills/dx-review/references/acts.md +86 -15
- package/templates/skills/dx-spec/SKILL.md +11 -19
- package/templates/skills/dx-spec/references/review-guide.md +15 -9
- package/templates/skills/dx-spec/references/spec-guide.md +12 -5
- package/templates/skills/dx-spec/references/stages.md +44 -24
- package/templates/skills/dx-spec-execute/SKILL.md +78 -28
- package/templates/startup.mjs +13 -13
- package/templates/tool-helper.sh +4 -2
- package/templates/webterm/README.md +41 -17
- package/templates/webterm/awake.js +27 -0
- package/templates/webterm/config.js +30 -0
- package/templates/webterm/public/app/alerts.js +92 -11
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +14 -0
- package/templates/webterm/public/app/dom.js +1 -0
- package/templates/webterm/public/app/frames.js +1 -1
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +4 -2
- package/templates/webterm/public/app/pane-shape.js +5 -3
- package/templates/webterm/public/app/pane-shell.js +45 -5
- package/templates/webterm/public/app/pane.js +8 -2
- package/templates/webterm/public/app/reviews-host.js +5 -1
- package/templates/webterm/public/app/state.js +4 -0
- package/templates/webterm/public/app/tabs.js +37 -3
- package/templates/webterm/public/app/tool-host.js +19 -3
- package/templates/webterm/public/app/workspace-color.js +1 -1
- package/templates/webterm/public/index.html +5 -1
- package/templates/webterm/public/styles.css +65 -0
- package/templates/webterm/server.js +24 -1
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/home.js +80 -0
- package/templates/webterm/tools/index.js +5 -4
- package/templates/webterm/tools/reviews.js +7 -3
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +0 -12
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +0 -1
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +0 -1
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +0 -153
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +0 -13
- package/templates/skills/dx-spec/references/principles-template.md +0 -2
|
@@ -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`}
|
|
3
|
-
`));let c=n.toISOString(),l=
|
|
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}let o=h(r),s=Ot(r,o);return{path:t,name:s?.name||o,files:i.files.length,modified:new Date(a).toISOString(),archived:R(t),status:s?.status??null,awaiting:s?.awaiting.length??0}}function Ot(e,t){try{return Ie(s(v(e,x,_e),`utf8`),t)}catch{return null}}function kt(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 At(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 jt(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 Mt(e){let t=[`# ${e.name||`A piece of work`}`,``],n=jt(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=jt(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 Nt(e,t){return Ft(e,t),le(V(e,`${z(t)}/${ne}`))}function Pt(e,t,n){H(e,t),B(e,`${z(t)}/${ne}`,`${JSON.stringify(n,null,2)}\n`)}function Ft(e,t){if(!n(t?F(e,t):e))throw new Ve(P,t,`no entry`)}function H(e,t){if(Ft(e,t),R(t))throw new M(P,t)}function It(e,t){return H(e,t),ue(V(e,`${z(t)}/${ie}`),t).annotations}function Lt(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 Rt(e,t){return`${e}-${t.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}`}function zt(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 Bt(e,t){return de(V(e,`${z(t)}/${re}`),t).rounds}function Vt(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=zt(s,Rt(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{Ht(e,t,n.queued??[])}catch{}return{path:u,round:d.rounds.length}}function Ht(e,t,n){if(n.length===0)return;let r=new Set(n),i=It(e,t),a=i.filter(e=>!r.has(e.id));a.length!==i.length&&Lt(e,t,a)}var U=class extends Error{code=`bad-request`;constructor(e){super(`specs: ${e}`),this.name=`Refused`}},W=`rounds`,Ut=`intake.md`,Wt=`intake-assets`;function Gt(e){return`${z(e)}/${_e}`}function Kt(e,t){try{return s(I(e,t),`utf8`)}catch{return null}}function G(e,t,n=`children`){Yt(e,t,n);let r=Kt(e,Gt(t));return{state:Ie(r,t===``?h(e):t.split(`/`).pop()??t),raw:r}}var qt=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function Jt(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 Yt(e,t,n=`children`){let r=t.trim();if(!r&&n!==`self`)throw new U(`which piece of work? name one`);if(n!==`self`&&!Xt(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 Xt(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,Gt(t),Le(r,n)),N(F(e,`${t}/README.md`),Mt(n))}function q(e,t,n){e.log.push({at:n,said:t})}function Zt(e){return e.replace(/[^A-Za-z0-9._-]+/g,`-`).replace(/^[-.]+/,``)||`attachment`}function Qt(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 $t(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=Qt(e,r);a(F(e,i),{recursive:!0});let o=[];for(let n of t.attachments){let t=`${Wt}/${Zt(n.name)}`;a(F(e,`${i}/${Wt}`),{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}/${Ut}`),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}/${Ut}`}}var en=`${W}/protocol.json`;function tn(e,t,n,r=new Date){Yt(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)}/${en}`,`${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:en,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 nn(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=rn(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 rn(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 an(e,t,n){let{state:r,raw:i}=G(e,t);return r.runRecommendations=n,K(e,t,r,i),r}function on(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 - ${sn(i.runDecisions)}`,o),K(e,t,i,a),i}function sn(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 cn(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 ln(e,t,n,r=new Date){Yt(e,t);let i=w(n.id);if(!i)throw new U(`an awaiting item needs an id`);qt(n.payload)&&n.payload.kind===`decision`&&Jt(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(un(s,n.payload))for(let n of c.awaiting)n.id===i||n.file!==s.file||un(n,dn(e,t,n.id))&&hn(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 un(e,t){return e.kind!==`gate`||!e.file?!1:!(qt(t)&&t.kind===`decision`)}function dn(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=Kt(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 fn(e,t,n){return J(e,t,n).answer??null}function pn(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}: ${gn[r.outcome]}`,s),K(e,t,u,d),{path:l,outcome:r.outcome}}function mn(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 hn(e,t,o,c,a,r.toISOString()),K(e,t,o,s),{path:`${z(t)}/${W}/${i}.json`,id:i}}function hn(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 gn={approved:`approved`,changes:`changes asked for`,answered:`answered`};function _n(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 vn(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 yn(e,t=null){if(e.protocol.some(e=>e.id===`quality-review`&&e.status===`running`)||e.runDecisions===null)return{kind:`spec`};let n=_n(e).filter(t=>vn(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 bn(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 xn(e,t,n,r=new Date){let i=r.toISOString(),{state:a,raw:o}=G(e,t),s=yn(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,`${bn(s,a.phases,u)}: ${d.verdict}`,i),K(e,t,a,o),a}var Sn=`spec/SPEC.md`;function Cn(e){try{return{at:e,before:s(e,`utf8`)}}catch{return{at:e,before:null}}}function wn(e){for(let t of e)t.before===null?d(t.at,{force:!0}):N(t.at,t.before)}function Tn(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
|
|
5
|
+
`))}function En(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=Cn(F(e,`${o}/${Sn}`)),_=[Cn(F(e,`${o}/README.md`)),h,Cn(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=Tn(h.before,c);e!==null&&N(h.at,e)}a(g(d),{recursive:!0}),u(F(e,o),d)}catch(e){throw wn(_),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`}},Dn=new Set([Ut,`README.md`]);function On(e,t){let n=t.split(`/`),r=n.pop()??``;return n.length===1&&n[0]&&Dn.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:[],kn=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function An(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:jn(t.wish)}}function jn(e){return{imagination:(typeof e==`object`&&e?e:{}).imagination===!0}}function Mn(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 Nn(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 Pn(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(!Un.includes(e))throw new Y(`a status is one of ${Un.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 Fn(e){let t=X(e,`an answer`),n=Z(t.outcome);if(!Ln.includes(n))throw new Y(`an outcome is one of ${Ln.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 In(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 Ln=[`approved`,`changes`,`answered`];function Rn(e){let t=Pe(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function zn(e){let t=Fe(X(e,`a run setup`));if(t===null)throw new Y(`a run setup is an object`);return t}function Bn(e){let t=X(e,`a withdrawal`);return{id:Z(t.id),reason:Z(t.reason)}}function Vn(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 Hn(r),r}function Hn(e){let t=e.payload;if(!kn(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((kn(r)&&Array.isArray(r.options)?r.options:[]).length<2)throw new Y(`a decision card offers two options at least`)}var Un=[`specifying`,`ready`,`executing`,`complete`],$=[`waiting`,`running`,`done`],Wn=[re,ie];function Gn(e){return Wn.some(t=>e.endsWith(`/${x}/${t}`))}function Kn(e){let t=nt(e.root),r=e.entries??`children`,i=e.onChange,a=i?yt(t,i,{shared:Gn,entries:r,coalesce:`per-path`}):null;return{root:t,dashboard:()=>kt(t,r),entry:e=>At(t,e,r),state:e=>Nt(t,e),writeState(e,n){Pt(t,e,le(typeof n==`string`?n:JSON.stringify(n)))},pending:e=>({...ce(e),annotations:It(t,e)}),writePending(e,n){Lt(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 Vt(t,e,{comments:i,queued:a})},filed:e=>Bt(t,e),toolPath(e){let r=On(t,e);if(!n(r))throw new Ve(P,e);return r},stages:()=>E,specState:e=>G(t,e,r).state,archive:e=>En(t,e,r),intake:e=>$t(t,An(e)),propose:(e,n)=>tn(t,e,Mn(n)),confirm:(e,n)=>nn(t,e,Nn(n)),recommend:(e,n)=>an(t,e,zn(n)),decide:(e,n)=>on(t,e,Rn(n)),post:(e,n)=>cn(t,e,Pn(n)),declare:(e,n)=>ln(t,e,Vn(n)),withdraw:(e,n)=>mn(t,e,Bn(n)),answers:(e,n)=>fn(t,e,n),answer:(e,n,r)=>pn(t,e,n,Fn(r)),review:(e,n)=>xn(t,e,In(n)),round:(e,n)=>J(t,e,n),file:e=>ft(P,t,e),close:()=>a?.close()}}export{Y as BadRequest,Kn 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
|
-
|
|
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
|
|
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
|
|
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.
|
|
29
|
-
|
|
30
|
-
The
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -28,6 +28,7 @@ specs stages
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
The answer holds `root` - the directory the tab serves - and the stage catalog.
|
|
31
|
+
Every path in this family resolves against that root.
|
|
31
32
|
A refusal means there is no pane.
|
|
32
33
|
Say this and stop:
|
|
33
34
|
|
|
@@ -123,7 +124,8 @@ Everything else in the spec directory is yours: `spec/SPEC.md`, the research, th
|
|
|
123
124
|
## The verbs
|
|
124
125
|
|
|
125
126
|
`specs <verb> [<spec>]` takes JSON on stdin where a verb needs a payload, and answers JSON on stdout.
|
|
126
|
-
`<spec>` is the directory name
|
|
127
|
+
`<spec>` is the piece of work's own path under the root, which is its directory name, or `archive/<directory>` for work that is filed away.
|
|
128
|
+
It is not the name the tab draws: that is what the work calls itself, and the two are different strings.
|
|
127
129
|
|
|
128
130
|
```
|
|
129
131
|
specs stages where the specs live, and the stage catalog
|
|
@@ -134,7 +136,7 @@ echo '<json>' | specs recommend <spec> recommend how the run is shaped, for t
|
|
|
134
136
|
echo '<json>' | specs await <spec> declare a gate, a round or a decision card
|
|
135
137
|
echo '<json>' | specs withdraw <spec> take back an ask nobody needs to answer
|
|
136
138
|
echo '<json>' | specs post <spec> say what is happening
|
|
137
|
-
echo '<json>' | specs review <spec> post one round of
|
|
139
|
+
echo '<json>' | specs review <spec> post one round of review
|
|
138
140
|
echo '{"id":"<id>"}' | specs answers <spec> read back what the user answered
|
|
139
141
|
```
|
|
140
142
|
|
|
@@ -262,6 +264,8 @@ Withdrawing is your act, not the user's, so it adds nothing to the approvals.
|
|
|
262
264
|
```json
|
|
263
265
|
{ "kind": "activity", "said": "Reading the checkout code" }
|
|
264
266
|
{ "kind": "stage", "stage": "codebase-analysis", "status": "done", "artifacts": ["spec/SPEC.md"] }
|
|
267
|
+
{ "kind": "plan", "phases": [{ "phase": "phase-01-the-state", "title": "The state" }] }
|
|
268
|
+
{ "kind": "phase", "phase": "phase-01-the-state", "title": "The state", "status": "done" }
|
|
265
269
|
{ "kind": "status", "status": "ready" }
|
|
266
270
|
{ "kind": "note", "said": "one dated line for the log" }
|
|
267
271
|
```
|
|
@@ -272,6 +276,7 @@ The board shows it while it is current, and stops the moment anything else is po
|
|
|
272
276
|
So post one for a step that is under way, and post the next thing when that step ends.
|
|
273
277
|
|
|
274
278
|
Post a stage as `running` when it starts, and as `done` with its artifacts when it ends.
|
|
279
|
+
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
280
|
|
|
276
281
|
**Reading an answer back:**
|
|
277
282
|
|
|
@@ -343,7 +348,6 @@ A commit that touches spec files alone is a `docs` commit.
|
|
|
343
348
|
A phase commit of a run takes the type that suits what that phase did, and names the phase in its description, as the second line above does.
|
|
344
349
|
|
|
345
350
|
This is built-in behaviour, and nothing exposes it as a setting, a form field or a run decision.
|
|
346
|
-
A project that wants another shape writes a principle in `principles.md`, which every run reads at start and honors best-effort - the standing every other principle has, and no more.
|
|
347
351
|
|
|
348
352
|
`/dx-spec-execute` states no style of its own: every commit it makes is written from this one.
|
|
349
353
|
|
|
@@ -360,7 +364,6 @@ The tab serves one root, and each piece of work is one directory under it.
|
|
|
360
364
|
|
|
361
365
|
```
|
|
362
366
|
<root>/
|
|
363
|
-
principles.md the engineering principles every run honors, and the user's to edit
|
|
364
367
|
<slug>/
|
|
365
368
|
README.md written by the tool, from the state - never by you
|
|
366
369
|
intake.md what the work started as
|
|
@@ -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
|
```
|
|
@@ -388,24 +392,12 @@ Outside spec work, `webpane` and the Pages tab are the right home for a page.
|
|
|
388
392
|
|
|
389
393
|
Keep a page standalone: no external stylesheet, no script that fetches anything, and images as data URLs.
|
|
390
394
|
|
|
391
|
-
## The bootstrap
|
|
392
|
-
|
|
393
|
-
Settle the root and the principles before step 1, and before the first step of `/dx-spec-execute`.
|
|
394
|
-
|
|
395
|
-
1. Read `root` from `specs stages`. That is the directory the Specs tab serves, and every path in this family resolves against it.
|
|
396
|
-
2. Read `<root>/principles.md`. It is plain markdown: a `# Principles` heading, then one `- <principle>` bullet each, and nothing else.
|
|
397
|
-
3. Where no file is there, write one from `references/principles-template.md` beside this file. Say in one line that you made it, and carry straight on.
|
|
398
|
-
4. Honor every principle the file holds for the rest of the run, best-effort - which is the standing each one has, and no more. A file with no bullets holds no principles, so there is nothing to honor.
|
|
399
|
-
|
|
400
|
-
A file that is already there produces no message at all.
|
|
401
|
-
Nothing else about how this family works is configurable, and the user changes a principle by editing that file.
|
|
402
|
-
|
|
403
395
|
## Flow
|
|
404
396
|
|
|
405
397
|
### 1. Start
|
|
406
398
|
|
|
407
399
|
Every start is this skill's own command, in the spelling of the session's agent.
|
|
408
|
-
The user types it by hand, or the Specs tab types it for them: the New-spec form files the intake and then types the command with the fresh directory's name, and the pick-up action types it with the
|
|
400
|
+
The user types it by hand, or the Specs tab types it for them: the New-spec form files the intake and then types the command with the fresh directory's name, and the pick-up action types it with the path of the work to continue.
|
|
409
401
|
The argument decides where to begin.
|
|
410
402
|
|
|
411
403
|
**It names a piece of work that exists.** Read `specs state <spec>`. Where the directory holds nothing but its intake - no confirmed protocol, no stage artifacts - read `intake.md` and go to step 4. Where the work is further along, go to step 3.
|
|
@@ -471,7 +463,7 @@ Rough rules for what to recommend:
|
|
|
471
463
|
- A new user-facing feature: exploration, interview, codebase analysis, technical specs, UI mocks, execution plan.
|
|
472
464
|
- Work in a subject you or the user do not know well: add research.
|
|
473
465
|
- 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.
|
|
466
|
+
- 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
467
|
|
|
476
468
|
**The wish.**
|
|
477
469
|
`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`.
|
|
@@ -16,13 +16,11 @@ Your focus is <the reviewer's one focus>, and the lenses that belong to it are <
|
|
|
16
16
|
Read every file in the spec directory at <path to the spec directory>, in this order, whichever of them exist: `README.md` first, since it tells you which stages this work ran, then `intake.md`, `spec/SPEC.md`, `research/RESEARCH.md`, `plan/PLAN.md` and the phase files beside it, and every page directly under `mocks/`.
|
|
17
17
|
Skip `mocks/candidates/`: it is a record of directions nobody chose, and it is not part of the work under review.
|
|
18
18
|
The screens to build are the mocks the `Mocks` section of `spec/SPEC.md` lists.
|
|
19
|
-
Read the principles file at <path to principles.md>.
|
|
20
19
|
Read the repo's own rule files, `CLAUDE.md` and `AGENTS.md`, wherever they exist.
|
|
21
20
|
|
|
22
21
|
Judge the artifacts as written.
|
|
23
22
|
Do not take the conclusions of the session that wrote them as settled: you were not in that session, and judging this work with fresh eyes is exactly your value.
|
|
24
23
|
You share no context with the other reviewers, you are not told what they found, and you must not ask another agent for context.
|
|
25
|
-
The deviations recorded in the plan's principles check are exceptions the user already approved, so raise one only when the reason written next to it does not hold.
|
|
26
24
|
|
|
27
25
|
Before you read anything, turn your lenses into a checklist: one check per lens, split further where a lens covers several things.
|
|
28
26
|
Then run every check, and return that checklist in your reply, with a pass or fail line per check.
|
|
@@ -34,6 +32,11 @@ Give it the severity of what it would be worth.
|
|
|
34
32
|
Give your findings no id and no state, and write no header block: the agent running the review assembles the report and fills those in.
|
|
35
33
|
A finding you are unsure about is still reported, and you say in its reasoning that it is a judgment call.
|
|
36
34
|
|
|
35
|
+
A check with nothing to measure the work against passes, and passes with nothing to report.
|
|
36
|
+
Repo rules is the usual one: many repos state few rules, and some state none.
|
|
37
|
+
Say so on the check line and move on.
|
|
38
|
+
Never invent a standard the repo and the artifacts do not state, and never report a finding only to have something to show.
|
|
39
|
+
|
|
37
40
|
Change no file and write nothing to disk.
|
|
38
41
|
Your reply is everything you produce.
|
|
39
42
|
|
|
@@ -50,12 +53,14 @@ Each lens has one line, so a brief names the lenses that belong to its focus.
|
|
|
50
53
|
- Ambiguity and untestable requirements - reads `spec/SPEC.md` for an open `[NEEDS CLARIFICATION: ...]` marker, a requirement with no acceptance criterion, and a requirement nobody could call pass or fail.
|
|
51
54
|
- Duplication and overlap - reads the artifacts for the same thing stated twice, in two places that can drift apart.
|
|
52
55
|
- Terminology drift - reads every artifact for one idea under several names, or one name over several ideas.
|
|
53
|
-
-
|
|
56
|
+
- Repo rules - reads the artifacts against the repo's own rule files, `CLAUDE.md` and `AGENTS.md`, wherever they exist.
|
|
57
|
+
A repo that states few rules, or none at all, is a repo where this lens passes with nothing to report.
|
|
54
58
|
- Better ways to do this - reads the design against the codebase for a simpler shape, and for code the repo already ships that does part of the work.
|
|
55
59
|
|
|
56
|
-
The consistency-and-completeness reviewer takes every lens except
|
|
60
|
+
The consistency-and-completeness reviewer takes every lens except repo rules and better ways to do this, which go to the reviewers of those names.
|
|
57
61
|
A focus outside this set takes the lenses that touch it.
|
|
58
62
|
Every lens goes to exactly one reviewer on the confirmed panel, so a panel that drops a focus hands that focus's lenses to the reviewer nearest to it.
|
|
63
|
+
The repo-rules lens is on every panel, so a panel with no reviewer of that name gives the lens to the consistency-and-completeness reviewer.
|
|
59
64
|
|
|
60
65
|
## The coverage lens
|
|
61
66
|
|
|
@@ -68,20 +73,21 @@ When the protocol has no execution plan stage, the lens checks instead that ever
|
|
|
68
73
|
|
|
69
74
|
## The severity levels
|
|
70
75
|
|
|
71
|
-
- **CRITICAL** - a
|
|
76
|
+
- **CRITICAL** - a violation of a repo rule that changes the work itself, a contradiction between artifacts, or a requirement with no plan coverage.
|
|
77
|
+
A repo rule about style or wording is a LOW finding, as every other style point is.
|
|
72
78
|
- **HIGH** - a conflicting or untestable requirement, an ambiguous security or performance attribute.
|
|
73
79
|
- **MEDIUM** - terminology drift, an edge case with missing detail.
|
|
74
80
|
- **LOW** - style and wording.
|
|
75
81
|
|
|
76
|
-
Fix a conflict with a
|
|
77
|
-
Never resolve it by softening the
|
|
82
|
+
Fix a conflict with a repo rule by changing the work.
|
|
83
|
+
Never resolve it by softening the rule.
|
|
78
84
|
|
|
79
85
|
## The report format
|
|
80
86
|
|
|
81
87
|
`review/REVIEW.md`:
|
|
82
88
|
|
|
83
89
|
```markdown
|
|
84
|
-
#
|
|
90
|
+
# Specs quality review - <title of the work>
|
|
85
91
|
|
|
86
92
|
- Run: YYYY-MM-DD
|
|
87
93
|
- Mode: fresh-context subagents | inline - the session that wrote the spec
|