@tech-leads-club/harness-toolkit 0.12.0 → 0.12.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.
@@ -1,4 +1,4 @@
1
- import{createRequire as F$}from"node:module";var l=F$(import.meta.url);import{existsSync as B,lstatSync as h8,readFileSync as S,readlinkSync as p8,realpathSync as K$,rmSync as c8,statSync as d8,unlinkSync as l8,writeFileSync as i8}from"node:fs";import{homedir as n8}from"node:os";import{basename as a8,dirname as M$,isAbsolute as O$,join as G,relative as s8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as MZ,dirname as OZ,join as D,resolve as CZ}from"node:path";function i(){return D(L(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:D(L(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:D(L(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||D(L(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function L$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var D$=L$(s);var T$=D$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var N5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",L5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var h9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function F($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
1
+ import{createRequire as I$}from"node:module";var l=I$(import.meta.url);import{existsSync as B,lstatSync as h8,readFileSync as S,readlinkSync as p8,realpathSync as K$,rmSync as c8,statSync as d8,unlinkSync as l8,writeFileSync as i8}from"node:fs";import{homedir as n8}from"node:os";import{basename as a8,dirname as M$,isAbsolute as O$,join as G,relative as s8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as MZ,dirname as OZ,join as D,resolve as CZ}from"node:path";function i(){return D(j(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:D(j(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:D(j(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||D(j(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function j$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var D$=j$(s);var T$=D$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var N5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",j5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var h9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function I($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
2
2
  `)}var H={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var Q8=["No harness decision in this window.","Whatever you just saw was the model, not a rail — the harness allowed everything it was asked about."].join(`
3
- `);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},secrets:{redactOutput:!0},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as Eq,readFileSync as y8,realpathSync as kq,writeFileSync as v8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function m8($){return z$($)}var g8="tlc-exec.mjs";function u8($){return JSON.stringify($??null).includes(g8)}function g($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=m8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((_)=>!u8(_));if(U.length!==W.length)q=!0;if(U.length>0)Q[z]=U}let X={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")X[z]=W;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function W$($){if(!f8($))return{ok:!0,settingsText:"",changed:!1};let Z=g(y8($,"utf8"));if(Z.ok&&Z.changed)v8($,Z.settingsText,"utf8");return Z}function u($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let J;try{J=JSON.parse($)}catch{return{kind:"unparsed"}}if(J===null||typeof J!=="object"||Array.isArray(J))return{kind:"unparsed"};let Q=J,q=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},X={},z=0,W=0;for(let[U,_]of Object.entries(q)){let d=Array.isArray(_)?_:[],j=d.filter((I$)=>!JSON.stringify(I$??null).includes(Z));if(z+=d.length-j.length,j.length>0)X[U]=j,W+=j.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:X},null,2)}
4
- `}}import{join as Y$,relative as vq,resolve as mq,sep as gq}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],nq=[Y$("tools","dev"),Y$("tools","__test__")];function r8($=process.env){let Z=n8(),J=b($);return{home:C($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(A(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(A(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return h8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(a8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=p8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function o8($,Z,J={relative:s8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return o8(x($),c(Z))}var t8="tlc-exec.mjs";function e8($){try{return d8($).size<4096&&S($,"utf8").includes(t8)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(e8(Z)){$.push({action:"remove",target:Z,detail:`${Q}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${Q} is a real file the installer did not write`});return}if(q==="target"&&!B$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!B$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function $Z($,Z){if(!B(Z))return;let J=g(S(Z,"utf8"));if(!J.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${J.error}`});return}if(!J.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function ZZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=u(J);switch(Q.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(Q.removed>0)$.push({action:"remove",target:Z,detail:`${Q.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${Q.removed} harness entries, keep the rest`})}}function QZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let X of V$){let z=G(Z,X);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=G(Z,k);if(B(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of G$){let z=G(Z,X);if(!B(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function JZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${N}`,detail:"the global package is reported, never removed for you"});$.push({action:"manual",target:"rm -rf .tlc/ in each repository",detail:"per-project config and state — this command does not search your disk for them"})}function qZ($,Z={}){let J=Z.purge===!0,Q=[];$Z(Q,$.claudeSettings),ZZ(Q,$.cursorHooks);for(let X of $.skillLinks)U$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)U$(Q,X,$.home,"the tlc launcher on PATH","target");let q=QZ(Q,$.home,J);return JZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function XZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=u(S(q.target,"utf8"));if(X.kind==="rewritten")i8(q.target,X.text,"utf8")}else if(q.action==="unlink")l8(q.target);else c8(q.target,{recursive:!0,force:!0});J.push(q)}catch(X){Q.push({item:q,reason:X instanceof Error?X.message:String(X)})}return{applied:J,failed:Q}}var zZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function WZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:H$($)}],footer:`already clean · reinstall with npm i -g ${N}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:zZ[W.action]})),X=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:H$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[Q?`${Z.applied.length} applied`:`${J.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:Q?`reinstall any time with npm i -g ${N}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function H$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function YZ($,Z,J=O){return F(WZ($,Z),J)}function VZ($=process.argv.slice(2)){let Z=r8(),J=qZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?XZ(J,Z):null;return console.log(YZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=VZ();export{r8 as uninstallTargets,WZ as uninstallScreen,YZ as uninstallReportText,qZ as planUninstall,C$ as pendingItems,VZ as main,o8 as isInsideRoot,c as canonicalise,XZ as applyUninstall};
3
+ `);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},secrets:{redactOutput:!0},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as kq,readFileSync as y8,realpathSync as xq,writeFileSync as v8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function m8($){return z$($)}var g8="tlc-exec.mjs";function u8($){return JSON.stringify($??null).includes(g8)}function g($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=m8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((_)=>!u8(_));if(U.length!==W.length)q=!0;if(U.length>0)Q[z]=U}let X={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")X[z]=W;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function W$($){if(!f8($))return{ok:!0,settingsText:"",changed:!1};let Z=g(y8($,"utf8"));if(Z.ok&&Z.changed)v8($,Z.settingsText,"utf8");return Z}function u($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let J;try{J=JSON.parse($)}catch{return{kind:"unparsed"}}if(J===null||typeof J!=="object"||Array.isArray(J))return{kind:"unparsed"};let Q=J,q=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},X={},z=0,W=0;for(let[U,_]of Object.entries(q)){let d=Array.isArray(_)?_:[],L=d.filter((F$)=>!JSON.stringify(F$??null).includes(Z));if(z+=d.length-L.length,L.length>0)X[U]=L,W+=L.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:X},null,2)}
4
+ `}}import{join as Y$,relative as mq,resolve as gq,sep as uq}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],aq=[Y$("tools","dev"),Y$("tools","__test__")];function r8($=process.env){let Z=n8(),J=b($);return{home:C($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(A(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(A(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return h8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(a8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=p8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function o8($,Z,J={relative:s8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return o8(x($),c(Z))}var t8="tlc-exec.mjs";function e8($){try{return d8($).size<4096&&S($,"utf8").includes(t8)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(e8(Z)){$.push({action:"remove",target:Z,detail:`${Q}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${Q} is a real file the installer did not write`});return}if(q==="target"&&!B$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!B$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function $Z($,Z){if(!B(Z))return;let J=g(S(Z,"utf8"));if(!J.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${J.error}`});return}if(!J.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function ZZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=u(J);switch(Q.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(Q.removed>0)$.push({action:"remove",target:Z,detail:`${Q.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${Q.removed} harness entries, keep the rest`})}}function QZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let X of V$){let z=G(Z,X);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=G(Z,k);if(B(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of G$){let z=G(Z,X);if(!B(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function JZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${N}`,detail:"the global package is reported, never removed for you"});$.push({action:"manual",target:"rm -rf .tlc/ in each repository",detail:"per-project config and state — this command does not search your disk for them"})}function qZ($,Z={}){let J=Z.purge===!0,Q=[];$Z(Q,$.claudeSettings),ZZ(Q,$.cursorHooks);for(let X of $.skillLinks)U$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)U$(Q,X,$.home,"the tlc launcher on PATH","target");let q=QZ(Q,$.home,J);return JZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function XZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=u(S(q.target,"utf8"));if(X.kind==="rewritten")i8(q.target,X.text,"utf8")}else if(q.action==="unlink")l8(q.target);else c8(q.target,{recursive:!0,force:!0});J.push(q)}catch(X){Q.push({item:q,reason:X instanceof Error?X.message:String(X)})}return{applied:J,failed:Q}}var zZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function WZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:H$($)}],footer:`already clean · reinstall with npm i -g ${N}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:zZ[W.action]})),X=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:H$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[Q?`${Z.applied.length} applied`:`${J.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:Q?`reinstall any time with npm i -g ${N}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function H$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function YZ($,Z,J=O){return I(WZ($,Z),J)}function VZ($=process.argv.slice(2)){let Z=r8(),J=qZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?XZ(J,Z):null;return console.log(YZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=VZ();export{r8 as uninstallTargets,WZ as uninstallScreen,YZ as uninstallReportText,qZ as planUninstall,C$ as pendingItems,VZ as main,o8 as isInsideRoot,c as canonicalise,XZ as applyUninstall};
package/docs/concepts.md CHANGED
@@ -409,10 +409,12 @@ Posture reaches `ask` and nothing else: it interrupts under `paired` and hardens
409
409
  A pattern trigger is policy rather than containment. `pr-open` and `push` recognize their `gh api` REST
410
410
  equivalent (`POST` to a path ending in `pulls`; `POST`/`PATCH` to a path touching `git/refs`) alongside the CLI
411
411
  shape, and `pr-merge` recognizes `gh pr merge` and `PUT` to a path ending in `merge` under `pulls`
412
- ([/decisions/ad-128.md](/decisions/ad-128.md)) — but a script written to disk and executed later, a command
413
- name built at runtime, a GraphQL mutation, `curl` direct to the API, a `gh api` call that flips a pull request
414
- to ready without going through `gh pr ready`, or a pull request opened, approved, or merged in a browser all
415
- still escape every trigger this project has. The rule covers the agent's shell path
412
+ ([/decisions/ad-128.md](/decisions/ad-128.md)) — scoped to the local checkout's own remote, so a `gh api` push
413
+ to an unrelated repository (a template repo, a sibling project) is never gated by this project's own local
414
+ state ([/decisions/ad-130.md](/decisions/ad-130.md)) — but a script written to disk and executed later, a
415
+ command name built at runtime, a GraphQL mutation, `curl` direct to the API, a `gh api` call that flips a pull
416
+ request to ready without going through `gh pr ready`, or a pull request opened, approved, or merged in a
417
+ browser all still escape every trigger this project has. The rule covers the agent's shell path
416
418
  ([/decisions/ad-100.md](/decisions/ad-100.md)); it was never meant to cover more than that.
417
419
 
418
420
  **The durable, non-bypassable boundary is GitHub's own branch protection, not this mechanism.** A required
package/docs/log.md CHANGED
@@ -13,9 +13,14 @@ Generated from `docs/decisions/` — do not edit by hand. Run `node tools/render
13
13
  A reserved file of the [OKF v0.1](/decisions/ad-013.md) bundle: entries grouped under ISO 8601 headings,
14
14
  newest first. For what landed in which npm release, see `CHANGELOG.md` at the repository root.
15
15
 
16
+ ## 2026-09-10
17
+
18
+ - **AD-130** — gh api pr-open/push/pr-merge detection is scoped to the local checkout's own remote ([/decisions/ad-130.md](/decisions/ad-130.md))
19
+
16
20
  ## 2026-09-09
17
21
 
18
22
  - **AD-128** — pr-open and push recognize their gh api equivalent, and pr-merge exists ([/decisions/ad-128.md](/decisions/ad-128.md))
23
+ - **AD-129** — Turn-scope git diffing runs at the event's own working directory, not the project root ([/decisions/ad-129.md](/decisions/ad-129.md))
19
24
 
20
25
  ## 2026-09-08
21
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -178,7 +178,7 @@ import {
178
178
  readRuleSources,
179
179
  recordObservation,
180
180
  } from "./rules/rules.store.ts";
181
- import { firingRules, triggerMatches } from "./rules/rules.trigger.ts";
181
+ import { firingRules, mentionsGhApi, triggerMatches } from "./rules/rules.trigger.ts";
182
182
  import { scanForSecrets } from "./secret-scan/secret-scan.service.ts";
183
183
  import { placeholderFor } from "./secret-scan/secret-scan.store.ts";
184
184
  import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
@@ -500,6 +500,7 @@ export const coreFacade = {
500
500
  build: buildRuleSet,
501
501
  firing: firingRules,
502
502
  triggerMatches,
503
+ mentionsGhApi,
503
504
  observations: readObservations,
504
505
  record: recordObservation,
505
506
  observationFrom,
@@ -20,6 +20,13 @@ export type TriggerContext = {
20
20
  event: string;
21
21
  toolName?: string;
22
22
  command?: string;
23
+ /**
24
+ * why optional, and why `null` differs from `undefined`: unset means the caller never resolved this
25
+ * dimension (every existing caller, unaffected). `null` means it tried and found no confirmable local
26
+ * repo, which fails an API-shaped match rather than skipping the check
27
+ * ([/decisions/ad-130.md](/decisions/ad-130.md)).
28
+ */
29
+ repoRemote?: { owner: string; repo: string } | null;
23
30
  };
24
31
 
25
32
  /**
@@ -176,12 +183,24 @@ function inferredApiMethod(words: readonly string[]): string {
176
183
  return words.some((word) => API_BODY_FLAGS.has(word)) ? "POST" : "GET";
177
184
  }
178
185
 
179
- function matchesApiShape(words: readonly string[], shape: ApiShape): boolean {
186
+ function matchesApiShape(
187
+ words: readonly string[],
188
+ shape: ApiShape,
189
+ repoRemote?: { owner: string; repo: string } | null,
190
+ ): boolean {
180
191
  const path = apiPathArgument(words);
181
192
  if (path === undefined) {
182
193
  return false;
183
194
  }
184
195
  const segments = apiPathSegments(path);
196
+ if (repoRemote !== undefined) {
197
+ if (repoRemote === null) {
198
+ return false;
199
+ }
200
+ if (segments[0] !== "repos" || segments[1] !== repoRemote.owner || segments[2] !== repoRemote.repo) {
201
+ return false;
202
+ }
203
+ }
185
204
  const last = segments[segments.length - 1];
186
205
  if (shape.lastSegment !== undefined && last !== shape.lastSegment) {
187
206
  return false;
@@ -228,12 +247,13 @@ const API_SHAPES: Partial<Record<"pr-open" | "commit" | "push" | "pr-merge", rea
228
247
  function matchesAnyShape(
229
248
  words: readonly string[],
230
249
  kind: "pr-open" | "commit" | "push" | "pr-merge",
250
+ repoRemote?: { owner: string; repo: string } | null,
231
251
  ): boolean {
232
252
  if (SHELL_SHAPES[kind].some((shape) => matchesShape(words, shape))) {
233
253
  return true;
234
254
  }
235
255
  const apiShapes = API_SHAPES[kind] ?? [];
236
- return apiShapes.some((shape) => matchesApiShape(words, shape));
256
+ return apiShapes.some((shape) => matchesApiShape(words, shape, repoRemote));
237
257
  }
238
258
 
239
259
  export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): boolean {
@@ -249,7 +269,9 @@ export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): b
249
269
  if (context.command === undefined) {
250
270
  return false;
251
271
  }
252
- return subCommands(context.command).some((words) => matchesAnyShape(words, trigger.kind));
272
+ return subCommands(context.command).some((words) =>
273
+ matchesAnyShape(words, trigger.kind, context.repoRemote),
274
+ );
253
275
  }
254
276
  default: {
255
277
  if (context.command === undefined) {
@@ -264,3 +286,13 @@ export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): b
264
286
  export function firingRules(rules: readonly Rule[], context: TriggerContext): Rule[] {
265
287
  return rules.filter((rule) => rule.enabled && triggerMatches(rule.on, context));
266
288
  }
289
+
290
+ /**
291
+ * why exported: a caller deciding whether `repoRemote` is worth a process spawn only needs to know whether
292
+ * the API-shaped path (the only one that ever reads it) could possibly be in play — a CLI shape never
293
+ * matches this phrase, so resolving the remote first would spend a spawn no verdict can use
294
+ * ([/decisions/ad-130.md](/decisions/ad-130.md)).
295
+ */
296
+ export function mentionsGhApi(command: string): boolean {
297
+ return subCommands(command).some((words) => findPhraseIndex(words, ["gh", "api"]) !== -1);
298
+ }
@@ -4,7 +4,7 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import type { Decision, HarnessEvent } from "../contracts/index.ts";
6
6
  import { coreFacade } from "../core/index.ts";
7
- import { listAddedLines, listTrackedFiles } from "../platform/git.ts";
7
+ import { listAddedLines, listTrackedFiles, localRepoRemote } from "../platform/git.ts";
8
8
  import type { HandlerContext } from "./run.ts";
9
9
  import { runLockedGate } from "./stop.ts";
10
10
  import {
@@ -20,12 +20,28 @@ import {
20
20
  // ([/decisions/ad-128.md](/decisions/ad-128.md)).
21
21
  const FULL_BATTERY_KINDS = ["push", "pr-open", "pr-merge"] as const;
22
22
 
23
- function isShipCommand(event: HarnessEvent): boolean {
23
+ /**
24
+ * why a cheap pass first: `localRepoRemote` is a process spawn, and this runs on every shell command. The
25
+ * gh-api shape only needs it to disambiguate an unrelated repository's push from this project's own
26
+ * ([/decisions/ad-130.md](/decisions/ad-130.md)); a CLI-shape match or no match at all never pays for it.
27
+ */
28
+ async function isShipCommand(event: HarnessEvent, gitRoot: string): Promise<boolean> {
24
29
  if (event.command === undefined) {
25
30
  return false;
26
31
  }
27
32
  const context = { event: event.event, command: event.command };
28
- return FULL_BATTERY_KINDS.some((kind) => coreFacade.rules.triggerMatches({ kind }, context));
33
+ if (!FULL_BATTERY_KINDS.some((kind) => coreFacade.rules.triggerMatches({ kind }, context))) {
34
+ return false;
35
+ }
36
+ // why: a CLI shape (`gh pr create`, `git push`) never reads `repoRemote` — only a `gh api` call can, so a
37
+ // command that never mentions one already has its final answer.
38
+ if (!coreFacade.rules.mentionsGhApi(event.command)) {
39
+ return true;
40
+ }
41
+ const repoRemote = await localRepoRemote(gitRoot);
42
+ return FULL_BATTERY_KINDS.some((kind) =>
43
+ coreFacade.rules.triggerMatches({ kind }, { ...context, repoRemote }),
44
+ );
29
45
  }
30
46
 
31
47
  function gateFailureMessage(gate: string, command: readonly string[], outputTail: string): string {
@@ -44,7 +60,8 @@ function gateFailureMessage(gate: string, command: readonly string[], outputTail
44
60
  * once something fired, one `git rev-parse`.
45
61
  */
46
62
  export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
47
- if (!isShipCommand(event)) {
63
+ const shaRoot = shaScopeRoot(event);
64
+ if (!(await isShipCommand(event, shaRoot))) {
48
65
  return { kind: "abstain" };
49
66
  }
50
67
  const { policy } = ctx;
@@ -52,10 +69,9 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
52
69
  const provider = event.provider;
53
70
  const sessionKey = event.sessionKey;
54
71
  const session = sessionIdFromKey(event);
55
- const scope = await computeTurnScope(root, provider, sessionKey, policy);
56
- const shaRoot = shaScopeRoot(event);
72
+ const scope = await computeTurnScope(root, shaRoot, provider, sessionKey, policy);
57
73
 
58
- const commentHits = await pendingCommentViolations(root, provider, sessionKey, policy);
74
+ const commentHits = await pendingCommentViolations(root, shaRoot, provider, sessionKey, policy);
59
75
  if (commentHits.length > 0) {
60
76
  return {
61
77
  kind: "deny",
@@ -143,13 +159,13 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
143
159
  }
144
160
 
145
161
  if (policy.duplication.enabled && scope.codeTargets.length > 0) {
146
- const added = await listAddedLines(root, scope.codeTargets, scope.turnBase);
147
- const tracked = await listTrackedFiles(root);
162
+ const added = await listAddedLines(shaRoot, scope.codeTargets, scope.turnBase);
163
+ const tracked = await listTrackedFiles(shaRoot);
148
164
  const scan = coreFacade.duplication.scanProject(
149
165
  tracked,
150
166
  (relativePath) => {
151
167
  try {
152
- return readFileSync(`${root}/${relativePath}`, "utf8");
168
+ return readFileSync(`${shaRoot}/${relativePath}`, "utf8");
153
169
  } catch {
154
170
  return null;
155
171
  }
@@ -490,7 +490,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
490
490
  * says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
491
491
  */
492
492
  const deferred: string[] = [];
493
- const changedFiles = await listChangedRepoFiles(root, turnBase);
493
+ const changedFiles = await listChangedRepoFiles(shaRoot, turnBase);
494
494
  const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
495
495
  const testTargets = filterTestTargets(changedFiles);
496
496
  // why: the comment rail scopes by the syntax catalog (40+ languages), not `codeTargets`'s nine-extension
@@ -653,7 +653,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
653
653
  coreFacade.observe.shouldObserve(policy.observe, "comments", policy.comments.enabled)
654
654
  ) {
655
655
  const hits = await coreFacade.commentPolicy.scanAddedComments(
656
- root,
656
+ shaRoot,
657
657
  commentTargets,
658
658
  policy.comments.mode,
659
659
  turnBase,
@@ -680,7 +680,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
680
680
 
681
681
  if (policy.comments.enabled && policy.comments.onViolation === "followup" && commentTargets.length > 0) {
682
682
  const hits = await coreFacade.commentPolicy.scanAddedComments(
683
- root,
683
+ shaRoot,
684
684
  commentTargets,
685
685
  policy.comments.mode,
686
686
  turnBase,
@@ -708,13 +708,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
708
708
  if (policy.supplyChain.enabled && changedFiles.length > 0) {
709
709
  const manifests = changedFiles.filter((path) => coreFacade.supplyChain.isManifest(path));
710
710
  if (manifests.length > 0) {
711
- const added = await listAddedLines(root, manifests, turnBase);
711
+ const added = await listAddedLines(shaRoot, manifests, turnBase);
712
712
  const outcome = coreFacade.supplyChain.inspectSupplyChain({
713
713
  changedFiles,
714
714
  added,
715
715
  readManifest: (relativePath) => {
716
716
  try {
717
- return readFileSync(join(root, relativePath), "utf8");
717
+ return readFileSync(join(shaRoot, relativePath), "utf8");
718
718
  } catch {
719
719
  return null;
720
720
  }
@@ -742,13 +742,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
742
742
  * ([/decisions/ad-071.md](/decisions/ad-071.md)).
743
743
  */
744
744
  if (policy.duplication.enabled && codeTargets.length > 0) {
745
- const added = await listAddedLines(root, codeTargets, turnBase);
746
- const tracked = await listTrackedFiles(root);
745
+ const added = await listAddedLines(shaRoot, codeTargets, turnBase);
746
+ const tracked = await listTrackedFiles(shaRoot);
747
747
  const scan = coreFacade.duplication.scanProject(
748
748
  tracked,
749
749
  (relativePath) => {
750
750
  try {
751
- return readFileSync(join(root, relativePath), "utf8");
751
+ return readFileSync(join(shaRoot, relativePath), "utf8");
752
752
  } catch {
753
753
  return null;
754
754
  }
@@ -94,9 +94,13 @@ export type TurnScope = {
94
94
  *
95
95
  * invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent or its seal diverged —
96
96
  * identical to `stop.ts`'s own fallback.
97
+ * why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
98
+ * (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
99
+ * branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
97
100
  */
98
101
  export async function computeTurnScope(
99
102
  root: string,
103
+ gitRoot: string,
100
104
  provider: string,
101
105
  sessionKey: string,
102
106
  policy: Pick<Policy, "codePaths">,
@@ -104,7 +108,7 @@ export async function computeTurnScope(
104
108
  const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
105
109
  const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
106
110
  const turnBase = handoff?.turn_base_sha ?? "HEAD";
107
- const changedFiles = await listChangedRepoFiles(root, turnBase);
111
+ const changedFiles = await listChangedRepoFiles(gitRoot, turnBase);
108
112
  const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
109
113
  const testTargets = filterTestTargets(changedFiles);
110
114
  const commentScope = changedFiles.filter((file) =>
@@ -128,6 +132,7 @@ export async function computeTurnScope(
128
132
  */
129
133
  export async function pendingCommentViolations(
130
134
  root: string,
135
+ gitRoot: string,
131
136
  provider: string,
132
137
  sessionKey: string,
133
138
  policy: Pick<Policy, "comments" | "codePaths">,
@@ -135,12 +140,12 @@ export async function pendingCommentViolations(
135
140
  if (!policy.comments.enabled || policy.comments.onViolation !== "followup") {
136
141
  return [];
137
142
  }
138
- const scope = await computeTurnScope(root, provider, sessionKey, policy);
143
+ const scope = await computeTurnScope(root, gitRoot, provider, sessionKey, policy);
139
144
  if (scope.commentTargets.length === 0) {
140
145
  return [];
141
146
  }
142
147
  return coreFacade.commentPolicy.scanAddedComments(
143
- root,
148
+ gitRoot,
144
149
  scope.commentTargets,
145
150
  policy.comments.mode,
146
151
  scope.turnBase,
@@ -1,5 +1,6 @@
1
1
  import type { Decision, HarnessEvent } from "../contracts/index.ts";
2
2
  import { coreFacade } from "../core/index.ts";
3
+ import { localRepoRemote } from "../platform/git.ts";
3
4
  import type { Handler, HandlerContext } from "./run.ts";
4
5
  import { main } from "./run.ts";
5
6
  import { shipGateVerdict } from "./ship-gate.ts";
@@ -26,7 +27,13 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
26
27
  if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
27
28
  return { kind: "abstain" };
28
29
  }
29
- const hits = await pendingCommentViolations(event.projectDir, event.provider, event.sessionKey, ctx.policy);
30
+ const hits = await pendingCommentViolations(
31
+ event.projectDir,
32
+ shaScopeRoot(event),
33
+ event.provider,
34
+ event.sessionKey,
35
+ ctx.policy,
36
+ );
30
37
  if (hits.length === 0) {
31
38
  return { kind: "abstain" };
32
39
  }
@@ -94,15 +101,24 @@ async function rulesDecision(event: HarnessEvent, ctx: HandlerContext): Promise<
94
101
  if (dryRun.outcomes.length === 0) {
95
102
  return { kind: "abstain" };
96
103
  }
97
- // why twice: the first pass answers whether any rule fired at all, which costs no git. Only then is the sha
98
- // worth a process, and the second pass is the one whose verdict counts.
99
- const sha = await currentGitSha(shaRoot);
100
- const verdict = coreFacade.rules.decideAction(event.projectDir, config, trigger, {
101
- sha,
102
- sessionKey: event.sessionKey,
103
- mode: ctx.policy.mode,
104
- shaRoot,
105
- });
104
+ // why: the first pass answers whether any rule fired at all, which costs no git. The remote is a second
105
+ // process, spent only when the command could possibly be a gh api call naming a different repository
106
+ // ([/decisions/ad-130.md](/decisions/ad-130.md)) — a CLI shape or an unrelated trigger never reads it.
107
+ const [sha, repoRemote] = await Promise.all([
108
+ currentGitSha(shaRoot),
109
+ event.command && coreFacade.rules.mentionsGhApi(event.command) ? localRepoRemote(shaRoot) : undefined,
110
+ ]);
111
+ const verdict = coreFacade.rules.decideAction(
112
+ event.projectDir,
113
+ config,
114
+ { ...trigger, repoRemote },
115
+ {
116
+ sha,
117
+ sessionKey: event.sessionKey,
118
+ mode: ctx.policy.mode,
119
+ shaRoot,
120
+ },
121
+ );
106
122
  return verdict.decision;
107
123
  }
108
124
 
@@ -185,3 +185,28 @@ export async function listTrackedFiles(projectDir: string): Promise<string[]> {
185
185
  }
186
186
  return result.output.split("\0").filter((path) => path !== "");
187
187
  }
188
+
189
+ export type RepoRef = { owner: string; repo: string };
190
+
191
+ /**
192
+ * why: both SSH (`git@host:owner/repo.git`) and HTTPS (`https://host/owner/repo(.git)?`) forms share the same
193
+ * tail shape — a `/` or `:` before the owner, a `/` before the repo, an optional `.git` and trailing slash.
194
+ * One pattern reads both without a URL parser this project has no other use for.
195
+ */
196
+ export function parseOwnerRepo(url: string): RepoRef | null {
197
+ const match = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim());
198
+ return match ? { owner: match[1] as string, repo: match[2] as string } : null;
199
+ }
200
+
201
+ /**
202
+ * why a fixed remote name: every repository this project has touched, including this one, names its own
203
+ * remote `origin` — a configurable name is generality nobody has asked for yet
204
+ * ([/decisions/ad-130.md](/decisions/ad-130.md)).
205
+ */
206
+ export async function localRepoRemote(projectDir: string, remoteName = "origin"): Promise<RepoRef | null> {
207
+ if (!existsSync(join(projectDir, ".git"))) {
208
+ return null;
209
+ }
210
+ const result = await runProcess({ command: ["git", "remote", "get-url", remoteName], cwd: projectDir });
211
+ return result.exitCode === 0 ? parseOwnerRepo(result.stdout.trim()) : null;
212
+ }