chamba 0.9.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 +7 -2
- 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/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/pane-apps/client/assets/api-Cyoc3E6K.css +1 -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/reviews-Bq8EwmG-.js +153 -0
- package/templates/pane-apps/client/assets/reviews-DzHtA35Q.css +1 -0
- package/templates/pane-apps/client/assets/{files-BkmyervY.js → search-DgFnnu-J.js} +1 -1
- package/templates/pane-apps/client/assets/specs-JvdNFtXR.js +13 -0
- package/templates/pane-apps/client/assets/{useNow-BHFKFraS.js → useNow-Ww-Gffnk.js} +1 -1
- package/templates/pane-apps/client/home/index.html +5 -5
- package/templates/pane-apps/client/reviews/index.html +6 -6
- package/templates/pane-apps/client/specs/index.html +4 -4
- package/templates/pane-apps/server/home.mjs +11 -12
- package/templates/pane-apps/server/specs.mjs +4 -4
- package/templates/skills/dx-spec/SKILL.md +4 -16
- package/templates/skills/dx-spec/references/review-guide.md +13 -7
- package/templates/skills/dx-spec/references/spec-guide.md +11 -4
- package/templates/skills/dx-spec/references/stages.md +38 -22
- package/templates/skills/dx-spec-execute/SKILL.md +5 -21
- package/templates/webterm/README.md +11 -8
- package/templates/webterm/awake.js +27 -0
- package/templates/webterm/config.js +10 -0
- package/templates/webterm/public/app/state.js +4 -0
- package/templates/webterm/public/app/tabs.js +37 -3
- package/templates/webterm/public/styles.css +13 -0
- package/templates/webterm/server.js +24 -1
- package/templates/webterm/tools/home.js +25 -3
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +0 -1
- package/templates/pane-apps/client/assets/home-3SeGzySM.css +0 -1
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +0 -1
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +0 -153
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +0 -1
- package/templates/pane-apps/client/assets/specs-Cabb15F0.js +0 -13
- package/templates/skills/dx-spec/references/principles-template.md +0 -2
- /package/templates/pane-apps/client/assets/{api-BBDf_rzz.js → api-CguEsvNp.js} +0 -0
|
@@ -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 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
|
|
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}/${
|
|
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};
|
|
@@ -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
|
|
@@ -346,7 +348,6 @@ A commit that touches spec files alone is a `docs` commit.
|
|
|
346
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.
|
|
347
349
|
|
|
348
350
|
This is built-in behaviour, and nothing exposes it as a setting, a form field or a run decision.
|
|
349
|
-
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.
|
|
350
351
|
|
|
351
352
|
`/dx-spec-execute` states no style of its own: every commit it makes is written from this one.
|
|
352
353
|
|
|
@@ -363,7 +364,6 @@ The tab serves one root, and each piece of work is one directory under it.
|
|
|
363
364
|
|
|
364
365
|
```
|
|
365
366
|
<root>/
|
|
366
|
-
principles.md the engineering principles every run honors, and the user's to edit
|
|
367
367
|
<slug>/
|
|
368
368
|
README.md written by the tool, from the state - never by you
|
|
369
369
|
intake.md what the work started as
|
|
@@ -392,24 +392,12 @@ Outside spec work, `webpane` and the Pages tab are the right home for a page.
|
|
|
392
392
|
|
|
393
393
|
Keep a page standalone: no external stylesheet, no script that fetches anything, and images as data URLs.
|
|
394
394
|
|
|
395
|
-
## The bootstrap
|
|
396
|
-
|
|
397
|
-
Settle the root and the principles before step 1, and before the first step of `/dx-spec-execute`.
|
|
398
|
-
|
|
399
|
-
1. Read `root` from `specs stages`. That is the directory the Specs tab serves, and every path in this family resolves against it.
|
|
400
|
-
2. Read `<root>/principles.md`. It is plain markdown: a `# Principles` heading, then one `- <principle>` bullet each, and nothing else.
|
|
401
|
-
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.
|
|
402
|
-
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.
|
|
403
|
-
|
|
404
|
-
A file that is already there produces no message at all.
|
|
405
|
-
Nothing else about how this family works is configurable, and the user changes a principle by editing that file.
|
|
406
|
-
|
|
407
395
|
## Flow
|
|
408
396
|
|
|
409
397
|
### 1. Start
|
|
410
398
|
|
|
411
399
|
Every start is this skill's own command, in the spelling of the session's agent.
|
|
412
|
-
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.
|
|
413
401
|
The argument decides where to begin.
|
|
414
402
|
|
|
415
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.
|
|
@@ -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,13 +73,14 @@ 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
|
|
|
@@ -5,15 +5,22 @@ The technical specs stage of `/dx-spec` uses this guide, and so does every other
|
|
|
5
5
|
**No fixed template.**
|
|
6
6
|
Write the sections the work needs, and name and order them to fit the work.
|
|
7
7
|
A small chore may need a few lines. A large feature may need many sections.
|
|
8
|
-
Common sections are: goal, context, scope and **non-goals**, requirements or behavior, edge cases and errors, data and contracts, `Documentation impact`, constraints, and acceptance criteria.
|
|
8
|
+
Common sections are: goal, context, scope and **non-goals**, requirements or behavior, edge cases and errors, data and contracts, `Documentation impact`, `Migrations`, constraints, and acceptance criteria.
|
|
9
9
|
For the constraints, follow the repo's own rules in `CLAUDE.md` or `AGENTS.md`.
|
|
10
10
|
Drop every section that does not apply.
|
|
11
11
|
|
|
12
|
-
**`Documentation impact`
|
|
13
|
-
|
|
12
|
+
**`Documentation impact` and `Migrations` are the two sections that always apply.**
|
|
13
|
+
The user confirms both in a round, as `references/stages.md` describes under **Documentation and migrations**.
|
|
14
|
+
|
|
15
|
+
Under `Documentation impact`, read the project's own documents - the README, the guides, and each document that sits beside the code the work touches - and name the ones the work makes not correct.
|
|
14
16
|
Write one line for each: the path to it, and what the work changes in it.
|
|
15
17
|
Where the work leaves every document correct, write one line that says so.
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
Under `Migrations`, name each migration the work needs.
|
|
20
|
+
Write one line for each: what is stored in the old shape, and what moves it to the new one.
|
|
21
|
+
Where the work needs none, write `None needed` and one line saying why.
|
|
22
|
+
|
|
23
|
+
The execution plan stage reads both sections by name, so head each one exactly this way.
|
|
17
24
|
Label anything left open as an **assumption** or an **open question**.
|
|
18
25
|
Write each date as an absolute date.
|
|
19
26
|
Reference code as `path:line`.
|
|
@@ -125,6 +125,28 @@ Either way, keep it self-contained: no external image, no script, nothing fetche
|
|
|
125
125
|
This is a judgment call, never a rule.
|
|
126
126
|
Most documents need no visual, and a visual nobody would look at is not worth writing.
|
|
127
127
|
|
|
128
|
+
## Documentation and migrations
|
|
129
|
+
|
|
130
|
+
Two readings of the work are the user's to confirm: which of the project's own documents the work makes not correct, and whether the work needs a migration.
|
|
131
|
+
Ask both once, in a round, whichever stages the protocol holds.
|
|
132
|
+
No form asks them.
|
|
133
|
+
|
|
134
|
+
Neither one can be answered before the code is read, so the round waits for that.
|
|
135
|
+
Codebase analysis is where the code is read, and it has no gate, so the round belongs to the first stage that runs after it.
|
|
136
|
+
Where the protocol holds no codebase analysis, it belongs to the first stage that reads the code.
|
|
137
|
+
|
|
138
|
+
Read the code that answers each one before you ask, and mark what the code gives as recommended.
|
|
139
|
+
That is what makes the recommendation an accurate one rather than a guess, and it is usually the answer: the user confirms what is already there.
|
|
140
|
+
|
|
141
|
+
For the migrations, judge what a migration means for the work in hand.
|
|
142
|
+
There is no fixed definition, and the judgment is yours: stored data, a file on disk, a saved shape, a contract something outside this build already holds.
|
|
143
|
+
Report the judgment whichever way it falls.
|
|
144
|
+
`None needed`, with a short reason, is an answer the user can argue with.
|
|
145
|
+
A question nobody asked gives the user nothing to argue with at all.
|
|
146
|
+
|
|
147
|
+
Write both answers into the `Documentation impact` and `Migrations` sections of `SPEC.md`, which the execution plan stage reads by name.
|
|
148
|
+
`references/spec-guide.md` holds the shape of each one.
|
|
149
|
+
|
|
128
150
|
## High-level exploration
|
|
129
151
|
|
|
130
152
|
Think together before you commit to anything.
|
|
@@ -228,6 +250,7 @@ Read the project's own documents while you map the code: the README, the guides,
|
|
|
228
250
|
Find the ones the planned work makes not correct, and write them into the `Documentation impact` section of `spec/SPEC.md`, as `references/spec-guide.md` describes it.
|
|
229
251
|
Name each document, and say which part of it the work makes not correct.
|
|
230
252
|
Where the work leaves every document correct, write that, so the stages after this one know the question was asked.
|
|
253
|
+
What is written here is a reading of the code, and the user confirms it in the round under **Documentation and migrations**.
|
|
231
254
|
|
|
232
255
|
This stage is where the code is read as a whole, so it is where **Cross-check a stored answer against the code** in `SKILL.md` runs.
|
|
233
256
|
Read every answer the user has given against what the code says, and raise what disagrees: the stage settings the protocol form carries, and what a round settled earlier, the interview's own answers included.
|
|
@@ -291,7 +314,7 @@ This stage recommends them as data, and writes none of them into `PLAN.md`.
|
|
|
291
314
|
|
|
292
315
|
Break the work into phases.
|
|
293
316
|
**Each phase fits one session, and leaves the repo in a working state.**
|
|
294
|
-
Write `plan/PLAN.md`: the goal
|
|
317
|
+
Write `plan/PLAN.md`: the goal and the phase list with checkboxes.
|
|
295
318
|
Give each phase its goal and its definition of done.
|
|
296
319
|
Decide the phase boundaries, the goals and the definitions of done now, for every phase. They are cheap to get right in advance.
|
|
297
320
|
|
|
@@ -300,13 +323,6 @@ Prefer quality, simplicity, robustness, scalability and long-term maintainabilit
|
|
|
300
323
|
When something has grown unwieldy, restructure it by judgment: refactor, split, or extract.
|
|
301
324
|
Never restructure by a number set in advance.
|
|
302
325
|
|
|
303
|
-
Check the plan against the principles the bootstrap read.
|
|
304
|
-
Record the outcome as a `## Principles check` section: one line per principle the work touches, saying how the plan honors it.
|
|
305
|
-
When a plan must break a principle, record it in a table with the columns `Deviation | Why needed | Simpler alternative rejected because`.
|
|
306
|
-
Look for the simpler alternative before you record a deviation, and say why you rejected it.
|
|
307
|
-
A deviation is an exception the user decides on at the gate.
|
|
308
|
-
It is not a way to soften a principle, so the usual fix for a conflict is a different plan.
|
|
309
|
-
|
|
310
326
|
When `SPEC.md` carries requirement IDs, end every phase entry with a `Covers: FR-03, FR-04` line.
|
|
311
327
|
Every ID appears in one phase's `Covers` line at least, and a requirement may span several phases.
|
|
312
328
|
An ID no phase covers means a missing phase, or scope that belongs in the non-goals.
|
|
@@ -316,6 +332,10 @@ Add the phase, or move the requirement, before the gate. Ask the user when the c
|
|
|
316
332
|
Name the document in the definition of done of the phase that changes what it describes, so the run makes the document correct as it changes the code.
|
|
317
333
|
Do not collect the documents into a phase of their own at the end.
|
|
318
334
|
|
|
335
|
+
**A phase covers each migration the spec's `Migrations` section names.**
|
|
336
|
+
Name the migration in the definition of done of the phase that makes it necessary, so the run moves what is stored as it changes the shape.
|
|
337
|
+
Do not collect the migrations into a phase of their own at the end.
|
|
338
|
+
|
|
319
339
|
**Post the phases as soon as `PLAN.md` is on disk:**
|
|
320
340
|
|
|
321
341
|
```
|
|
@@ -339,7 +359,7 @@ Recommend `per-phase` for a plan of several phases, because a phase that is comm
|
|
|
339
359
|
Recommend `single` for small work that reads as one change.
|
|
340
360
|
Recommend `none` where the user is likely to want to read the whole thing before any of it lands.
|
|
341
361
|
Recommend a new branch for work of any size that touches shared code, and the current branch for a small change on a branch that is already the work's own.
|
|
342
|
-
Name the branch `spec/<slug>`, with this spec's own slug
|
|
362
|
+
Name the branch `spec/<slug>`, with this spec's own slug.
|
|
343
363
|
|
|
344
364
|
**The pauses.**
|
|
345
365
|
A pause is where the run stops and waits for the user.
|
|
@@ -387,7 +407,7 @@ An executing agent loads `PLAN.md` and its one phase file, never the whole plan.
|
|
|
387
407
|
|
|
388
408
|
`PLAN.md` follows this layout.
|
|
389
409
|
Copy the opening note roughly as written, and adapt the rest to the work.
|
|
390
|
-
Include the
|
|
410
|
+
Include the `Covers:` bullets only when `SPEC.md` carries IDs:
|
|
391
411
|
|
|
392
412
|
```markdown
|
|
393
413
|
# <Title of the work>
|
|
@@ -400,14 +420,6 @@ Include the deviation table only when the plan breaks a principle, and the `Cove
|
|
|
400
420
|
|
|
401
421
|
<what the work achieves, in a line or two>
|
|
402
422
|
|
|
403
|
-
## Principles check
|
|
404
|
-
|
|
405
|
-
<one line per principle the work touches, saying how the plan honors it>
|
|
406
|
-
|
|
407
|
-
| Deviation | Why needed | Simpler alternative rejected because |
|
|
408
|
-
| --- | --- | --- |
|
|
409
|
-
| <the principle the plan breaks> | <why the plan needs to> | <the simpler alternative, and why it was rejected> |
|
|
410
|
-
|
|
411
423
|
## Phases
|
|
412
424
|
|
|
413
425
|
- [ ] Phase 1 - <name>
|
|
@@ -419,7 +431,7 @@ Include the deviation table only when the plan breaks a principle, and the `Cove
|
|
|
419
431
|
```
|
|
420
432
|
|
|
421
433
|
Consider a visual beside the plan.
|
|
422
|
-
Then gate on `plan/PLAN.md`, saying the phases in a line each
|
|
434
|
+
Then gate on `plan/PLAN.md`, saying the phases in a line each.
|
|
423
435
|
Say in one line what you recommended for the run, and that the user settles it when they press Execute.
|
|
424
436
|
On changes: revise the plan, post the phases again where they moved, recommend the run again where the change made the recommendation wrong, and gate again.
|
|
425
437
|
Loop until the user approves it.
|
|
@@ -436,9 +448,13 @@ It holds the reviewer briefs, the lenses, the severities, and the report format.
|
|
|
436
448
|
Ask who reviews the work, in a round.
|
|
437
449
|
Propose a panel sized to the work and its risk, one reviewer per focus, and mark your proposal recommended.
|
|
438
450
|
Give a short line saying what each reviewer looks at.
|
|
439
|
-
The default proposal has three reviewers: consistency and completeness,
|
|
451
|
+
The default proposal has three reviewers: consistency and completeness, repo rules, and better ways to do this.
|
|
440
452
|
**Include the "better ways to do this" reviewer in every proposal.**
|
|
441
|
-
|
|
453
|
+
**The repo-rules lens is on every panel, with or without a reviewer of that name.**
|
|
454
|
+
Work of any size is judged against what the repo asks of it, so that lens is never what a smaller panel loses.
|
|
455
|
+
Bigger or riskier work may add a focus of its own - security, performance, the work's own subject.
|
|
456
|
+
Small work may run with two reviewers: consistency and completeness, and better ways to do this.
|
|
457
|
+
`references/review-guide.md` says which reviewer then takes up the lenses of a focus that is not on the panel, the repo-rules lens included.
|
|
442
458
|
Always offer an option that turns the review off.
|
|
443
459
|
Say in its description that nothing gets checked independently, and that the spec goes straight to the rewrite and finalize.
|
|
444
460
|
|
|
@@ -470,7 +486,7 @@ Dismiss a finding you disagree with, and say why in the report.
|
|
|
470
486
|
Fix a finding that is plainly right, apply the fix, and mark it applied.
|
|
471
487
|
Bring a taste or scope finding to the user, "better ways" suggestions included, with the reviewer's reasoning.
|
|
472
488
|
Bring a finding to the user too when it is right but the fix is not obvious, or when acting on it would undo something the user approved at an earlier gate.
|
|
473
|
-
Resolve a conflict with a
|
|
489
|
+
Resolve a conflict with a repo rule by changing the work, never the rule.
|
|
474
490
|
|
|
475
491
|
**The report and the gate.**
|
|
476
492
|
Write `review/REVIEW.md` in the format `references/review-guide.md` defines.
|
|
@@ -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
|
|
|
@@ -146,13 +147,9 @@ So where the state holds decisions they bind you, and where it holds none there
|
|
|
146
147
|
|
|
147
148
|
## The plan is the user's instruction
|
|
148
149
|
|
|
149
|
-
The user chose everything in the plan at spec time: the phases,
|
|
150
|
+
The user chose everything in the plan at spec time: the phases, and the definition of done of each one.
|
|
150
151
|
All of it binds this run.
|
|
151
152
|
|
|
152
|
-
A recorded deviation is an approved exception.
|
|
153
|
-
Build the work to match it, and never return it quietly to the principle it excepts.
|
|
154
|
-
A principle the plan does not except stays binding.
|
|
155
|
-
|
|
156
153
|
Your own limits do not justify a change to the plan.
|
|
157
154
|
When you cannot do what the plan asks, that is a stop condition, and the **Stop and ask** section says how to raise it.
|
|
158
155
|
Never replace the plan's instruction with a weaker one and carry on.
|
|
@@ -162,24 +159,12 @@ A choice of your own that changes nothing - which of two equal ways to read a fi
|
|
|
162
159
|
An instruction from the user in this conversation overrides the plan.
|
|
163
160
|
The plan loses to the user, and never to you.
|
|
164
161
|
|
|
165
|
-
## 0. Bootstrap
|
|
166
|
-
|
|
167
|
-
Settle the root and the principles first:
|
|
168
|
-
|
|
169
|
-
1. Read `root` from `specs stages`. That is the directory the Specs tab serves, and every path in this family resolves against it.
|
|
170
|
-
2. Read `<root>/principles.md`. It is plain markdown: a `# Principles` heading, then one `- <principle>` bullet each, and nothing else.
|
|
171
|
-
3. Where no file is there, write one from `../dx-spec/references/principles-template.md`. Say in one line that you made it, and carry straight on.
|
|
172
|
-
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.
|
|
173
|
-
|
|
174
|
-
A file that is already there produces no message at all.
|
|
175
|
-
Nothing else about how this family works is configurable, and the user changes a principle by editing that file.
|
|
176
|
-
|
|
177
162
|
## 1. Choose the spec
|
|
178
163
|
|
|
179
164
|
A candidate is a piece of work under the root, outside `archive/`, whose plan still holds an unticked phase.
|
|
180
165
|
|
|
181
|
-
The user may name one.
|
|
182
|
-
|
|
166
|
+
The user may name one, and the tab names one by its path.
|
|
167
|
+
An argument that matches one spec exactly is an instruction, not a guess.
|
|
183
168
|
Start on it, and ask nothing.
|
|
184
169
|
|
|
185
170
|
In three cases the spec is a guess of yours:
|
|
@@ -352,8 +337,7 @@ An agent that has the capability but cannot make it work is a different case: th
|
|
|
352
337
|
Tell each reviewer:
|
|
353
338
|
|
|
354
339
|
- Its focus, and to check the work against what was asked.
|
|
355
|
-
- Which files to read: `spec/SPEC.md`, `plan/PLAN.md`, the phase files written so far, and the
|
|
356
|
-
- That the plan's principles check records deviations as approved exceptions, not as findings.
|
|
340
|
+
- Which files to read: `spec/SPEC.md`, `plan/PLAN.md`, the phase files written so far, and the repo's own rule files, `CLAUDE.md` and `AGENTS.md`, wherever they exist.
|
|
357
341
|
- How to see the changes: name the commit the work started from, so it can diff against that, and point at the working tree when nothing is committed.
|
|
358
342
|
|
|
359
343
|
Each reviewer returns a verdict with concrete findings, each pointing at a file and a line.
|