@tech-leads-club/harness-toolkit 0.16.0 → 0.16.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 I$}from"node:module";var i=I$(import.meta.url);import{existsSync as B,lstatSync as c8,readFileSync as S,readlinkSync as d8,realpathSync as K$,rmSync as l8,statSync as i8,unlinkSync as n8,writeFileSync as a8}from"node:fs";import{homedir as s8}from"node:os";import{basename as r8,dirname as M$,isAbsolute as O$,join as G,relative as o8,resolve as p}from"node:path";import{homedir as L}from"node:os";import{delimiter as FZ,dirname as CZ,join as D,resolve as IZ}from"node:path";function n(){return D(L(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??n()}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 a(){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 v=6;var S$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var L5=new Set([...S$,"nc","ncat","socat","telnet","lwp-request"]);var b$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",A5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${b$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function O($){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 r9=[O({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}),O({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}),O({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}),O({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}),O({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}),O({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 n$={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 a$($){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),s$=`${q$}[0m`;function r$($=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 o$={ok:"success",warn:"warning",fail:"error",info:"info"},t$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},g=16;function E($=r$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${s$}`:q,J=(Q,q)=>Z(`38;2;${a$(n$[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=g)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(o$[Q],t$[Q])} ${q}`}}var F=E(!1);var e$=2;function $8($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function Z8($,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=$8()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(g,...$.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?Z8(z,J-e$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
1
+ import{createRequire as _$}from"node:module";var i=_$(import.meta.url);import{existsSync as B,lstatSync as c8,readFileSync as S,readlinkSync as d8,realpathSync as M$,rmSync as l8,statSync as i8,unlinkSync as n8,writeFileSync as a8}from"node:fs";import{homedir as s8}from"node:os";import{basename as r8,dirname as O$,isAbsolute as F$,join as G,relative as o8,resolve as p}from"node:path";import{homedir as L}from"node:os";import{delimiter as FZ,dirname as CZ,join as D,resolve as IZ}from"node:path";function n(){return D(L(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??n()}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 a(){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 D$($){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 T$=D$(s);var A$=T$.size;var v=6;var S$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var L5=new Set([...S$,"nc","ncat","socat","telnet","lwp-request"]);var b$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",A5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${b$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function O($){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 r9=[O({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}),O({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}),O({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}),O({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}),O({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}),O({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 n$={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 a$($){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 X$=String.fromCharCode(27),s$=`${X$}[0m`;function r$($=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 o$={ok:"success",warn:"warning",fail:"error",info:"info"},t$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},g=16;function E($=r$()){let Z=(Q,q)=>$?`${X$}[${Q}m${q}${s$}`:q,J=(Q,q)=>Z(`38;2;${a$(n$[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=g)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(o$[Q],t$[Q])} ${q}`}}var F=E(!1);var e$=2;function $8($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function Z8($,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=$8()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(g,...$.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?Z8(z,J-e$):[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 J8=["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 q8={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:v},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:{...q8}},mcpPrime:[],bootstrapExtra:[]};var _="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as v8,mkdirSync as rX,readFileSync as m8,realpathSync as oX,writeFileSync as g8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function u8($){return z$($)}var h8="tlc-exec.mjs";function p8($){return JSON.stringify($??null).includes(h8)}function u($){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=u8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((N)=>!p8(N));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(!v8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(m8($,"utf8"));if(Z.ok&&Z.changed)g8($,Z.settingsText,"utf8");return Z}function h($,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,N]of Object.entries(q)){let l=Array.isArray(N)?N:[],j=l.filter((C$)=>!JSON.stringify(C$??null).includes(Z));if(z+=l.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 q4,resolve as X4,sep as z4}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],K4=[Y$("tools","dev"),Y$("tools","__test__")];function t8($=process.env){let Z=s8(),J=b($);return{home:C($),binLinks:a().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 c($){try{return c8($).isSymbolicLink()}catch{return!1}}function d($){let Z=p($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return p($);J.unshift(r8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=d8($);return d(O$(Z)?Z:p(M$($),Z))}catch{return d($)}}}function e8($,Z,J={relative:o8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return e8(x($),d(Z))}var $Z="tlc-exec.mjs";function ZZ($){try{return i8($).size<4096&&S($,"utf8").includes($Z)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!c(Z))return;if(!c(Z)){if(ZZ(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 QZ($,Z){if(!B(Z))return;let J=u(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 JZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=h(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(c(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 XZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${_}`,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 zZ($,Z={}){let J=Z.purge===!0,Q=[];QZ(Q,$.claudeSettings),JZ(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 XZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function F$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function WZ($,Z){let J=[],Q=[];for(let q of F$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=h(S(q.target,"utf8"));if(X.kind==="rewritten")a8(q.target,X.text,"utf8")}else if(q.action==="unlink")n8(q.target);else l8(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 YZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function VZ($,Z){let J=F$($),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 ${_}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:YZ[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 ${_}, 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 GZ($,Z,J=F){return I(VZ($,Z),J)}function BZ($=process.argv.slice(2)){let Z=t8(),J=zZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?WZ(J,Z):null;return console.log(GZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(i.main==i.module)process.exitCode=BZ();export{t8 as uninstallTargets,VZ as uninstallScreen,GZ as uninstallReportText,zZ as planUninstall,F$ as pendingItems,BZ as main,e8 as isInsideRoot,d as canonicalise,WZ as applyUninstall};
3
+ `);var q8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},z$={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:v},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:{...q8}},mcpPrime:[],bootstrapExtra:[]};var _="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as v8,mkdirSync as oX,readFileSync as m8,realpathSync as tX,writeFileSync as g8}from"node:fs";function W$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function u8($){return W$($)}var h8="tlc-exec.mjs";function p8($){return JSON.stringify($??null).includes(h8)}function u($){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(!W$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=u8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((N)=>!p8(N));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 Y$($){if(!v8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(m8($,"utf8"));if(Z.ok&&Z.changed)g8($,Z.settingsText,"utf8");return Z}function h($,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,N]of Object.entries(q)){let l=Array.isArray(N)?N:[],j=l.filter((I$)=>!JSON.stringify(I$??null).includes(Z));if(z+=l.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 V$,relative as X4,resolve as z4,sep as W4}from"node:path";var G$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],B$=["config.json","state","flags","rules"],M4=[V$("tools","dev"),V$("tools","__test__")];function t8($=process.env){let Z=s8(),J=b($);return{home:C($),binLinks:a().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 c($){try{return c8($).isSymbolicLink()}catch{return!1}}function d($){let Z=p($),J=[];for(;;)try{return G(M$(Z),...J)}catch{let Q=O$(Z);if(Q===Z)return p($);J.unshift(r8(Z)),Z=Q}}function x($){try{return M$($)}catch{try{let Z=d8($);return d(F$(Z)?Z:p(O$($),Z))}catch{return d($)}}}function e8($,Z,J={relative:o8,isAbsolute:F$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function U$($,Z){return e8(x($),d(Z))}var $Z="tlc-exec.mjs";function ZZ($){try{return i8($).size<4096&&S($,"utf8").includes($Z)}catch{return!1}}function H$($,Z,J,Q,q){if(!B(Z)&&!c(Z))return;if(!c(Z)){if(ZZ(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"&&!U$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!U$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function QZ($,Z){if(!B(Z))return;let J=u(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 JZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=h(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(c(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 G$){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 B$){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 XZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${_}`,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 zZ($,Z={}){let J=Z.purge===!0,Q=[];QZ(Q,$.claudeSettings),JZ(Q,$.cursorHooks);for(let X of $.skillLinks)H$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)H$(Q,X,$.home,"the tlc launcher on PATH","target");let q=qZ(Q,$.home,J);return XZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function WZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)Y$(q.target);else if(q.action==="unmerge"){let X=h(S(q.target,"utf8"));if(X.kind==="rewritten")a8(q.target,X.text,"utf8")}else if(q.action==="unlink")n8(q.target);else l8(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 YZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function VZ($,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:K$($)}],footer:`already clean · reinstall with npm i -g ${_}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:YZ[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:K$($)},...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 ${_}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function K$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function GZ($,Z,J=F){return I(VZ($,Z),J)}function BZ($=process.argv.slice(2)){let Z=t8(),J=zZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?WZ(J,Z):null;return console.log(GZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(i.main==i.module)process.exitCode=BZ();export{t8 as uninstallTargets,VZ as uninstallScreen,GZ as uninstallReportText,zZ as planUninstall,C$ as pendingItems,BZ as main,e8 as isInsideRoot,d as canonicalise,WZ as applyUninstall};
package/docs/log.md CHANGED
@@ -13,6 +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-19
17
+
18
+ - **AD-145** — a session's last real shell cwd is recalled wherever the host reports none ([/decisions/ad-145.md](/decisions/ad-145.md))
19
+
20
+ ## 2026-09-16
21
+
22
+ - **AD-144** — turn_base_sha records the git root it came from, and a mismatch falls back to HEAD ([/decisions/ad-144.md](/decisions/ad-144.md))
23
+
16
24
  ## 2026-09-12
17
25
 
18
26
  - **AD-139** — lessonsView joins ProviderPort, closing the one dispatcher that bypassed it ([/decisions/ad-139.md](/decisions/ad-139.md))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -36,6 +36,17 @@ export type HandoffProviderSlice = {
36
36
  * ([/decisions/ad-058.md](/decisions/ad-058.md)).
37
37
  */
38
38
  turn_base_sha?: string;
39
+ /**
40
+ * why: the git root `turn_base_sha` was captured from. A session that moves between independent
41
+ * worktrees can submit one turn's prompt from one root and stop it from another; without this, the
42
+ * stale sha from the first root gets diffed against the second, reading months of someone else's
43
+ * history as "added this turn" ([/decisions/ad-144.md](/decisions/ad-144.md)).
44
+ */
45
+ turn_base_root?: string;
46
+ // why: the last real cwd a shell-execution event reported for this session, recalled at an event kind
47
+ // that reports none. Without it, that event scopes every gate to the shared workspace root even when this
48
+ // session's own shell moved into a worktree ([/decisions/ad-145.md](/decisions/ad-145.md)).
49
+ last_shell_cwd?: string;
39
50
  plan_paths?: string[];
40
51
  plan_at?: string;
41
52
  plan_snippet?: string;
@@ -1,3 +1,5 @@
1
+ import { isInside } from "../floor/floor.paths.ts";
2
+
1
3
  /**
2
4
  * Where the init skill has to be linked, and what counts as a link that works.
3
5
  *
@@ -72,8 +74,8 @@ export function linkHealth(
72
74
  return { state: "dangling", target, resolved };
73
75
  }
74
76
  const resolveHome = probe.realpath ?? ((path: string) => path);
75
- const home = resolveHome(runtimeHome).replace(/\/+$/, "");
76
- return resolved === home || resolved.startsWith(`${home}/`)
77
+ const home = resolveHome(runtimeHome);
78
+ return isInside(home, resolved)
77
79
  ? { state: "ok", target, resolved }
78
80
  : { state: "outside-runtime", target, resolved };
79
81
  }
@@ -42,7 +42,14 @@ export function manifestFor(relativePath: string): ManifestEntry | null {
42
42
 
43
43
  /** why: the alternative lockfiles an ecosystem accepts. A project using pnpm has locked just as firmly as one using npm. */
44
44
  export const ALTERNATE_LOCKFILES: Readonly<Record<string, readonly string[]>> = {
45
- "package.json": ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "npm-shrinkwrap.json", "bun.lockb"],
45
+ "package.json": [
46
+ "package-lock.json",
47
+ "pnpm-lock.yaml",
48
+ "yarn.lock",
49
+ "npm-shrinkwrap.json",
50
+ "bun.lock",
51
+ "bun.lockb",
52
+ ],
46
53
  "pyproject.toml": ["poetry.lock", "pdm.lock", "uv.lock"],
47
54
  };
48
55
 
@@ -1,5 +1,6 @@
1
1
  import type { HarnessEvent } from "../contracts/index.ts";
2
2
  import { coreFacade } from "../core/index.ts";
3
+ import { gitRootOf } from "../platform/git.ts";
3
4
  import type { Handler, HandlerContext } from "./run.ts";
4
5
  import { main } from "./run.ts";
5
6
  import { currentGitSha, obsConfigFor, shaScopeRoot } from "./support.ts";
@@ -14,10 +15,14 @@ export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: Han
14
15
  // why: the exact AD-114 divergence, now against turn_base_sha instead of a rule's proof sha — a worktree
15
16
  // session recorded the main checkout's HEAD as the turn's start, so every gate diffing against it saw a
16
17
  // whole untouched file read as "added this turn" ([/decisions/ad-117.md](/decisions/ad-117.md)).
17
- const sha = await currentGitSha(shaScopeRoot(event));
18
- if (sha) {
18
+ const shaRoot = await shaScopeRoot(event);
19
+ const [sha, gitRoot] = await Promise.all([currentGitSha(shaRoot), gitRootOf(shaRoot)]);
20
+ // why: a sha with no recorded root cannot later be told apart from one captured in a different git root
21
+ // entirely — `resolveTurnBase` treats that ambiguity as unsafe, so both are written together or neither is
22
+ // ([/decisions/ad-144.md](/decisions/ad-144.md)).
23
+ if (sha && gitRoot) {
19
24
  await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, event.sessionKey, {
20
- slice: { turn_base_sha: sha },
25
+ slice: { turn_base_sha: sha, turn_base_root: gitRoot },
21
26
  });
22
27
  }
23
28
  return { kind: "abstain" };
@@ -68,7 +68,7 @@ function gateFailureMessage(
68
68
  * once something fired, one `git rev-parse`.
69
69
  */
70
70
  export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
71
- const shaRoot = shaScopeRoot(event);
71
+ const shaRoot = await shaScopeRoot(event);
72
72
  if (!(await isShipCommand(event, shaRoot))) {
73
73
  return { kind: "abstain" };
74
74
  }
@@ -18,6 +18,7 @@ import {
18
18
  currentGitSha,
19
19
  formatLessonsBlock,
20
20
  obsConfigFor,
21
+ resolveTurnBase,
21
22
  sessionIdFromKey,
22
23
  shaScopeRoot,
23
24
  } from "./support.ts";
@@ -204,7 +205,11 @@ export async function runLockedGate(args: {
204
205
  pendingCredit: PendingLessonCredit | undefined;
205
206
  }): Promise<GateRun> {
206
207
  const command = [...args.command, ...args.argvFiles];
207
- const inputs = coreFacade.gate.computeInputsHash(args.root, args.recordFiles, command);
208
+ // why: `args.recordFiles`/`args.argvFiles` are the turn's changed files, resolved against `shaRoot` — the
209
+ // same split AD-129 already applied to diffing. Hashing or running against `args.root` instead reads a
210
+ // worktree session's own changed file from the unrelated main checkout that happens to share its path
211
+ // ([/decisions/ad-145.md](/decisions/ad-145.md)).
212
+ const inputs = coreFacade.gate.computeInputsHash(args.shaRoot, args.recordFiles, command);
208
213
  const cached = coreFacade.gate.cachedVerdict(
209
214
  coreFacade.gate.readLastGate(args.root, args.sessionKey),
210
215
  args.gate,
@@ -221,7 +226,7 @@ export async function runLockedGate(args: {
221
226
  args.provider,
222
227
  args.session,
223
228
  async () => {
224
- const result = await runCommand(args.root, args.command, args.argvFiles);
229
+ const result = await runCommand(args.shaRoot, args.command, args.argvFiles);
225
230
  return coreFacade.gate.writeLastGate({
226
231
  root: args.root,
227
232
  sessionKey: args.sessionKey,
@@ -466,7 +471,7 @@ async function decideStopRules(
466
471
  export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerContext): Promise<Decision> => {
467
472
  const { policy, capabilities } = ctx;
468
473
  const root = event.projectDir;
469
- const shaRoot = shaScopeRoot(event);
474
+ const shaRoot = await shaScopeRoot(event);
470
475
  const provider = event.provider;
471
476
  const sessionKey = event.sessionKey;
472
477
  const session = sessionIdFromKey(event);
@@ -489,8 +494,9 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
489
494
  // past its own changes, and every gate below then saw an empty diff and skipped — the comment gate in a repo
490
495
  // whose task was "schema v2 + tests + commit" ([/decisions/ad-058.md](/decisions/ad-058.md)).
491
496
  //
492
- // invariant: absent, this is the string `HEAD`, which is exactly the previous behaviour.
493
- const turnBase = handoff.turn_base_sha ?? "HEAD";
497
+ // invariant: absent, or captured from a different git root than `shaRoot`, this is the string `HEAD`,
498
+ // which is exactly the previous behaviour for the absent case ([/decisions/ad-144.md](/decisions/ad-144.md)).
499
+ const turnBase = await resolveTurnBase(handoff, shaRoot);
494
500
  /**
495
501
  * why: collected rather than returned. A turn may defer more than one gate to the same neighbour, and the reply
496
502
  * says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
@@ -1,4 +1,4 @@
1
- import { statSync } from "node:fs";
1
+ import { existsSync, statSync } from "node:fs";
2
2
  import type { EffortLevel, HarnessEvent } from "../contracts/index.ts";
3
3
  import {
4
4
  type CommentFinding,
@@ -8,7 +8,13 @@ import {
8
8
  type PendingLessonCredit,
9
9
  type Policy,
10
10
  } from "../core/index.ts";
11
- import { filterCodeTargets, filterTestTargets, gitRootOf, listChangedRepoFiles } from "../platform/git.ts";
11
+ import {
12
+ filterCodeTargets,
13
+ filterTestTargets,
14
+ gitCommonDirOf,
15
+ gitRootOf,
16
+ listChangedRepoFiles,
17
+ } from "../platform/git.ts";
12
18
  import { runProcess } from "../platform/process.ts";
13
19
  import { type ProviderPort, providers } from "../providers/index.ts";
14
20
 
@@ -66,16 +72,34 @@ export async function currentGitBranch(root: string): Promise<string | null> {
66
72
  return branch.length > 0 ? branch : null;
67
73
  }
68
74
 
69
- /**
70
- * why: `event.projectDir` prefers `CLAUDE_PROJECT_DIR`, which the host deliberately keeps pointed at the
71
- * session's original root — including inside a git worktree, where it would otherwise return the wrong
72
- * HEAD for a `since HEAD` rule proof. `event.cwd` is the field the host actually moves when the agent is
73
- * working in a worktree or after a `cd`. Only the rules engine's proof sha needs this; every other use of
74
- * `event.projectDir` (state dir, policy config, presence) is deliberately left alone
75
- * ([/decisions/ad-114.md](/decisions/ad-114.md)).
76
- */
77
- export function shaScopeRoot(event: HarnessEvent): string {
78
- return event.cwd ?? event.projectDir;
75
+ // why: `event.projectDir` prefers `CLAUDE_PROJECT_DIR`, kept pointed at the session's original root on
76
+ // purpose, including inside a worktree. `event.cwd` is the field a host actually moves — present on every
77
+ // Claude Code hook, but on Cursor only `beforeShellExecution` carries it ([/decisions/ad-114.md](/decisions/ad-114.md)).
78
+ // Absent, `recallSessionCwd` is the next best answer for an event, like `stop`, that never gets one.
79
+ export async function shaScopeRoot(event: HarnessEvent): Promise<string> {
80
+ if (event.cwd) {
81
+ return event.cwd;
82
+ }
83
+ const recalled = coreFacade.handoff.readHandoff(
84
+ event.projectDir,
85
+ event.provider,
86
+ event.sessionKey,
87
+ ).last_shell_cwd;
88
+ return recallSessionCwd(recalled, event.projectDir);
89
+ }
90
+
91
+ // why: trusted only once confirmed against `projectDir`'s own git object database — a deleted worktree, or a
92
+ // stale value a different repository entirely once wrote to this same session key, must not redirect a gate
93
+ // into the wrong directory ([/decisions/ad-145.md](/decisions/ad-145.md)).
94
+ async function recallSessionCwd(recalled: string | undefined, projectDir: string): Promise<string> {
95
+ if (!recalled || recalled === projectDir || !existsSync(recalled)) {
96
+ return projectDir;
97
+ }
98
+ const [recalledCommon, projectCommon] = await Promise.all([
99
+ gitCommonDirOf(recalled),
100
+ gitCommonDirOf(projectDir),
101
+ ]);
102
+ return recalledCommon !== null && recalledCommon === projectCommon ? recalled : projectDir;
79
103
  }
80
104
 
81
105
  export type TurnScope = {
@@ -92,8 +116,9 @@ export type TurnScope = {
92
116
  * at `stop` or, since AD-116, before `commit`/`push`/`pr-open` ships it. A second, differently-scoped
93
117
  * answer to the same question is the drift AD-071 already named once ([/decisions/ad-116.md](/decisions/ad-116.md)).
94
118
  *
95
- * invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent or its seal diverged —
96
- * identical to `stop.ts`'s own fallback.
119
+ * invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent, its seal diverged, or
120
+ * `resolveTurnBase` finds a different git root — identical to `stop.ts`'s own fallback
121
+ * ([/decisions/ad-144.md](/decisions/ad-144.md)).
97
122
  * why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
98
123
  * (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
99
124
  * branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
@@ -107,7 +132,7 @@ export async function computeTurnScope(
107
132
  ): Promise<TurnScope> {
108
133
  const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
109
134
  const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
110
- const turnBase = handoff?.turn_base_sha ?? "HEAD";
135
+ const turnBase = await resolveTurnBase(handoff, gitRoot);
111
136
  const rawChangedFiles = await listChangedRepoFiles(gitRoot, turnBase);
112
137
  const otherSessionFiles = await coreFacade.presence.filesClaimedByOtherLiveSessions(
113
138
  root,
@@ -159,6 +184,27 @@ export async function pendingCommentViolations(
159
184
  );
160
185
  }
161
186
 
187
+ /**
188
+ * why: `turn_base_sha` alone cannot tell "this session's own HEAD" from "the HEAD of a worktree the shell
189
+ * was sitting in three turns ago" — both are just a sha. Comparing the git root it was captured from
190
+ * against the root being diffed against is what tells the two apart, the same way an absent
191
+ * `turn_base_sha` already falls back to `HEAD` rather than diffing against nothing, instead of diffing one
192
+ * repository's turn against an unrelated one's entire history ([/decisions/ad-144.md](/decisions/ad-144.md)).
193
+ *
194
+ * invariant: a slice with a sha but no recorded root (state written before this existed) resolves to
195
+ * `HEAD`, the same as no slice at all — safe by default, and self-healing on the next `prompt.submit`.
196
+ */
197
+ export async function resolveTurnBase(
198
+ slice: { turn_base_sha?: string; turn_base_root?: string } | undefined,
199
+ gitRoot: string,
200
+ ): Promise<string> {
201
+ if (!slice?.turn_base_sha || !slice.turn_base_root) {
202
+ return "HEAD";
203
+ }
204
+ const currentRoot = await gitRootOf(gitRoot);
205
+ return currentRoot !== null && currentRoot === slice.turn_base_root ? slice.turn_base_sha : "HEAD";
206
+ }
207
+
162
208
  export async function currentGitSha(root: string): Promise<string | null> {
163
209
  const gitRoot = await gitRootOf(root);
164
210
  if (gitRoot === null) {
@@ -309,7 +355,7 @@ export async function observeForRules(
309
355
  if (!coreFacade.rules.wants(event.projectDir, config, event)) {
310
356
  return;
311
357
  }
312
- const sha = await currentGitSha(shaScopeRoot(event));
358
+ const sha = await currentGitSha(await shaScopeRoot(event));
313
359
  coreFacade.rules.observe(event.projectDir, config, event, {
314
360
  sha,
315
361
  sessionKey: event.sessionKey,
@@ -6,7 +6,7 @@ import { normalizeSeparators } from "../platform/sanitize.ts";
6
6
  import { readClaudeUsage } from "../providers/index.ts";
7
7
  import type { Handler, HandlerContext } from "./run.ts";
8
8
  import { main } from "./run.ts";
9
- import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules } from "./support.ts";
9
+ import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules, resolveTurnBase } from "./support.ts";
10
10
 
11
11
  const OBS_KIND_BY_EVENT: Partial<Record<HarnessEventKind, ObsKind>> = {
12
12
  "tool.after": "tool.end",
@@ -73,7 +73,7 @@ async function commentEditAdvisory(event: HarnessEvent, ctx: HandlerContext): Pr
73
73
  event.projectDir,
74
74
  targets,
75
75
  policy.comments.mode,
76
- handoff.turn_base_sha ?? "HEAD",
76
+ await resolveTurnBase(handoff, event.projectDir),
77
77
  );
78
78
  if (hits.length === 0) {
79
79
  return null;
@@ -31,7 +31,7 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
31
31
  if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
32
32
  return { kind: "abstain" };
33
33
  }
34
- const shaRoot = shaScopeRoot(event);
34
+ const shaRoot = await shaScopeRoot(event);
35
35
  const hits = await pendingCommentViolations(
36
36
  event.projectDir,
37
37
  shaRoot,
@@ -206,7 +206,7 @@ async function rulesDecision(event: HarnessEvent, ctx: HandlerContext): Promise<
206
206
  command: event.command,
207
207
  toolInput: event.toolInput,
208
208
  };
209
- const shaRoot = shaScopeRoot(event);
209
+ const shaRoot = await shaScopeRoot(event);
210
210
  const dryRun = coreFacade.rules.decideAction(event.projectDir, config, trigger, {
211
211
  sha: null,
212
212
  sessionKey: event.sessionKey,
@@ -305,10 +305,30 @@ async function handleToolBefore(event: HarnessEvent, ctx: HandlerContext): Promi
305
305
  return { kind: "allow" };
306
306
  }
307
307
 
308
+ // why: the one event a Cursor session ever reports a real cwd on — read back by `shaScopeRoot` at `stop`,
309
+ // which gets none at all ([/decisions/ad-145.md](/decisions/ad-145.md)). A no-op once the value already
310
+ // matches, so a session sitting in one directory pays this exactly once, not on every command.
311
+ async function recordShellCwd(event: HarnessEvent): Promise<void> {
312
+ if (event.event !== "shell.before" || !event.cwd) {
313
+ return;
314
+ }
315
+ const current = coreFacade.handoff.readHandoff(event.projectDir, event.provider, event.sessionKey);
316
+ if (current.last_shell_cwd === event.cwd) {
317
+ return;
318
+ }
319
+ await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, event.sessionKey, {
320
+ slice: { last_shell_cwd: event.cwd },
321
+ });
322
+ }
323
+
308
324
  export const toolBeforeHandler: Handler = async (
309
325
  event: HarnessEvent,
310
326
  ctx: HandlerContext,
311
327
  ): Promise<Decision> => {
328
+ // why: an observation, not a decision — recorded before the floor so it is captured regardless of what
329
+ // this specific command is ultimately allowed to do ([/decisions/ad-145.md](/decisions/ad-145.md)).
330
+ await recordShellCwd(event);
331
+
312
332
  // invariant: the floor runs first and reads no policy, so no config value and no agent edit can
313
333
  // reach a decision before it.
314
334
  const floor = coreFacade.floor.evaluateFloor({
@@ -1,6 +1,7 @@
1
1
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { realpath } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
+ import { join, resolve } from "node:path";
4
5
  import { runProcess } from "./process.ts";
5
6
  import { normalizeSeparators } from "./sanitize.ts";
6
7
 
@@ -32,6 +33,31 @@ export async function gitRootOf(dir: string): Promise<string | null> {
32
33
  return root.length > 0 ? root : null;
33
34
  }
34
35
 
36
+ // why: every worktree of one repository shares this path, unlike `gitRootOf`'s own toplevel, which is
37
+ // worktree-specific by design — this is what tells "two worktrees of one repo" apart from "two unrelated repos".
38
+ export async function gitCommonDirOf(dir: string): Promise<string | null> {
39
+ let result: { exitCode: number; stdout: string };
40
+ try {
41
+ result = await runProcess({ command: ["git", "rev-parse", "--git-common-dir"], cwd: dir });
42
+ } catch {
43
+ return null;
44
+ }
45
+ if (result.exitCode !== 0) {
46
+ return null;
47
+ }
48
+ const raw = result.stdout.trim();
49
+ if (raw.length === 0) {
50
+ return null;
51
+ }
52
+ // why: `/tmp` and macOS's `/var/folders` are themselves symlinks, so two paths naming the same real
53
+ // directory can disagree as strings unless both are resolved through the same filesystem call.
54
+ try {
55
+ return await realpath(resolve(dir, raw));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
35
61
  async function gitLines(projectDir: string, args: string[]): Promise<string[]> {
36
62
  const result = await runProcess({ command: ["git", ...args], cwd: projectDir });
37
63
  if (result.exitCode !== 0) {
@@ -28,7 +28,7 @@ const ENTRY_SPECS: readonly EntrySpec[] = [
28
28
  { hookEvent: "preCompact", handler: "compact-before", timeoutSeconds: 5 },
29
29
  { hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5, failClosed: true },
30
30
  { hookEvent: "subagentStop", handler: "subagent-stop", timeoutSeconds: 5 },
31
- { hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 5, failClosed: true },
31
+ { hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
32
32
  { hookEvent: "postToolUse", handler: "tool-after", timeoutSeconds: 5 },
33
33
  { hookEvent: "postToolUseFailure", handler: "tool-failure", timeoutSeconds: 5 },
34
34
  { hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10, failClosed: true },