@paniolo/scan 0.1.0 → 0.1.1
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/cli.js
CHANGED
|
@@ -121,7 +121,7 @@ ${t}`)}function Ve(e){return Sn.test(e.commands.join(`
|
|
|
121
121
|
`))).map(i=>i.path).toSorted()},validation:{scripts:r.toSorted(),configs:e.configs.filter(i=>i.kind==="test").map(i=>i.path).toSorted(),ciWorkflows:e.workflows.filter(i=>/(npm test|npm run test|vitest|jest|playwright|cypress)/i.test(i.commands.join(`
|
|
122
122
|
`))).map(i=>i.path).toSorted()},memory:{docs:e.memoryDocs.toSorted(),indexes:e.memoryIndexes.toSorted()},localContext:{conventions:e.localContextConventions.toSorted(),nestedAgentsFiles:e.nestedAgentsFiles.toSorted()},correctionLoop:{scripts:s.toSorted(),prTemplates:e.prTemplates.toSorted(),ciWorkflows:o,maintenanceScripts:e.guidanceMaintenanceScripts.toSorted(),prTemplateChecks:e.aiHarnessPrTemplates.toSorted(),guidanceCiWorkflows:o}}}async function vn(e){let t=await Ue(e),n=Cn(await _(e,"package.json")),r=[],s=[],o=[],i=[],a=[],c=[],u=[],p=[],m=[],I={customChecker:!1,packageScript:!1,lintRules:!1,stopHook:!1,guidance:!1};I.packageScript=Object.entries(n.scripts).some(([d,k])=>/(^|:)(lint:jsdoc|check:jsdoc)(:|$)/i.test(d)||/find-missing-jsdoc|check-jsdoc-files/i.test(k));for(let d of t){let k=wn(d);if(k!==null){let S={...k},A=await _(e,d);k.kind==="typecheck"&&A!==null&&/"strict"\s*:\s*true/.test(A)&&(S.capabilities=[...k.capabilities,"strict-typecheck"]),k.kind==="lint"&&A!==null&&/jsdoc\/require-(returns|param)/i.test(A)&&(I.lintRules=!0),r.push(S),m.push(await G(e,d,"enforcement","config",S.capabilities))}if(Is(d)){let S=await An(e,d);if(S!==null){s.push(S);let A=await G(e,d,"workflow","automation",["ci-workflow"]);m.push(A)}}if(Ls(d)){o.push(d);let S=await _(e,d);S!==null&&Ts(S)&&i.push(d);let A=await G(e,d,"automation","pull-request",["pr-template"]);m.push(A)}if(d.endsWith(".md")&&xn(d)&&(a.push(d),Rn(d)&&c.push(d),m.push(await G(e,d,"memory","docs",["memory"]))),d.endsWith(".md")&&Fs(d)&&u.push(d),(d==="scripts/find-missing-jsdoc/analyzeFile.ts"||d==="scripts/find-missing-jsdoc/check-jsdoc-files.bun.ts")&&(I.customChecker=!0),(d==="docs/ai/code-comment-best-practices.md"||d==="skills/code-comment-best-practices/SKILL.md")&&(I.guidance=!0),d.startsWith(".github/hooks/")&&d.endsWith(".json")){let S=await _(e,d);S!==null&&/lint-on-stop|find-missing-jsdoc|jsdoc/i.test(S)&&(I.stopHook=!0)}if(d.endsWith("/AGENTS.md")){p.push(d);let S=await G(e,d,"local-context","nested-entry",["nested-agents-md"]);m.push(S)}}return n.path!==null&&await Es(e,n.path)&&m.push(await G(e,n.path,"automation","project",["package-scripts"])),{packageJson:n,configs:r.toSorted((d,k)=>d.path.localeCompare(k.path)),workflows:s.toSorted((d,k)=>d.path.localeCompare(k.path)),prTemplates:o,guidanceMaintenanceScripts:Object.entries(n.scripts).filter(([d,k])=>We(d,k)).map(([d])=>d).toSorted(),aiHarnessPrTemplates:i.toSorted(),guidanceCiWorkflows:s.filter(d=>Ve(d)).map(d=>d.path).toSorted(),memoryDocs:a,memoryIndexes:c.toSorted(),localContextConventions:u.toSorted(),nestedAgentsFiles:p,surfaces:m.toSorted((d,k)=>d.path.localeCompare(k.path)),jsdocEnforcement:I}}import ni from"node:path";import{readdir as Qo}from"node:fs/promises";import Zo from"node:path";async function En(e,t){let n=[];try{n=await Qo(Zo.join(e,t),{withFileTypes:!0})}catch{return[]}return n.filter(r=>r.isFile()).map(r=>`${t}/${r.name}`)}import{readFile as ei,stat as ti}from"node:fs/promises";import Gs from"node:path";var Ps=[".claude/settings.json",".claude/settings.local.json"],Ns=[".env",".env.local",".env.development",".env.development.local",".env.production",".env.production.local",".env.test"],_s=[".gitleaks.toml",".github/gitleaks.toml",".github/secret_scanning.yml",".github/secret_scanning.yaml"],Ds=[".pre-commit-config.yaml",".pre-commit-config.yml"],Ms=["config/worker-vars.list","config/env-secrets.dev.list","config/env-secrets.staging.list","config/env-secrets.production.list","scripts/env/run-with-env/run-with-env.bun.ts"],$s=/gitleaks|trufflehog|detect-secrets|ggshield/i,Os=/(?:\.\/)?(?:[\w.-]+\/)+[\w.-]+\.(?:tsx?|mts|cts|mjs|cjs|js|sh|bash|py)/g;async function L(e,t){try{return await ei(Gs.join(e,t),"utf8")}catch{return null}}async function D(e,t){try{return(await ti(Gs.join(e,t))).isFile()}catch{return!1}}function j(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Be(e){try{return{ok:!0,value:JSON.parse(e)}}catch{return{ok:!1,value:null}}}function js(e){let{permissions:t}=e;return!j(t)||!Array.isArray(t.allow)?[]:t.allow.filter(n=>typeof n=="string")}function Ws(e){let{permissions:t}=e;return!j(t)||typeof t.defaultMode!="string"?null:t.defaultMode}function Ke(e){let t=e.match(Os)??[];return[...new Set(t.map(n=>n.replace(/^\.\//,"")))]}async function Vs(e,t,n){let r=await D(e,n),s=r?await L(e,n):null;return{reference:{source:t,path:n,exists:r},content:s}}async function In(e,t){for(let n of await En(e,".claude/hooks")){let r=await L(e,n);r!==null&&t.push({source:n,path:n,isStop:/stop/i.test(ni.posix.basename(n)),content:r})}}async function Ln(e){for(let t of Ms)if(await D(e,t))return!0;return!1}function Tn(e){let t=e.hooks;if(t===void 0)return{hooks:[],malformed:!1};if(!j(t))return{hooks:[],malformed:!0};let n=[],r=!1;for(let[s,o]of Object.entries(t)){if(!Array.isArray(o)){r=!0;continue}for(let i of o){if(!j(i)||!Array.isArray(i.hooks)){r=!0;continue}for(let a of i.hooks){if(!j(a)||typeof a.command!="string"){r=!0;continue}n.push({event:s,command:a.command})}}}return{hooks:n,malformed:r}}async function Fn(e,t){let n=await L(e,t);if(n===null)return null;let{ok:r,value:s}=Be(n);if(!r||!j(s))return{path:t,valid:!1,allow:[],defaultMode:null,hooks:[],hooksMalformed:!1};let{hooks:o,malformed:i}=Tn(s);return{path:t,valid:!0,allow:js(s),defaultMode:Ws(s),hooks:o,hooksMalformed:i}}async function Hn(e){let t=".codex/hooks.json",n=await L(e,t);if(n===null)return null;let{ok:r,value:s}=Be(n);if(!r)return{path:t,valid:!1,referencedScripts:[]};let o=[];for(let i of Ke(JSON.stringify(s))){let a=await D(e,i);o.push({source:t,path:i,exists:a})}return{path:t,valid:!0,referencedScripts:o}}async function Pn(e){let t=[];for(let n of Ns)await D(e,n)&&t.push(n);return t}async function Nn(e,t){let n=[],r=[],s=t.flatMap(o=>o.hooks.map(i=>({surface:o,hook:i})));for(let{surface:o,hook:i}of s){let a=`${o.path} (${i.event} hook)`,c=[i.command];for(let u of Ke(i.command)){let{reference:p,content:m}=await Vs(e,a,u);r.push(p),m!==null&&c.push(m)}n.push({source:a,path:null,isStop:i.event==="Stop",content:c.join(`
|
|
123
123
|
`)})}return{hookScripts:n,referencedClaudeScripts:r}}async function _n(e){let t=[];for(let n of _s)await D(e,n)&&t.push(n);for(let n of Ds){let r=await L(e,n);r!==null&&$s.test(r)&&t.push(n)}return t}async function Se(e){let t=[];for(let u of Ps){let p=await Fn(e,u);p!==null&&t.push(p)}let{hookScripts:n,referencedClaudeScripts:r}=await Nn(e,t);await In(e,n);let[s,o,i,a,c]=await Promise.all([Hn(e),L(e,".gitignore"),Pn(e),_n(e),Ln(e)]);return{claudeSettings:t,referencedClaudeScripts:r,hookScripts:n,codexHooks:s,gitignore:o,envFilesPresent:i,secretScanningFiles:a,usesKeyringSecrets:c}}import{readFile as ri,stat as si}from"node:fs/promises";import Us from"node:path";async function J(e){let t=Us.join(e,".github/hooks"),n=!1;try{n=(await si(t)).isDirectory()}catch{n=!1}let r=Us.join(e,".vscode/settings.json"),s=null,o=null;try{let i=await ri(r,"utf8");o=i,s=pe(i)}catch{s=null,o=null}return{settings:s,settingsRaw:o,hooksDirExists:n}}function Dn(e){let t=e.contextBudget;if(t===void 0)return{findings:[],records:[]};let n=[],r=[];for(let s of t.harnesses)if(s.detected)for(let o of s.files)r.push({checkId:"adapter-context-budget",dimension:"maintainability",curve:"asymptoticDecay",measuredValue:o.lines,referenceValue:t.maxRecommendedFileLines}),!(o.lines<=t.maxRecommendedFileLines)&&n.push({ruleId:"adapter-context-budget",severity:"info",message:`${o.path} contributes ${String(o.lines)} always-loaded lines for ${s.harness}; recommended maximum per file is ${String(t.maxRecommendedFileLines)}.`,file:o.path,line:null,harnesses:[s.harness],hint:"Split detailed guidance into linked docs and keep always-loaded instruction files compact."});return{findings:n,records:r}}function Mn(e){let t=e.contextBudget;if(t===void 0)return{findings:[],records:[]};let n=[],r=[];for(let s of t.harnesses)s.detected&&(r.push({checkId:"always-loaded-budget",dimension:"maintainability",curve:"asymptoticDecay",measuredValue:s.totalLines,referenceValue:s.maxRecommendedLines}),s.status!=="ok"&&n.push({ruleId:"always-loaded-budget",severity:"info",message:`${s.harness} always-loaded context is ${String(s.totalLines)} lines; recommended maximum is ${String(s.maxRecommendedLines)}.`,file:null,line:null,harnesses:[s.harness],hint:"Keep root adapters as short indexes and move deep guidance into linked docs, skills, agents, or workflows."}));return{findings:n,records:r}}async function $n(e){return e.files.some(n=>n.path.startsWith("agents/"))?await ee(e.repoRoot,".github/agents")?{findings:[{ruleId:"no-duplicate-agent-trees",severity:"error",message:".github/agents/ duplicates root agents/; use agents/ only.",file:".github/agents",line:null,harnesses:[],hint:"Remove .github/agents/ and keep canonical agents/ at the repo root."}],records:[]}:{findings:[],records:[]}:{findings:[],records:[]}}async function On(e){if(!e.files.some(s=>s.path.startsWith("skills/")))return{findings:[],records:[]};let n=[],r=[".github/skills",".cursor/skills"];for(let s of r)await ee(e.repoRoot,s)&&n.push({ruleId:"no-duplicate-skill-trees",severity:"error",message:`${s}/ duplicates root skills/; use skills/ only.`,file:s,line:null,harnesses:[],hint:"Remove the shadow tree and keep canonical skills/ at the repo root."});return{findings:n,records:[]}}var qs=/!?\[([^\]]*)\]\(([^)]+)\)/g;function z(e){let t=[],n=qs.exec(e);for(;n!==null;){let[r]=n;r.startsWith("!")||t.push({label:n[1]??"",href:(n[2]??"").trim()}),n=qs.exec(e)}return t}var oi=new Set(["url","path","href","link","..."]);function we(e){return e.length===0||oi.has(e.toLowerCase())||e.startsWith("http://")||e.startsWith("https://")||e.startsWith("mailto:")||e.startsWith("tel:")?!0:(e.split("#")[0]??"").length===0}function xe(e,t){let n=e.split(`
|
|
124
|
-
`);for(let r=0;r<n.length;r+=1){let s=n[r];if(s!==void 0&&s.includes(t))return r+1}return null}function Gn(e){let t=e.replaceAll("\\","/").split("/"),n=[];for(let r of t)if(!(r===""||r===".")){if(r===".."){n.pop();continue}n.push(r)}return n.join("/")}function Re(e,t){let n=(t.split("#")[0]??"").trim();if(n.length===0)return null;let r=n;try{r=decodeURIComponent(n)}catch{r=n}if(r.startsWith("/"))return r.slice(1).replaceAll("\\","/");let s=e.includes("/")?e.slice(0,e.lastIndexOf("/")):"",o=s.length===0?r:`${s}/${r}`;return Gn(o)}var ii=["docs/","skills/","agents/",".cursor/",".github/",".codex/",".agent/",".claude/"],ai=new Set(["AGENTS.md","CLAUDE.md","GEMINI.md","README.md",".github/copilot-instructions.md"]);function Bs(e){let t=e.replaceAll("\\","/").replace(/\/+$/,"");return ai.has(t)?!0:ii.some(n=>t===n.slice(0,-1)||t.startsWith(n))}var ci=["",".md","/README.md","/index.md"];async function Ks(e,t){for(let n of ci){let r=`${t}${n}`.replaceAll("\\","/");if(await ee(e,r))return!0}return!1}function jn(e){return/my-doc\.md|example\.spec\.|\/example\//i.test(e)}function Wn(e){return e.endsWith("/")?!0:e.endsWith(".md")||e.endsWith(".mdc")}var li=8;async function Vn(e){let t=[];for(let n of e.files){if(!n.path.endsWith(".md")&&!n.path.endsWith(".mdc"))continue;let r=f(e,n.path);if(r===null)continue;let s=0;for(let o of z(r)){if(we(o.href))continue;let i=Re(n.path,o.href);if(!(i===null||!Bs(i)||!Wn(i)||jn(i)||await Ks(e.repoRoot,i))&&(t.push({ruleId:"guidance-links-resolve",severity:"warn",message:`Broken link in ${n.path}: (${o.href}) does not resolve.`,file:n.path,line:xe(r,o.href),harnesses:[],hint:"Fix the path or add the missing guidance file."}),s+=1,s>=li))break}}return{findings:t,records:[]}}import{readFile as ui}from"node:fs/promises";import di from"node:path";async function Un(e){try{let t=await ui(di.join(e,"package.json"),"utf8"),n=JSON.parse(t);if(h(n)&&"scripts"in n){let{scripts:r}=n;if(h(r))return r}return null}catch{return null}}function qn(e){return e===null?!1:Object.entries(e).some(([t,n])=>t.toLowerCase().includes("qmd")||typeof n=="string"&&n.toLowerCase().includes("qmd"))}async function Bn(e){let t=T(e),n=e.files.filter(s=>s.path.startsWith("docs/ai/")).length;if(!t&&n<2)return{findings:[],records:[]};let r=await Un(e.repoRoot);return qn(r)?{findings:[],records:[]}:{findings:[{ruleId:"qmd-script-present",severity:"info",message:'package.json is missing a "qmd" (or equivalent) script for skill/doc search.',file:"package.json",line:null,harnesses:[],hint:'Add something like "qmd": "bun run ./scripts/qmd/qmd.bun.ts" and document usage in AGENTS.md.'}],records:[]}}async function Kn(e){let t=await J(e.repoRoot);return t.hooksDirExists?t.settings?.["chat.useCustomAgentHooks"]===!0?{findings:[],records:[]}:{findings:[{ruleId:"vscode-custom-hooks",severity:"warn",message:".github/hooks/ exists but chat.useCustomAgentHooks is not true in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.useCustomAgentHooks": true so Cursor and Copilot load workspace hooks.'}],records:[]}:{findings:[],records:[]}}async function Jn(e){let t=await J(e.repoRoot),n=[];return v(t,"chat.agentSkillsLocations","skills")||n.push({ruleId:"vscode-skills-location",severity:"warn",message:"chat.agentSkillsLocations should point at skills/ in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.agentSkillsLocations": { "skills/": true }.'}),v(t,"chat.agentFilesLocations","agents")||n.push({ruleId:"vscode-agents-location",severity:"warn",message:"chat.agentFilesLocations should point at agents/ in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.agentFilesLocations": { "agents/": true }.'}),{findings:n,records:[]}}function zn(e){let t=[];for(let n of Ee){if(!R(e,n))continue;let r=f(e,n);r===null||qr(r)||t.push({ruleId:"adapter-points-to-shared",severity:"warn",message:`${n} should reference AGENTS.md and docs/ai/rules.md.`,file:n,line:null,harnesses:[],hint:"Keep adapters thin; point at shared layers instead of duplicating rules."})}return{findings:t,records:[]}}function Yn(e){let t=[];for(let n of te(e,"agents/")){if(!n.endsWith(".agent.md"))continue;let r=f(e,n);if(r===null)continue;let s=Ie(r);if(s===null){t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing YAML frontmatter.`,file:n,line:null,harnesses:[]});continue}Z(s,"name")||t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing a name field in frontmatter.`,file:n,line:null,harnesses:[]}),Z(s,"description")||t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing a description field in frontmatter.`,file:n,line:null,harnesses:[]})}return{findings:t,records:[]}}function Xn(e){if(!T(e)||!R(e,"AGENTS.md"))return{findings:[],records:[]};let t=f(e,"AGENTS.md");return t!==null&&Le(t)?{findings:[],records:[]}:{findings:[{ruleId:"agents-md-mentions-skills",severity:"info",message:"AGENTS.md should point at docs/ai/available-skills.md or the qmd search workflow.",file:"AGENTS.md",line:null,harnesses:[],hint:"Document how agents discover skills (index doc or npm run qmd -- search)."}],records:[]}}function Qn(e){if(!e.harnesses.includes("claude")||!R(e,"CLAUDE.md"))return{findings:[],records:[]};let t=f(e,"CLAUDE.md");return t!==null&&Te(t)?{findings:[],records:[]}:{findings:[{ruleId:"claude-agent-routing",severity:"info",message:"CLAUDE.md should include an Agent Routing table pointing at agents/*.agent.md.",file:"CLAUDE.md",line:null,harnesses:["claude"],hint:"Add a markdown table mapping task types to shared agents/*.agent.md files."}],records:[]}}function Zn(e){return!e.endsWith(".md")&&!e.endsWith(".mdc")?!1:e.startsWith("docs/")||e.includes("/docs/")}function er(e,t){for(let n of z(t)){if(we(n.href))continue;let r=Re(e,n.href);if(r!==null&&Zn(r))return!0}return!1}var pi=50;function tr(e){let t=[];for(let n of e.files){if(!n.path.startsWith("skills/")||!n.path.endsWith("SKILL.md")||(n.lines??0)<=pi)continue;let r=f(e,n.path);r===null||er(n.path,r)||t.push({ruleId:"skill-doc-deep-links",severity:"info",message:`${n.path} does not deep-link into a durable doc under docs/.`,file:n.path,line:null,harnesses:[],hint:"Pair the skill with a docs/ reference and link it (for example a **Details:** link to a docs/ page) instead of inlining all detail."})}return{findings:t,records:[]}}function nr(e){let t=[];for(let n of te(e,"skills/")){if(!n.endsWith("/SKILL.md")&&!n.endsWith("SKILL.md"))continue;let r=f(e,n);if(r===null)continue;let s=Ie(r);if(s===null){t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing YAML frontmatter.`,file:n,line:null,harnesses:[]});continue}Z(s,"name")||t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing a name field in frontmatter.`,file:n,line:null,harnesses:[]}),Z(s,"description")||t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing a description field in frontmatter.`,file:n,line:null,harnesses:[]})}return{findings:t,records:[]}}function rr(e){let t=[];for(let n of te(e,"skills/")){if(!n.endsWith("SKILL.md"))continue;let r=O(e,n);r!==null&&r>rt&&t.push({ruleId:"skill-line-count",severity:"warn",message:`${n} has ${String(r)} lines; keep skills under ${String(rt)}.`,file:n,line:null,harnesses:[],hint:"Move detail to docs/; keep SKILL.md as a pointer."})}return{findings:t,records:[]}}var Js="docs/ai/available-skills.md";function sr(e){return T(e)?R(e,Js)?{findings:[],records:[]}:{findings:[{ruleId:"skills-index",severity:"info",message:`${Js} is missing; add a skill slug index when using skills/.`,file:null,line:null,harnesses:[],hint:"List each skills/<slug>/ folder so agents can pick a minimal skill set."}],records:[]}:{findings:[],records:[]}}function Ce(e,t,n,r){return{ruleId:e,severity:"warn",message:`${t} has ${String(n)} lines; keep root adapters thin (max ${String($)}).`,file:t,line:null,harnesses:[r],hint:"Move detailed guidance to docs/ai/ and skills/; keep the adapter as a pointer."}}var fi=["AGENTS.md","CLAUDE.md","GEMINI.md",".github/copilot-instructions.md",".agent/README.md"],zs=["IMPORTANT","CRITICAL","MANDATORY","REQUIRED","MUST NOT","MUST","NEVER","ALWAYS","SHALL","DO NOT"],Ys=.03,Xs=40,Qs=[{pattern:/\byou are an? (?:expert|senior|world[- ]class|10x|highly skilled|seasoned|professional)\b/i,label:"identity framing (\u201Cyou are a/an \u2026\u201D)"},{pattern:/\bact as an?\b/i,label:"roleplay framing (\u201Cact as a \u2026\u201D)"},{pattern:/\bas an ai (?:language model|assistant)\b/i,label:"AI self-reference"},{pattern:/\bworld[- ]class\b/i,label:"superlative filler (\u201Cworld-class\u201D)"}];function Je(e){return e.replaceAll(/```[\s\S]*?```/g," ")}function Zs(e){let t=e.match(/\S+/g);return t===null?0:t.length}function ze(e){return fi.filter(t=>f(e,t)!==null)}function or(e){let t=0;for(let n of zs){let r=e.match(new RegExp(`\\b${n}\\b`,"g"));r!==null&&(t+=r.length)}return t}function ir(e){let t=[];for(let n of ze(e)){let r=f(e,n);if(r===null)continue;let s=Je(r),o=Zs(s);if(o<Xs)continue;let i=or(s),a=i/o;if(a<=Ys)continue;let c=(a*100).toFixed(1);t.push({ruleId:"emphasis-keyword-density",severity:"info",message:`${n} uses ${String(i)} all-caps emphasis keyword(s) across ${String(o)} words (${c}%); emphasis loses force when overused.`,file:n,line:null,harnesses:[],hint:"Reserve IMPORTANT/MUST/NEVER for the few rules that truly need them; rewrite the rest as plain, action-oriented instructions."})}return{findings:t,records:[]}}function ar(e){let t=[];for(let n of ze(e)){let r=f(e,n);if(r===null)continue;let s=Je(r),o=Qs.filter(({pattern:i})=>i.test(s)).map(({label:i})=>i);o.length!==0&&t.push({ruleId:"identity-language-absent",severity:"info",message:`${n} contains identity/roleplay filler: ${o.join(", ")}.`,file:n,line:null,harnesses:[],hint:"Drop identity framing; state what to do, not who to be. Action-oriented rules outperform persona prompts."})}return{findings:t,records:[]}}var eo=[{id:"emphasis-keyword-density",title:"Emphasis-keyword density",severity:"info",category:"instruction-quality",dimension:"maintainability",harnesses:[],check:ir},{id:"identity-language-absent",title:"No identity-language filler",severity:"info",category:"instruction-quality",dimension:"maintainability",harnesses:[],check:ar}];var mi=/\b(?:curl|wget|ncat|telnet|scp)\b|\bnc\s|Invoke-WebRequest|\bfetch\s*\(|\baxios\b|http\.request|urllib|requests\.(?:get|post|put|patch)/i,gi=/localhost|127\.0\.0\.1|0\.0\.0\.0|::1/i;function cr(e){let t=[];for(let n of e.content.split(/\r?\n/)){let r=n.trim();mi.test(r)&&!gi.test(r)&&t.push(r)}return t}var hi=new Set(["Bash","Write","Edit","MultiEdit","NotebookEdit"]);function lr(e){let t=e.trim();if(t==="")return!1;if(t==="*")return!0;if(t.startsWith("mcp__"))return t.includes("*");let n=t.match(/^([A-Za-z_][\w-]*)(?:\((.*)\))?$/);if(n===null)return!1;let[,r="",s]=n;if(!hi.has(r))return!1;if(s===void 0)return!0;let o=s.trim();return o===""||o==="*"||o===":*"}var yi=/^[0-9a-f]{40}$/i;function ur(e){let t=new Set,n=/^\s*-?\s*uses:\s*['"]?([^'"\s#]+)['"]?/gm;for(let r of e.matchAll(n)){let[,s]=r;if(s===void 0||s.startsWith("./")||s.startsWith("docker://"))continue;let o=s.includes("@")?s.slice(s.lastIndexOf("@")+1):"";yi.test(o)||t.add(s)}return[...t]}var M={findings:[],records:[]},ki=new Set(["bypassPermissions","acceptEdits"]),Si="stop_hook_active";function Y(e){return e.security??null}var wi={id:"no-dangerous-auto-approve",title:"No dangerous auto-approve permissions",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.claudeSettings){r.defaultMode!==null&&ki.has(r.defaultMode)&&n.push({ruleId:"no-dangerous-auto-approve",severity:"error",message:`${r.path} sets permissions.defaultMode "${r.defaultMode}", auto-approving actions without prompting.`,file:r.path,line:null,harnesses:["claude"],hint:'Use "default" or "plan" mode and allow-list specific commands instead of bypassing prompts.'});for(let s of r.allow.filter(lr))n.push({ruleId:"no-dangerous-auto-approve",severity:"error",message:`${r.path} auto-approves "${s}", a blanket grant over a dangerous capability.`,file:r.path,line:null,harnesses:["claude"],hint:"Scope allow-list entries to specific commands, e.g. Bash(npm run test:*) instead of Bash(*)."})}return{findings:n,records:[]}}},xi={id:"hook-no-network-exfil",title:"Hooks do not exfiltrate over the network",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.hookScripts){let[s]=cr(r);s!==void 0&&n.push({ruleId:"hook-no-network-exfil",severity:"error",message:`${r.source} makes a network call (\`${s.slice(0,80)}\`); hooks run automatically and can exfiltrate repo data.`,file:r.path,line:null,harnesses:[],hint:"Remove the network call or restrict it to localhost; move outbound calls into explicit, reviewable steps."})}return{findings:n,records:[]}}},Ri={id:"hook-stop-circuit-breaker",title:"Stop hooks guard against infinite loops",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.hookScripts)!r.isStop||r.content.includes(Si)||n.push({ruleId:"hook-stop-circuit-breaker",severity:"warn",message:`${r.source} is a Stop hook that never checks stop_hook_active; a blocking Stop hook can loop the agent indefinitely.`,file:r.path,line:null,harnesses:[],hint:"Read stop_hook_active from the hook input and exit without blocking when it is true."});return{findings:n,records:[]}}},Ci={id:"env-files-gitignored",title:"Env files are gitignored",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let r=t.gitignore!==null&&/^\s*!?[*.]*\.env(?:[.*]|\b|\/)?/m.test(t.gitignore)?[]:t.envFilesPresent;if(r.length===0)return M;let s=t.usesKeyringSecrets?"This repo uses keyring-managed secrets; delete the committed .env file rather than relying on it.":"Gitignore `.env`/`.env.*`, or move secrets to an OS keyring (the run-with-env pattern) so they never touch disk.";return{findings:[{ruleId:"env-files-gitignored",severity:"warn",message:`${r.join(", ")} present but not gitignored; secrets risk being committed.`,file:r[0]??null,line:null,harnesses:[],hint:s}],records:[]}}};function bi(e){return e.some(t=>/gitleaks|trufflehog|detect-secrets|ggshield/i.test(t.content))}var Ai={id:"secret-scanning-configured",title:"Secret scanning is configured",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=e.intelligenceLayer?.workflows??[];return t.secretScanningFiles.length>0||bi(n)?M:{findings:[{ruleId:"secret-scanning-configured",severity:"warn",message:"No secret-scanning configuration found (gitleaks, trufflehog, detect-secrets, or a pre-commit secret hook).",file:null,line:null,harnesses:[],hint:"Add a gitleaks/trufflehog CI step or a detect-secrets pre-commit hook so committed credentials are caught."}],records:[]}}},vi={id:"no-pull-request-target",title:"No risky pull_request_target workflows",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=e.intelligenceLayer?.workflows??[],n=[];for(let r of t)/(^|\n)\s*(?:-\s*)?pull_request_target\s*:?/.test(r.content)&&n.push({ruleId:"no-pull-request-target",severity:"error",message:`${r.path} triggers on pull_request_target, which runs with repo secrets against untrusted PR code.`,file:r.path,line:null,harnesses:[],hint:"Prefer pull_request; if pull_request_target is required, never check out or execute PR head code with secrets in scope."});return{findings:n,records:[]}}},Ei={id:"actions-sha-pinned",title:"GitHub Actions pinned to commit SHAs",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=e.intelligenceLayer?.workflows??[],n=[];for(let r of t){let s=ur(r.content);s.length!==0&&n.push({ruleId:"actions-sha-pinned",severity:"warn",message:`${r.path} uses actions pinned to mutable refs: ${s.join(", ")}.`,file:r.path,line:null,harnesses:[],hint:"Pin each action to a full 40-character commit SHA (e.g. actions/checkout@<sha>) to prevent tag-hijack supply-chain attacks."})}return{findings:n,records:[]}}},Ii={id:"claude-hooks-valid",title:"Claude hooks config is valid",severity:"error",category:"hooks",dimension:"guardrails",harnesses:["claude"],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.claudeSettings){if(!r.valid){n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.path} is not valid JSON.`,file:r.path,line:null,harnesses:["claude"],hint:"Fix the JSON syntax; an unparseable settings file disables every configured hook silently."});continue}r.hooksMalformed&&n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.path} has a malformed hooks block; entries must be { matcher?, hooks: [{ type, command }] }.`,file:r.path,line:null,harnesses:["claude"],hint:"Match the documented hooks schema so the configured commands actually run."})}for(let r of t.referencedClaudeScripts.filter(s=>!s.exists))n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.source} references missing script ${r.path}.`,file:r.path,line:null,harnesses:["claude"],hint:"Create the script or fix the path; a missing hook command fails open and skips the check."});return{findings:n,records:[]}}},Li={id:"codex-hooks-valid",title:"Codex hooks config is valid",severity:"error",category:"hooks",dimension:"guardrails",harnesses:["codex"],check(e){let t=Y(e);if(t===null||t.codexHooks===null)return M;let n=t.codexHooks,r=[];n.valid||r.push({ruleId:"codex-hooks-valid",severity:"error",message:`${n.path} is not valid JSON.`,file:n.path,line:null,harnesses:["codex"],hint:"Fix the JSON syntax so the configured codex hooks load."});for(let s of n.referencedScripts.filter(o=>!o.exists))r.push({ruleId:"codex-hooks-valid",severity:"error",message:`${n.path} references missing script ${s.path}.`,file:s.path,line:null,harnesses:["codex"],hint:"Create the script or fix the path so the hook command runs."});return{findings:r,records:[]}}},to=[wi,xi,Ri,Ci,Ai,vi,Ei,Ii,Li];var Ti={id:"shared-agents-md",title:"Shared AGENTS.md entry point",severity:"warn",category:"structure",dimension:"layering",harnesses:[],check(e){return R(e,"AGENTS.md")?{findings:[],records:[]}:{findings:[{ruleId:"shared-agents-md",severity:"warn",message:"AGENTS.md is missing.",file:null,line:null,harnesses:[],hint:"Add AGENTS.md as the repo-wide AI entry point with workflow, safety, and pointers to canonical docs."}],records:[]}}},Fi={id:"shared-rules-doc",title:"Canonical rules doc",severity:"warn",category:"structure",dimension:"layering",harnesses:[],check(e){return R(e,"docs/ai/rules.md")?{findings:[],records:[]}:{findings:[{ruleId:"shared-rules-doc",severity:"warn",message:"docs/ai/rules.md is missing.",file:null,line:null,harnesses:[],hint:"Add docs/ai/rules.md as the canonical coding-rules reference."}],records:[]}}},Hi={id:"adapter-thin-claude",title:"Thin CLAUDE.md adapter",severity:"warn",category:"adapter",dimension:"layering",harnesses:["claude"],check(e){let t=O(e,"CLAUDE.md");return t===null||t<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-claude","CLAUDE.md",t,"claude")],records:[]}}},Pi={id:"adapter-thin-gemini",title:"Thin GEMINI.md adapter",severity:"warn",category:"adapter",dimension:"layering",harnesses:["gemini"],check(e){let t=O(e,"GEMINI.md");return t===null||t<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-gemini","GEMINI.md",t,"gemini")],records:[]}}},Ni={id:"adapter-thin-copilot",title:"Thin Copilot instructions",severity:"warn",category:"adapter",dimension:"layering",harnesses:["copilot"],check(e){let t=".github/copilot-instructions.md",n=O(e,t);return n===null||n<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-copilot",t,n,"copilot")],records:[]}}},_i={id:"adapter-points-to-shared",title:"Adapters reference shared layers",severity:"warn",category:"adapter",dimension:"layering",harnesses:[],check:zn},Di={id:"skill-frontmatter",title:"Skill frontmatter",severity:"error",category:"skills",dimension:"maintainability",harnesses:[],check:nr},Mi={id:"skill-line-count",title:"Skill line budget",severity:"warn",category:"skills",dimension:"maintainability",harnesses:[],check:rr},$i={id:"agent-frontmatter",title:"Agent frontmatter",severity:"error",category:"agents",dimension:"maintainability",harnesses:[],check:Yn},Oi={id:"skills-index",title:"Skill slug index",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:sr},Gi={id:"claude-agent-routing",title:"Claude agent routing table",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:["claude"],check:Qn},ji={id:"agents-md-mentions-skills",title:"AGENTS.md skill discovery",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:Xn},Wi={id:"skill-doc-deep-links",title:"Skills deep-link into docs",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:tr},no=[Ti,Fi,Hi,Pi,Ni,_i,Di,Mi,$i,Oi,Gi,ji,Wi,...eo,...to];function W(e,t,n,r,s){e.push(...s.findings),t.push(...s.records),s.records.length===0&&t.push({checkId:n,dimension:r,rawScore:s.findings.length>0?0:1})}function be(e,t){return e.length===0?!0:e.some(n=>t.includes(n))}function dr(e,t,n){return be(n,e.harnesses)?t===null||t.length===0?!0:n.some(r=>t.includes(r)):!1}function pr(e,t,n){return be(e.harnesses,t.harnesses)?n===null||n.length===0||e.harnesses.length===0?!0:e.harnesses.some(r=>n.includes(r)):!1}function X(e){return e.trim().replaceAll(/\s+/g," ")}function fr(e){let t=X(e);return!(t.length<20||/^#{1,6}\s/.test(t)||/^[-*]\s/.test(t)&&t.length<40||t.includes("AGENTS.md")||t.includes("docs/ai/rules.md"))}function Ye(e){return e.map(t=>X(t)).filter(t=>t.length>0).join(`
|
|
124
|
+
`);for(let r=0;r<n.length;r+=1){let s=n[r];if(s!==void 0&&s.includes(t))return r+1}return null}function Gn(e){let t=e.replaceAll("\\","/").split("/"),n=[];for(let r of t)if(!(r===""||r===".")){if(r===".."){n.pop();continue}n.push(r)}return n.join("/")}function Re(e,t){let n=(t.split("#")[0]??"").trim();if(n.length===0)return null;let r=n;try{r=decodeURIComponent(n)}catch{r=n}if(r.startsWith("/"))return r.slice(1).replaceAll("\\","/");let s=e.includes("/")?e.slice(0,e.lastIndexOf("/")):"",o=s.length===0?r:`${s}/${r}`;return Gn(o)}var ii=["docs/","skills/","agents/",".cursor/",".github/",".codex/",".agent/",".claude/"],ai=new Set(["AGENTS.md","CLAUDE.md","GEMINI.md","README.md",".github/copilot-instructions.md"]);function Bs(e){let t=e.replaceAll("\\","/").replace(/\/+$/,"");return ai.has(t)?!0:ii.some(n=>t===n.slice(0,-1)||t.startsWith(n))}var ci=["",".md","/README.md","/index.md"];async function Ks(e,t){for(let n of ci){let r=`${t}${n}`.replaceAll("\\","/");if(await ee(e,r))return!0}return!1}function jn(e){return/my-doc\.md|example\.spec\.|\/example\//i.test(e)}function Wn(e){return e.endsWith("/")?!0:e.endsWith(".md")||e.endsWith(".mdc")}var li=8;async function Vn(e){let t=[];for(let n of e.files){if(!n.path.endsWith(".md")&&!n.path.endsWith(".mdc"))continue;let r=f(e,n.path);if(r===null)continue;let s=0;for(let o of z(r)){if(we(o.href))continue;let i=Re(n.path,o.href);if(!(i===null||!Bs(i)||!Wn(i)||jn(i)||await Ks(e.repoRoot,i))&&(t.push({ruleId:"guidance-links-resolve",severity:"warn",message:`Broken link in ${n.path}: (${o.href}) does not resolve.`,file:n.path,line:xe(r,o.href),harnesses:[],hint:"Fix the path or add the missing guidance file."}),s+=1,s>=li))break}}return{findings:t,records:[]}}import{readFile as ui}from"node:fs/promises";import di from"node:path";async function Un(e){try{let t=await ui(di.join(e,"package.json"),"utf8"),n=JSON.parse(t);if(h(n)&&"scripts"in n){let{scripts:r}=n;if(h(r))return r}return null}catch{return null}}function qn(e){return e===null?!1:Object.entries(e).some(([t,n])=>t.toLowerCase().includes("qmd")||typeof n=="string"&&n.toLowerCase().includes("qmd"))}async function Bn(e){let t=T(e),n=e.files.filter(s=>s.path.startsWith("docs/ai/")).length;if(!t&&n<2)return{findings:[],records:[]};let r=await Un(e.repoRoot);return qn(r)?{findings:[],records:[]}:{findings:[{ruleId:"qmd-script-present",severity:"info",message:'package.json is missing a "qmd" (or equivalent) script for skill/doc search.',file:"package.json",line:null,harnesses:[],hint:'Add something like "qmd": "bun run ./scripts/qmd/qmd.bun.ts" and document usage in AGENTS.md.'}],records:[]}}async function Kn(e){let t=await J(e.repoRoot);return t.hooksDirExists?t.settings?.["chat.useCustomAgentHooks"]===!0?{findings:[],records:[]}:{findings:[{ruleId:"vscode-custom-hooks",severity:"warn",message:".github/hooks/ exists but chat.useCustomAgentHooks is not true in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.useCustomAgentHooks": true so Cursor and Copilot load workspace hooks.'}],records:[]}:{findings:[],records:[]}}async function Jn(e){let t=await J(e.repoRoot),n=[];return v(t,"chat.agentSkillsLocations","skills")||n.push({ruleId:"vscode-skills-location",severity:"warn",message:"chat.agentSkillsLocations should point at skills/ in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.agentSkillsLocations": { "skills/": true }.'}),v(t,"chat.agentFilesLocations","agents")||n.push({ruleId:"vscode-agents-location",severity:"warn",message:"chat.agentFilesLocations should point at agents/ in .vscode/settings.json.",file:".vscode/settings.json",line:null,harnesses:["cursor","copilot"],hint:'Set "chat.agentFilesLocations": { "agents/": true }.'}),{findings:n,records:[]}}function zn(e){let t=[];for(let n of Ee){if(!R(e,n))continue;let r=f(e,n);r===null||qr(r)||t.push({ruleId:"adapter-points-to-shared",severity:"warn",message:`${n} should reference AGENTS.md and docs/ai/rules.md.`,file:n,line:null,harnesses:[],hint:"Keep adapters thin; point at shared layers instead of duplicating rules."})}return{findings:t,records:[]}}function Yn(e){let t=[];for(let n of te(e,"agents/")){if(!n.endsWith(".agent.md"))continue;let r=f(e,n);if(r===null)continue;let s=Ie(r);if(s===null){t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing YAML frontmatter.`,file:n,line:null,harnesses:[]});continue}Z(s,"name")||t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing a name field in frontmatter.`,file:n,line:null,harnesses:[]}),Z(s,"description")||t.push({ruleId:"agent-frontmatter",severity:"error",message:`${n} is missing a description field in frontmatter.`,file:n,line:null,harnesses:[]})}return{findings:t,records:[]}}function Xn(e){if(!T(e)||!R(e,"AGENTS.md"))return{findings:[],records:[]};let t=f(e,"AGENTS.md");return t!==null&&Le(t)?{findings:[],records:[]}:{findings:[{ruleId:"agents-md-mentions-skills",severity:"info",message:"AGENTS.md should point at docs/ai/available-skills.md or the qmd search workflow.",file:"AGENTS.md",line:null,harnesses:[],hint:"Document how agents discover skills (index doc or npm run qmd -- search)."}],records:[]}}function Qn(e){if(!e.harnesses.includes("claude")||!R(e,"CLAUDE.md"))return{findings:[],records:[]};let t=f(e,"CLAUDE.md");return t!==null&&Te(t)?{findings:[],records:[]}:{findings:[{ruleId:"claude-agent-routing",severity:"info",message:"CLAUDE.md should include an Agent Routing table pointing at agents/*.agent.md.",file:"CLAUDE.md",line:null,harnesses:["claude"],hint:"Add a markdown table mapping task types to shared agents/*.agent.md files."}],records:[]}}function Zn(e){return!e.endsWith(".md")&&!e.endsWith(".mdc")?!1:e.startsWith("docs/")||e.includes("/docs/")}function er(e,t){for(let n of z(t)){if(we(n.href))continue;let r=Re(e,n.href);if(r!==null&&Zn(r))return!0}return!1}var pi=50;function tr(e){let t=[];for(let n of e.files){if(!n.path.startsWith("skills/")||!n.path.endsWith("SKILL.md")||(n.lines??0)<=pi)continue;let r=f(e,n.path);r===null||er(n.path,r)||t.push({ruleId:"skill-doc-deep-links",severity:"info",message:`${n.path} does not deep-link into a durable doc under docs/.`,file:n.path,line:null,harnesses:[],hint:"Pair the skill with a docs/ reference and link it (for example a **Details:** link to a docs/ page) instead of inlining all detail."})}return{findings:t,records:[]}}function nr(e){let t=[];for(let n of te(e,"skills/")){if(!n.endsWith("/SKILL.md")&&!n.endsWith("SKILL.md"))continue;let r=f(e,n);if(r===null)continue;let s=Ie(r);if(s===null){t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing YAML frontmatter.`,file:n,line:null,harnesses:[]});continue}Z(s,"name")||t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing a name field in frontmatter.`,file:n,line:null,harnesses:[]}),Z(s,"description")||t.push({ruleId:"skill-frontmatter",severity:"error",message:`${n} is missing a description field in frontmatter.`,file:n,line:null,harnesses:[]})}return{findings:t,records:[]}}function rr(e){let t=[];for(let n of te(e,"skills/")){if(!n.endsWith("SKILL.md"))continue;let r=O(e,n);r!==null&&r>rt&&t.push({ruleId:"skill-line-count",severity:"warn",message:`${n} has ${String(r)} lines; keep skills under ${String(rt)}.`,file:n,line:null,harnesses:[],hint:"Move detail to docs/; keep SKILL.md as a pointer."})}return{findings:t,records:[]}}var Js="docs/ai/available-skills.md";function sr(e){return T(e)?R(e,Js)?{findings:[],records:[]}:{findings:[{ruleId:"skills-index",severity:"info",message:`${Js} is missing; add a skill slug index when using skills/.`,file:null,line:null,harnesses:[],hint:"List each skills/<slug>/ folder so agents can pick a minimal skill set."}],records:[]}:{findings:[],records:[]}}function Ce(e,t,n,r){return{ruleId:e,severity:"warn",message:`${t} has ${String(n)} lines; keep root adapters thin (max ${String($)}).`,file:t,line:null,harnesses:[r],hint:"Move detailed guidance to docs/ai/ and skills/; keep the adapter as a pointer."}}var fi=["AGENTS.md","CLAUDE.md","GEMINI.md",".github/copilot-instructions.md",".agent/README.md"],zs=["IMPORTANT","CRITICAL","MANDATORY","REQUIRED","MUST NOT","MUST","NEVER","ALWAYS","SHALL","DO NOT"],Ys=.03,Xs=40,Qs=[{pattern:/\byou are an? (?:expert|senior|world[- ]class|10x|highly skilled|seasoned|professional)\b/i,label:"identity framing (\u201Cyou are a/an \u2026\u201D)"},{pattern:/\bact as an?\b/i,label:"roleplay framing (\u201Cact as a \u2026\u201D)"},{pattern:/\bas an ai (?:language model|assistant)\b/i,label:"AI self-reference"},{pattern:/\bworld[- ]class\b/i,label:"superlative filler (\u201Cworld-class\u201D)"}];function Je(e){return e.replaceAll(/```[\s\S]*?```/g," ")}function Zs(e){let t=e.match(/\S+/g);return t===null?0:t.length}function ze(e){return fi.filter(t=>f(e,t)!==null)}function or(e){let t=0;for(let n of zs){let r=e.match(new RegExp(`\\b${n}\\b`,"g"));r!==null&&(t+=r.length)}return t}function ir(e){let t=[];for(let n of ze(e)){let r=f(e,n);if(r===null)continue;let s=Je(r),o=Zs(s);if(o<Xs)continue;let i=or(s),a=i/o;if(a<=Ys)continue;let c=(a*100).toFixed(1);t.push({ruleId:"emphasis-keyword-density",severity:"info",message:`${n} uses ${String(i)} all-caps emphasis keyword(s) across ${String(o)} words (${c}%); emphasis loses force when overused.`,file:n,line:null,harnesses:[],hint:"Reserve IMPORTANT/MUST/NEVER for the few rules that truly need them; rewrite the rest as plain, action-oriented instructions."})}return{findings:t,records:[]}}function ar(e){let t=[];for(let n of ze(e)){let r=f(e,n);if(r===null)continue;let s=Je(r),o=Qs.filter(({pattern:i})=>i.test(s)).map(({label:i})=>i);o.length!==0&&t.push({ruleId:"identity-language-absent",severity:"info",message:`${n} contains identity/roleplay filler: ${o.join(", ")}.`,file:n,line:null,harnesses:[],hint:"Drop identity framing; state what to do, not who to be. Action-oriented rules outperform persona prompts."})}return{findings:t,records:[]}}var eo=[{id:"emphasis-keyword-density",title:"Emphasis-keyword density",severity:"info",category:"instruction-quality",dimension:"maintainability",harnesses:[],check:ir},{id:"identity-language-absent",title:"No identity-language filler",severity:"info",category:"instruction-quality",dimension:"maintainability",harnesses:[],check:ar}];var mi=/\b(?:curl|wget|ncat|telnet|scp)\b|\bnc\s|Invoke-WebRequest|\bfetch\s*\(|\baxios\b|http\.request|urllib|requests\.(?:get|post|put|patch)/i,gi=/localhost|127\.0\.0\.1|0\.0\.0\.0|::1/i;function cr(e){let t=[];for(let n of e.content.split(/\r?\n/)){let r=n.trim();mi.test(r)&&!gi.test(r)&&t.push(r)}return t}var hi=new Set(["Bash","Write","Edit","MultiEdit","NotebookEdit"]);function lr(e){let t=e.trim();if(t==="")return!1;if(t==="*")return!0;if(t.startsWith("mcp__"))return t.includes("*");let n=t.match(/^([A-Za-z_][\w-]*)(?:\((.*)\))?$/);if(n===null)return!1;let[,r="",s]=n;if(!hi.has(r))return!1;if(s===void 0)return!0;let o=s.trim();return o===""||o==="*"||o===":*"}var yi=/^[0-9a-f]{40}$/i;function ur(e){let t=new Set,n=/^\s*-?\s*uses:\s*['"]?([^'"\s#]+)['"]?/gm;for(let r of e.matchAll(n)){let[,s]=r;if(s===void 0||s.startsWith("./")||s.startsWith("docker://"))continue;let o=s.includes("@")?s.slice(s.lastIndexOf("@")+1):"";yi.test(o)||t.add(s)}return[...t]}var M={findings:[],records:[]},ki=new Set(["bypassPermissions","acceptEdits"]),Si="stop_hook_active";function Y(e){return e.security??null}var wi={id:"no-dangerous-auto-approve",title:"No dangerous auto-approve permissions",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.claudeSettings){r.defaultMode!==null&&ki.has(r.defaultMode)&&n.push({ruleId:"no-dangerous-auto-approve",severity:"error",message:`${r.path} sets permissions.defaultMode "${r.defaultMode}", auto-approving actions without prompting.`,file:r.path,line:null,harnesses:["claude"],hint:'Use "default" or "plan" mode and allow-list specific commands instead of bypassing prompts.'});for(let s of r.allow.filter(lr))n.push({ruleId:"no-dangerous-auto-approve",severity:"error",message:`${r.path} auto-approves "${s}", a blanket grant over a dangerous capability.`,file:r.path,line:null,harnesses:["claude"],hint:"Scope allow-list entries to specific commands, e.g. Bash(npm run test:*) instead of Bash(*)."})}return{findings:n,records:[]}}},xi={id:"hook-no-network-exfil",title:"Hooks do not exfiltrate over the network",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.hookScripts){let[s]=cr(r);s!==void 0&&n.push({ruleId:"hook-no-network-exfil",severity:"error",message:`${r.source} makes a network call (\`${s.slice(0,80)}\`); hooks run automatically and can exfiltrate repo data.`,file:r.path,line:null,harnesses:[],hint:"Remove the network call or restrict it to localhost; move outbound calls into explicit, reviewable steps."})}return{findings:n,records:[]}}},Ri={id:"hook-stop-circuit-breaker",title:"Stop hooks guard against infinite loops",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.hookScripts)!r.isStop||r.content.includes(Si)||n.push({ruleId:"hook-stop-circuit-breaker",severity:"warn",message:`${r.source} is a Stop hook that never checks stop_hook_active; a blocking Stop hook can loop the agent indefinitely.`,file:r.path,line:null,harnesses:[],hint:"Read stop_hook_active from the hook input and exit without blocking when it is true."});return{findings:n,records:[]}}},Ci={id:"env-files-gitignored",title:"Env files are gitignored",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let r=t.gitignore!==null&&/^\s*!?[*.]*\.env(?:[.*]|\b|\/)?/m.test(t.gitignore)?[]:t.envFilesPresent;if(r.length===0)return M;let s=t.usesKeyringSecrets?"This repo uses keyring-managed secrets; delete the committed .env file rather than relying on it.":"Gitignore `.env`/`.env.*`, or move secrets to an OS keyring (the run-with-env pattern) so they never touch disk.";return{findings:[{ruleId:"env-files-gitignored",severity:"warn",message:`${r.join(", ")} present but not gitignored; secrets risk being committed.`,file:r[0]??null,line:null,harnesses:[],hint:s}],records:[]}}};function bi(e){return e.some(t=>/gitleaks|trufflehog|detect-secrets|ggshield/i.test(t.content))}var Ai={id:"secret-scanning-configured",title:"Secret scanning is configured",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=Y(e);if(t===null)return M;let n=e.intelligenceLayer?.workflows??[];return t.secretScanningFiles.length>0||bi(n)?M:{findings:[{ruleId:"secret-scanning-configured",severity:"warn",message:"No secret-scanning configuration found (gitleaks, trufflehog, detect-secrets, or a pre-commit secret hook).",file:null,line:null,harnesses:[],hint:"Add a gitleaks/trufflehog CI step or a detect-secrets pre-commit hook so committed credentials are caught."}],records:[]}}},vi={id:"no-pull-request-target",title:"No risky pull_request_target workflows",severity:"error",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=e.intelligenceLayer?.workflows??[],n=[];for(let r of t)/\bpull_request_target\b/.test(r.content)&&n.push({ruleId:"no-pull-request-target",severity:"error",message:`${r.path} triggers on pull_request_target, which runs with repo secrets against untrusted PR code.`,file:r.path,line:null,harnesses:[],hint:"Prefer pull_request; if pull_request_target is required, never check out or execute PR head code with secrets in scope."});return{findings:n,records:[]}}},Ei={id:"actions-sha-pinned",title:"GitHub Actions pinned to commit SHAs",severity:"warn",category:"security",dimension:"guardrails",harnesses:[],check(e){let t=e.intelligenceLayer?.workflows??[],n=[];for(let r of t){let s=ur(r.content);s.length!==0&&n.push({ruleId:"actions-sha-pinned",severity:"warn",message:`${r.path} uses actions pinned to mutable refs: ${s.join(", ")}.`,file:r.path,line:null,harnesses:[],hint:"Pin each action to a full 40-character commit SHA (e.g. actions/checkout@<sha>) to prevent tag-hijack supply-chain attacks."})}return{findings:n,records:[]}}},Ii={id:"claude-hooks-valid",title:"Claude hooks config is valid",severity:"error",category:"hooks",dimension:"guardrails",harnesses:["claude"],check(e){let t=Y(e);if(t===null)return M;let n=[];for(let r of t.claudeSettings){if(!r.valid){n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.path} is not valid JSON.`,file:r.path,line:null,harnesses:["claude"],hint:"Fix the JSON syntax; an unparseable settings file disables every configured hook silently."});continue}r.hooksMalformed&&n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.path} has a malformed hooks block; entries must be { matcher?, hooks: [{ type, command }] }.`,file:r.path,line:null,harnesses:["claude"],hint:"Match the documented hooks schema so the configured commands actually run."})}for(let r of t.referencedClaudeScripts.filter(s=>!s.exists))n.push({ruleId:"claude-hooks-valid",severity:"error",message:`${r.source} references missing script ${r.path}.`,file:r.path,line:null,harnesses:["claude"],hint:"Create the script or fix the path; a missing hook command fails open and skips the check."});return{findings:n,records:[]}}},Li={id:"codex-hooks-valid",title:"Codex hooks config is valid",severity:"error",category:"hooks",dimension:"guardrails",harnesses:["codex"],check(e){let t=Y(e);if(t===null||t.codexHooks===null)return M;let n=t.codexHooks,r=[];n.valid||r.push({ruleId:"codex-hooks-valid",severity:"error",message:`${n.path} is not valid JSON.`,file:n.path,line:null,harnesses:["codex"],hint:"Fix the JSON syntax so the configured codex hooks load."});for(let s of n.referencedScripts.filter(o=>!o.exists))r.push({ruleId:"codex-hooks-valid",severity:"error",message:`${n.path} references missing script ${s.path}.`,file:s.path,line:null,harnesses:["codex"],hint:"Create the script or fix the path so the hook command runs."});return{findings:r,records:[]}}},to=[wi,xi,Ri,Ci,Ai,vi,Ei,Ii,Li];var Ti={id:"shared-agents-md",title:"Shared AGENTS.md entry point",severity:"warn",category:"structure",dimension:"layering",harnesses:[],check(e){return R(e,"AGENTS.md")?{findings:[],records:[]}:{findings:[{ruleId:"shared-agents-md",severity:"warn",message:"AGENTS.md is missing.",file:null,line:null,harnesses:[],hint:"Add AGENTS.md as the repo-wide AI entry point with workflow, safety, and pointers to canonical docs."}],records:[]}}},Fi={id:"shared-rules-doc",title:"Canonical rules doc",severity:"warn",category:"structure",dimension:"layering",harnesses:[],check(e){return R(e,"docs/ai/rules.md")?{findings:[],records:[]}:{findings:[{ruleId:"shared-rules-doc",severity:"warn",message:"docs/ai/rules.md is missing.",file:null,line:null,harnesses:[],hint:"Add docs/ai/rules.md as the canonical coding-rules reference."}],records:[]}}},Hi={id:"adapter-thin-claude",title:"Thin CLAUDE.md adapter",severity:"warn",category:"adapter",dimension:"layering",harnesses:["claude"],check(e){let t=O(e,"CLAUDE.md");return t===null||t<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-claude","CLAUDE.md",t,"claude")],records:[]}}},Pi={id:"adapter-thin-gemini",title:"Thin GEMINI.md adapter",severity:"warn",category:"adapter",dimension:"layering",harnesses:["gemini"],check(e){let t=O(e,"GEMINI.md");return t===null||t<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-gemini","GEMINI.md",t,"gemini")],records:[]}}},Ni={id:"adapter-thin-copilot",title:"Thin Copilot instructions",severity:"warn",category:"adapter",dimension:"layering",harnesses:["copilot"],check(e){let t=".github/copilot-instructions.md",n=O(e,t);return n===null||n<=$?{findings:[],records:[]}:{findings:[Ce("adapter-thin-copilot",t,n,"copilot")],records:[]}}},_i={id:"adapter-points-to-shared",title:"Adapters reference shared layers",severity:"warn",category:"adapter",dimension:"layering",harnesses:[],check:zn},Di={id:"skill-frontmatter",title:"Skill frontmatter",severity:"error",category:"skills",dimension:"maintainability",harnesses:[],check:nr},Mi={id:"skill-line-count",title:"Skill line budget",severity:"warn",category:"skills",dimension:"maintainability",harnesses:[],check:rr},$i={id:"agent-frontmatter",title:"Agent frontmatter",severity:"error",category:"agents",dimension:"maintainability",harnesses:[],check:Yn},Oi={id:"skills-index",title:"Skill slug index",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:sr},Gi={id:"claude-agent-routing",title:"Claude agent routing table",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:["claude"],check:Qn},ji={id:"agents-md-mentions-skills",title:"AGENTS.md skill discovery",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:Xn},Wi={id:"skill-doc-deep-links",title:"Skills deep-link into docs",severity:"info",category:"discoverability",dimension:"discoverability",harnesses:[],check:tr},no=[Ti,Fi,Hi,Pi,Ni,_i,Di,Mi,$i,Oi,Gi,ji,Wi,...eo,...to];function W(e,t,n,r,s){e.push(...s.findings),t.push(...s.records),s.records.length===0&&t.push({checkId:n,dimension:r,rawScore:s.findings.length>0?0:1})}function be(e,t){return e.length===0?!0:e.some(n=>t.includes(n))}function dr(e,t,n){return be(n,e.harnesses)?t===null||t.length===0?!0:n.some(r=>t.includes(r)):!1}function pr(e,t,n){return be(e.harnesses,t.harnesses)?n===null||n.length===0||e.harnesses.length===0?!0:e.harnesses.some(r=>n.includes(r)):!1}function X(e){return e.trim().replaceAll(/\s+/g," ")}function fr(e){let t=X(e);return!(t.length<20||/^#{1,6}\s/.test(t)||/^[-*]\s/.test(t)&&t.length<40||t.includes("AGENTS.md")||t.includes("docs/ai/rules.md"))}function Ye(e){return e.map(t=>X(t)).filter(t=>t.length>0).join(`
|
|
125
125
|
`)}var Xe=3,Vi=80;function mr(e,t){let n=e.split(`
|
|
126
126
|
`),r=Ye(t.split(`
|
|
127
127
|
`)),s=[],o=new Set;for(let i=0;i<=n.length-Xe;i+=1){if(o.has(i))continue;let a=n.slice(i,i+Xe);if(!a.every(u=>fr(u)))continue;let c=Ye(a);if(!(c.length<Vi)&&r.includes(c)){s.push({startLine:i+1,lineCount:Xe,sample:X(a[0]??"")}),o.add(i);for(let u=1;u<Xe;u+=1)o.add(i+u)}}return s}var gr="docs/ai/rules.md";function hr(e){let t=f(e,gr);if(t===null)return{findings:[],records:[]};let n=[];for(let r of Ee){let s=f(e,r);if(s===null)continue;let o=mr(s,t);for(let i of o)n.push({ruleId:"adapter-content-duplication",severity:"warn",message:`${r} duplicates ${String(i.lineCount)} lines from ${gr} near line ${String(i.startLine)}.`,file:r,line:i.startLine,harnesses:[],hint:`Replace duplicated rules with a pointer to ${gr}.`})}return{findings:n,records:[]}}function se(e){return e.intelligenceLayer??null}function oe(e){return e.packageJson.path!==null||e.workflows.length>0||e.prTemplates.length>0}function g(e,t,n,r){return{ruleId:e,severity:"info",message:t,file:r,line:null,harnesses:[],hint:n}}function yr(e){let t=se(e);return t===null||!oe(t)?{findings:[],records:[]}:t.guidanceCiWorkflows.length>0?{findings:[],records:[]}:{findings:[g("ci-guidance-lint","No CI guidance-maintenance gate was detected.","Run lint:md, check:ai-system, check:links, scan:self, or an equivalent guidance check in CI.",t.workflows[0]?.path??t.packageJson.path)],records:[]}}function w(e){return e.intelligenceLayer??null}function ie(e){return Object.entries(e.packageJson.scripts).map(([t,n])=>({name:t,command:n}))}function ae(e,t,n){return ie(e).some(r=>t.test(r.name)||n.test(r.command))}function ro(e,t){return ie(e).filter(n=>t.test(n.name)||t.test(n.command)).map(n=>n.name).toSorted()}function ce(e){return e.workflows.map(t=>t.commands.join(`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dimensions": {
|
|
3
|
+
"layering": { "weight": 1, "max_score": 100 },
|
|
4
|
+
"sharing": { "weight": 1, "max_score": 100 },
|
|
5
|
+
"discoverability": { "weight": 1, "max_score": 100 },
|
|
6
|
+
"harnessWiring": { "weight": 1, "max_score": 100 },
|
|
7
|
+
"maintainability": { "weight": 1, "max_score": 100 },
|
|
8
|
+
"guardrails": { "weight": 1, "max_score": 100 },
|
|
9
|
+
"session": { "weight": 1, "max_score": 100 },
|
|
10
|
+
"deep": { "weight": 1, "max_score": 100 }
|
|
11
|
+
},
|
|
12
|
+
"check_weights": {}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paniolo/scan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Scan a repository for AI coding agent harness best practices (diagnostic only)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/**/*.js",
|
|
20
|
+
"dist/**/*.json",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|