@jawache/flow 0.1.1 → 0.1.2

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/dist/packs.mjs CHANGED
@@ -1,49 +1,49 @@
1
- function b(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function V(t){let e=[],n="",o=()=>{n!==""&&e.push({kind:"literal",text:n}),n=""};for(let r=0;r<t.length;r++){let s=t[r];if(s==="*")o(),t[r+1]==="*"?(e.push({kind:"globstar"}),r++,t[r+1]==="/"&&r++):e.push({kind:"star"});else if(s==="?")o(),e.push({kind:"single"});else if(s==="{"){let a=t.indexOf("}",r);if(a===-1){n+=s;continue}o(),e.push({kind:"options",options:t.slice(r+1,a).split(",")}),r=a}else n+=s??""}return o(),e}function q(t){switch(t.kind){case"literal":return b(t.text);case"star":return"[^/]*";case"globstar":return".*";case"single":return"[^/]";case"options":return`(${t.options.map(b).join("|")})`}}function we(t){return new RegExp(`^${V(t).map(q).join("")}$`)}function xe(t,e){return we(e).test(t)}function S(t,e){return(e||[]).some(n=>xe(t,n))}function ve(t){let e=t.includes("/")?t.slice(0,t.lastIndexOf("/")):".",n=t.slice(t.lastIndexOf("/")+1),o=n.lastIndexOf("."),r=o>0?n.slice(0,o):n,s=e==="."?r:`${e}/${r}`;return{dir:e,base:n,name:r,path:s}}function Y(t,e){let n=ve(e);return t.replace(/\{(dir|base|name|path)\}/g,(o,r)=>n[r])}var Se={write:["edit"],command:["edit","commit"],commit:["commit"],delete:["delete"],"turn-end":["turn"]},Wt=Object.keys(Se);var p="write",y="command",l="commit",z="delete";var E="session",k="touch";var Ee=["write","delete","commit","touch"];var Kt=[...Ee,"command"];var Re=Symbol.for("flow.category");function M(t,e){return{[Re]:!0,name:t,classify:e}}var Ce=Symbol.for("flow.settings");function g(t){return e=>{let n=t(e);return Object.defineProperty(n,Ce,{value:e,enumerable:!1,configurable:!0}),n}}function J(t,e){let n=o=>J({...t,...o},e);return{...e(t),at:(...o)=>n({at:o}),for:(...o)=>n({for:o}),on:(...o)=>n({on:o}),ignore:(...o)=>n({ignore:o}),check:o=>n({check:o}),message:o=>n({message:o}),text:o=>n({text:o}),file:o=>n({file:o}),disabled:o=>n({disabled:o===void 0?{}:{reason:o}}),test:o=>n({test:o}),description:o=>n({description:o})}}var oe=t=>({spec:t});function c(){return J({kind:"guardrail"},oe)}function h(){return J({kind:"breadcrumb"},oe)}var $e=Symbol.for("flow.pack"),je=Symbol.for("flow.ref");function m(t,e){let n=typeof e=="function"&&e.length>0;return re(t,[],{name:t,build:r=>typeof e=="function"?e(r):e,takesParams:n})}function re(t,e,n){let o={[je]:{pack:t,id:e.join(".")}};return n&&(o[$e]=n),new Proxy(o,{get(r,s){return s in r?r[s]:typeof s=="string"?re(t,[...e,s]):void 0}})}function R(t,e,n=" \xB7 "){return e.length===0?t.ok():t.fail(e.join(n))}function j(t){return t.file??{path:"",content:""}}function Pe(t,e){let n=t.split(`
2
- `),o=[];for(let r of e){let s=new RegExp(r);n.forEach((a,i)=>{s.test(a)&&o.push({pattern:r,line:i+1})})}return o}var W=g(t=>e=>{let n=Pe(j(e).content,t.ban);return R(e,n.map(o=>`line ${o.line}: matches /${o.pattern}/`))}),x=g(t=>e=>{let n=e.command??"",o=t.ban.filter(r=>new RegExp(r).test(n));return R(e,o.map(r=>`matches banned /${r}/`))});function D(t){return`(?:${t.join("|")})[^"\`]*(?:"(?:(?!<<-?')[^"])*"[^"\`]*)*"(?:(?!<<-?')[^"])*\``}var B=["You are recording prose through a shell command, and a backtick inside a DOUBLE-quoted argument is not Markdown \u2014 the shell RUNS it and splices its output into your text before the command ever sees it.","On an append-only record there is no undo: no amend, no redact. And once the binary has argv the splice has already happened, so this is the only moment it can be caught.","Fix: SINGLE-quote the argument (backticks are literal there), or feed the prose on stdin with a QUOTED heredoc \u2014 `git commit -F -` plus `<<'EOF'`, which this rule treats as safe.","ALREADY single-quoted and still blocked? Then your PROSE carries an unbalanced double quote before the backtick, and the matcher cannot tell that quote from a real open argument. Nothing would splice \u2014 this one is the rule's cost, not your mistake. Balance the quote, drop it, or use the heredoc; do not go back to double quotes to make it pass."].join(`
3
- `),A=g(t=>async e=>{let{path:n}=j(e);return t.existingOnly===!0?e.moment!=="delete"&&!await e.fs.exists(n)?e.ok():e.fail(`'${n}' is append-only \u2014 an existing file here is history and may not be edited or deleted (adding a new one is fine)`):e.fail(`'${n}' is a protected path (do not edit directly)`)}),Q=g(t=>async e=>{let n=Y(t.sibling,j(e).path);return await e.fs.exists(n)?e.ok():e.fail(`required sibling missing: ${n}`)}),Me=/\.(ts|tsx|mts|cts|js|mjs|cjs|jsx)$/;function De(t,e){let n=t.replace(/\\/g,"/"),o=e.root.replace(/^\/+|\/+$/g,"")+"/",r=n.indexOf(o);if(r===-1)return{ok:!0};let s=n.slice(r+o.length);if(e.folders?.length){let f=s.split("/")[0]??"",$=f.replace(/\.[^.]+$/,"");if(f!==""&&!e.folders.includes(f)&&!e.folders.includes($))return{ok:!1,tier:"folder",found:f}}if(!e.allow.length)return{ok:!0};let a=Me.exec(n);if(!a)return{ok:!0};if(n.endsWith(".d.ts"))return{ok:!0};if(!s.includes("/"))return{ok:!0};let d=s.slice(s.lastIndexOf("/")+1).slice(0,-a[0].length);return(d.endsWith(".test")?d.slice(0,-5)!==(e.thinking??""):!e.allow.includes(d))?{ok:!1,tier:"filename"}:{ok:!0}}function Be(t,e,n){return t.ok?"":t.tier==="folder"?`'${t.found}' is not one of the names ${n.root}/ may hold: ${n.folders?.join(" \xB7 ")??""} (found ${e})`:`non-canonical file in a ${n.root} feature folder: ${e} (allowed: ${n.allow.join(" \xB7 ")}${n.thinking===void 0?"":` \u2014 only ${n.thinking} may carry a .test sibling`})`}var ee=g(t=>e=>{let{path:n}=j(e),o=De(n,t);return o.ok?e.ok():e.fail(Be(o,n,t))});function Ge(t,e){let n=t;for(let o of(e??"").split(".").filter(Boolean)){if(n==null||typeof n!="object"||!(o in n))return{found:!1};n=n[o]}return{found:!0,value:n}}function Ie(t,e){let n=[],{path:o}=e,r=Ge(t,o);if(!r.found)return e.required===!0&&n.push(`"${o}" is missing (required)`),n;let s=r.value;if(e.keysPrefixedWith!==void 0){if(!s||typeof s!="object")return n;for(let a of Object.keys(s))a.startsWith(e.keysPrefixedWith)||n.push(`"${o}.${a}" must be prefixed "${e.keysPrefixedWith}"`)}return"equals"in e&&JSON.stringify(s)!==JSON.stringify(e.equals)&&n.push(`"${o}" must equal ${JSON.stringify(e.equals)} (is ${JSON.stringify(s)})`),e.matches!==void 0&&!new RegExp(e.matches).test(String(s))&&n.push(`"${o}" (${JSON.stringify(s)}) must match /${e.matches}/`),n}function Ne(t,e){let n;try{n=JSON.parse(t)}catch{return[]}return e.flatMap(o=>Ie(n,o))}var T=g(t=>e=>R(e,Ne(j(e).content,t.assert)));function Fe(t,e){return t.filter(n=>!new RegExp(`\\b${b(n)}\\b`).test(e))}async function _e(t){let{parse:e,Lang:n}=await import("@ast-grep/napi"),o=e(n.Tsx,t).root(),r=new Set;for(let s of["export function $N($$$) { $$$ }","export class $N { $$$ }","export const $N = $V"])for(let a of o.findAll(s)){let i=a.getMatch("N");i&&r.add(i.text())}return[...r]}var te=g(t=>async e=>{let{path:n,content:o}=j(e),r=Y(t.sibling,n);if(!await e.fs.exists(r))return e.fail(`sibling test not found: ${r}`);let s=await e.fs.read(r),a=Fe(await _e(o),s);return R(e,a.map(i=>`exported '${i}' is never referenced in ${r}`))});async function ae(t){return t.staged?t.staged:t.git.stagedFiles()}function Le(t,e){let n=[];for(let o of e)t.some(r=>S(r,o.if))&&(t.some(r=>S(r,o.thenAny))||n.push(`${o.if.join(", ")} changed but none of [${o.thenAny.join(", ")}] did in the same commit`));return n}var He=g(t=>async e=>R(e,Le(await ae(e),t.groups))),H=new Set([";","&&","||","|","&","|&",`
4
- `]),We=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--exec-path","--config-env"]);function Ke(t,e){for(let n=e;n<t.length;n++){if(t[n]==="\\"){n++;continue}if(t[n]==='"')return n}return-1}function ne(t){let e=t,n=[],o="",r=!1,s=0,a=()=>{r&&n.push(o),o="",r=!1};for(;s<e.length;){let i=e[s];if(i==="'"||i==='"'){let d=i==="'"?e.indexOf("'",s+1):Ke(e,s+1);if(d===-1)return null;o+=i==='"'?e.slice(s+1,d).replace(/\\(["\\$`])/g,"$1"):e.slice(s+1,d),r=!0,s=d+1}else if(i==="\\"&&s+1<e.length)o+=e[s+1],r=!0,s+=2;else if(i===`
1
+ function b(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function se(t){return t.startsWith("^")||t.includes("(?")||t.includes("|")||t.endsWith("$")}function q(t){let e=[],n="",o=()=>{n!==""&&e.push({kind:"literal",text:n}),n=""};for(let r=0;r<t.length;r++){let s=t[r];if(s==="*")o(),t[r+1]==="*"?(e.push({kind:"globstar"}),r++,t[r+1]==="/"&&r++):e.push({kind:"star"});else if(s==="?")o(),e.push({kind:"single"});else if(s==="{"){let a=t.indexOf("}",r);if(a===-1){n+=s;continue}o(),e.push({kind:"options",options:t.slice(r+1,a).split(",")}),r=a}else n+=s??""}return o(),e}function Y(t){switch(t.kind){case"literal":return b(t.text);case"star":return"[^/]*";case"globstar":return".*";case"single":return"[^/]";case"options":return`(${t.options.map(b).join("|")})`}}function Ee(t){return new RegExp(`^${q(t).map(Y).join("")}$`)}function Re(t,e){return Ee(e).test(t)}function S(t,e){return(e||[]).some(n=>Re(t,n))}function Ce(t){let e=t.includes("/")?t.slice(0,t.lastIndexOf("/")):".",n=t.slice(t.lastIndexOf("/")+1),o=n.lastIndexOf("."),r=o>0?n.slice(0,o):n,s=e==="."?r:`${e}/${r}`;return{dir:e,base:n,name:r,path:s}}function z(t,e){let n=Ce(e);return t.replace(/\{(dir|base|name|path)\}/g,(o,r)=>n[r])}var $e={write:["edit"],command:["edit","commit"],commit:["commit"],delete:["delete"],"turn-end":["turn"]},zt=Object.keys($e);var f="write",y="command",u="commit",J="delete";var E="session",k="touch";var Ae=["write","delete","commit","touch"];var Jt=[...Ae,"command"];var je=Symbol.for("flow.category");function M(t,e){return{[je]:!0,name:t,classify:e}}var Te=Symbol.for("flow.settings"),Oe=Symbol.for("flow.faults");function Z(t,e){return e.length>0&&Object.defineProperty(t,Oe,{value:e,enumerable:!1,configurable:!0}),t}function g(t){return e=>{let n=t(e);return Object.defineProperty(n,Te,{value:e,enumerable:!1,configurable:!0}),n}}function X(t,e){let n=o=>X({...t,...o},e);return{...e(t),at:(...o)=>n({at:o}),for:(...o)=>n({for:o}),on:(...o)=>n({on:o}),ignore:(...o)=>n({ignore:o}),check:o=>n({check:o}),message:o=>n({message:o}),text:o=>n({text:o}),file:o=>n({file:o}),disabled:o=>n({disabled:o===void 0?{}:{reason:o}}),test:o=>n({test:o}),description:o=>n({description:o})}}var ie=t=>({spec:t});function c(){return X({kind:"guardrail"},ie)}function h(){return X({kind:"breadcrumb"},ie)}var Pe=Symbol.for("flow.pack"),Me=Symbol.for("flow.ref");function m(t,e){let n=typeof e=="function"&&e.length>0;return ae(t,[],{name:t,build:r=>typeof e=="function"?e(r):e,takesParams:n})}function ae(t,e,n){let o={[Me]:{pack:t,id:e.join(".")}};return n&&(o[Pe]=n),new Proxy(o,{get(r,s){return s in r?r[s]:typeof s=="string"?ae(t,[...e,s]):void 0}})}function R(t,e,n=" \xB7 "){return e.length===0?t.ok():t.fail(e.join(n))}function A(t){return t.file??{path:"",content:""}}function Ie(t,e){let n=t.split(`
2
+ `),o=[];for(let r of e){let s=new RegExp(r);n.forEach((a,i)=>{s.test(a)&&o.push({pattern:r,line:i+1})})}return o}var W=g(t=>e=>{let n=Ie(A(e).content,t.ban);return R(e,n.map(o=>`line ${o.line}: matches /${o.pattern}/`))}),x=g(t=>e=>{let n=e.command??"",o=t.ban.filter(r=>new RegExp(r).test(n));return R(e,o.map(r=>`matches banned /${r}/`))});function D(t){return`(?:${t.join("|")})[^"\`]*(?:"(?:(?!<<-?')[^"])*"[^"\`]*)*"(?:(?!<<-?')[^"])*\``}var B=["You are recording prose through a shell command, and a backtick inside a DOUBLE-quoted argument is not Markdown \u2014 the shell RUNS it and splices its output into your text before the command ever sees it.","On an append-only record there is no undo: no amend, no redact. And once the binary has argv the splice has already happened, so this is the only moment it can be caught.","Fix: SINGLE-quote the argument (backticks are literal there), or feed the prose on stdin with a QUOTED heredoc \u2014 `git commit -F -` plus `<<'EOF'`, which this rule treats as safe.","ALREADY single-quoted and still blocked? Then your PROSE carries an unbalanced double quote before the backtick, and the matcher cannot tell that quote from a real open argument. Nothing would splice \u2014 this one is the rule's cost, not your mistake. Balance the quote, drop it, or use the heredoc; do not go back to double quotes to make it pass."].join(`
3
+ `),j=g(t=>async e=>{let{path:n}=A(e);return t.existingOnly===!0?e.moment!=="delete"&&!await e.fs.exists(n)?e.ok():e.fail(`'${n}' is append-only \u2014 an existing file here is history and may not be edited or deleted (adding a new one is fine)`):e.fail(`'${n}' is a protected path (do not edit directly)`)}),te=g(t=>async e=>{let n=z(t.sibling,A(e).path);return await e.fs.exists(n)?e.ok():e.fail(`required sibling missing: ${n}`)}),Ne=/\.(ts|tsx|mts|cts|js|mjs|cjs|jsx)$/;function Fe(t,e){let n=t.replace(/\\/g,"/"),o=e.root.replace(/^\/+|\/+$/g,"")+"/",r=n.indexOf(o);if(r===-1)return{ok:!0};let s=n.slice(r+o.length);if(e.folders?.length){let p=s.split("/")[0]??"",$=p.replace(/\.[^.]+$/,"");if(p!==""&&!e.folders.includes(p)&&!e.folders.includes($))return{ok:!1,tier:"folder",found:p}}if(!e.allow.length)return{ok:!0};let a=Ne.exec(n);if(!a)return{ok:!0};if(n.endsWith(".d.ts"))return{ok:!0};if(!s.includes("/"))return{ok:!0};let d=s.slice(s.lastIndexOf("/")+1).slice(0,-a[0].length);return(d.endsWith(".test")?d.slice(0,-5)!==(e.thinking??""):!e.allow.includes(d))?{ok:!1,tier:"filename"}:{ok:!0}}function Le(t,e,n){return t.ok?"":t.tier==="folder"?`'${t.found}' is not one of the names ${n.root}/ may hold: ${n.folders?.join(" \xB7 ")??""} (found ${e})`:`non-canonical file in a ${n.root} feature folder: ${e} (allowed: ${n.allow.join(" \xB7 ")}${n.thinking===void 0?"":` \u2014 only ${n.thinking} may carry a .test sibling`})`}var ne=g(t=>e=>{let{path:n}=A(e),o=Fe(n,t);return o.ok?e.ok():e.fail(Le(o,n,t))});function _e(t,e){let n=t;for(let o of(e??"").split(".").filter(Boolean)){if(n==null||typeof n!="object"||!(o in n))return{found:!1};n=n[o]}return{found:!0,value:n}}function He(t,e){let n=[],{path:o}=e,r=_e(t,o);if(!r.found)return e.required===!0&&n.push(`"${o}" is missing (required)`),n;let s=r.value;if(e.keysPrefixedWith!==void 0){if(!s||typeof s!="object")return n;for(let a of Object.keys(s))a.startsWith(e.keysPrefixedWith)||n.push(`"${o}.${a}" must be prefixed "${e.keysPrefixedWith}"`)}return"equals"in e&&JSON.stringify(s)!==JSON.stringify(e.equals)&&n.push(`"${o}" must equal ${JSON.stringify(e.equals)} (is ${JSON.stringify(s)})`),e.matches!==void 0&&!new RegExp(e.matches).test(String(s))&&n.push(`"${o}" (${JSON.stringify(s)}) must match /${e.matches}/`),n}function We(t,e){let n;try{n=JSON.parse(t)}catch{return[]}return e.flatMap(o=>He(n,o))}var T=g(t=>e=>R(e,We(A(e).content,t.assert)));function Ke(t,e){return t.filter(n=>!new RegExp(`\\b${b(n)}\\b`).test(e))}async function Ue(t){let{parse:e,Lang:n}=await import("@ast-grep/napi"),o=e(n.Tsx,t).root(),r=new Set;for(let s of["export function $N($$$) { $$$ }","export class $N { $$$ }","export const $N = $V"])for(let a of o.findAll(s)){let i=a.getMatch("N");i&&r.add(i.text())}return[...r]}var oe=g(t=>async e=>{let{path:n,content:o}=A(e),r=z(t.sibling,n);if(!await e.fs.exists(r))return e.fail(`sibling test not found: ${r}`);let s=await e.fs.read(r),a=Ke(await Ue(o),s);return R(e,a.map(i=>`exported '${i}' is never referenced in ${r}`))});async function ue(t){return t.staged?t.staged:t.git.stagedFiles()}function Ve(t,e){let n=[];for(let o of e)t.some(r=>S(r,o.if))&&(t.some(r=>S(r,o.thenAny))||n.push(`${o.if.join(", ")} changed but none of [${o.thenAny.join(", ")}] did in the same commit`));return n}var qe=g(t=>async e=>R(e,Ve(await ue(e),t.groups))),H=new Set([";","&&","||","|","&","|&",`
4
+ `]),Ye=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--exec-path","--config-env"]);function ze(t,e){for(let n=e;n<t.length;n++){if(t[n]==="\\"){n++;continue}if(t[n]==='"')return n}return-1}function re(t){let e=t,n=[],o="",r=!1,s=0,a=()=>{r&&n.push(o),o="",r=!1};for(;s<e.length;){let i=e[s];if(i==="'"||i==='"'){let d=i==="'"?e.indexOf("'",s+1):ze(e,s+1);if(d===-1)return null;o+=i==='"'?e.slice(s+1,d).replace(/\\(["\\$`])/g,"$1"):e.slice(s+1,d),r=!0,s=d+1}else if(i==="\\"&&s+1<e.length)o+=e[s+1],r=!0,s+=2;else if(i===`
5
5
  `)a(),n.push(`
6
- `),s++;else if(/\s/.test(i))a(),s++;else if(i===";"||i==="|"||i==="&"){a();let d=e.slice(s,s+2);d==="&&"||d==="||"||d==="|&"?(n.push(d),s+=2):(n.push(i),s++)}else o+=i,r=!0,s++}return a(),n}function O(t){let e=ne(t);if(!e)return null;let n=[],o=0,r=!0;for(;o<e.length;){let s=e[o];if(H.has(s)){r=!0,o++;continue}if(r&&s==="git"){let a=[],i=o+1;for(;i<e.length&&!H.has(e[i]);)a.push(e[i++]);let d=0;for(;d<a.length;){let u=a[d];if(We.has(u))d+=2;else if(u.startsWith("-"))d+=1;else break}n.push({globals:a.slice(0,d),subcommand:a[d]??null,args:a.slice(d+1)}),o=i,r=!1;continue}r=!1,o++}return n}function Ue(t){let e=/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/.exec(t);if(!e)return null;let n=e[2],r=t.slice(e.index+e[0].length).split(`
6
+ `),s++;else if(/\s/.test(i))a(),s++;else if(i===";"||i==="|"||i==="&"){a();let d=e.slice(s,s+2);d==="&&"||d==="||"||d==="|&"?(n.push(d),s+=2):(n.push(i),s++)}else o+=i,r=!0,s++}return a(),n}function O(t){let e=re(t);if(!e)return null;let n=[],o=0,r=!0;for(;o<e.length;){let s=e[o];if(H.has(s)){r=!0,o++;continue}if(r&&s==="git"){let a=[],i=o+1;for(;i<e.length&&!H.has(e[i]);)a.push(e[i++]);let d=0;for(;d<a.length;){let l=a[d];if(Ye.has(l))d+=2;else if(l.startsWith("-"))d+=1;else break}n.push({globals:a.slice(0,d),subcommand:a[d]??null,args:a.slice(d+1)}),o=i,r=!1;continue}r=!1,o++}return n}function Je(t){let e=/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/.exec(t);if(!e)return null;let n=e[2],r=t.slice(e.index+e[0].length).split(`
7
7
  `).slice(1),s=r.findIndex(i=>i.trim()===n);if(s===-1)return null;let a=r.slice(0,s).join(`
8
8
  `).trim();return a===""?null:a}function G(t){let e=O(t);if(!e)return null;let n=e.find(r=>r.subcommand==="commit");if(!n)return null;let o=[];for(let r=0;r<n.args.length;r++){let s=n.args[r];if(s==="-m"||s==="--message"){let a=n.args[r+1];if(a===void 0)break;o.push(a),r++;continue}s.startsWith("-m")&&s!=="-m"?o.push(s.slice(2)):s.startsWith("--message=")&&o.push(s.slice(10))}return o.length?o.join(`
9
9
 
10
- `):Ue(t)}function v(t){return`'${t.replace(/'/g,"'\\''")}'`}function I(t){let e=[],n=ne(t),o=n?.[1];n?.[0]==="cd"&&o!==void 0&&!o.startsWith("-")&&!H.has(o)&&H.has(n[2]??"")&&e.push("-C",v(o));let r=O(t)?.[0];if(!r)return e.join(" ");for(let s=0;s<r.globals.length-1;s++)r.globals[s]==="-C"&&e.push("-C",v(r.globals[s+1]));return e.join(" ")}function de(t,e){return new RegExp(`^[ \\t]*${b(e)}[ \\t]*:`,"im").test(t)}function Ve(t,e,n=[]){let o=new Set(n),r=new Set;for(let s of t.split(`
11
- `)){if(!s.startsWith("+")||s.startsWith("+++"))continue;let a=s.slice(1);for(let i of e){let d=new RegExp(i,"g"),u;for(;(u=d.exec(a))!==null;)u[1]!==void 0&&!o.has(u[1])&&r.add(u[1]),u[0]===""&&d.lastIndex++}}return[...r]}function qe(t){let{message:e,state:n,whenChanged:o,whenAdded:r,except:s,namesAdded:a,token:i}=t,d=new Set(n.added),u=[];for(let f of n.changed)s&&S(f,s)||(d.has(f)&&r&&S(f,r)?u.push(`\`${f}\` (new)`):o&&S(f,o)&&u.push(`\`${f}\` (changed)`));for(let{name:f,file:$}of a??[])u.push(`\`${f}\` (added in ${$})`);return u.length?de(e,i)?[]:[`${u.join(" \xB7 ")} \u2014 and this commit's message records no reason. Put \`${i}: <why>\` on a line of its own in the message; the act is fine, doing it silently is not.`]:[]}function Z(t){return t.split(`
12
- `).map(e=>e.trim()).filter(Boolean)}var N=g(t=>async e=>{let n=G(e.command??"");if(n==null)return e.ok();let o=`git ${I(e.command??"")}`.trimEnd(),r={changed:[],added:[]};if(t.whenChanged||t.whenAdded){let[i,d,u]=await Promise.all([e.exec(`${o} diff HEAD --name-only`),e.exec(`${o} diff HEAD --name-only --diff-filter=A`),e.exec(`${o} ls-files --others --exclude-standard`)]);if(i.code!==0||d.code!==0||u.code!==0)return e.ok();let f=Z(u.stdout);r={changed:[...new Set([...Z(i.stdout),...f])],added:[...Z(d.stdout),...f]}}let s=[];for(let i of t.diffAdds??[]){let d=await e.exec(`${o} diff HEAD -- ${v(i.file)}`);if(d.code!==0)return e.ok();for(let u of Ve(d.stdout,i.patterns,i.known??[]))s.push({name:u,file:i.file})}let a=qe({message:n,state:r,whenChanged:t.whenChanged,whenAdded:t.whenAdded,except:t.except,namesAdded:s,token:t.token});return R(e,a)});function Ye(t,e,n){if(n===""||e.length===0)return!1;let o=-1,r=-1;return t.forEach((s,a)=>{s.did==="edit"&&S(s.path,e)&&(o=a),s.did==="run"&&s.command.includes(n)&&(r=a)}),o>=0&&o>r}var ze=g(t=>e=>e.turn&&Ye(e.turn,t.edited,t.mustRun)?e.fail(`edited ${t.edited.join(", ")} this turn but never ran \`${t.mustRun}\` afterwards`):e.ok());function Je(t,e){let n=e??[];return!n.length||!t.length?!1:!t.some(o=>S(o,n))}function ce(t,e=8){let n=t.trim().split(`
10
+ `):Je(t)}function v(t){return`'${t.replace(/'/g,"'\\''")}'`}function I(t){let e=[],n=re(t),o=n?.[1];n?.[0]==="cd"&&o!==void 0&&!o.startsWith("-")&&!H.has(o)&&H.has(n[2]??"")&&e.push("-C",v(o));let r=O(t)?.[0];if(!r)return e.join(" ");for(let s=0;s<r.globals.length-1;s++)r.globals[s]==="-C"&&e.push("-C",v(r.globals[s+1]));return e.join(" ")}function pe(t,e){return new RegExp(`^[ \\t]*${b(e)}[ \\t]*:`,"im").test(t)}function Ze(t,e,n=[]){let o=new Set(n),r=new Set;for(let s of t.split(`
11
+ `)){if(!s.startsWith("+")||s.startsWith("+++"))continue;let a=s.slice(1);for(let i of e){let d=new RegExp(i,"g"),l;for(;(l=d.exec(a))!==null;)l[1]!==void 0&&!o.has(l[1])&&r.add(l[1]),l[0]===""&&d.lastIndex++}}return[...r]}function Xe(t){let{message:e,state:n,whenChanged:o,whenAdded:r,except:s,namesAdded:a,token:i}=t,d=new Set(n.added),l=[];for(let p of n.changed)s&&S(p,s)||(d.has(p)&&r&&S(p,r)?l.push(`\`${p}\` (new)`):o&&S(p,o)&&l.push(`\`${p}\` (changed)`));for(let{name:p,file:$}of a??[])l.push(`\`${p}\` (added in ${$})`);return l.length?pe(e,i)?[]:[`${l.join(" \xB7 ")} \u2014 and this commit's message records no reason. Put \`${i}: <why>\` on a line of its own in the message; the act is fine, doing it silently is not.`]:[]}function Q(t){return t.split(`
12
+ `).map(e=>e.trim()).filter(Boolean)}var N=g(t=>async e=>{let n=G(e.command??"");if(n==null)return e.ok();let o=`git ${I(e.command??"")}`.trimEnd(),r={changed:[],added:[]};if(t.whenChanged||t.whenAdded){let[i,d,l]=await Promise.all([e.exec(`${o} diff HEAD --name-only`),e.exec(`${o} diff HEAD --name-only --diff-filter=A`),e.exec(`${o} ls-files --others --exclude-standard`)]);if(i.code!==0||d.code!==0||l.code!==0)return e.ok();let p=Q(l.stdout);r={changed:[...new Set([...Q(i.stdout),...p])],added:[...Q(d.stdout),...p]}}let s=[];for(let i of t.diffAdds??[]){let d=await e.exec(`${o} diff HEAD -- ${v(i.file)}`);if(d.code!==0)return e.ok();for(let l of Ze(d.stdout,i.patterns,i.known??[]))s.push({name:l,file:i.file})}let a=Xe({message:n,state:r,whenChanged:t.whenChanged,whenAdded:t.whenAdded,except:t.except,namesAdded:s,token:t.token});return R(e,a)});function Qe(t,e,n){if(n===""||e.length===0)return!1;let o=-1,r=-1;return t.forEach((s,a)=>{s.did==="edit"&&S(s.path,e)&&(o=a),s.did==="run"&&s.command.includes(n)&&(r=a)}),o>=0&&o>r}var et=g(t=>e=>e.turn&&Qe(e.turn,t.edited,t.mustRun)?e.fail(`edited ${t.edited.join(", ")} this turn but never ran \`${t.mustRun}\` afterwards`):e.ok());function tt(t,e){let n=e??[];return!n.length||!t.length?!1:!t.some(o=>S(o,n))}function fe(t,e=8){let n=t.trim().split(`
13
13
  `).filter(Boolean),o=n.filter(r=>/\b(fail|failed|error|not ok|assert|expected|cannot|undefined)\b/i.test(r)||/[✖✗✘]/.test(r));return(o.length?o:n.slice(-6)).slice(0,e).join(`
14
- `)}var C=g(t=>async e=>{let n=await ae(e);if(Je(n,t.changed))return e.ok();let o=t.run.includes("{files}");if(o&&n.length===0)return e.ok();let r=o?t.run.replace("{files}",n.map(v).join(" ")):t.run,s=await e.exec(r);return s.code===0?e.ok():e.fail(`\`${r}\` failed (exit ${s.code}):
15
- ${ce(s.stderr||s.stdout)}`)});function L(t){let e=V(t),n=e[e.length-1]?.kind==="globstar";return`^${(n?e.slice(0,-1):e).map(q).join("")}${n?"":"$"}`}function Ze(t){return t==="node:*"?{core:!0}:t.startsWith("node:")?{path:`^${b(t)}$`}:t.includes("/")||t.includes("*")||t.includes(".")?{path:L(t)}:{path:`node_modules/${b(t)}(/|$)`}}function _(t,e){let n=[],o=!1;for(let s of t)for(let a of e[s]??[]){let i=Ze(a);i.core?o=!0:i.path&&n.push(i.path)}let r={};return n.length&&(r.path=n),o&&(r.dependencyTypes=["core"]),r}function Xe(t){let e=t.layers,n=[],o=[],r=(t.except??[]).includes("type-only"),s=i=>r?{...i,dependencyTypesNot:["type-only"]}:i;for(let[i,d]of Object.entries(t.only??{})){let f={pathNot:_(d,e).path??[]};n.push({name:`${i}-only`,severity:"error",comment:`${i} may import only: ${d.join(", ")} \u2014 an unlisted import blocks here (add it to a layer, or keep it out).`,from:_([i],e),to:s(f)})}for(let i of t.forbid??[])if(i==="circular")n.push({name:"no-circular",severity:"error",comment:"no dependency cycles",from:{pathNot:"node_modules"},to:{circular:!0}});else if(i==="orphans")n.push({name:"no-orphans",severity:"warn",comment:"unreferenced module",from:{orphan:!0,pathNot:"node_modules|\\.d\\.ts$"},to:{}});else if(i.cyclesBetween!==void 0){let d=i.cyclesBetween,u=(e[d]??[])[0]??"",f=L(u).replace(/\$$/,"").replace(/\[\^\/\]\*$/,"([^/]+)");n.push({name:`no-cycles-between-${d}`,severity:"error",comment:i.why??`${d} must not import sideways`,from:{path:`${f}/`},to:s({path:`${f.replace(/\(\[\^\/\]\+\)/,"[^/]+")}/`,pathNot:"$1/"})})}else if(i.from!==void 0&&i.to!==void 0){let d={..._([i.to],e)};i.transitive===!0&&(d.reachable=!0),n.push({name:`no-${i.from}-to-${i.to}${i.transitive===!0?"-transitive":""}`,severity:"error",comment:i.why??`${i.from} must not import ${i.to}`,from:_([i.from],e),to:s(d)})}for(let i of t.require??[])o.push({name:`must-import-${i.import}`,severity:"error",comment:i.why??`modules in ${i.in} must import ${i.import}`,module:{path:L(i.in)},to:{path:L(i.import)}});for(let i of t.warn??[])i==="sdp"&&n.push({name:"sdp",severity:"warn",comment:"depends on a more-unstable module (Stable Dependencies Principle)",from:{},to:{moreUnstable:!0}});let a={enhancedResolveOptions:{extensions:[".js",".jsx",".ts",".tsx",".mjs",".cjs",".json"]},doNotFollow:{path:"node_modules"},tsPreCompilationDeps:!1,...t.options??{}};return{forbidden:n,required:o,options:a}}var se="FLOW_DEPCRUISE_CONFIG";function Qe(t){let e=JSON.stringify(Xe(t));return["d=$(mktemp -d)",`cat > "$d/dc.json" <<'${se}'`,e,se,`npx --no-install depcruise --config "$d/dc.json" --output-type json ${v(t.scan)}`,"rc=$?",'rm -rf "$d"',"exit $rc"].join(`
16
- `)}function et(t){let e;try{e=JSON.parse(t.stdout||"")}catch{return t.code===0?[]:[`depcruise failed (exit ${t.code}): ${ce(t.stderr||t.stdout,3)}`]}return(e.summary?.violations??[]).map(n=>{let o=n.rule?.name??"fence",r=n.comment??n.rule?.comment??"";return`${o}: ${n.from} \u2192 ${n.to}${r?` \u2014 ${r}`:""}`})}var tt=g(t=>async e=>R(e,et(await e.exec(Qe(t))),`
17
- `)),le={html:"Html",css:"Css",javascript:"JavaScript",js:"JavaScript",typescript:"TypeScript",ts:"TypeScript",tsx:"Tsx",jsx:"Tsx"},nt=Object.keys(le),X=new Set,ie=Symbol.for("flow.grammars");function ot(){let t=globalThis,e=t[ie];if(e!==void 0)return e;let n={declared:[],present:()=>!1};return t[ie]=n,n}function rt(t,e){return`\`${t}\`: no library at ${e}. A grammar is a BUILD ARTEFACT \u2014 machine-specific, never committed \u2014 so this is the ordinary state of a fresh clone. Build it (the repo that declares a grammar carries the recipe that builds it), or every ast-grep rule on that language refuses rather than passing quietly.`}async function st(t){let{Lang:e,registerDynamicLanguage:n}=await import("@ast-grep/napi"),o=le[t];if(o!==void 0)return{ok:!0,lang:e[o]};if(X.has(t))return{ok:!0,lang:t};let{declared:r,present:s}=ot(),a=r.find(i=>i.name===t);if(a!==void 0)return s(a.libraryPath)?(n({[t]:{libraryPath:a.libraryPath,extensions:[...a.extensions],...a.languageSymbol===void 0?{}:{languageSymbol:a.languageSymbol}}}),X.add(t),{ok:!0,lang:t}):{ok:!1,detail:rt(t,a.libraryPath)};try{let i=await import(`@ast-grep/lang-${t}`);return n({[t]:i.default??i}),X.add(t),{ok:!0,lang:t}}catch{return{ok:!1,detail:`unknown ast-grep grammar: '${t}'. Built in: ${nt.join(", ")}. Anything else is either a published package \u2014 \`npm i -D @ast-grep/lang-${t}\` \u2014 or a grammar you build yourself and declare in flow.config.ts's \`grammars\` setting.`}}}async function it(t,e,n){let o=await st(n);if(!o.ok)return o;let{parse:r}=await import("@ast-grep/napi");return{ok:!0,hits:r(o.lang,t).root().findAll({rule:e}).map(a=>`${a.range().start.line+1}: ${a.text()}`)}}var w=g(t=>async e=>{let n=await it(j(e).content,t.rule,t.language);return n.ok?R(e,n.hits):e.fail(n.detail)});function F(t){return e=>{let n=e.agentType;return t.parent===!0?!e.subagent&&n===void 0:n===void 0?!1:(t.types??[]).includes(n)?!0:(t.generic??[]).includes(n)?(t.brief??[]).some(o=>e.head.includes(o)):!1}}var Xt=14400*1e3;var at=m("docs",(t={})=>{let e=t.root??"docs",n=t.allow??[];return{docs:h().at(k).on(`${e}/**`).description("The two audiences, the admission test, and the discipline that keeps docs true.").text(["Two audiences, two doors, opposite rules. `docs/user/` is for PEOPLE: always HTML, entered through index.html, and multimodal on purpose \u2014 images, SVGs and short recordings are encouraged wherever they explain better than sentences. The measure of a user doc is how little work the reader has to do.","`docs/agent/` is ARCHIVAL CONTEXT, loaded on demand, entered through README.md as an index of pointers. The admission test: could a fresh agent derive this from the code, the config or the guard layer? Then it does not belong \u2014 derivable content is replication, and replication goes stale silently while still reading as true. Only expensive syntheses live here: understanding that took hours to establish and would take hours to re-derive. Pointers outward, never copies inward; this folder must never compete with the breadcrumb layer.","The style law, both sides: state CURRENT FACTS in the present tense. No litigation, no rationale essays, no history \u2014 the journal owns the past. Where a decision would otherwise look wrong, one Chesterton-fence line pointing at the decision record, never a retelling.","The failure mode is silent staleness, not error: a doc stays literally correct while a whole task's machinery lands unmentioned, and nothing complains. So if the work you just shipped changed a boundary, a component's job or the lifecycle, the doc changes in the SAME commit."].join(`
14
+ `)}var C=g(t=>async e=>{let n=await ue(e);if(tt(n,t.changed))return e.ok();let o=t.run.includes("{files}");if(o&&n.length===0)return e.ok();let r=o?t.run.replace("{files}",n.map(v).join(" ")):t.run,s=await e.exec(r);return s.code===0?e.ok():e.fail(`\`${r}\` failed (exit ${s.code}):
15
+ ${fe(s.stderr||s.stdout)}`)});function _(t){let e=q(t),n=e[e.length-1]?.kind==="globstar";return`^${(n?e.slice(0,-1):e).map(Y).join("")}${n?"":"$"}`}function de(t){return t==="node:*"?{core:!0}:t.startsWith("node:")?{path:`^${b(t)}$`}:t.includes("/")||t.includes("*")||t.includes(".")?{path:_(t)}:{path:`node_modules/${b(t)}(/|$)`}}function K(t){return t===void 0?{path:[],not:[]}:Array.isArray(t)?{path:t,not:[]}:{path:t.path,not:t.not??[]}}function L(t,e){let n=[],o=[],r=!1;for(let a of t){let{path:i,not:d}=K(e[a]);for(let l of i){let p=de(l);p.core?r=!0:p.path&&n.push(p.path)}for(let l of d){let p=de(l);p.path&&o.push(p.path)}}let s={};return n.length&&(s.path=n),o.length&&(s.pathNot=o),r&&(s.dependencyTypes=["core"]),s}function nt(t){let e=t.layers,n=[],o=[],r=(t.except??[]).includes("type-only"),s=i=>r?{...i,dependencyTypesNot:["type-only"]}:i;for(let[i,d]of Object.entries(t.only??{})){let p={pathNot:L(d,e).path??[]};n.push({name:`${i}-only`,severity:"error",comment:`${i} may import only: ${d.join(", ")} \u2014 an unlisted import blocks here (add it to a layer, or keep it out).`,from:L([i],e),to:s(p)})}for(let i of t.forbid??[])if(i==="circular")n.push({name:"no-circular",severity:"error",comment:"no dependency cycles",from:{pathNot:"node_modules"},to:{circular:!0}});else if(i==="orphans")n.push({name:"no-orphans",severity:"warn",comment:"unreferenced module",from:{orphan:!0,pathNot:"node_modules|\\.d\\.ts$"},to:{}});else if(i.cyclesBetween!==void 0){let d=i.cyclesBetween,l=K(e[d]).path[0]??"",p=_(l).replace(/\$$/,"").replace(/\[\^\/\]\*$/,"([^/]+)");n.push({name:`no-cycles-between-${d}`,severity:"error",comment:i.why??`${d} must not import sideways`,from:{path:`${p}/`},to:s({path:`${p.replace(/\(\[\^\/\]\+\)/,"[^/]+")}/`,pathNot:"$1/"})})}else if(i.from!==void 0&&i.to!==void 0){let d={...L([i.to],e)};i.transitive===!0&&(d.reachable=!0),n.push({name:`no-${i.from}-to-${i.to}${i.transitive===!0?"-transitive":""}`,severity:"error",comment:i.why??`${i.from} must not import ${i.to}`,from:L([i.from],e),to:s(d)})}for(let i of t.require??[])o.push({name:`must-import-${i.import}`,severity:"error",comment:i.why??`modules in ${i.in} must import ${i.import}`,module:{path:_(i.in)},to:{path:_(i.import)}});for(let i of t.warn??[])i==="sdp"&&n.push({name:"sdp",severity:"warn",comment:"depends on a more-unstable module (Stable Dependencies Principle)",from:{},to:{moreUnstable:!0}});let a={enhancedResolveOptions:{extensions:[".js",".jsx",".ts",".tsx",".mjs",".cjs",".json"]},doNotFollow:{path:"node_modules"},tsPreCompilationDeps:!1,...t.options??{}};return{forbidden:n,required:o,options:a}}var ce="FLOW_DEPCRUISE_CONFIG";function ot(t){let e=JSON.stringify(nt(t));return["d=$(mktemp -d)",`cat > "$d/dc.json" <<'${ce}'`,e,ce,`npx --no-install depcruise --config "$d/dc.json" --output-type json ${v(t.scan)}`,"rc=$?",'rm -rf "$d"',"exit $rc"].join(`
16
+ `)}function rt(t){let e;try{e=JSON.parse(t.stdout||"")}catch{return t.code===0?[]:[`depcruise failed (exit ${t.code}): ${fe(t.stderr||t.stdout,3)}`]}return(e.summary?.violations??[]).map(n=>{let o=n.rule?.name??"fence",r=n.comment??n.rule?.comment??"";return`${o}: ${n.from} \u2192 ${n.to}${r?` \u2014 ${r}`:""}`})}function st(t){let e=[];for(let[n,o]of Object.entries(t)){let{path:r,not:s}=K(o);s.includes("node:*")&&e.push(`binds a depcruise layer \`${n}\` whose \`not\` list excludes \`node:*\`. A builtin is matched as a dependency TYPE, not a path, and the only exclusion available says "not a core module at all" \u2014 wider than this layer means. Say the layer's globs without it, or forbid the edge you actually mean.`);for(let a of[...r,...s])se(a)&&e.push(`binds a depcruise layer \`${n}\` whose entry is a regular expression, not a glob: \`${a}\`. Layers are GLOBS here \u2014 \`src/pages/**\`, \`src/lib/auth/**\` \u2014 and a regex is escaped into a literal, so the fence would load, report armed and match nothing. For a layer with holes in it, say \`{ path: ["src/pages/**"], not: ["src/pages/api/**"] }\` rather than a lookahead.`)}return e}function it(t){let e=[];for(let[n,o]of Object.entries(t.only??{}))for(let r of o)K(t.layers[r]).not.length>0&&e.push(`binds a depcruise \`only\` for \`${n}\` naming the layer \`${r}\`, which has a \`not\` list. An allowlist is compiled by inverting the layers it names, and that inversion reads \`path\` alone \u2014 so \`${r}\`'s exclusions would be silently dropped and the allowlist would be wider than it reads. Give \`${r}\` plain globs, or say the exclusion as a \`forbid\` edge.`);return e}var at=g(t=>Z(async n=>R(n,rt(await n.exec(ot(t))),`
17
+ `),[...st(t.layers),...it(t)])),ge={html:"Html",css:"Css",javascript:"JavaScript",js:"JavaScript",typescript:"TypeScript",ts:"TypeScript",tsx:"Tsx",jsx:"Tsx"},dt=Object.keys(ge),ee=new Set,le=Symbol.for("flow.grammars");function ct(){let t=globalThis,e=t[le];if(e!==void 0)return e;let n={declared:[],present:()=>!1};return t[le]=n,n}function lt(t,e){return`\`${t}\`: no library at ${e}. A grammar is a BUILD ARTEFACT \u2014 machine-specific, never committed \u2014 so this is the ordinary state of a fresh clone. Build it (the repo that declares a grammar carries the recipe that builds it), or every ast-grep rule on that language refuses rather than passing quietly.`}async function ut(t){let{Lang:e,registerDynamicLanguage:n}=await import("@ast-grep/napi"),o=ge[t];if(o!==void 0)return{ok:!0,lang:e[o]};if(ee.has(t))return{ok:!0,lang:t};let{declared:r,present:s}=ct(),a=r.find(i=>i.name===t);if(a!==void 0)return s(a.libraryPath)?(n({[t]:{libraryPath:a.libraryPath,extensions:[...a.extensions],...a.languageSymbol===void 0?{}:{languageSymbol:a.languageSymbol}}}),ee.add(t),{ok:!0,lang:t}):{ok:!1,detail:lt(t,a.libraryPath)};try{let i=await import(`@ast-grep/lang-${t}`);return n({[t]:i.default??i}),ee.add(t),{ok:!0,lang:t}}catch{return{ok:!1,detail:`unknown ast-grep grammar: '${t}'. Built in: ${dt.join(", ")}. Anything else is either a published package \u2014 \`npm i -D @ast-grep/lang-${t}\` \u2014 or a grammar you build yourself and declare in flow.config.ts's \`grammars\` setting.`}}}async function pt(t,e,n){let o=await ut(n);if(!o.ok)return o;let{parse:r}=await import("@ast-grep/napi");return{ok:!0,hits:r(o.lang,t).root().findAll({rule:e}).map(a=>`${a.range().start.line+1}: ${a.text()}`)}}var w=g(t=>async e=>{let n=await pt(A(e).content,t.rule,t.language);return n.ok?R(e,n.hits):e.fail(n.detail)});function F(t){return e=>{let n=e.agentType;return t.parent===!0?!e.subagent&&n===void 0:n===void 0?!1:(t.types??[]).includes(n)?!0:(t.generic??[]).includes(n)?(t.brief??[]).some(o=>e.head.includes(o)):!1}}var rn=14400*1e3;var ft=m("docs",(t={})=>{let e=t.root??"docs",n=t.allow??[];return{docs:h().at(k).on(`${e}/**`).description("The two audiences, the admission test, and the discipline that keeps docs true.").text(["Two audiences, two doors, opposite rules. `docs/user/` is for PEOPLE: always HTML, entered through index.html, and multimodal on purpose \u2014 images, SVGs and short recordings are encouraged wherever they explain better than sentences. The measure of a user doc is how little work the reader has to do.","`docs/agent/` is ARCHIVAL CONTEXT, loaded on demand, entered through README.md as an index of pointers. The admission test: could a fresh agent derive this from the code, the config or the guard layer? Then it does not belong \u2014 derivable content is replication, and replication goes stale silently while still reading as true. Only expensive syntheses live here: understanding that took hours to establish and would take hours to re-derive. Pointers outward, never copies inward; this folder must never compete with the breadcrumb layer.","The style law, both sides: state CURRENT FACTS in the present tense. No litigation, no rationale essays, no history \u2014 the journal owns the past. Where a decision would otherwise look wrong, one Chesterton-fence line pointing at the decision record, never a retelling.","The failure mode is silent staleness, not error: a doc stays literally correct while a whole task's machinery lands unmentioned, and nothing complains. So if the work you just shipped changed a boundary, a component's job or the lifecycle, the doc changes in the SAME commit."].join(`
18
18
  `)),userDocsStyle:h().at(k).on(`${e}/user/**`).description("The ruled style of docs/user \u2014 Di\xE1taxis page discipline, the language rules, captured output.").text(["The user docs have a ruled style \u2014 hold it:","\xB7 One page, one job (the Di\xE1taxis framework, https://diataxis.fr): the quick start teaches by doing (explanation is linked, never inlined); the guide solves situations; reference pages state facts for lookup, no narrative.",'\xB7 Language (per the Google developer style guide, https://developers.google.com/style): second person, present tense, active voice. Define a term on first use or do not use it. Spend words in proportion to difficulty \u2014 one sentence for the simple thing, the full walkthrough for the hard one. No "simply", no filler.',"\xB7 Code examples are TypeScript, formatted as the repo formats it \u2014 a config example is copied from a file that compiles, never typed into the page.","\xB7 Every claim and printed output is captured from the shipped binary, never paraphrased \u2014 re-run the example before you change its text."].join(`
19
- `)),docsShape:c().at(p,l).on(`${e}/**`).description("docs/ holds exactly user/ and agent/ \u2014 the folder allowlist.").check(ee({root:e,allow:n,folders:["user","agent"]})).message("docs/ holds exactly two doors: `user/` (HTML, for people) and `agent/` (archival context, for agents). A third folder or a loose file at the top is documentation with no declared audience, which is how a docs folder becomes a drawer.").test({pass:[{path:`${e}/user/index.html`,content:""}],block:[{path:`${e}/notes/scratch.md`,content:""}]}),noMarkdownInUserDocs:c().at(p,l).on(`${e}/user/**/*.md`,`${e}/user/**/*.mdx`).description("No .md or .mdx under docs/user \u2014 user docs are HTML, and assets of every other kind are welcome.").check(A({})).message("User docs are HTML \u2014 that is what makes them multimodal by default (images, SVGs, video inline) and what stops them turning into paragraphs nobody reads. Markdown belongs in `docs/agent/`. Every other asset type is welcome under `docs/user/`.").test({block:[{path:`${e}/user/guide.md`,content:"# hi"}]})}});var dt="**/*.test.*",pe=t=>t.replace(/(\.[^./]+)$/,".test$1"),ct=m("fcis",t=>{let e=t.sibling??"{dir}/{name}.test.ts",n=t.tests??dt;return{fcis:h().at(k).on(...t.homes).description("The functional-core/imperative-shell mental model, for when you're working in pure-core code.").text(['Functional Core, Imperative Shell (Bernhardt, "Boundaries", SCNA 2012).',"Decisions live in pure functions, in this repo's declared pure home (see the fcis entries in flow.config.ts): no I/O, no clock, no randomness, no env \u2014 inject them as arguments; determinism IS testability.","The shell is the wiring around the core \u2014 full of functions, but never of decisions: a branch that encodes a rule or a threshold belongs in pure, even if it is currently wrapped in an `await`. Values cross the boundary, never live objects.","The shell is covered by integration tests, not unit tests."].join(`
20
- `)),noSideEffectsInPure:c().at(p,l).on(...t.files).ignore("**/*.test.ts").description("Pure code performs no I/O and reads no clock, env or randomness \u2014 a structural match, not a regex.").check(w({language:"typescript",rule:{any:[{pattern:"Date.now()"},{pattern:"new Date()"},{pattern:"Math.random()"},{pattern:"crypto.randomUUID()"},{pattern:"crypto.getRandomValues($$$)"},{pattern:"fetch($$$)"},{pattern:"setTimeout($$$)"},{pattern:"setInterval($$$)"},{pattern:"process.env"},{pattern:"Bun.spawn($$$)"},{pattern:"Bun.file($$$)"}]}})).message(["Impurity in the functional core. Pure code takes no clock / randomness / I/O / process.env \u2014 inject them as arguments and keep the decision pure."," clock \u2192 new Date() / Date.now() (accept an injected `now`)"," random \u2192 Math.random / crypto (accept an injected id/rng)"," I/O \u2192 fetch / timers / spawn (do it in the shell)"," env \u2192 process.env (pass config in)","Move the side effect to the shell and pass the pure input down."].join(`
21
- `)).test({pass:[{path:"cli/pure/a.ts",content:"export const at = (iso: string) => new Date(iso);"}],block:[{path:"cli/pure/a.ts",content:"export const now = () => new Date();"}]}),noShellInPure:c().at(p,l).on(...t.files).ignore("**/*.test.ts").description("Pure code imports no filesystem, process, socket or OS module \u2014 the leak the call-site rule cannot see.").check(w({language:"typescript",rule:{kind:"import_statement",has:{kind:"string",regex:`^["'](node:)?(fs|fs/promises|child_process|net|dns|tls|http|https|http2|os|cluster|worker_threads|readline|repl|v8|vm|zlib|dgram|inspector)["']$`}}})).message(["A shell module imported into pure code. The functional core DECIDES; reaching for the filesystem, a process, a socket or the OS is the shell's job, and an import of one is a leak whatever the file then does with it."," fs \xB7 child_process \xB7 net \xB7 dns \xB7 http(s) \xB7 os \xB7 worker_threads \xB7 cluster","Move the effect into the shell around this file and pass the values in. (`node:path` and `node:url` are string algebra and stay allowed, as does the test runner. Your own modules are fenced by the import-boundaries entry, per repo.)"].join(`
22
- `)).test({pass:[{path:"cli/pure/a.ts",content:'import { join } from "node:path";'}],block:[{path:"cli/pure/a.ts",content:'import * as fs from "node:fs";'}]}),noThrowInPure:c().at(p,l).on(...t.files).ignore("**/*.test.ts").description("Pure decisions return errors as values, never throw control flow.").check(w({language:"typescript",rule:{any:[{pattern:"throw $X"},{pattern:"Promise.reject($$$)"}]}})).message("`throw` / Promise.reject in the functional core. Errors are VALUES here \u2014 return a Result / tagged union (ok/err) so the failure path shows in the signature and the caller must handle both arms. Adapt thrown errors to Results at the boundary, not inside pure code.").test({pass:[{path:"cli/pure/a.ts",content:"export const f = () => ({ ok: false as const });"}],block:[{path:"cli/pure/a.ts",content:'export const f = () => { throw new Error("x"); };'}]}),pureHasTest:c().at(p,l).on(...t.files).ignore("**/*.test.ts").description("A pure file has its sibling test \u2014 the test-first artefact exists.").check(Q({sibling:e})).message("The pure core is test-first (red \u2192 green): {dir}/{name}.test.ts is missing. Write the failing test, then the code.").test({pass:[{path:"cli/pure/a.ts",content:"",world:{fs:{"cli/pure/a.test.ts":""}}}],block:[{path:"cli/pure/b.ts",content:""}]}),pureExportsTested:c().at(p,l).on(...t.files).ignore("**/*.test.ts").description("Every exported pure function is at least referenced in its test \u2014 the 'wrote no test' nudge.").check(te({sibling:e})).message("An exported pure function is never referenced in its test. Cheap nudge, not coverage \u2014 but 'added a function, wrote no test' is exactly the miss it catches.").test({pass:[{path:"cli/pure/a.ts",content:"export const x = 1;",world:{fs:{"cli/pure/a.test.ts":"expect(x)"}}}],block:[{path:"cli/pure/a.ts",content:"export const y = 1;",world:{fs:{"cli/pure/a.test.ts":"nothing"}}}]}),pureCovered:c().at(l).description("Blocks a commit if the pure core's coverage gate fails \u2014 only runs when pure files changed.").check(C({changed:t.homes,run:t.coverage})).message("Coverage of the pure core fell below the bar \u2014 add tests for the new and changed code. (Runs only when pure files changed.)").test({pass:[{staged:["README.md"],world:{exec:{[t.coverage]:{code:1}}}}],block:[{staged:[t.example],world:{exec:{[t.coverage]:{code:1,stdout:"ERROR: Coverage 91% < 95%"}}}}]}),newPureFileNeedsReason:c().at(y).description("A commit adding a non-test file to the pure core must say why the pure file it belongs in isn't the place.").check(N({whenAdded:t.files,except:[n],token:"new-pure-file"})).message("Adding a file to the pure core must say why the pure file it belongs in isn't the place \u2014 put `new-pure-file: <why not the existing file>` on a line of its own in the commit message. The verifier reads that reason at the next phase boundary.").test({pass:[{command:`git commit -m 'feat: x
19
+ `)),docsShape:c().at(f,u).on(`${e}/**`).description("docs/ holds exactly user/ and agent/ \u2014 the folder allowlist.").check(ne({root:e,allow:n,folders:["user","agent"]})).message("docs/ holds exactly two doors: `user/` (HTML, for people) and `agent/` (archival context, for agents). A third folder or a loose file at the top is documentation with no declared audience, which is how a docs folder becomes a drawer.").test({pass:[{path:`${e}/user/index.html`,content:""}],block:[{path:`${e}/notes/scratch.md`,content:""}]}),noMarkdownInUserDocs:c().at(f,u).on(`${e}/user/**/*.md`,`${e}/user/**/*.mdx`).description("No .md or .mdx under docs/user \u2014 user docs are HTML, and assets of every other kind are welcome.").check(j({})).message("User docs are HTML \u2014 that is what makes them multimodal by default (images, SVGs, video inline) and what stops them turning into paragraphs nobody reads. Markdown belongs in `docs/agent/`. Every other asset type is welcome under `docs/user/`.").test({block:[{path:`${e}/user/guide.md`,content:"# hi"}]})}});var gt="**/*.test.*",me=t=>t.replace(/(\.[^./]+)$/,".test$1"),ht=m("fcis",t=>{let e=t.sibling??"{dir}/{name}.test.ts",n=t.tests??gt;return{fcis:h().at(k).on(...t.homes).description("The functional-core/imperative-shell mental model, for when you're working in pure-core code.").text(['Functional Core, Imperative Shell (Bernhardt, "Boundaries", SCNA 2012).',"Decisions live in pure functions, in this repo's declared pure home (see the fcis entries in flow.config.ts): no I/O, no clock, no randomness, no env \u2014 inject them as arguments; determinism IS testability.","The shell is the wiring around the core \u2014 full of functions, but never of decisions: a branch that encodes a rule or a threshold belongs in pure, even if it is currently wrapped in an `await`. Values cross the boundary, never live objects.","The shell is covered by integration tests, not unit tests."].join(`
20
+ `)),noSideEffectsInPure:c().at(f,u).on(...t.files).ignore("**/*.test.ts").description("Pure code performs no I/O and reads no clock, env or randomness \u2014 a structural match, not a regex.").check(w({language:"typescript",rule:{any:[{pattern:"Date.now()"},{pattern:"new Date()"},{pattern:"Math.random()"},{pattern:"crypto.randomUUID()"},{pattern:"crypto.getRandomValues($$$)"},{pattern:"fetch($$$)"},{pattern:"setTimeout($$$)"},{pattern:"setInterval($$$)"},{pattern:"process.env"},{pattern:"Bun.spawn($$$)"},{pattern:"Bun.file($$$)"}]}})).message(["Impurity in the functional core. Pure code takes no clock / randomness / I/O / process.env \u2014 inject them as arguments and keep the decision pure."," clock \u2192 new Date() / Date.now() (accept an injected `now`)"," random \u2192 Math.random / crypto (accept an injected id/rng)"," I/O \u2192 fetch / timers / spawn (do it in the shell)"," env \u2192 process.env (pass config in)","Move the side effect to the shell and pass the pure input down."].join(`
21
+ `)).test({pass:[{path:"cli/pure/a.ts",content:"export const at = (iso: string) => new Date(iso);"}],block:[{path:"cli/pure/a.ts",content:"export const now = () => new Date();"}]}),noShellInPure:c().at(f,u).on(...t.files).ignore("**/*.test.ts").description("Pure code imports no filesystem, process, socket or OS module \u2014 the leak the call-site rule cannot see.").check(w({language:"typescript",rule:{kind:"import_statement",has:{kind:"string",regex:`^["'](node:)?(fs|fs/promises|child_process|net|dns|tls|http|https|http2|os|cluster|worker_threads|readline|repl|v8|vm|zlib|dgram|inspector)["']$`}}})).message(["A shell module imported into pure code. The functional core DECIDES; reaching for the filesystem, a process, a socket or the OS is the shell's job, and an import of one is a leak whatever the file then does with it."," fs \xB7 child_process \xB7 net \xB7 dns \xB7 http(s) \xB7 os \xB7 worker_threads \xB7 cluster","Move the effect into the shell around this file and pass the values in. (`node:path` and `node:url` are string algebra and stay allowed, as does the test runner. Your own modules are fenced by the import-boundaries entry, per repo.)"].join(`
22
+ `)).test({pass:[{path:"cli/pure/a.ts",content:'import { join } from "node:path";'}],block:[{path:"cli/pure/a.ts",content:'import * as fs from "node:fs";'}]}),noThrowInPure:c().at(f,u).on(...t.files).ignore("**/*.test.ts").description("Pure decisions return errors as values, never throw control flow.").check(w({language:"typescript",rule:{any:[{pattern:"throw $X"},{pattern:"Promise.reject($$$)"}]}})).message("`throw` / Promise.reject in the functional core. Errors are VALUES here \u2014 return a Result / tagged union (ok/err) so the failure path shows in the signature and the caller must handle both arms. Adapt thrown errors to Results at the boundary, not inside pure code.").test({pass:[{path:"cli/pure/a.ts",content:"export const f = () => ({ ok: false as const });"}],block:[{path:"cli/pure/a.ts",content:'export const f = () => { throw new Error("x"); };'}]}),pureHasTest:c().at(f,u).on(...t.files).ignore("**/*.test.ts").description("A pure file has its sibling test \u2014 the test-first artefact exists.").check(te({sibling:e})).message("The pure core is test-first (red \u2192 green): {dir}/{name}.test.ts is missing. Write the failing test, then the code.").test({pass:[{path:"cli/pure/a.ts",content:"",world:{fs:{"cli/pure/a.test.ts":""}}}],block:[{path:"cli/pure/b.ts",content:""}]}),pureExportsTested:c().at(f,u).on(...t.files).ignore("**/*.test.ts").description("Every exported pure function is at least referenced in its test \u2014 the 'wrote no test' nudge.").check(oe({sibling:e})).message("An exported pure function is never referenced in its test. Cheap nudge, not coverage \u2014 but 'added a function, wrote no test' is exactly the miss it catches.").test({pass:[{path:"cli/pure/a.ts",content:"export const x = 1;",world:{fs:{"cli/pure/a.test.ts":"expect(x)"}}}],block:[{path:"cli/pure/a.ts",content:"export const y = 1;",world:{fs:{"cli/pure/a.test.ts":"nothing"}}}]}),pureCovered:c().at(u).description("Blocks a commit if the pure core's coverage gate fails \u2014 only runs when pure files changed.").check(C({changed:t.homes,run:t.coverage})).message("Coverage of the pure core fell below the bar \u2014 add tests for the new and changed code. (Runs only when pure files changed.)").test({pass:[{staged:["README.md"],world:{exec:{[t.coverage]:{code:1}}}}],block:[{staged:[t.example],world:{exec:{[t.coverage]:{code:1,stdout:"ERROR: Coverage 91% < 95%"}}}}]}),newPureFileNeedsReason:c().at(y).description("A commit adding a non-test file to the pure core must say why the pure file it belongs in isn't the place.").check(N({whenAdded:t.files,except:[n],token:"new-pure-file"})).message("Adding a file to the pure core must say why the pure file it belongs in isn't the place \u2014 put `new-pure-file: <why not the existing file>` on a line of its own in the commit message. The verifier reads that reason at the next phase boundary.").test({pass:[{command:`git commit -m 'feat: x
23
23
 
24
- new-pure-file: a whole new tier'`,world:{exec:{"git diff HEAD --name-only":{stdout:t.example},"git diff HEAD --name-only --diff-filter=A":{stdout:t.example},"git ls-files --others --exclude-standard":{stdout:""}}}},{command:'git commit -m "test: cover it"',world:{exec:{"git diff HEAD --name-only":{stdout:pe(t.example)},"git diff HEAD --name-only --diff-filter=A":{stdout:pe(t.example)},"git ls-files --others --exclude-standard":{stdout:""}}}}],block:[{command:'git commit -m "feat: x"',world:{exec:{"git diff HEAD --name-only":{stdout:t.example},"git diff HEAD --name-only --diff-filter=A":{stdout:t.example},"git ls-files --others --exclude-standard":{stdout:""}}}}]})}});var lt=m("flow",t=>{let e=t.hostSurface??[".claude/settings.json",".claude/settings.local.json",".claude/agents/**",".claude/skills/**"];return{orientation:h().at(E).description("What the guard layer is and how it steers \u2014 shown at the start of every session.").text(["This repo is guarded by flow. Guardrails block risky edits before they land (and again at commit); breadcrumbs steer you by area as you touch files.","The whole guard is `flow.config.ts` \u2014 every rule that fires is reachable from that one file, whether it comes from a pack the config imports or from a pack this repo writes itself. Nothing resolves at run time and nothing defaults: what you read there is what fires. Authoring a new entry is always welcome; never loosen a guardrail to get an edit through \u2014 that is the user's call.",'There is no bypass token anywhere in this system. A guardrail is on or off, and turning one off is `override(pack.entry).disabled("why")` in flow.config.ts \u2014 a committed change, visible in review.',"If the config will not load, every gated moment refuses until it is fixed. A guard that fails open is a guard that lies about being there. The ONE exception is the repair itself: while the config is broken you may still write `flow.config.ts` and the pack files it imports, because otherwise the rule that demands a fix also forbids it. Commands stay blocked, and so does the commit gate \u2014 nothing written under that exception reaches a commit until the config loads green."].join(`
24
+ new-pure-file: a whole new tier'`,world:{exec:{"git diff HEAD --name-only":{stdout:t.example},"git diff HEAD --name-only --diff-filter=A":{stdout:t.example},"git ls-files --others --exclude-standard":{stdout:""}}}},{command:'git commit -m "test: cover it"',world:{exec:{"git diff HEAD --name-only":{stdout:me(t.example)},"git diff HEAD --name-only --diff-filter=A":{stdout:me(t.example)},"git ls-files --others --exclude-standard":{stdout:""}}}}],block:[{command:'git commit -m "feat: x"',world:{exec:{"git diff HEAD --name-only":{stdout:t.example},"git diff HEAD --name-only --diff-filter=A":{stdout:t.example},"git ls-files --others --exclude-standard":{stdout:""}}}}]})}});var mt=m("flow",t=>{let e=t.hostSurface??[".claude/settings.json",".claude/settings.local.json",".claude/agents/**",".claude/skills/**"];return{orientation:h().at(E).description("What the guard layer is and how it steers \u2014 shown at the start of every session.").text(["This repo is guarded by flow. Guardrails block risky edits before they land (and again at commit); breadcrumbs steer you by area as you touch files.","The whole guard is `flow.config.ts` \u2014 every rule that fires is reachable from that one file, whether it comes from a pack the config imports or from a pack this repo writes itself. Nothing resolves at run time and nothing defaults: what you read there is what fires. Authoring a new entry is always welcome; never loosen a guardrail to get an edit through \u2014 that is the user's call.",'There is no bypass token anywhere in this system. A guardrail is on or off, and turning one off is `override(pack.entry).disabled("why")` in flow.config.ts \u2014 a committed change, visible in review.',"If the config will not load, every gated moment refuses until it is fixed. A guard that fails open is a guard that lies about being there. The ONE exception is the repair itself: while the config is broken you may still write `flow.config.ts` and the pack files it imports, because otherwise the rule that demands a fix also forbids it. Commands stay blocked, and so does the commit gate \u2014 nothing written under that exception reaches a commit until the config loads green."].join(`
25
25
  `)),editingTheGuardrails:h().at(k).on("flow.config.ts",...t.packs,...e).description("A nudge when you edit the guardrails themselves; it never blocks.").text(["You are editing the guardrails themselves.","If this edit weakens, disables or removes a guardrail or a permission because it just blocked you \u2014 stop and confirm with the user first.","Authoring new entries is always fine; loosening one to get an edit through is the user's call, not yours.","An AGENT DEFINITION is a permission surface too: its `tools:` list decides what that agent can do, and the installed link is live \u2014 the file you are editing is the one that takes effect, with nothing between your edit and every future session. Widening a tool list, or adding a permissionMode, is the user's call \u2014 a mode added as a hardening is as likely to be a loosening.","Whatever you are authoring here \u2014 a `.message(\u2026)`, a finding format, a warning \u2014 name what was SEEN (which file, which commit, which command) and what that specific cause wants done. A condition that cannot produce a distinguishing message is two rules, not one: split it. A detector that emits the same sentence for causes the reader must answer differently teaches them to talk past it, and then fails silently on the one occasion it was right.","Every guardrail carries its own cases. Add the block case first \u2014 a rule whose block case passes is a rule that catches nothing, and `flow test` is what says so."].join(`
26
- `)),noDeleteGuardrails:c().at(z).on("flow.config.ts",...t.packs,".githooks/pre-commit").description("Stops a command from deleting the committed guard surface. No bypass \u2014 disable entries in the file instead.").check(A({})).message('That command would delete this repo\'s guard surface (flow.config.ts binds every rule; the packs it imports are where this repo writes its own; .githooks/pre-commit is what runs them at the gate). Deleting any of them is the same attack. There is no bypass \u2014 `override(pack.entry).disabled("why")` inside flow.config.ts is the visible, committed route.').test({block:[{path:"flow.config.ts",content:""},{path:".githooks/pre-commit",content:""}]})}});function ut(t){let e=t.includes("--staged")||t.includes("-S"),n=t.includes("--worktree")||t.includes("-W");if(e&&!n)return[];let o=[],r=!1;for(let s=0;s<t.length;s++){let a=t[s];if(r){o.push(a);continue}if(a==="--"){r=!0;continue}if(a==="-s"||a==="--source"){s++;continue}a.startsWith("-")||o.push(a)}return o}function pt(t){let e=O(t);if(!e)return[];let n=[];for(let{subcommand:o,args:r}of e)if(o==="checkout"){let s=r.indexOf("--");s!==-1&&n.push(...r.slice(s+1))}else if(o==="restore")n.push(...ut(r));else if(o==="reset"&&r.includes("--hard"))n.push(".");else if(o==="clean"&&r.some(s=>/^-[a-zA-Z]*[fx]/.test(s)||s==="--force")){let s=r.filter(a=>!a.startsWith("-"));n.push(...s.length?s:["."])}return n}var ft=new Set(["-b","-B","--orphan","--pathspec-from-file"]);function gt(t){let e=O(t);if(!e)return[];let n=[];for(let{subcommand:o,args:r}of e)if(!(o!=="checkout"||r.includes("--")))for(let s=0;s<r.length;s++){let a=r[s];ft.has(a)?s++:a.startsWith("-")||n.push(a)}return n}function ht(t,e){return t.split(`
27
- `).filter(n=>n!==""&&(e||!n.startsWith("??"))).map(n=>n.slice(3))}var mt=g(t=>async e=>{let n=e.command??"",o=/(^|[;&|\n]\s*)git\s+clean\b/.test(n),r=`git ${I(n)}`.trimEnd(),s=pt(n),a=gt(n);if(a.length>0){let u=await e.exec(`${r} ls-files -- ${a.map(v).join(" ")}`);u.code===0&&s.push(...u.stdout.split(`
28
- `).filter(f=>f!==""))}if(s.length===0)return e.ok();let i=await e.exec(`${r} status --porcelain -- ${s.map(v).join(" ")}`);if(i.code!==0)return e.ok();let d=ht(i.stdout,o);return d.length===0?e.ok():e.fail(d.map(u=>`would discard uncommitted edits to ${u} \u2014 use \`git stash\` instead (recoverable)`).join(`
29
- `))});function yt(t,e){let n=e.map(b).join("|");return new RegExp(`^(${n})(\\([^)]+\\))?!?: .+`).test(t.split(`
30
- `)[0]??"")}var kt=g(t=>e=>{let n=G(e.command??"");return n===null||yt(n,t.types)?e.ok():e.fail(`commit header "${n.split(`
31
- `)[0]??""}" is not Conventional Commits (type(scope)!: description)`)});function bt(t){let e=t.split(`
32
- `)[0]??"";return/^(release(\([^)]*\))?|chore\([^)]*release[^)]*\))!?:/i.test(e)}var wt=/["']?\bversion["']?\s*[:=]\s*["']?(\d+\.\d+\.\d+[^"',\s]*)/;function xt(t){let e=[],n=[];for(let o of t.split(`
33
- `)){if(o.startsWith("+++")||o.startsWith("---"))continue;let r=wt.exec(o);r?.[1]&&(o.startsWith("+")?e.push(r[1]):o.startsWith("-")&&n.push(r[1]))}return e.some(o=>!n.includes(o))}var vt=g(t=>async e=>{let n=G(e.command??"");if(n===null||bt(n))return e.ok();let o=`git ${I(e.command??"")}`.trimEnd(),r=await e.exec(`${o} diff HEAD -- ${v(t.versionFile)}`);return r.code!==0||!xt(r.stdout)?e.ok():e.fail(`this commit hand-edits the \`version\` field in ${t.versionFile}. The version is COMPUTED from the commit history at release time \u2014 run \`${t.recipe}\`, which writes the version, the changelog and the tag together. (A real release commit, headed \`release:\` or \`chore(release):\`, passes.)`)}),St=["feat","fix","docs","style","refactor","perf","test","build","ci","chore","revert","release"],K=["Co","Authored","By"].join("-"),P=["Cl","aude"].join(""),fe=["cl","aude","\\.com/","cl","aude","-code"].join(""),ge=["cl","aude",".com/","cl","aude","-code"].join(""),Et="\u{1F916}",Rt=m("git",t=>{let e=t.versionFile??"package.json",n=t.types??St;return{orientation:h().at(E).description("The whole commit contract \u2014 how commits are written, how versions are computed, what is blocked.").text([`Commits, PRs and issues carry NO Claude/Anthropic attribution \u2014 no ${K} line, no "Generated with Claude", no mention. Author them as your own.`,"Commit headers speak Conventional Commits \u2014 `type(scope)!: description`. The TYPE is your judgement: `fix` patches behaviour, `feat` adds it, `refactor` changes neither (and must not change test expectations); a `!` or a `BREAKING CHANGE:` footer drives the SemVer major. The grammar is enforced; the choice is yours.",`The version number is computed from this history at release (\`${t.release}\`) \u2014 never hand-edit a version field or a changelog.`,"A `git checkout` / `git restore` / `reset --hard` / `clean -f` that would silently wipe uncommitted work is blocked outright \u2014 there is no bypass token. Use `git stash` instead; it is recoverable."].join(`
34
- `)),noAiAttributionInCommits:c().at(y).description("Blocks Claude/Anthropic attribution in commits, PRs and issues.").check(x({ban:[`git commit[\\s\\S]*[Cc]o-[Aa]uthored-[Bb]y:[\\s\\S]*(${P}|[Aa]nthropic)`,`git commit[\\s\\S]*[Gg]enerated with[\\s\\S]*${P}`,`git commit[\\s\\S]*(${fe}|\u{1F916} Generated)`,`gh (pr|issue)[\\s\\S]*[Cc]o-[Aa]uthored-[Bb]y:[\\s\\S]*(${P}|[Aa]nthropic)`,`gh (pr|issue)[\\s\\S]*(${fe}|\u{1F916} Generated)`]})).message(`Commit / PR / issue body contains Claude or Anthropic attribution. Remove it (the ${K} line, the 'Generated with Claude' line, any Claude/Anthropic mention) and retry.`).test({pass:['git commit -m "fix(auth): renew the session"'],block:[`git commit -m "fix(auth): renew the session
26
+ `)),noDeleteGuardrails:c().at(J).on("flow.config.ts",...t.packs,".githooks/pre-commit").description("Stops a command from deleting the committed guard surface. No bypass \u2014 disable entries in the file instead.").check(j({})).message('That command would delete this repo\'s guard surface (flow.config.ts binds every rule; the packs it imports are where this repo writes its own; .githooks/pre-commit is what runs them at the gate). Deleting any of them is the same attack. There is no bypass \u2014 `override(pack.entry).disabled("why")` inside flow.config.ts is the visible, committed route.').test({block:[{path:"flow.config.ts",content:""},{path:".githooks/pre-commit",content:""}]})}});function yt(t){let e=t.includes("--staged")||t.includes("-S"),n=t.includes("--worktree")||t.includes("-W");if(e&&!n)return[];let o=[],r=!1;for(let s=0;s<t.length;s++){let a=t[s];if(r){o.push(a);continue}if(a==="--"){r=!0;continue}if(a==="-s"||a==="--source"){s++;continue}a.startsWith("-")||o.push(a)}return o}function kt(t){let e=O(t);if(!e)return[];let n=[];for(let{subcommand:o,args:r}of e)if(o==="checkout"){let s=r.indexOf("--");s!==-1&&n.push(...r.slice(s+1))}else if(o==="restore")n.push(...yt(r));else if(o==="reset"&&r.includes("--hard"))n.push(".");else if(o==="clean"&&r.some(s=>/^-[a-zA-Z]*[fx]/.test(s)||s==="--force")){let s=r.filter(a=>!a.startsWith("-"));n.push(...s.length?s:["."])}return n}var bt=new Set(["-b","-B","--orphan","--pathspec-from-file"]);function wt(t){let e=O(t);if(!e)return[];let n=[];for(let{subcommand:o,args:r}of e)if(!(o!=="checkout"||r.includes("--")))for(let s=0;s<r.length;s++){let a=r[s];bt.has(a)?s++:a.startsWith("-")||n.push(a)}return n}function xt(t,e){return t.split(`
27
+ `).filter(n=>n!==""&&(e||!n.startsWith("??"))).map(n=>n.slice(3))}var vt=g(t=>async e=>{let n=e.command??"",o=/(^|[;&|\n]\s*)git\s+clean\b/.test(n),r=`git ${I(n)}`.trimEnd(),s=kt(n),a=wt(n);if(a.length>0){let l=await e.exec(`${r} ls-files -- ${a.map(v).join(" ")}`);l.code===0&&s.push(...l.stdout.split(`
28
+ `).filter(p=>p!==""))}if(s.length===0)return e.ok();let i=await e.exec(`${r} status --porcelain -- ${s.map(v).join(" ")}`);if(i.code!==0)return e.ok();let d=xt(i.stdout,o);return d.length===0?e.ok():e.fail(d.map(l=>`would discard uncommitted edits to ${l} \u2014 use \`git stash\` instead (recoverable)`).join(`
29
+ `))});function St(t,e){let n=e.map(b).join("|");return new RegExp(`^(${n})(\\([^)]+\\))?!?: .+`).test(t.split(`
30
+ `)[0]??"")}var Et=g(t=>e=>{let n=G(e.command??"");return n===null||St(n,t.types)?e.ok():e.fail(`commit header "${n.split(`
31
+ `)[0]??""}" is not Conventional Commits (type(scope)!: description)`)});function Rt(t){let e=t.split(`
32
+ `)[0]??"";return/^(release(\([^)]*\))?|chore\([^)]*release[^)]*\))!?:/i.test(e)}var Ct=/["']?\bversion["']?\s*[:=]\s*["']?(\d+\.\d+\.\d+[^"',\s]*)/;function $t(t){let e=[],n=[];for(let o of t.split(`
33
+ `)){if(o.startsWith("+++")||o.startsWith("---"))continue;let r=Ct.exec(o);r?.[1]&&(o.startsWith("+")?e.push(r[1]):o.startsWith("-")&&n.push(r[1]))}return e.some(o=>!n.includes(o))}var At=g(t=>async e=>{let n=G(e.command??"");if(n===null||Rt(n))return e.ok();let o=`git ${I(e.command??"")}`.trimEnd(),r=await e.exec(`${o} diff HEAD -- ${v(t.versionFile)}`);return r.code!==0||!$t(r.stdout)?e.ok():e.fail(`this commit hand-edits the \`version\` field in ${t.versionFile}. The version is COMPUTED from the commit history at release time \u2014 run \`${t.recipe}\`, which writes the version, the changelog and the tag together. (A real release commit, headed \`release:\` or \`chore(release):\`, passes.)`)}),jt=["feat","fix","docs","style","refactor","perf","test","build","ci","chore","revert","release"],U=["Co","Authored","By"].join("-"),P=["Cl","aude"].join(""),ye=["cl","aude","\\.com/","cl","aude","-code"].join(""),ke=["cl","aude",".com/","cl","aude","-code"].join(""),Tt="\u{1F916}",Ot=m("git",t=>{let e=t.versionFile??"package.json",n=t.types??jt;return{orientation:h().at(E).description("The whole commit contract \u2014 how commits are written, how versions are computed, what is blocked.").text([`Commits, PRs and issues carry NO Claude/Anthropic attribution \u2014 no ${U} line, no "Generated with Claude", no mention. Author them as your own.`,"Commit headers speak Conventional Commits \u2014 `type(scope)!: description`. The TYPE is your judgement: `fix` patches behaviour, `feat` adds it, `refactor` changes neither (and must not change test expectations); a `!` or a `BREAKING CHANGE:` footer drives the SemVer major. The grammar is enforced; the choice is yours.",`The version number is computed from this history at release (\`${t.release}\`) \u2014 never hand-edit a version field or a changelog.`,"A `git checkout` / `git restore` / `reset --hard` / `clean -f` that would silently wipe uncommitted work is blocked outright \u2014 there is no bypass token. Use `git stash` instead; it is recoverable."].join(`
34
+ `)),noAiAttributionInCommits:c().at(y).description("Blocks Claude/Anthropic attribution in commits, PRs and issues.").check(x({ban:[`git commit[\\s\\S]*[Cc]o-[Aa]uthored-[Bb]y:[\\s\\S]*(${P}|[Aa]nthropic)`,`git commit[\\s\\S]*[Gg]enerated with[\\s\\S]*${P}`,`git commit[\\s\\S]*(${ye}|\u{1F916} Generated)`,`gh (pr|issue)[\\s\\S]*[Cc]o-[Aa]uthored-[Bb]y:[\\s\\S]*(${P}|[Aa]nthropic)`,`gh (pr|issue)[\\s\\S]*(${ye}|\u{1F916} Generated)`]})).message(`Commit / PR / issue body contains Claude or Anthropic attribution. Remove it (the ${U} line, the 'Generated with Claude' line, any Claude/Anthropic mention) and retry.`).test({pass:['git commit -m "fix(auth): renew the session"'],block:[`git commit -m "fix(auth): renew the session
35
35
 
36
- ${K}: ${P} <noreply@anthropic.com>"`,`gh pr create --body "${K}: ${P}"`,`git commit -m "fix(auth): renew the session
36
+ ${U}: ${P} <noreply@anthropic.com>"`,`gh pr create --body "${U}: ${P}"`,`git commit -m "fix(auth): renew the session
37
37
 
38
38
  Generated with ${P} Code"`,`git commit -m "fix(auth): renew the session
39
39
 
40
- ${Et} Generated with ${ge}"`,`gh issue create --body "opened with ${ge}"`]}),noGitDiscard:c().at(y).description("Blocks a git command that would wipe uncommitted work \u2014 checkout/restore/reset --hard/clean -f. No bypass; use git stash.").check(mt({})).message("This git command would discard uncommitted edits with no undo. Use `git stash` instead \u2014 it's recoverable. There is no opt-out token; disabling this entry in flow.config.ts is the visible route.").test({pass:[{command:"git checkout main",world:{exec:{"git ls-files":{stdout:""}}}},{command:"git checkout src/x.ts",world:{exec:{"git ls-files":{stdout:"src/x.ts"},"git status --porcelain":{stdout:""}}}},"git checkout -b docs",{command:"cd ../other && git checkout -- src/x.ts",world:{exec:{"git -C '../other' status --porcelain":{stdout:""}}}},"git status",{command:"git restore --staged src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:""}}}},{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:"?? other.ts"}}}},"git reset --soft HEAD~1","git clean -n"],block:[{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git checkout src/x.ts",world:{exec:{"git ls-files":{stdout:"src/x.ts"},"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"cd ../other && git checkout src/x.ts",world:{exec:{"git -C '../other' ls-files":{stdout:"src/x.ts"},"git -C '../other' status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git restore src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git restore --staged --worktree src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git clean -fd",world:{exec:{"git status --porcelain":{stdout:"?? junk.txt"}}}},{command:"git clean --force src/",world:{exec:{"git status --porcelain":{stdout:"?? src/junk.txt"}}}},{command:"git reset --hard",world:{exec:{"git status --porcelain":{stdout:" M a.ts"}}}},{command:`git status
40
+ ${Tt} Generated with ${ke}"`,`gh issue create --body "opened with ${ke}"`]}),noGitDiscard:c().at(y).description("Blocks a git command that would wipe uncommitted work \u2014 checkout/restore/reset --hard/clean -f. No bypass; use git stash.").check(vt({})).message("This git command would discard uncommitted edits with no undo. Use `git stash` instead \u2014 it's recoverable. There is no opt-out token; disabling this entry in flow.config.ts is the visible route.").test({pass:[{command:"git checkout main",world:{exec:{"git ls-files":{stdout:""}}}},{command:"git checkout src/x.ts",world:{exec:{"git ls-files":{stdout:"src/x.ts"},"git status --porcelain":{stdout:""}}}},"git checkout -b docs",{command:"cd ../other && git checkout -- src/x.ts",world:{exec:{"git -C '../other' status --porcelain":{stdout:""}}}},"git status",{command:"git restore --staged src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:""}}}},{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:"?? other.ts"}}}},"git reset --soft HEAD~1","git clean -n"],block:[{command:"git checkout -- src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git checkout src/x.ts",world:{exec:{"git ls-files":{stdout:"src/x.ts"},"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"cd ../other && git checkout src/x.ts",world:{exec:{"git -C '../other' ls-files":{stdout:"src/x.ts"},"git -C '../other' status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git restore src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git restore --staged --worktree src/x.ts",world:{exec:{"git status --porcelain":{stdout:" M src/x.ts"}}}},{command:"git clean -fd",world:{exec:{"git status --porcelain":{stdout:"?? junk.txt"}}}},{command:"git clean --force src/",world:{exec:{"git status --porcelain":{stdout:"?? src/junk.txt"}}}},{command:"git reset --hard",world:{exec:{"git status --porcelain":{stdout:" M a.ts"}}}},{command:`git status
41
41
  git checkout -- a.ts`,world:{exec:{"git status --porcelain":{stdout:" M a.ts"}}}},{command:"git -C ../other checkout -- src/x.ts",world:{exec:{"git -C '../other' status --porcelain":{stdout:" M src/x.ts"}}}}]}),noForcePush:c().at(y).description("Blocks a force-push \u2014 rewriting pushed history is the one loss a stash cannot undo.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*git\\s+push[^\\n]*\\s(--force|-f)(\\s|$)","(?:^|[\\n;&|(]\\s*)\\s*git\\s+push[^\\n]*--force-with-lease"]})).message("Force-push rewrites history that other clones (and every open PR) already have \u2014 the one loss no stash can undo. Push a new commit that corrects the old one. A repo whose workflow genuinely rewrites a scratch remote disables this entry in flow.config.ts, visibly.").test({pass:["git push origin main",`cat > notes.md <<'EOF'
42
42
  never run git push --force here
43
- EOF`],block:["git push --force origin main","git push -f","git push --force-with-lease origin main","git status && git push --force origin main"]}),conventionalCommitFormat:c().at(y).description("The commit header must speak Conventional Commits v1.0.0 (checked before git commit runs).").check(kt({types:n})).message(["Commit header must match Conventional Commits v1.0.0: type(scope)!: description \u2014 e.g. `fix(auth): renew session on token refresh`.",`The type is one of: ${n.join(" \xB7 ")}.`].join(`
43
+ EOF`],block:["git push --force origin main","git push -f","git push --force-with-lease origin main","git status && git push --force origin main"]}),conventionalCommitFormat:c().at(y).description("The commit header must speak Conventional Commits v1.0.0 (checked before git commit runs).").check(Et({types:n})).message(["Commit header must match Conventional Commits v1.0.0: type(scope)!: description \u2014 e.g. `fix(auth): renew session on token refresh`.",`The type is one of: ${n.join(" \xB7 ")}.`].join(`
44
44
  `)).test({pass:['git commit -m "fix(auth): renew session on token refresh"',"git status",'git commit -m "feat(cli)!: the config grammar is closed"'],block:['git commit -m "fixed the auth thing"','git commit -m "feat a thing"',`git commit -m 'tidied things up
45
45
 
46
- feat: the thing I actually did'`]}),noShellSubstitutionInProse:c().at(y).description("A backtick inside a double-quoted argument of a commit or a PR body is live command substitution, not Markdown.").check(x({ban:[D(["git\\s+commit\\b[^\\n]*?-{1,2}[a-zA-Z]*m","gh\\s+(?:pr|issue)\\b"])]})).message(B).test({pass:["git commit -F - <<'EOF'\nfix: a `thing`\nEOF","git commit -m 'fix: run `just test` first'"],block:['git commit -m "fix: run `just test` first"','gh pr create --body "see `just gate`"']}),node:{versionIsSemver:c().at(p,l).on(e).description("The manifest's version is valid SemVer 2.0.0 \u2014 the number the computed release writes. See https://semver.org/").check(T({assert:[{path:"version",matches:"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$"}]})).message(`\`${e}\` needs a valid SemVer 2.0.0 \`version\` (MAJOR.MINOR.PATCH[-prerelease][+build]).`).test({pass:[{path:e,content:'{"version":"1.2.3"}'}],block:[{path:e,content:'{"version":"1.2"}'}]}),noHandEditedVersion:c().at(y).description("The version field is written by the release commit, never by hand.").check(vt({versionFile:e,recipe:t.release})).message(`The version is computed from the commit history at release time \u2014 run \`${t.release}\`, which writes the version, the changelog and the tag together. A real release commit (\`release:\` / \`chore(release):\`) passes.`).test({pass:[{command:'git commit -m "chore(release): 1.2.3"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
46
+ feat: the thing I actually did'`]}),noShellSubstitutionInProse:c().at(y).description("A backtick inside a double-quoted argument of a commit or a PR body is live command substitution, not Markdown.").check(x({ban:[D(["git\\s+commit\\b[^\\n]*?-{1,2}[a-zA-Z]*m","gh\\s+(?:pr|issue)\\b"])]})).message(B).test({pass:["git commit -F - <<'EOF'\nfix: a `thing`\nEOF","git commit -m 'fix: run `just test` first'"],block:['git commit -m "fix: run `just test` first"','gh pr create --body "see `just gate`"']}),node:{versionIsSemver:c().at(f,u).on(e).description("The manifest's version is valid SemVer 2.0.0 \u2014 the number the computed release writes. See https://semver.org/").check(T({assert:[{path:"version",matches:"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$"}]})).message(`\`${e}\` needs a valid SemVer 2.0.0 \`version\` (MAJOR.MINOR.PATCH[-prerelease][+build]).`).test({pass:[{path:e,content:'{"version":"1.2.3"}'}],block:[{path:e,content:'{"version":"1.2"}'}]}),noHandEditedVersion:c().at(y).description("The version field is written by the release commit, never by hand.").check(At({versionFile:e,recipe:t.release})).message(`The version is computed from the commit history at release time \u2014 run \`${t.release}\`, which writes the version, the changelog and the tag together. A real release commit (\`release:\` / \`chore(release):\`) passes.`).test({pass:[{command:'git commit -m "chore(release): 1.2.3"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
47
47
  + "version": "1.2.3"`}}}},{command:'git commit -m "release: 1.2.3"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
48
48
  + "version": "1.2.3"`}}}},{command:'git commit -m "release(flow)!: 2.0.0"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
49
49
  + "version": "2.0.0"`}}}},{command:'git commit -m "chore: add a field"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
@@ -57,11 +57,11 @@ feat: the thing I actually did'`]}),noShellSubstitutionInProse:c().at(y).descrip
57
57
  release: 1.2.3'`,world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
58
58
  + "version": "1.2.3"`}}}},{command:'git commit -m "chore: add the field"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:'+ "version": "0.1.0"'}}}},{command:'git commit -m "fix: a thing"',world:{exec:{[`git diff HEAD -- '${e}'`]:{stdout:`- "version": "1.0.0"
59
59
  + "version": "1.0.0-rc.1"`}}}},{command:'git -C ../other commit -m "fix: a thing"',world:{exec:{[`git -C '../other' diff HEAD -- '${e}'`]:{stdout:`- "version": "1.2.2"
60
- + "version": "1.2.3"`}}}}]})}}});var Ct=new Set(["set","alias","export","import","mod","unexport"]);function $t(t,e=[]){let n=new Set(e),o=t.split(`
61
- `),r=[];for(let s=0;s<o.length;s++){let a=o[s],i=/^(@?[A-Za-z_][A-Za-z0-9_-]*)(\s|:)/.exec(a);if(!i?.[1])continue;let d=i[1].replace(/^@/,"");if(Ct.has(d)||n.has(d))continue;let u=a.indexOf(":");if(u===-1||a[u+1]==="=")continue;let f=!1;for(let $=s-1;$>=0;$--){let U=o[$];if(!/^\[.*\]\s*$/.test(U))break;if(/\bdoc\(/.test(U)||/\bprivate\b/.test(U)){f=!0;break}}f||r.push({name:d,line:s+1})}return r}var jt=g(t=>e=>{let n=$t(e.file?.content??"",t.exempt??[]);return n.length===0?e.ok():e.fail(n.map(o=>`recipe '${o.name}' (line ${o.line}) has no [doc("\u2026")] attribute`).join(`
62
- `))}),At=m("justfile",t=>{let e=t.tools??"tools",n=t.recipes??{};return{orientation:h().at(E).description("Where the repo's tooling is catalogued, what earns a recipe, and what stays a script.").text(["The justfile is this repo's tool catalogue \u2014 the single source of truth for the tooling you can confidently reach for. Run `just` FIRST to see what you can do here; do not discover commands from package.json or memory.",`A recipe means: reach for this repeatedly, with confidence. Recipes stay thin \u2014 the code behind one lives in \`${e}/\`; one-off operational scripts live there too and never become recipes.`,`A one-off command or chain is fine to run directly. Anything you'll run more than once becomes a script; anything a human should also run becomes a recipe (with a [doc("\u2026")]).`,"If a service offers a CLI, prefer it over an MCP \u2014 a command is recorded, guardable and reproducible."].join(`
60
+ + "version": "1.2.3"`}}}}]})}}});var Pt=new Set(["set","alias","export","import","mod","unexport"]);function Mt(t,e=[]){let n=new Set(e),o=t.split(`
61
+ `),r=[];for(let s=0;s<o.length;s++){let a=o[s],i=/^(@?[A-Za-z_][A-Za-z0-9_-]*)(\s|:)/.exec(a);if(!i?.[1])continue;let d=i[1].replace(/^@/,"");if(Pt.has(d)||n.has(d))continue;let l=a.indexOf(":");if(l===-1||a[l+1]==="=")continue;let p=!1;for(let $=s-1;$>=0;$--){let V=o[$];if(!/^\[.*\]\s*$/.test(V))break;if(/\bdoc\(/.test(V)||/\bprivate\b/.test(V)){p=!0;break}}p||r.push({name:d,line:s+1})}return r}var Dt=g(t=>e=>{let n=Mt(e.file?.content??"",t.exempt??[]);return n.length===0?e.ok():e.fail(n.map(o=>`recipe '${o.name}' (line ${o.line}) has no [doc("\u2026")] attribute`).join(`
62
+ `))}),Bt=m("justfile",t=>{let e=t.tools??"tools",n=t.recipes??{};return{orientation:h().at(E).description("Where the repo's tooling is catalogued, what earns a recipe, and what stays a script.").text(["The justfile is this repo's tool catalogue \u2014 the single source of truth for the tooling you can confidently reach for. Run `just` FIRST to see what you can do here; do not discover commands from package.json or memory.",`A recipe means: reach for this repeatedly, with confidence. Recipes stay thin \u2014 the code behind one lives in \`${e}/\`; one-off operational scripts live there too and never become recipes.`,`A one-off command or chain is fine to run directly. Anything you'll run more than once becomes a script; anything a human should also run becomes a recipe (with a [doc("\u2026")]).`,"If a service offers a CLI, prefer it over an MCP \u2014 a command is recorded, guardable and reproducible."].join(`
63
63
  `)),toolsHome:h().at(k).on(`${e}/**`).description(`The fork every file in ${e}/ faces \u2014 catalogue entry, or one-shot.`).text([`You are writing into \`${e}/\` \u2014 the implementation layer, not the catalogue. Decide which of two things this file is:`,`Repeatable \u2014 part of the catalogue? Then it also needs a thin justfile recipe pointing at it, with a [doc("\u2026")] \u2014 a tool that exists only in \`${e}/\` is undiscoverable.`,"A one-off (a migration, a backfill, a workflow step)? Then it gets NO recipe \u2014 one-shots promoted into the catalogue are how `just --list` becomes noise and stops being trustworthy."].join(`
64
- `)),node:{noPackageScripts:c().at(p,l).on("package.json").description("package.json scripts stays empty \u2014 the justfile is the single command source.").check(T({assert:[{path:"scripts",keysPrefixedWith:"//"}]})).message("package.json `scripts` must stay empty \u2014 the justfile is the catalogue, and a second command surface is one an agent reads instead of it. Only //-prefixed comment keys are allowed; move the command into a justfile recipe.").test({pass:[{path:"package.json",content:'{"scripts":{"//":"see the justfile"}}'}],block:[{path:"package.json",content:'{"scripts":{"dev":"vite"}}'}]})},justfileDocs:c().at(p,l).on("justfile").description("Every justfile recipe carries an explicit [doc] \u2014 `just --list` is the catalogue's help screen.").check(jt({exempt:t.exempt})).message("Every non-private justfile recipe needs an explicit [doc(\"\u2026\")] \u2014 `just --list` is the tool catalogue's help screen, and an undocumented recipe is a tool nobody can choose. ([private] recipes are exempt; they're hidden from the listing.)").test({pass:[{path:"justfile",content:`[doc("Run the suite.")]
64
+ `)),node:{noPackageScripts:c().at(f,u).on("package.json").description("package.json scripts stays empty \u2014 the justfile is the single command source.").check(T({assert:[{path:"scripts",keysPrefixedWith:"//"}]})).message("package.json `scripts` must stay empty \u2014 the justfile is the catalogue, and a second command surface is one an agent reads instead of it. Only //-prefixed comment keys are allowed; move the command into a justfile recipe.").test({pass:[{path:"package.json",content:'{"scripts":{"//":"see the justfile"}}'}],block:[{path:"package.json",content:'{"scripts":{"dev":"vite"}}'}]})},justfileDocs:c().at(f,u).on("justfile").description("Every justfile recipe carries an explicit [doc] \u2014 `just --list` is the catalogue's help screen.").check(Dt({exempt:t.exempt})).message("Every non-private justfile recipe needs an explicit [doc(\"\u2026\")] \u2014 `just --list` is the tool catalogue's help screen, and an undocumented recipe is a tool nobody can choose. ([private] recipes are exempt; they're hidden from the listing.)").test({pass:[{path:"justfile",content:`[doc("Run the suite.")]
65
65
  test:
66
66
  npx vitest run
67
67
  `},{path:"justfile",content:`[private]
@@ -97,20 +97,20 @@ b:
97
97
  `},{path:"justfile",content:`@quiet:
98
98
  echo hi
99
99
  `}]}),...Object.keys(n).length===0?{}:{useTheRecipe:c().at(y).description("A command the repo has a recipe for is refused, and the refusal names the recipe.").check(x({ban:Object.keys(n)})).message(["This repo has a recipe for that command \u2014 use it. The catalogue is the tooling you can reach for with confidence, and a raw command run beside it is the one nobody sees, nobody documents and nobody can change in one place.",...Object.entries(n).map(([o,r])=>`\xB7 \`${o}\` \u2192 \`${r}\``)].join(`
100
- `)).test({pass:[Object.values(n)[0]??"just test"],block:[Object.keys(n)[0]??"npx vitest run"]})}}});var Tt=["dependencies","devDependencies","peerDependencies","optionalDependencies"],he=["package-lock.json","pnpm-lock.yaml","bun.lock","bun.lockb","yarn.lock"];function Ot(t,e){let n,o;try{n=t.trim()===""?{}:JSON.parse(t),o=e.trim()===""?{}:JSON.parse(e)}catch{return["(package.json is not parseable JSON on one side of this commit)"]}let r=[];for(let s of Tt){let a=n[s]??{},i=o[s]??{};for(let d of new Set([...Object.keys(a),...Object.keys(i)]))a[d]!==i[d]&&!r.includes(d)&&r.push(d)}return r}var Pt=g(t=>async e=>{let n=e.staged??[];if(!n.includes("package.json")||n.some(a=>he.includes(a)))return e.ok();let[o,r]=await Promise.all([e.exec("git show HEAD:package.json"),e.exec("git show :package.json")]),s=Ot(o.code===0?o.stdout:"",r.code===0?r.stdout:"");return s.length===0?e.ok():e.fail(`this commit moves ${s.map(a=>`\`${a}\``).join(", ")} in package.json but stages no lockfile`)}),Mt=m("node",{dependencies:h().at(k).on("package.json",...he).description("When to reach for a package, how to vet it, and what a commit that adds one owes.").text(["Before building it, check whether it's been solved: a maintained package that does the job beats hand-rolled code \u2014 ours has no users, no reviewers and no fixes coming.","When you DO add one, vet it: is it alive \u2014 downloads, recent releases, active issues? Most-downloaded isn't the bar (a newer, trending package can be the right call); the red flag is a package nobody uses and nobody maintains \u2014 that is the supply-chain risk (OWASP A03).","Adding a dependency is a decision that carries its reasoning: put `new-dep: <name> \u2014 <why, what else was checked>` on a line of its own in the commit message; the rule blocks a bare addition.","Pin the version; the lockfile change ships in the SAME commit."].join(`
100
+ `)).test({pass:[Object.values(n)[0]??"just test"],block:[Object.keys(n)[0]??"npx vitest run"]})}}});var Gt=["dependencies","devDependencies","peerDependencies","optionalDependencies"],be=["package-lock.json","pnpm-lock.yaml","bun.lock","bun.lockb","yarn.lock"];function It(t,e){let n,o;try{n=t.trim()===""?{}:JSON.parse(t),o=e.trim()===""?{}:JSON.parse(e)}catch{return["(package.json is not parseable JSON on one side of this commit)"]}let r=[];for(let s of Gt){let a=n[s]??{},i=o[s]??{};for(let d of new Set([...Object.keys(a),...Object.keys(i)]))a[d]!==i[d]&&!r.includes(d)&&r.push(d)}return r}var Nt=g(t=>async e=>{let n=e.staged??[];if(!n.includes("package.json")||n.some(a=>be.includes(a)))return e.ok();let[o,r]=await Promise.all([e.exec("git show HEAD:package.json"),e.exec("git show :package.json")]),s=It(o.code===0?o.stdout:"",r.code===0?r.stdout:"");return s.length===0?e.ok():e.fail(`this commit moves ${s.map(a=>`\`${a}\``).join(", ")} in package.json but stages no lockfile`)}),Ft=m("node",{dependencies:h().at(k).on("package.json",...be).description("When to reach for a package, how to vet it, and what a commit that adds one owes.").text(["Before building it, check whether it's been solved: a maintained package that does the job beats hand-rolled code \u2014 ours has no users, no reviewers and no fixes coming.","When you DO add one, vet it: is it alive \u2014 downloads, recent releases, active issues? Most-downloaded isn't the bar (a newer, trending package can be the right call); the red flag is a package nobody uses and nobody maintains \u2014 that is the supply-chain risk (OWASP A03).","Adding a dependency is a decision that carries its reasoning: put `new-dep: <name> \u2014 <why, what else was checked>` on a line of its own in the commit message; the rule blocks a bare addition.","Pin the version; the lockfile change ships in the SAME commit."].join(`
101
101
  `)),newDependencyNeedsReason:c().at(y).description("A commit that moves package.json must record why, and what else was checked.").check(N({whenChanged:["package.json"],token:"new-dep"})).message("This commit moves package.json with no reason recorded. A dependency is a supply-chain surface \u2014 put `new-dep: <name> \u2014 <why, and what else was checked>` on a line of its own in the commit message. Doing it is fine; doing it silently is not.").test({pass:[{command:`git commit -m 'feat: x
102
102
 
103
- new-dep: zod \u2014 checked ajv'`,world:{exec:{"git diff HEAD --name-only":{stdout:"package.json"},"git diff HEAD --name-only --diff-filter=A":{stdout:""},"git ls-files --others --exclude-standard":{stdout:""}}}}],block:[{command:'git commit -m "feat: x"',world:{exec:{"git diff HEAD --name-only":{stdout:"package.json"},"git diff HEAD --name-only --diff-filter=A":{stdout:""},"git ls-files --others --exclude-standard":{stdout:""}}}}]}),lockfileInStep:c().at(l).description("A dependency move and the lockfile land in the same commit \u2014 deps stay exactly pinned.").check(Pt({})).message("A dependency moved in package.json but no lockfile is staged. Run the install and stage the lockfile in the same commit \u2014 a lockfile one commit behind is a build that resolves differently on the next machine.").test({pass:[{staged:["src/a.ts"]},{staged:["package.json","package-lock.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:"{}"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"name":"a","dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"name":"b","dependencies":{"zod":"1.0.0"}}'}}}}],block:[{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"dependencies":{"zod":"2.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:"{ not json at all"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:"{}"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"devDependencies":{"vitest":"1.0.0"}}'},"git show :package.json":{stdout:'{"devDependencies":{"vitest":"2.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"peerDependencies":{"react":"^19"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"optionalDependencies":{"fsevents":"^2"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"devDependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{code:128,stderr:"path does not exist in HEAD"},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}}]})});var Dt=[".env.keys",".env.*.local"],Bt=["AKIA","IOSFODNN","7EXAMPLE"].join(""),Gt=m("secrets",(t={})=>{let e=t.publicPrefixes??["PUBLIC_"],n=t.localFiles??[...Dt,".dev.vars"],o=t.envFiles??".env*",r=t.extraSecretShapes??[],s=t.encrypt??`dotenvx encrypt ${e.map(d=>`-ek '${d}*'`).join(" ")}`,a=t.dx===void 0?"dotenvx run -f .env.<env> -- <cmd>":`${t.dx} <env> <cmd>`,i=`^(?!\\s*#)${e.map(d=>`(?!${d})`).join("")}(?!DOTENV_PUBLIC_KEY)[A-Za-z_][A-Za-z0-9_]*\\s*=\\s*(?!["']?encrypted:)(?!["']?\\s*$).+`;return{orientation:h().at(E).description("How secrets work here \u2014 the three tiers, the one seam, and what you may read.").text([`Env files in this repo are COMMITTED and ENCRYPTED (dotenvx): every value is an \`encrypted:\u2026\` blob except ${e.map(d=>`${d}*`).join(" / ")} config, which bundlers inline at build. Reading and editing them is expected work for you, not a violation \u2014 your training says avoid env files, and for plaintext env files it is right; for these it is wrong.`,"Three tiers, and every value lives in exactly one. Committed and encrypted: `.env`, `.env.production`, `.env.staging`. Local ephemera, never committed and never encrypted: `.env.keys` (the decryption key \u2014 the ONE file you never read), `.env.*.local`. Runtime platform secrets: set per environment in the host platform's own secret store \u2014 a runtime secret added anywhere else does not reach the running service.",`One seam: \`${a}\` decrypts and injects. EVERYTHING env-dependent goes through it \u2014 the dev server, a migration, a one-off script. Never \`source .env\`, never a hand-rolled dotenv import.`,"The trap worth knowing: `dotenvx decrypt` with no key present succeeds and writes EMPTY values over your file. If a decrypt looks suspiciously quiet, check `.env.keys` exists before you commit anything."].join(`
104
- `)),envEncrypted:c().at(p,l).on(o).ignore(...n).description(`A committed env file carries no plaintext value \u2014 everything but ${e.join(" / ")} is sealed.`).check(W({ban:[i]})).message(`A plaintext value in a committed env file \u2014 this is the leak the whole model exists to prevent, and git keeps it forever. Seal it with \`${s}\` (${e.map(d=>`${d}*`).join(", ")} and the DOTENV_PUBLIC_KEY header stay plaintext by design).`).test({pass:[{path:".env",content:`DOTENV_PUBLIC_KEY=03ab
103
+ new-dep: zod \u2014 checked ajv'`,world:{exec:{"git diff HEAD --name-only":{stdout:"package.json"},"git diff HEAD --name-only --diff-filter=A":{stdout:""},"git ls-files --others --exclude-standard":{stdout:""}}}}],block:[{command:'git commit -m "feat: x"',world:{exec:{"git diff HEAD --name-only":{stdout:"package.json"},"git diff HEAD --name-only --diff-filter=A":{stdout:""},"git ls-files --others --exclude-standard":{stdout:""}}}}]}),lockfileInStep:c().at(u).description("A dependency move and the lockfile land in the same commit \u2014 deps stay exactly pinned.").check(Nt({})).message("A dependency moved in package.json but no lockfile is staged. Run the install and stage the lockfile in the same commit \u2014 a lockfile one commit behind is a build that resolves differently on the next machine.").test({pass:[{staged:["src/a.ts"]},{staged:["package.json","package-lock.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:"{}"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"name":"a","dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"name":"b","dependencies":{"zod":"1.0.0"}}'}}}}],block:[{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"dependencies":{"zod":"2.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:"{ not json at all"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:"{}"}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"devDependencies":{"vitest":"1.0.0"}}'},"git show :package.json":{stdout:'{"devDependencies":{"vitest":"2.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"peerDependencies":{"react":"^19"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:"{}"},"git show :package.json":{stdout:'{"optionalDependencies":{"fsevents":"^2"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{stdout:'{"devDependencies":{"zod":"1.0.0"}}'},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}},{staged:["package.json"],world:{exec:{"git show HEAD:package.json":{code:128,stderr:"path does not exist in HEAD"},"git show :package.json":{stdout:'{"dependencies":{"zod":"1.0.0"}}'}}}}]})});var Lt=[".env.keys",".env.*.local"],_t=["AKIA","IOSFODNN","7EXAMPLE"].join(""),Ht=m("secrets",(t={})=>{let e=t.publicPrefixes??["PUBLIC_"],n=t.localFiles??[...Lt,".dev.vars"],o=t.envFiles??".env*",r=t.extraSecretShapes??[],s=t.encrypt??`dotenvx encrypt ${e.map(d=>`-ek '${d}*'`).join(" ")}`,a=t.dx===void 0?"dotenvx run -f .env.<env> -- <cmd>":`${t.dx} <env> <cmd>`,i=`^(?!\\s*#)${e.map(d=>`(?!${d})`).join("")}(?!DOTENV_PUBLIC_KEY)[A-Za-z_][A-Za-z0-9_]*\\s*=\\s*(?!["']?encrypted:)(?!["']?\\s*$).+`;return{orientation:h().at(E).description("How secrets work here \u2014 the three tiers, the one seam, and what you may read.").text([`Env files in this repo are COMMITTED and ENCRYPTED (dotenvx): every value is an \`encrypted:\u2026\` blob except ${e.map(d=>`${d}*`).join(" / ")} config, which bundlers inline at build. Reading and editing them is expected work for you, not a violation \u2014 your training says avoid env files, and for plaintext env files it is right; for these it is wrong.`,"Three tiers, and every value lives in exactly one. Committed and encrypted: `.env`, `.env.production`, `.env.staging`. Local ephemera, never committed and never encrypted: `.env.keys` (the decryption key \u2014 the ONE file you never read), `.env.*.local`. Runtime platform secrets: set per environment in the host platform's own secret store \u2014 a runtime secret added anywhere else does not reach the running service.",`One seam: \`${a}\` decrypts and injects. EVERYTHING env-dependent goes through it \u2014 the dev server, a migration, a one-off script. Never \`source .env\`, never a hand-rolled dotenv import.`,"The trap worth knowing: `dotenvx decrypt` with no key present succeeds and writes EMPTY values over your file. If a decrypt looks suspiciously quiet, check `.env.keys` exists before you commit anything."].join(`
104
+ `)),envEncrypted:c().at(f,u).on(o).ignore(...n).description(`A committed env file carries no plaintext value \u2014 everything but ${e.join(" / ")} is sealed.`).check(W({ban:[i]})).message(`A plaintext value in a committed env file \u2014 this is the leak the whole model exists to prevent, and git keeps it forever. Seal it with \`${s}\` (${e.map(d=>`${d}*`).join(", ")} and the DOTENV_PUBLIC_KEY header stay plaintext by design).`).test({pass:[{path:".env",content:`DOTENV_PUBLIC_KEY=03ab
105
105
  API_KEY=encrypted:BE9d
106
- PUBLIC_URL=https://x`}],block:[{path:".env",content:"API_KEY=sk-live-abcdef"}]}),noKeysFileInCommits:c().at(l).on(...n).description("The decryption key and the local overlays can never be committed \u2014 the backstop when gitignore breaks.").check(A({})).message("This file is machine-local and must never be committed: `.env.keys` is the decryption key for every sealed value in this repo, and the `.local` overlays hold real, unencrypted values. The gitignore normally stops this \u2014 if you got here, the gitignore is broken.").test({pass:[{staged:[".env"],world:{fs:{".env":"API_KEY=encrypted:BE9d"}}}],block:[{staged:[".env.keys"],world:{fs:{".env.keys":"k"}}}]}),noSecretsInCommits:c().at(l).on("**").ignore("**/*.test.*","**/*.spec.*").description("Staged content is scanned for live-key shapes \u2014 the leak that arrives outside an env file.").check(W({ban:["sk_live_[0-9a-zA-Z]{16,}","rk_live_[0-9a-zA-Z]{16,}","AKIA[0-9A-Z]{16}","-----BEGIN [A-Z ]*PRIVATE KEY-----","ghp_[0-9A-Za-z]{30,}","xox[baprs]-[0-9A-Za-z-]{10,}",...r]})).message(`A live credential shape in staged content. Secrets belong in an encrypted env file (\`${s}\`) or in the platform's runtime secret store \u2014 never in source, a fixture or a doc. If this is a real key, rotate it: staging it is already most of the way to publishing it.`).test({pass:[{staged:["src/a.ts"],world:{fs:{"src/a.ts":"const key = process.env.KEY;"}}}],block:[{staged:["src/a.ts"],world:{fs:{"src/a.ts":`const k = '${Bt}';`}}}]})}});var It=["**/*.test.ts","**/*.test.tsx","**/*.test.js","**/*.test.mjs","**/*.spec.ts","**/*.spec.tsx","**/*.spec.js"],Nt=m("tdd",t=>{let e=t.tests??It;return{testingStrategy:h().at(k).on(...e).description("How to spend testing effort (mostly integration) and what to test (behaviour, not implementation).").text(["Spend where confidence-per-cost is highest (the Testing Trophy, Kent C. Dodds): static analysis base, modest unit layer, DOMINANT integration layer, thin e2e cap.","Test behaviour as the user sees it, never implementation details \u2014 if a behaviour-preserving refactor breaks a test, the test was wrong.","Mock only at owned boundaries: the network edge (MSW), not your own modules."].join(`
107
- `)),vitest:c().at(p,l).on(...e).description("The fleet runs vitest \u2014 a second framework in a test file is a suite nothing here can check.").check(w({language:"typescript",rule:{kind:"import_statement",has:{kind:"string",regex:`^["'](node:test|node:assert|node:assert/strict|jest|@jest/globals|mocha|jasmine|ava|tap|tape|uvu)["']$`}}})).message(['A second test framework in a test file. This fleet runs vitest \u2014 every idiom rule below reads vitest spellings, so a file under another runner is a file nothing here can check, and a suite that only one command knows how to run. Import `test` / `expect` / `describe` / `vi` from "vitest".',"A repo that genuinely runs another framework disables this entry in its config, on the record, rather than mixing two."].join(`
108
- `)).test({pass:[{path:"a.test.ts",content:'import { test, expect } from "vitest";'}],block:[{path:"a.test.ts",content:'import { test } from "node:test";'}]}),vitestIdioms:{noMockInternal:c().at(p,l).on(...e).description("Mock at owned boundaries only \u2014 never your own modules.").check(w({language:"typescript",rule:{kind:"call_expression",all:[{has:{field:"function",regex:"^vi\\.mock$"}},{has:{kind:"arguments",has:{kind:"string",regex:`^['"]\\.`}}}]}})).message("Module-mocking your own code (a relative import). This pins the test to the file layout and skips the real integration \u2014 test behaviour through the public surface and mock at the boundary you own instead (network \u2192 MSW; clock/random \u2192 inject).").test({pass:[{path:"a.test.ts",content:'vi.mock("axios");'}],block:[{path:"a.test.ts",content:'vi.mock("./neighbour.ts");'}]}),noOnlyInTests:c().at(p,l).on(...e).description("A committed `.only` silently disables the rest of the suite.").check(w({language:"tsx",rule:{kind:"call_expression",has:{field:"function",regex:"\\.only$"}}})).message("A focused test (.only) silently skips the rest of the suite \u2014 CI goes green while nothing runs. Remove the focus before you commit.").test({pass:[{path:"a.test.ts",content:'it("works", () => { expect(1).toBe(1); });'}],block:[{path:"a.test.ts",content:'it.only("works", () => { expect(1).toBe(1); });'}]}),noNetworkStubs:c().at(p,l).on(...e).description("Stubbing fetch or mocking an HTTP client tests the stub; intercept at the network edge instead.").check(w({language:"tsx",rule:{any:[{pattern:"global.fetch = $X"},{pattern:"globalThis.fetch = $X"},{kind:"call_expression",all:[{has:{field:"function",regex:"^vi\\.mock$"}},{has:{kind:"arguments",has:{kind:"string",regex:`^['"](axios|node-fetch|got|ky|undici)['"]$`}}}]}]}})).message("Hand-rolled network stub. Mock at the network boundary with MSW (Testing Trophy: mock only what you own \u2014 the edge, not the client), so request building, serialisation and error mapping stay under test.").test({pass:[{path:"a.test.ts",content:'server.use(http.get("/x", () => HttpResponse.json({})));'}],block:[{path:"a.test.ts",content:"global.fetch = vi.fn();"}]}),noEmptyTest:c().at(p,l).on(...e).description("A test with no assertions is a green light for nothing.").check(w({language:"tsx",rule:{any:[{pattern:"it($DESC, () => {})"},{pattern:"it($DESC, async () => {})"},{pattern:"test($DESC, () => {})"},{pattern:"test($DESC, async () => {})"}]}})).message("Empty test body \u2014 it passes forever and counts as coverage. Write the assertion, or be honest with `it.todo('\u2026')` until you do.").test({pass:[{path:"a.test.ts",content:'it.todo("covers the retry path");'}],block:[{path:"a.test.ts",content:'it("covers the retry path", () => {});'}]})},commitRunsTests:c().at(l).description("Runs the deterministic suite at commit and blocks if anything fails.").check(C({run:t.run})).message("The commit suite failed \u2014 the commit is blocked until it is green. (Run it yourself to see what broke.)").test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.run]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.run]:{code:1,stdout:"1 failed"}}}}]})}});var Ft=m("typescript",t=>{let e=t.strictOptions??["strict","noUncheckedIndexedAccess"],n=t.tsconfigBase,o=[...n===void 0?[]:[{path:"extends",matches:`${b(n)}$`,required:!0}],...e.map(r=>({path:`compilerOptions.${r}`,equals:!0}))];return{strictTypesNoInvalidStates:h().at(k).on("**/tsconfig*.json","eslint.config.*").description("Use the type system as a design tool \u2014 strict stays on, impossible states stay impossible.").text([`Strictness is not yours to weaken: ${e.map(r=>`\`${r}\``).join(" and ")} ${n===void 0?"stay on wherever they are set":`come from the committed \`${n}\``}. A red squiggle is a defect found, not a setting to relax \u2014 an exception is a visible, local, per-rule override, on the record.`,'Model state so the impossible cannot be constructed. Two sibling booleans (isLoading + isError) are a smell: a discriminated union makes the invalid combination unrepresentable (Vanderkam, Effective TypeScript; Feldman, "Make Impossible States Impossible").',"The linter holds the escape hatches (@ts-expect-error, explicit any, double assertions) at error, and it does that job better than any rule we could write \u2014 which is why this pack points at the linter rather than carrying patterns of its own."].join(`
109
- `)),tsconfigStrict:c().at(p,l).on("**/tsconfig.json").description(n===void 0?`Every tsconfig.json keeps ${e.join(" and ")} on.`:`Every tsconfig.json extends ${n} and keeps ${e.join(" and ")} on.`).check(T({assert:o})).message([n===void 0?`This tsconfig turns off one of ${e.join(" / ")}. A red squiggle is a defect found, not a setting to relax.`:`This tsconfig must extend \`${n}\`, and it must not turn ${e.join(" or ")} back off. Weakening the strictness means changing the shared base once, visibly, not this project quietly.`].join(`
110
- `)).test({pass:[{path:"cli/tsconfig.json",content:n===void 0?'{"compilerOptions":{"strict":true,"noUncheckedIndexedAccess":true}}':`{"extends":"${n}"}`}],block:[{path:"cli/tsconfig.json",content:'{"compilerOptions":{"strict":false}}'},{path:"cli/tsconfig.json",content:n===void 0?'{"compilerOptions":{"noUncheckedIndexedAccess":false}}':`{"extends":"${n}","compilerOptions":{"strict":false}}`}]}),...t.eslintBase===void 0?{}:{eslintFromBase:c().at(p,l).on("eslint.config.js","eslint.config.mjs").description(`The repo's eslint config imports ${t.eslintBase}.`).check(w({language:"javascript",rule:{kind:"program",not:{has:{stopBy:"end",kind:"string",regex:b(t.eslintBase)}}}})).message([`This eslint config does not import \`${t.eslintBase}\`. The shared rules live there \u2014 a config that does not import it lints to a private standard while this repo's guard claims the shared one.`,`Fix: \`import base from "${t.eslintBase}";\` and spread it, then add only what is genuinely local to this repo.`].join(`
106
+ PUBLIC_URL=https://x`}],block:[{path:".env",content:"API_KEY=sk-live-abcdef"}]}),noKeysFileInCommits:c().at(u).on(...n).description("The decryption key and the local overlays can never be committed \u2014 the backstop when gitignore breaks.").check(j({})).message("This file is machine-local and must never be committed: `.env.keys` is the decryption key for every sealed value in this repo, and the `.local` overlays hold real, unencrypted values. The gitignore normally stops this \u2014 if you got here, the gitignore is broken.").test({pass:[{staged:[".env"],world:{fs:{".env":"API_KEY=encrypted:BE9d"}}}],block:[{staged:[".env.keys"],world:{fs:{".env.keys":"k"}}}]}),noSecretsInCommits:c().at(u).on("**").ignore("**/*.test.*","**/*.spec.*").description("Staged content is scanned for live-key shapes \u2014 the leak that arrives outside an env file.").check(W({ban:["sk_live_[0-9a-zA-Z]{16,}","rk_live_[0-9a-zA-Z]{16,}","AKIA[0-9A-Z]{16}","-----BEGIN [A-Z ]*PRIVATE KEY-----","ghp_[0-9A-Za-z]{30,}","xox[baprs]-[0-9A-Za-z-]{10,}",...r]})).message(`A live credential shape in staged content. Secrets belong in an encrypted env file (\`${s}\`) or in the platform's runtime secret store \u2014 never in source, a fixture or a doc. If this is a real key, rotate it: staging it is already most of the way to publishing it.`).test({pass:[{staged:["src/a.ts"],world:{fs:{"src/a.ts":"const key = process.env.KEY;"}}}],block:[{staged:["src/a.ts"],world:{fs:{"src/a.ts":`const k = '${_t}';`}}}]})}});var Wt=["**/*.test.ts","**/*.test.tsx","**/*.test.js","**/*.test.mjs","**/*.spec.ts","**/*.spec.tsx","**/*.spec.js"],Kt=m("tdd",t=>{let e=t.tests??Wt;return{testingStrategy:h().at(k).on(...e).description("How to spend testing effort (mostly integration) and what to test (behaviour, not implementation).").text(["Spend where confidence-per-cost is highest (the Testing Trophy, Kent C. Dodds): static analysis base, modest unit layer, DOMINANT integration layer, thin e2e cap.","Test behaviour as the user sees it, never implementation details \u2014 if a behaviour-preserving refactor breaks a test, the test was wrong.","Mock only at owned boundaries: the network edge (MSW), not your own modules."].join(`
107
+ `)),vitest:c().at(f,u).on(...e).description("The fleet runs vitest \u2014 a second framework in a test file is a suite nothing here can check.").check(w({language:"typescript",rule:{kind:"import_statement",has:{kind:"string",regex:`^["'](node:test|node:assert|node:assert/strict|jest|@jest/globals|mocha|jasmine|ava|tap|tape|uvu)["']$`}}})).message(['A second test framework in a test file. This fleet runs vitest \u2014 every idiom rule below reads vitest spellings, so a file under another runner is a file nothing here can check, and a suite that only one command knows how to run. Import `test` / `expect` / `describe` / `vi` from "vitest".',"A repo that genuinely runs another framework disables this entry in its config, on the record, rather than mixing two."].join(`
108
+ `)).test({pass:[{path:"a.test.ts",content:'import { test, expect } from "vitest";'}],block:[{path:"a.test.ts",content:'import { test } from "node:test";'}]}),vitestIdioms:{noMockInternal:c().at(f,u).on(...e).description("Mock at owned boundaries only \u2014 never your own modules.").check(w({language:"typescript",rule:{kind:"call_expression",all:[{has:{field:"function",regex:"^vi\\.mock$"}},{has:{kind:"arguments",has:{kind:"string",regex:`^['"]\\.`}}}]}})).message("Module-mocking your own code (a relative import). This pins the test to the file layout and skips the real integration \u2014 test behaviour through the public surface and mock at the boundary you own instead (network \u2192 MSW; clock/random \u2192 inject).").test({pass:[{path:"a.test.ts",content:'vi.mock("axios");'}],block:[{path:"a.test.ts",content:'vi.mock("./neighbour.ts");'}]}),noOnlyInTests:c().at(f,u).on(...e).description("A committed `.only` silently disables the rest of the suite.").check(w({language:"tsx",rule:{kind:"call_expression",has:{field:"function",regex:"\\.only$"}}})).message("A focused test (.only) silently skips the rest of the suite \u2014 CI goes green while nothing runs. Remove the focus before you commit.").test({pass:[{path:"a.test.ts",content:'it("works", () => { expect(1).toBe(1); });'}],block:[{path:"a.test.ts",content:'it.only("works", () => { expect(1).toBe(1); });'}]}),noNetworkStubs:c().at(f,u).on(...e).description("Stubbing fetch or mocking an HTTP client tests the stub; intercept at the network edge instead.").check(w({language:"tsx",rule:{any:[{pattern:"global.fetch = $X"},{pattern:"globalThis.fetch = $X"},{kind:"call_expression",all:[{has:{field:"function",regex:"^vi\\.mock$"}},{has:{kind:"arguments",has:{kind:"string",regex:`^['"](axios|node-fetch|got|ky|undici)['"]$`}}}]}]}})).message("Hand-rolled network stub. Mock at the network boundary with MSW (Testing Trophy: mock only what you own \u2014 the edge, not the client), so request building, serialisation and error mapping stay under test.").test({pass:[{path:"a.test.ts",content:'server.use(http.get("/x", () => HttpResponse.json({})));'}],block:[{path:"a.test.ts",content:"global.fetch = vi.fn();"}]}),noEmptyTest:c().at(f,u).on(...e).description("A test with no assertions is a green light for nothing.").check(w({language:"tsx",rule:{any:[{pattern:"it($DESC, () => {})"},{pattern:"it($DESC, async () => {})"},{pattern:"test($DESC, () => {})"},{pattern:"test($DESC, async () => {})"}]}})).message("Empty test body \u2014 it passes forever and counts as coverage. Write the assertion, or be honest with `it.todo('\u2026')` until you do.").test({pass:[{path:"a.test.ts",content:'it.todo("covers the retry path");'}],block:[{path:"a.test.ts",content:'it("covers the retry path", () => {});'}]})},commitRunsTests:c().at(u).description("Runs the deterministic suite at commit and blocks if anything fails.").check(C({run:t.run})).message("The commit suite failed \u2014 the commit is blocked until it is green. (Run it yourself to see what broke.)").test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.run]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.run]:{code:1,stdout:"1 failed"}}}}]})}});var Ut=m("typescript",t=>{let e=t.strictOptions??["strict","noUncheckedIndexedAccess"],n=t.tsconfigBase,o=[...n===void 0?[]:[{path:"extends",matches:`${b(n)}$`,required:!0}],...e.map(r=>({path:`compilerOptions.${r}`,equals:!0}))];return{strictTypesNoInvalidStates:h().at(k).on("**/tsconfig*.json","eslint.config.*").description("Use the type system as a design tool \u2014 strict stays on, impossible states stay impossible.").text([`Strictness is not yours to weaken: ${e.map(r=>`\`${r}\``).join(" and ")} ${n===void 0?"stay on wherever they are set":`come from the committed \`${n}\``}. A red squiggle is a defect found, not a setting to relax \u2014 an exception is a visible, local, per-rule override, on the record.`,'Model state so the impossible cannot be constructed. Two sibling booleans (isLoading + isError) are a smell: a discriminated union makes the invalid combination unrepresentable (Vanderkam, Effective TypeScript; Feldman, "Make Impossible States Impossible").',"The linter holds the escape hatches (@ts-expect-error, explicit any, double assertions) at error, and it does that job better than any rule we could write \u2014 which is why this pack points at the linter rather than carrying patterns of its own."].join(`
109
+ `)),tsconfigStrict:c().at(f,u).on("**/tsconfig.json").description(n===void 0?`Every tsconfig.json keeps ${e.join(" and ")} on.`:`Every tsconfig.json extends ${n} and keeps ${e.join(" and ")} on.`).check(T({assert:o})).message([n===void 0?`This tsconfig turns off one of ${e.join(" / ")}. A red squiggle is a defect found, not a setting to relax.`:`This tsconfig must extend \`${n}\`, and it must not turn ${e.join(" or ")} back off. Weakening the strictness means changing the shared base once, visibly, not this project quietly.`].join(`
110
+ `)).test({pass:[{path:"cli/tsconfig.json",content:n===void 0?'{"compilerOptions":{"strict":true,"noUncheckedIndexedAccess":true}}':`{"extends":"${n}"}`}],block:[{path:"cli/tsconfig.json",content:'{"compilerOptions":{"strict":false}}'},{path:"cli/tsconfig.json",content:n===void 0?'{"compilerOptions":{"noUncheckedIndexedAccess":false}}':`{"extends":"${n}","compilerOptions":{"strict":false}}`}]}),...t.eslintBase===void 0?{}:{eslintFromBase:c().at(f,u).on("eslint.config.js","eslint.config.mjs").description(`The repo's eslint config imports ${t.eslintBase}.`).check(w({language:"javascript",rule:{kind:"program",not:{has:{stopBy:"end",kind:"string",regex:b(t.eslintBase)}}}})).message([`This eslint config does not import \`${t.eslintBase}\`. The shared rules live there \u2014 a config that does not import it lints to a private standard while this repo's guard claims the shared one.`,`Fix: \`import base from "${t.eslintBase}";\` and spread it, then add only what is genuinely local to this repo.`].join(`
111
111
  `)).test({pass:[{path:"eslint.config.js",content:`import base from "${t.eslintBase}";
112
112
  export default [...base];
113
113
  `}],block:[{path:"eslint.config.js",content:`export default [];
114
- `}]})},commitRunsTsc:c().at(l).description("Runs the whole-project typecheck at commit and blocks on any type error.").check(C({run:t.typecheck})).message(`The typecheck failed (\`${t.typecheck}\`) \u2014 the commit is blocked until types are clean. tsc catches the cross-file mismatches a file-at-a-time build never sees: a change that compiled here and broke a caller there.`).test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.typecheck]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.typecheck]:{code:1,stdout:"a.ts(1,1): error TS2322"}}}}]}),commitRunsEslint:c().at(l).description("Runs the lint at commit and blocks on any error.").check(C({run:t.lint})).message(`The lint failed (\`${t.lint}\`) \u2014 the commit is blocked until it is clean. This is where the escape hatches are held (floating promises, explicit any, ts-comments); fix the finding, or override that one rule visibly and locally.`).test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.lint]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.lint]:{code:1,stdout:"error Unsafe assignment"}}}}]})}});var me=["general-purpose","claude"],ye=M("parent",F({parent:!0})),ke=M("builder",F({types:["builder"],generic:me,brief:["Follow `/work build`","Follow /work build","You are the builder"]})),be=M("checker",F({types:["verifier","review","codeops","docops","devops","secops"],generic:me,brief:["Follow `/work verify`","Follow /work verify"]})),_t=m("work",{planIsTheParents:c().at(y).for(ke).description("A builder owns its phase, not the plan \u2014 the verdict, the amendment and the cost record are the parent's.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+plan\\s+(?:verdict|amend)\\b","(?:^|[\\n;&|(]\\s*)\\s*work\\s+(?:save|task)\\b","(?:^|[\\n;&|(]\\s*)\\s*work\\s+cost\\s+record\\b"]})).message(["You are a builder, and this command is the parent's. Yours: `work plan status` \xB7 `work plan tick` \xB7 `work plan decision`.","A verdict or an amendment needs the whole plan in view, and you can only see one phase. `work cost record` is worse than refused \u2014 it would succeed: every record is an INCREMENT since the last stamp, so filing one mid-phase bills the phase before yours for work it never did.","What you have is an ask. Say it to the parent \u2014 what you hit, what your boxes do not cover, what you would do \u2014 and carry on with the boxes that are yours while you wait."].join(`
115
- `)).test({pass:["work plan status --json","work plan tick F7.B1 --evidence diff:abc123","work cost"],block:["work plan verdict F7 --pass --cite x","work save","work cost record F7"]}),checkersDoNotWrite:c().at(p).for(be).description("A verifier and the review lenses sit at a boundary they must not write to.").check(t=>t.fail(`${t.file?.path??"a file"} \u2014 a checker reads`)).message("You were spawned to JUDGE this work, and a judge that edits the thing it is judging has destroyed the evidence. Report what you found \u2014 PASS or FAIL with citations \u2014 and let the rung that owns the code change it. (The verifier's own tool list already withholds Edit and Write; this is the half that holds for every review lens too, and it holds at the rail rather than in a frontmatter list somebody can widen.)").test({block:[{path:"src/thing.ts",content:"x"}]}),ticksAreTheChilds:c().at(y).for(ye).description("The parent records verdicts; a box is ticked by the child that built it, with its evidence.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+plan\\s+tick\\b"]})).message("A box is ticked by the rung that built it. You are supervising: you did not do the work, so you cannot supply the evidence, and a tick whose proof came from reading a report is exactly the provenance the journal exists to prevent. If a child left a box unticked, that is the finding \u2014 record the verdict on it.").test({pass:["work plan verdict F7 --pass --cite x"],block:["work plan tick F7.B1 --evidence diff:abc"]}),noAgentInboxItems:c().at(y).description("The inbox is the human's speccing queue \u2014 an agent raises a finding in chat, never files one.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+inbox\\s+new\\b"]})).message(["`work inbox new` is the human's own door, and this command rail is only ever crossed by an agent \u2014 a person typing in a terminal has no hooks in front of them. So the fact that this fired means an agent is filing.","The inbox is a SPECCING QUEUE, not a findings drawer: every item in it is something its owner has decided to think about, and an item nobody chose to add is a decision taken away from them and returned as a chore. Work is born through `/work spec`, which only a clear human instruction opens.","What you have is a finding. Say it in the chat \u2014 headline, why it matters, what you would do \u2014 and let the human decide whether it becomes an item, a spec, or nothing."].join(`
116
- `)).test({pass:["work inbox list",'echo "the human runs work inbox new themselves" >> note.md'],block:['work inbox new --summary "a thing"',"echo hi && work inbox new --summary x"]}),noShellSubstitutionInProse:c().at(y).description("A backtick inside a double-quoted argument of a work verb that records prose is live command substitution, not Markdown.").check(x({ban:[D(["work\\s+(?:plan|task|recap|record|spec|complete)\\b"])]})).message(B).test({pass:["work plan decision 'chose the `chain` builder' --chose x --reverse y","work plan status --json"],block:['work plan decision "chose the `chain` builder" --chose x --reverse y']})});export{ke as builder,be as checker,at as docs,ct as fcis,lt as flow,Rt as git,At as justfile,Mt as node,ye as parent,Gt as secrets,Nt as tdd,Ft as typescript,_t as work};
114
+ `}]})},commitRunsTsc:c().at(u).description("Runs the whole-project typecheck at commit and blocks on any type error.").check(C({run:t.typecheck})).message(`The typecheck failed (\`${t.typecheck}\`) \u2014 the commit is blocked until types are clean. tsc catches the cross-file mismatches a file-at-a-time build never sees: a change that compiled here and broke a caller there.`).test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.typecheck]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.typecheck]:{code:1,stdout:"a.ts(1,1): error TS2322"}}}}]}),commitRunsEslint:c().at(u).description("Runs the lint at commit and blocks on any error.").check(C({run:t.lint})).message(`The lint failed (\`${t.lint}\`) \u2014 the commit is blocked until it is clean. This is where the escape hatches are held (floating promises, explicit any, ts-comments); fix the finding, or override that one rule visibly and locally.`).test({pass:[{staged:["cli/a.ts"],world:{exec:{[t.lint]:{code:0}}}}],block:[{staged:["cli/a.ts"],world:{exec:{[t.lint]:{code:1,stdout:"error Unsafe assignment"}}}}]})}});var we=["general-purpose","claude"],xe=M("parent",F({parent:!0})),ve=M("builder",F({types:["builder"],generic:we,brief:["Follow `/work build`","Follow /work build","You are the builder"]})),Se=M("checker",F({types:["verifier","review","codeops","docops","devops","secops"],generic:we,brief:["Follow `/work verify`","Follow /work verify"]})),Vt=m("work",{planIsTheParents:c().at(y).for(ve).description("A builder owns its phase, not the plan \u2014 the verdict, the amendment and the cost record are the parent's.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+plan\\s+(?:verdict|amend)\\b","(?:^|[\\n;&|(]\\s*)\\s*work\\s+(?:save|task)\\b","(?:^|[\\n;&|(]\\s*)\\s*work\\s+cost\\s+record\\b"]})).message(["You are a builder, and this command is the parent's. Yours: `work plan status` \xB7 `work plan tick` \xB7 `work plan decision`.","A verdict or an amendment needs the whole plan in view, and you can only see one phase. `work cost record` is worse than refused \u2014 it would succeed: every record is an INCREMENT since the last stamp, so filing one mid-phase bills the phase before yours for work it never did.","What you have is an ask. Say it to the parent \u2014 what you hit, what your boxes do not cover, what you would do \u2014 and carry on with the boxes that are yours while you wait."].join(`
115
+ `)).test({pass:["work plan status --json","work plan tick F7.B1 --evidence diff:abc123","work cost"],block:["work plan verdict F7 --pass --cite x","work save","work cost record F7"]}),checkersDoNotWrite:c().at(f).for(Se).description("A verifier and the review lenses sit at a boundary they must not write to.").check(t=>t.fail(`${t.file?.path??"a file"} \u2014 a checker reads`)).message("You were spawned to JUDGE this work, and a judge that edits the thing it is judging has destroyed the evidence. Report what you found \u2014 PASS or FAIL with citations \u2014 and let the rung that owns the code change it. (The verifier's own tool list already withholds Edit and Write; this is the half that holds for every review lens too, and it holds at the rail rather than in a frontmatter list somebody can widen.)").test({block:[{path:"src/thing.ts",content:"x"}]}),ticksAreTheChilds:c().at(y).for(xe).description("The parent records verdicts; a box is ticked by the child that built it, with its evidence.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+plan\\s+tick\\b"]})).message("A box is ticked by the rung that built it. You are supervising: you did not do the work, so you cannot supply the evidence, and a tick whose proof came from reading a report is exactly the provenance the journal exists to prevent. If a child left a box unticked, that is the finding \u2014 record the verdict on it.").test({pass:["work plan verdict F7 --pass --cite x"],block:["work plan tick F7.B1 --evidence diff:abc"]}),noAgentInboxItems:c().at(y).description("The inbox is the human's speccing queue \u2014 an agent raises a finding in chat, never files one.").check(x({ban:["(?:^|[\\n;&|(]\\s*)\\s*work\\s+inbox\\s+new\\b"]})).message(["`work inbox new` is the human's own door, and this command rail is only ever crossed by an agent \u2014 a person typing in a terminal has no hooks in front of them. So the fact that this fired means an agent is filing.","The inbox is a SPECCING QUEUE, not a findings drawer: every item in it is something its owner has decided to think about, and an item nobody chose to add is a decision taken away from them and returned as a chore. Work is born through `/work spec`, which only a clear human instruction opens.","What you have is a finding. Say it in the chat \u2014 headline, why it matters, what you would do \u2014 and let the human decide whether it becomes an item, a spec, or nothing."].join(`
116
+ `)).test({pass:["work inbox list",'echo "the human runs work inbox new themselves" >> note.md'],block:['work inbox new --summary "a thing"',"echo hi && work inbox new --summary x"]}),noShellSubstitutionInProse:c().at(y).description("A backtick inside a double-quoted argument of a work verb that records prose is live command substitution, not Markdown.").check(x({ban:[D(["work\\s+(?:plan|task|recap|record|spec|complete)\\b"])]})).message(B).test({pass:["work plan decision 'chose the `chain` builder' --chose x --reverse y","work plan status --json"],block:['work plan decision "chose the `chain` builder" --chose x --reverse y']})});export{ve as builder,Se as checker,ft as docs,ht as fcis,mt as flow,Ot as git,Bt as justfile,Ft as node,xe as parent,Ht as secrets,Kt as tdd,Ut as typescript,Vt as work};
@@ -584,10 +584,24 @@ export interface RequireEdge {
584
584
  * language, and a fence written in regexes is a fence nobody re-reads — the compiled literals of
585
585
  * one such config were dead for four days while the guard's own status reported it healthy.
586
586
  */
587
+ /**
588
+ * One layer: the globs that are in it, and — optionally — the globs that are NOT.
589
+ *
590
+ * `not` exists because the shape a real architecture wants is "this folder, except these few": the
591
+ * marketing surface of a site is `src/pages/**` minus the handful of subtrees that are endpoints.
592
+ * Written as an enumeration of what IS marketing, the fence silently stops covering the next folder
593
+ * somebody adds — which is the failure this dialect is supposed to prevent, not cause. It compiles
594
+ * to dependency-cruiser's own `pathNot`, which is the same pair the `only` rule already uses.
595
+ */
596
+ export interface Layer {
597
+ readonly path: readonly string[];
598
+ readonly not?: readonly string[];
599
+ }
587
600
  export interface Dialect {
588
601
  /** What to cruise. A GLOB, because a bare directory makes depcruise cruise zero modules. */
589
602
  readonly scan: string;
590
- readonly layers: Readonly<Record<string, readonly string[]>>;
603
+ /** Each layer, as globs — a bare list, or `{ path, not }` when it has holes in it. */
604
+ readonly layers: Readonly<Record<string, readonly string[] | Layer>>;
591
605
  /** Fail-closed allowlist: a layer may reach ONLY these layers. */
592
606
  readonly only?: Readonly<Record<string, readonly string[]>>;
593
607
  /** Forbidden edges, each carrying the `why` its violation prints. */
@@ -635,7 +649,12 @@ export declare function entryToMatcher(entry: string): {
635
649
  path?: string;
636
650
  };
637
651
  /** Layer names → a depcruise matcher fragment. `path` is an OR-list; builtins become a type. */
638
- export declare function layersMatcher(names: readonly string[], layers: Readonly<Record<string, readonly string[]>>): Matcher;
652
+ /** The globs a layer holds, whichever of the two ways it was written. */
653
+ export declare function layerGlobs(layer: readonly string[] | Layer | undefined): {
654
+ path: readonly string[];
655
+ not: readonly string[];
656
+ };
657
+ export declare function layersMatcher(names: readonly string[], layers: Readonly<Record<string, readonly string[] | Layer>>): Matcher;
639
658
  /** The whole dialect → a native `{ forbidden, required, options }` config. */
640
659
  export declare function compileDialect(rule: Dialect): NativeConfig;
641
660
  /**
@@ -660,6 +679,25 @@ export declare function compileDialect(rule: Dialect): NativeConfig;
660
679
  export declare function depcruiseCommand(dialect: Dialect): string;
661
680
  /** The tool's JSON report → one hit per violation, each naming the fence and its `why`. */
662
681
  export declare function depcruiseHits(result: ExecResult): string[];
682
+ /**
683
+ * Every layer entry that is a regex where a glob belongs — the fault `depcruise` refuses to load with.
684
+ *
685
+ * MEASURED, NOT IMAGINED: the first repo to convert its guard to flow brought its fence across
686
+ * verbatim, layers and all, from an engine whose layers WERE regexes. They compiled to
687
+ * `^\^src/pages/\([^/]!app/…` — a pattern no module path can match — and the fence loaded, reported
688
+ * armed, walked the graph and found nothing, for as long as it took somebody to plant a probe and
689
+ * watch the commit sail through. A rule that cannot do its job must refuse to load.
690
+ */
691
+ export declare function nonGlobLayers(layers: Readonly<Record<string, readonly string[] | Layer>>): string[];
692
+ /**
693
+ * A layer with holes, named in an `only` allowlist — refused, because the holes would be silent.
694
+ *
695
+ * `only` is compiled by INVERTING the allowed layers: everything not in them blocks. The inversion
696
+ * reads the `path` globs and nothing else, so a layer's `not` would simply not be there — the
697
+ * allowlist would quietly be wider than the layer it names. Rather than half-apply it, this says so
698
+ * at load: use plain globs for a layer that an `only` names, or express the exclusion as a `forbid`.
699
+ */
700
+ export declare function holesInAnOnlyList(opts: Dialect): string[];
663
701
  /**
664
702
  * The generic import-fence runner.
665
703
  *
@@ -1,4 +1,21 @@
1
1
  export declare function escapeRe(s: string): string;
2
+ /**
3
+ * Is that a REGULAR EXPRESSION where this dialect wants a glob?
4
+ *
5
+ * It lives beside `escapeRe` because it is that function read backwards: these are the shapes
6
+ * `escapeRe` turns into literals, which is how a pattern written as a regex comes to match nothing
7
+ * at all while reading as a fence. Four of them — a leading `^`, a `(?` group, a `|`, a trailing `$`
8
+ * — and nothing broader is claimed: this is not a regex detector, it is the list of characters whose
9
+ * meaning is silently lost here.
10
+ *
11
+ * ANY `|` COUNTS, with no carve-out for a backslash before it. The dialect has no escape character:
12
+ * `\|` is a literal backslash followed by a pipe, and a path with either in it is not a path anyone
13
+ * has. A carve-out would only ever wave through the thing it was meant to catch.
14
+ *
15
+ * It was found the hard way — a repo's fence, brought across from an engine whose layers WERE
16
+ * regexes, loaded green and refused nothing for a week.
17
+ */
18
+ export declare function notAGlob(entry: string): boolean;
2
19
  /** One piece of a glob. The whole dialect: `**` · `*` · `?` · `{a,b}` · literal text. */
3
20
  export type GlobToken = {
4
21
  readonly kind: "literal";
@@ -1,8 +1,8 @@
1
1
  export { guardrail, breadcrumb, type Sentence, type EntrySpec, type GuardrailSpec, type BreadcrumbSpec, type GuardrailSentence, type BreadcrumbSentence, type EntryGroup, type DisabledMark, } from "./language/domain.ts";
2
2
  export { definePack, pack, override, defineConfig, type Pack, type PackDefinition, type EntryRef, type RefTarget, type FlowConfig, type Binding, type PackBinding, type OverrideBinding, type ConfigSentence, } from "./language/domain.ts";
3
- export { defineCheck, type Check, type Ctx, type World, type Verdict, type ExecResult, type TurnAction, type Case, type CaseWorld, type Cases, } from "./language/domain.ts";
3
+ export { defineCheck, withFaults, faultsOf, type Check, type Ctx, type World, type Verdict, type ExecResult, type TurnAction, type Case, type CaseWorld, type Cases, } from "./language/domain.ts";
4
4
  export { defineCategory, type Category, type Classifier, type SessionFacts } from "./language/domain.ts";
5
- export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
5
+ export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type Layer, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
6
6
  export { substitutionInProse, SUBSTITUTION_MESSAGE } from "./checks/domain.ts";
7
7
  export { commitMessage, gitDirPrefix, gitInvocations, givesReason, quoteArg, tokenizeCommand, type GitInvocation, } from "./checks/domain.ts";
8
8
  export { spawnedAs, type SpawnRecipe } from "./engine/domain.ts";