@tech-leads-club/harness-toolkit 0.4.0 → 0.4.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 U$}from"node:module";var l=U$(import.meta.url);import{existsSync as B,lstatSync as w8,readFileSync as S,readlinkSync as y8,realpathSync as V$,rmSync as v8,statSync as m8,unlinkSync as u8,writeFileSync as g8}from"node:fs";import{homedir as h8}from"node:os";import{basename as p8,dirname as G$,isAbsolute as B$,join as G,relative as c8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as V2,join as j,resolve as G2}from"node:path";function i(){return j(L(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".claude")}function D(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||j(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 C$($){let Z=new Map;for(let Q of $){let J={line:Q.line,block:Q.block,middle:Q.middle};for(let X of Q.extensions)Z.set(X.toLowerCase(),J)}return Z}var I$=C$(s);var _$=I$.size;var y=6;var P$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var HZ=new Set([...P$,"nc","ncat","socat","telnet","lwp-request"]);var A$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",MZ=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${A$}))(?=[:)\\s,'"\`]|$)`,"g");var A="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:A,lastSeenAt:A,lastAccessedAt:A,updatedAt:A}}var J9=[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 c$={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 d$($){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((Q)=>Number.parseInt(Q,16)).join(";")}var $$=String.fromCharCode(27),l$=`${$$}[0m`;function i$($=process.env,Z=process.argv,Q=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return Q}var n$={ok:"success",warn:"warning",fail:"error",info:"info"},a$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function R($=i$()){let Z=(J,X)=>$?`${$$}[${J}m${X}${l$}`:X,Q=(J,X)=>Z(`38;2;${d$(c$[J])}`,X);return{enabled:$,paint:Q,bold:(J)=>Z("1",J),dim:(J)=>Q("textDim",J),heading:(J)=>Q("accent",`${V.rule} ${J} ${V.rule}`),footer:(J)=>Q("textDim",`${V.dash} ${J} ${V.dash}`),kv:(J,X,q=m)=>` ${Q("textMuted",`${J}:`.padEnd(q))} ${X}`,status:(J,X)=>`${Q(n$[J],a$[J])} ${X}`}}var O=R(!1);var s$=2;function r$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function o$($,Z){if($==="")return[""];let Q=[],J="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(J===""){J=X;continue}if(J.length+1+X.length<=Z){J=`${J} ${X}`;continue}Q.push(J),J=X}if(J!=="")Q.push(J);return Q.length===0?[""]:Q}function C($,Z,Q=r$()){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let X=Math.max(m,...$.sections.flatMap((q)=>(q.rows??[]).map((z)=>z.label.length+1)));for(let q of $.sections){if(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;J.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){J.push("");continue}let W=q.wrap?o$(z,Q-s$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.join(`
2
- `)}var K={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var t$=["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 e$={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},$8={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:K.includePayloads,maxAttrChars:K.maxAttrChars,sessionCostAlertUsd:K.sessionCostAlertUsd,retentionDays:K.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...e$}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as E8,mkdirSync as xX,readFileSync as k8,realpathSync as SX,writeFileSync as x8}from"node:fs";function Z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function S8($){return Z$($)}var f8="tlc-exec.mjs";function b8($){return JSON.stringify($??null).includes(f8)}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 Q=S8(Z.hooks)?Z.hooks:{},J={},X=!1;for(let[z,W]of Object.entries(Q)){let H=W.filter((_)=>!b8(_));if(H.length!==W.length)X=!0;if(H.length>0)J[z]=H}let q={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")q[z]=W;else if(Object.keys(J).length>0)q.hooks=J;return{ok:!0,settingsText:JSON.stringify(q,null,2),changed:X}}function J$($){if(!E8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(k8($,"utf8"));if(Z.ok&&Z.changed)x8($,Z.settingsText,"utf8");return Z}function g($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let Q;try{Q=JSON.parse($)}catch{return{kind:"unparsed"}}if(Q===null||typeof Q!=="object"||Array.isArray(Q))return{kind:"unparsed"};let J=Q,X=J.hooks!==null&&typeof J.hooks==="object"&&!Array.isArray(J.hooks)?J.hooks:{},q={},z=0,W=0;for(let[H,_]of Object.entries(X)){let d=Array.isArray(_)?_:[],N=d.filter((K$)=>!JSON.stringify(K$??null).includes(Z));if(z+=d.length-N.length,N.length>0)q[H]=N,W+=N.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...J,hooks:q},null,2)}
4
- `}}import{join as Q$,relative as uX,resolve as gX,sep as hX}from"node:path";var X$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],q$=["config.json","state","flags"],sX=[Q$("tools","dev"),Q$("tools","__test__")];function d8($=process.env){let Z=h8(),Q=f($);return{home:F($),binLinks:n().map((J)=>G(Q,J)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(D(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(D(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return w8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),Q=[];for(;;)try{return G(V$(Z),...Q)}catch{let J=G$(Z);if(J===Z)return h($);Q.unshift(p8(Z)),Z=J}}function x($){try{return V$($)}catch{try{let Z=y8($);return c(B$(Z)?Z:h(G$($),Z))}catch{return c($)}}}function l8($,Z,Q={relative:c8,isAbsolute:B$}){if($===Z)return!0;let J=Q.relative(Z,$);return J!==""&&!J.startsWith("..")&&!Q.isAbsolute(J)}function z$($,Z){return l8(x($),c(Z))}var i8="tlc-exec.mjs";function n8($){try{return m8($).size<4096&&S($,"utf8").includes(i8)}catch{return!1}}function W$($,Z,Q,J,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(n8(Z)){$.push({action:"remove",target:Z,detail:`${J}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${J} is a real file the installer did not write`});return}if(X==="target"&&!z$(Z,Q)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let q=X==="location"&&!z$(Z,Q);$.push({action:"unlink",target:Z,detail:q?`${J}, stale — points at ${x(Z)}`:J})}function a8($,Z){if(!B(Z))return;let Q=u(S(Z,"utf8"));if(!Q.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${Q.error}`});return}if(!Q.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function s8($,Z){let Q=B(Z)?S(Z,"utf8"):null,J=g(Q);switch(J.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(J.removed>0)$.push({action:"remove",target:Z,detail:`${J.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${J.removed} harness entries, keep the rest`})}}function r8($,Z,Q){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 q of X$){let z=G(Z,q);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let X=G(Z,k);if(B(X))$.push({action:"remove",target:X,detail:"install marker"});for(let q of q$){let z=G(Z,q);if(!B(z))continue;$.push(Q?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function o8($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${I}`,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 t8($,Z={}){let Q=Z.purge===!0,J=[];a8(J,$.claudeSettings),s8(J,$.cursorHooks);for(let q of $.skillLinks)W$(J,q,$.home,"skill link","location");for(let q of $.binLinks)W$(J,q,$.home,"the tlc launcher on PATH","target");let X=r8(J,$.home,Q);return o8(J,$.home),{items:J,purge:Q,homeIsLink:X}}function H$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function e8($,Z){let Q=[],J=[];for(let X of H$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)J$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")g8(X.target,q.text,"utf8")}else if(X.action==="unlink")u8(X.target);else v8(X.target,{recursive:!0,force:!0});Q.push(X)}catch(q){J.push({item:X,reason:q instanceof Error?q.message:String(q)})}return{applied:Q,failed:J}}var $2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function Z2($,Z){let Q=H$($),J=Z!==null;if(Q.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:Y$($)}],footer:`already clean · reinstall with npm i -g ${I}, then \`tlc harness install\``};let X=Q.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:$2[W.action]})),q=$.items.filter((W)=>W.action==="keep"),z=[{title:J?"REMOVED":"WOULD REMOVE",rows:X},...q.length>0?[{title:"KEPT",rows:q.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:Y$($)},...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:[J?`${Z.applied.length} applied`:`${Q.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:J?`reinstall any time with npm i -g ${I}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function Y$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function J2($,Z,Q=O){return C(Z2($,Z),Q)}function Q2($=process.argv.slice(2)){let Z=d8(),Q=t8(Z,{purge:$.includes("--purge")}),J=$.includes("--yes")?e8(Q,Z):null;return console.log(J2(Q,J,R())),J!==null&&J.failed.length>0?1:0}if(l.main==l.module)process.exitCode=Q2();export{d8 as uninstallTargets,Z2 as uninstallScreen,J2 as uninstallReportText,t8 as planUninstall,H$ as pendingItems,Q2 as main,l8 as isInsideRoot,c as canonicalise,e8 as applyUninstall};
1
+ import{createRequire as M$}from"node:module";var l=M$(import.meta.url);import{existsSync as B,lstatSync as m8,readFileSync as S,readlinkSync as u8,realpathSync as G$,rmSync as g8,statSync as h8,unlinkSync as p8,writeFileSync as c8}from"node:fs";import{homedir as d8}from"node:os";import{basename as l8,dirname as B$,isAbsolute as H$,join as G,relative as i8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as H2,dirname as K2,join as L,resolve as U2}from"node:path";function i(){return L(j(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function P(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".claude")}function D(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||L(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 I$($){let Z=new Map;for(let Q of $){let J={line:Q.line,block:Q.block,middle:Q.middle};for(let X of Q.extensions)Z.set(X.toLowerCase(),J)}return Z}var _$=I$(s);var N$=_$.size;var y=6;var A$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var OZ=new Set([...A$,"nc","ncat","socat","telnet","lwp-request"]);var R$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",IZ=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${R$}))(?=[:)\\s,'"\`]|$)`,"g");var A="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:A,lastSeenAt:A,lastAccessedAt:A,updatedAt:A}}var z9=[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 d$={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 l$($){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((Q)=>Number.parseInt(Q,16)).join(";")}var $$=String.fromCharCode(27),i$=`${$$}[0m`;function n$($=process.env,Z=process.argv,Q=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return Q}var a$={ok:"success",warn:"warning",fail:"error",info:"info"},s$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function R($=n$()){let Z=(J,X)=>$?`${$$}[${J}m${X}${i$}`:X,Q=(J,X)=>Z(`38;2;${l$(d$[J])}`,X);return{enabled:$,paint:Q,bold:(J)=>Z("1",J),dim:(J)=>Q("textDim",J),heading:(J)=>Q("accent",`${V.rule} ${J} ${V.rule}`),footer:(J)=>Q("textDim",`${V.dash} ${J} ${V.dash}`),kv:(J,X,q=m)=>` ${Q("textMuted",`${J}:`.padEnd(q))} ${X}`,status:(J,X)=>`${Q(a$[J],s$[J])} ${X}`}}var O=R(!1);var r$=2;function o$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function t$($,Z){if($==="")return[""];let Q=[],J="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(J===""){J=X;continue}if(J.length+1+X.length<=Z){J=`${J} ${X}`;continue}Q.push(J),J=X}if(J!=="")Q.push(J);return Q.length===0?[""]:Q}function C($,Z,Q=o$()){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let X=Math.max(m,...$.sections.flatMap((q)=>(q.rows??[]).map((z)=>z.label.length+1)));for(let q of $.sections){if(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;J.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){J.push("");continue}let W=q.wrap?t$(z,Q-r$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.join(`
2
+ `)}var K={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var $8=["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 Z8={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:y},obs:{globalSpool:!1,includePayloads:K.includePayloads,maxAttrChars:K.maxAttrChars,sessionCostAlertUsd:K.sessionCostAlertUsd,retentionDays:K.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...Z8}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as S8,mkdirSync as mX,readFileSync as f8,realpathSync as uX,writeFileSync as b8}from"node:fs";function J$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function w8($){return J$($)}var y8="tlc-exec.mjs";function v8($){return JSON.stringify($??null).includes(y8)}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(!J$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let Q=w8(Z.hooks)?Z.hooks:{},J={},X=!1;for(let[z,W]of Object.entries(Q)){let H=W.filter((_)=>!v8(_));if(H.length!==W.length)X=!0;if(H.length>0)J[z]=H}let q={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")q[z]=W;else if(Object.keys(J).length>0)q.hooks=J;return{ok:!0,settingsText:JSON.stringify(q,null,2),changed:X}}function Q$($){if(!S8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(f8($,"utf8"));if(Z.ok&&Z.changed)b8($,Z.settingsText,"utf8");return Z}function g($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let Q;try{Q=JSON.parse($)}catch{return{kind:"unparsed"}}if(Q===null||typeof Q!=="object"||Array.isArray(Q))return{kind:"unparsed"};let J=Q,X=J.hooks!==null&&typeof J.hooks==="object"&&!Array.isArray(J.hooks)?J.hooks:{},q={},z=0,W=0;for(let[H,_]of Object.entries(X)){let d=Array.isArray(_)?_:[],N=d.filter((U$)=>!JSON.stringify(U$??null).includes(Z));if(z+=d.length-N.length,N.length>0)q[H]=N,W+=N.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...J,hooks:q},null,2)}
4
+ `}}import{join as X$,relative as iX,resolve as nX,sep as aX}from"node:path";var q$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],z$=["config.json","state","flags"],J4=[X$("tools","dev"),X$("tools","__test__")];function n8($=process.env){let Z=d8(),Q=f($);return{home:F($),binLinks:n().map((J)=>G(Q,J)),claudeSettings:G(P(),"settings.json"),cursorHooks:G(D(),"hooks.json"),skillLinks:[G(P(),"skills","harness-init"),G(D(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return m8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),Q=[];for(;;)try{return G(G$(Z),...Q)}catch{let J=B$(Z);if(J===Z)return h($);Q.unshift(l8(Z)),Z=J}}function x($){try{return G$($)}catch{try{let Z=u8($);return c(H$(Z)?Z:h(B$($),Z))}catch{return c($)}}}function a8($,Z,Q={relative:i8,isAbsolute:H$}){if($===Z)return!0;let J=Q.relative(Z,$);return J!==""&&!J.startsWith("..")&&!Q.isAbsolute(J)}function W$($,Z){return a8(x($),c(Z))}var s8="tlc-exec.mjs";function r8($){try{return h8($).size<4096&&S($,"utf8").includes(s8)}catch{return!1}}function Y$($,Z,Q,J,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(r8(Z)){$.push({action:"remove",target:Z,detail:`${J}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${J} is a real file the installer did not write`});return}if(X==="target"&&!W$(Z,Q)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let q=X==="location"&&!W$(Z,Q);$.push({action:"unlink",target:Z,detail:q?`${J}, stale — points at ${x(Z)}`:J})}function o8($,Z){if(!B(Z))return;let Q=u(S(Z,"utf8"));if(!Q.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${Q.error}`});return}if(!Q.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function t8($,Z){let Q=B(Z)?S(Z,"utf8"):null,J=g(Q);switch(J.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(J.removed>0)$.push({action:"remove",target:Z,detail:`${J.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${J.removed} harness entries, keep the rest`})}}function e8($,Z,Q){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 q of q$){let z=G(Z,q);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let X=G(Z,k);if(B(X))$.push({action:"remove",target:X,detail:"install marker"});for(let q of z$){let z=G(Z,q);if(!B(z))continue;$.push(Q?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function $2($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${I}`,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 Z2($,Z={}){let Q=Z.purge===!0,J=[];o8(J,$.claudeSettings),t8(J,$.cursorHooks);for(let q of $.skillLinks)Y$(J,q,$.home,"skill link","location");for(let q of $.binLinks)Y$(J,q,$.home,"the tlc launcher on PATH","target");let X=e8(J,$.home,Q);return $2(J,$.home),{items:J,purge:Q,homeIsLink:X}}function K$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function J2($,Z){let Q=[],J=[];for(let X of K$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)Q$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")c8(X.target,q.text,"utf8")}else if(X.action==="unlink")p8(X.target);else g8(X.target,{recursive:!0,force:!0});Q.push(X)}catch(q){J.push({item:X,reason:q instanceof Error?q.message:String(q)})}return{applied:Q,failed:J}}var Q2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function X2($,Z){let Q=K$($),J=Z!==null;if(Q.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:V$($)}],footer:`already clean · reinstall with npm i -g ${I}, then \`tlc harness install\``};let X=Q.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:Q2[W.action]})),q=$.items.filter((W)=>W.action==="keep"),z=[{title:J?"REMOVED":"WOULD REMOVE",rows:X},...q.length>0?[{title:"KEPT",rows:q.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:V$($)},...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:[J?`${Z.applied.length} applied`:`${Q.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:J?`reinstall any time with npm i -g ${I}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function V$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function q2($,Z,Q=O){return C(X2($,Z),Q)}function z2($=process.argv.slice(2)){let Z=n8(),Q=Z2(Z,{purge:$.includes("--purge")}),J=$.includes("--yes")?J2(Q,Z):null;return console.log(q2(Q,J,R())),J!==null&&J.failed.length>0?1:0}if(l.main==l.module)process.exitCode=z2();export{n8 as uninstallTargets,X2 as uninstallScreen,q2 as uninstallReportText,Z2 as planUninstall,K$ as pendingItems,z2 as main,a8 as isInsideRoot,c as canonicalise,J2 as applyUninstall};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -95,6 +95,7 @@ import {
95
95
  recordObs,
96
96
  } from "./observability/observability.service.ts";
97
97
  import { getRollup, pruneObs, pruneSpool, readSignalEvents } from "./observability/observability.store.ts";
98
+ import { resolveObsLevel } from "./observability/observability.types.ts";
98
99
  import {
99
100
  decisionsFrom,
100
101
  NOTHING_WAS_THE_HARNESS,
@@ -110,6 +111,7 @@ import {
110
111
  } from "./observe/observe.service.ts";
111
112
  import { detectDeviations, detectPlan } from "./plan/plan.detect.ts";
112
113
  import { evaluatePlanGate, planVerdict } from "./plan/plan.service.ts";
114
+ import { DEFAULTS } from "./policy/policy.defaults.ts";
113
115
  import { guardPolicySurface } from "./policy/policy.guard.ts";
114
116
  import {
115
117
  acceptPolicySources,
@@ -354,6 +356,7 @@ export const coreFacade = {
354
356
  },
355
357
  observability: {
356
358
  DEFAULT_OBS,
359
+ resolveObsLevel,
357
360
  recordObs,
358
361
  recordFromEvent,
359
362
  recordAudit,
@@ -399,6 +402,10 @@ export const coreFacade = {
399
402
  activeRails,
400
403
  operatorBootstrapLines,
401
404
  loadPolicy,
405
+ // why on the facade and not exported from the barrel: `core/index.ts` publishes types and this object only, so
406
+ // a caller outside `core` reaching for the shipped defaults comes through here
407
+ // ([/decisions/ad-004.md](/decisions/ad-004.md), [/decisions/ad-101.md](/decisions/ad-101.md)).
408
+ DEFAULTS,
402
409
  resolvedWithoutProjectTier,
403
410
  shadowedKeys,
404
411
  pruneShadowed,
@@ -43,7 +43,19 @@ export function policySourceFingerprint(root: string): PolicySource[] {
43
43
  join(projectStateDir(root), MODE_FILE),
44
44
  ...FLAG_FILES.map((flag) => join(flagsDir(root), flag)),
45
45
  ];
46
- return paths.map((path) => ({ path, hash: hashOf(path) }));
46
+ /**
47
+ * hazard: listed as-is, and two of these collide. With the project root at a home directory,
48
+ * `projectConfigPath(root)` resolves to the machine config path — so the same file appeared twice in every
49
+ * listing, and `acceptPolicySources` would write the same entry twice. The duplicate carried no information and
50
+ * read as a defect ([/decisions/ad-101.md](/decisions/ad-101.md)).
51
+ *
52
+ * invariant: deduplicated by path, first occurrence winning, so the project tier keeps its position when it is
53
+ * also the machine tier. The hash is the same either way — it is one file.
54
+ */
55
+ const seen = new Set<string>();
56
+ return paths
57
+ .filter((path) => !seen.has(path) && seen.add(path) !== undefined)
58
+ .map((path) => ({ path, hash: hashOf(path) }));
47
59
  }
48
60
 
49
61
  function baselinePath(root: string, sessionKey: string): string {
@@ -27,71 +27,67 @@ function sameValue(a: unknown, b: unknown): boolean {
27
27
  }
28
28
 
29
29
  /**
30
- * Every leaf in `project` whose value the lower tiers already resolve to.
30
+ * One walk, two questions.
31
31
  *
32
- * `resolved` is the policy as it would be with the project config absent `DEFAULTS` merged with the user tier.
32
+ * hazard: `shadowedKeys` and `pruneShadowed` were two recursions over the same tree applying the same leaf test
33
+ * so a change to what counts as a restatement had to be made twice, and the report and the pruner could disagree
34
+ * about the same config ([/decisions/ad-101.md](/decisions/ad-101.md)).
35
+ *
36
+ * `resolved` is the policy as it would be with the project config absent: the shipped defaults merged with this
37
+ * machine's tier.
33
38
  */
34
- export function shadowedKeys(
39
+ function walk(
35
40
  project: Record<string, unknown>,
36
41
  resolved: Record<string, unknown>,
37
- prefix = "",
38
- ): ShadowedKey[] {
39
- const found: ShadowedKey[] = [];
42
+ prefix: string,
43
+ ): { shadowed: ShadowedKey[]; kept: Record<string, unknown> } {
44
+ const shadowed: ShadowedKey[] = [];
45
+ const kept: Record<string, unknown> = {};
40
46
  for (const [key, value] of Object.entries(project)) {
41
- // why skipped: `version` is the config's own shape marker rather than a setting, so naming it is required
42
- // rather than redundant.
47
+ // why kept and never reported: `version` marks the config's shape rather than a setting, so naming it is
48
+ // required rather than redundant.
43
49
  if (prefix === "" && key === "version") {
50
+ kept[key] = value;
44
51
  continue;
45
52
  }
46
53
  const path = prefix === "" ? key : `${prefix}.${key}`;
47
54
  const below = resolved[key];
48
55
  if (isPlainObject(value) && isPlainObject(below)) {
49
- found.push(...shadowedKeys(value, below, path));
56
+ const inner = walk(value, below, path);
57
+ shadowed.push(...inner.shadowed);
58
+ // why empty blocks go: `{ shipGate: {} }` decides nothing, and leaving it behind would make a pruned config
59
+ // read as though it had opinions.
60
+ if (Object.keys(inner.kept).length > 0) {
61
+ kept[key] = inner.kept;
62
+ }
50
63
  continue;
51
64
  }
52
65
  if (sameValue(value, below)) {
53
- found.push({ path, value });
66
+ shadowed.push({ path, value });
67
+ } else {
68
+ kept[key] = value;
54
69
  }
55
70
  }
56
- return found;
71
+ return { shadowed, kept };
72
+ }
73
+
74
+ /** What `doctor` reports: every leaf whose value the tiers below already resolve to. */
75
+ export function shadowedKeys(
76
+ project: Record<string, unknown>,
77
+ resolved: Record<string, unknown>,
78
+ ): ShadowedKey[] {
79
+ return walk(project, resolved, "").shadowed;
57
80
  }
58
81
 
59
82
  /**
60
- * The project config with every restatement removed.
61
- *
62
- * why this and not just a report: `init` wrote the whole default policy when a project had no config yet, and the
63
- * wizard wrote every knob it collected — so a fresh project shadowed the machine tier in dozens of places before
64
- * anyone had chosen anything. Reporting it after the fact leaves the operator to undo it by hand; not writing it
65
- * is the fix ([/decisions/ad-101.md](/decisions/ad-101.md)).
83
+ * What `init` writes: the same config with every restatement removed.
66
84
  *
67
85
  * invariant: pruning cannot change the effective policy. A leaf is dropped only when the tiers below already
68
86
  * resolve to it, so the merge produces the same value with the key absent.
69
- *
70
- * why empty objects go too: `{ shipGate: {} }` is a block that decides nothing, and leaving it behind would make
71
- * a pruned config read as though it had opinions.
72
87
  */
73
88
  export function pruneShadowed(
74
89
  project: Record<string, unknown>,
75
90
  resolved: Record<string, unknown>,
76
- prefix = "",
77
91
  ): Record<string, unknown> {
78
- const kept: Record<string, unknown> = {};
79
- for (const [key, value] of Object.entries(project)) {
80
- if (prefix === "" && key === "version") {
81
- kept[key] = value;
82
- continue;
83
- }
84
- const below = resolved[key];
85
- if (isPlainObject(value) && isPlainObject(below)) {
86
- const inner = pruneShadowed(value, below, `${prefix}${key}.`);
87
- if (Object.keys(inner).length > 0) {
88
- kept[key] = inner;
89
- }
90
- continue;
91
- }
92
- if (!sameValue(value, below)) {
93
- kept[key] = value;
94
- }
95
- }
96
- return kept;
92
+ return walk(project, resolved, "").kept;
97
93
  }
@@ -1,5 +1,6 @@
1
+ import { existsSync, realpathSync } from "node:fs";
1
2
  import { homedir } from "node:os";
2
- import { delimiter, join, resolve } from "node:path";
3
+ import { delimiter, dirname, join, resolve } from "node:path";
3
4
 
4
5
  function harnessDir(root: string): string {
5
6
  return join(root, ".tlc", "harness");
@@ -62,6 +63,33 @@ export function runtimeSpoolPath(): string {
62
63
  return join(runtimeStateDir(), "obs-spool.jsonl");
63
64
  }
64
65
 
66
+ /**
67
+ * The project a command is being run in: the nearest directory at or above `from` that holds a harness directory.
68
+ *
69
+ * why upward discovery: it is what every tool an operator already knows does — `git` walks up for `.git`, npm and
70
+ * cargo for their manifests, biome and eslint for their config. Anything else makes the operator's shell position
71
+ * part of the interface. Measured before this existed: `tlc harness status` run from `src/` reported the project as
72
+ * `src`, found no config there, fell back to the machine tier, and printed a posture the project had not set. Every
73
+ * command had that shape, and `policy accept` was only where it hurt enough to notice
74
+ * ([/decisions/ad-101.md](/decisions/ad-101.md)).
75
+ *
76
+ * invariant: `null` when there is none, so a first `init` still resolves to the working directory rather than to
77
+ * whichever ancestor happens to be a project.
78
+ */
79
+ export function findProjectRoot(from: string, exists: (path: string) => boolean = existsSync): string | null {
80
+ let current = resolve(from);
81
+ for (;;) {
82
+ if (exists(harnessDir(current))) {
83
+ return current;
84
+ }
85
+ const parent = dirname(current);
86
+ if (parent === current) {
87
+ return null;
88
+ }
89
+ current = parent;
90
+ }
91
+ }
92
+
65
93
  export function projectConfigPath(root: string): string {
66
94
  return join(harnessDir(root), "config.json");
67
95
  }
@@ -149,6 +177,40 @@ export function launcherNames(): readonly string[] {
149
177
  return ["tlc", "tlc.cmd"];
150
178
  }
151
179
 
180
+ /**
181
+ * The one place `PATH` is split.
182
+ *
183
+ * hazard: four implementations walked it — the CLI's executable finder, `doctor`'s, the launcher's Bun probe, and
184
+ * a third added for the install check. Three of them were the same function with different extension lists
185
+ * ([/decisions/ad-101.md](/decisions/ad-101.md)).
186
+ */
187
+ export function pathEntries(env: NodeJS.ProcessEnv = process.env): string[] {
188
+ return (env.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
189
+ }
190
+
191
+ /**
192
+ * why the extensions: on Windows a command is `tlc`, `tlc.cmd`, `tlc.exe`, `tlc.bat` or `tlc.ps1` depending on who
193
+ * generated it, and npm's shim is a `.cmd`. Asking for all of them costs one `existsSync` per entry and removes the
194
+ * only reason the two finders differed.
195
+ */
196
+ export const EXECUTABLE_EXTENSIONS = ["", ".exe", ".cmd", ".bat", ".ps1"] as const;
197
+
198
+ export function executableOnPath(
199
+ name: string,
200
+ env: NodeJS.ProcessEnv = process.env,
201
+ exists = existsSync,
202
+ ): string | null {
203
+ for (const dir of pathEntries(env)) {
204
+ for (const ext of EXECUTABLE_EXTENSIONS) {
205
+ const candidate = join(dir, `${name}${ext}`);
206
+ if (exists(candidate)) {
207
+ return candidate;
208
+ }
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+
152
214
  /**
153
215
  * Whether a shell would find something in this directory.
154
216
  *
@@ -156,6 +218,26 @@ export function launcherNames(): readonly string[] {
156
218
  * healthy link while the command still does not exist.
157
219
  */
158
220
  export function isOnPath(dir: string, env: NodeJS.ProcessEnv = process.env): boolean {
159
- const entries = (env.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
160
- return entries.some((entry) => resolve(entry) === resolve(dir));
221
+ return pathEntries(env).some((entry) => resolve(entry) === resolve(dir));
222
+ }
223
+
224
+ /**
225
+ * Whether two paths name the same directory, following links.
226
+ *
227
+ * why not `resolve` alone: `resolve` is lexical, so a runtime home that is a symlink to a checkout compares
228
+ * unequal to the checkout and every guard built on it misses. That is how `install` came to delete the checkout it
229
+ * pointed at ([/decisions/ad-100.md](/decisions/ad-100.md)).
230
+ *
231
+ * why the fallback: a destination that does not exist yet has no real path, and a first install is exactly that
232
+ * case. Then the lexical answer is the only one there is, and it is right — nothing is there to alias.
233
+ */
234
+ export function sameLocation(a: string, b: string): boolean {
235
+ const real = (path: string): string => {
236
+ try {
237
+ return realpathSync(path);
238
+ } catch {
239
+ return resolve(path);
240
+ }
241
+ };
242
+ return real(a) === real(b);
161
243
  }
@@ -1,6 +1,7 @@
1
1
  export { renderClaudeLessonsView } from "./claude/claude.lessons-view.ts";
2
2
  export type { TranscriptUsage } from "./claude/claude.transcript.ts";
3
3
  export { readClaudeUsage } from "./claude/claude.transcript.ts";
4
+ export { claudeWiring, mergeClaudeSettings } from "./claude/claude.wiring.ts";
4
5
  export { renderCursorLessonsView } from "./cursor/cursor.lessons-view.ts";
5
6
  export type { DegradeOptions } from "./provider.degrade.ts";
6
7
  export { degrade, truncateContext } from "./provider.degrade.ts";
package/tools/doctor.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
3
  import { homedir, platform as osPlatform } from "node:os";
4
- import { basename, delimiter, dirname, join } from "node:path";
5
- import { runtimePathKind } from "../bin/tlc-cli.ts";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { runtimePathKind, runtimeVersion } from "../bin/tlc-cli.ts";
6
6
  import { findBunOnPath, writeRuntimeCache } from "../bin/tlc-exec.mjs";
7
7
  import { isCursorWired } from "../bin/write-user-hooks.mjs";
8
8
  import type { ProviderWiring } from "../src/contracts/index.ts";
9
9
  import { coreFacade } from "../src/core/index.ts";
10
10
  import { emitJson, takeJsonFlag } from "../src/platform/cli-output.ts";
11
11
  import {
12
+ executableOnPath,
12
13
  launcherBinDir,
13
14
  projectConfigPath,
14
15
  projectStateDir,
@@ -215,36 +216,6 @@ export function checkPrices(
215
216
  ];
216
217
  }
217
218
 
218
- /**
219
- * Where a shell would find the `tlc` command, if anywhere.
220
- *
221
- * hazard: this row used to pass when `~/.local/bin/tlc` existed **or** `<runtime home>/bin/tlc` existed. The
222
- * second is part of every install, so the check could not fail — and it printed the first path either way, so an
223
- * operator whose command was not on PATH read a passing row naming a file they did not have
224
- * ([/decisions/ad-034.md](/decisions/ad-034.md), [/decisions/ad-097.md](/decisions/ad-097.md)).
225
- *
226
- * why the four names: an npm global install writes the shims for its platform — bare on POSIX, `.cmd` and `.ps1`
227
- * on Windows. Trying all four everywhere costs four `existsSync` calls and needs no platform branch.
228
- */
229
- export function resolveOnPath(
230
- command: string,
231
- env: NodeJS.ProcessEnv = process.env,
232
- exists = existsSync,
233
- ): string | null {
234
- for (const dir of (env.PATH ?? "").split(delimiter)) {
235
- if (!dir) {
236
- continue;
237
- }
238
- for (const name of [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`]) {
239
- const candidate = join(dir, name);
240
- if (exists(candidate)) {
241
- return candidate;
242
- }
243
- }
244
- }
245
- return null;
246
- }
247
-
248
219
  /**
249
220
  * The operator's own rules: which apply, where each came from, and any that can never be satisfied here.
250
221
  *
@@ -309,10 +280,18 @@ export function checkRules(root: string): Check[] {
309
280
  }
310
281
 
311
282
  export function checkRuntimePaths(home: string, platform: NodeJS.Platform): Check[] {
283
+ const version = runtimeVersion(home);
312
284
  const launcher = join(home, "bin", "tlc-exec.mjs");
313
285
  const distSample = join(home, "dist", "stop.mjs");
314
- const onPath = resolveOnPath("tlc");
286
+ const onPath = executableOnPath("tlc");
315
287
  return [
288
+ {
289
+ level: version === null ? "warn" : "ok",
290
+ name: "harness version",
291
+ // why a row rather than a footer: an operator asking "which version is this" was reading twenty rows that
292
+ // never said ([/decisions/ad-101.md](/decisions/ad-101.md)).
293
+ detail: version ?? `unknown — no readable package.json at ${home}`,
294
+ },
316
295
  { level: "ok", name: "platform", detail: platform },
317
296
  { level: existsSync(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
318
297
  runtimeOwnershipCheck(home),
@@ -1,9 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join, relative, resolve, sep } from "node:path";
4
4
  import { NPM_MARKER, NPM_PACKAGE, wireRuntime } from "../bin/tlc-cli.ts";
5
5
  import { linkDir } from "../src/platform/links.ts";
6
- import { conventionalRuntimeHome, runtimeHome, runtimeHomeWasChosen } from "../src/platform/paths.ts";
6
+ import {
7
+ conventionalRuntimeHome,
8
+ runtimeHome,
9
+ runtimeHomeWasChosen,
10
+ sameLocation,
11
+ } from "../src/platform/paths.ts";
7
12
  import { type Row, render, type Screen } from "../src/platform/screen.ts";
8
13
  import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
9
14
 
@@ -72,28 +77,17 @@ export function originRoot(env: NodeJS.ProcessEnv = process.env): string {
72
77
  }
73
78
 
74
79
  /**
75
- * hazard: this compared `resolve(source)` with `resolve(dest)`, and `resolve` does not follow a symlink. An
76
- * operator who installed with `--link` has a runtime home that *is* a link to their checkout, so the two paths
77
- * differed lexically while naming the same directory — the in-place guard missed, `rmSync` followed the link, and
78
- * the first entry of the payload deleted the checkout's own `bin/` before `cpSync` failed on the source it had
79
- * just removed. Measured on this repository: the gate ate its own `bin/` ([/decisions/ad-100.md](/decisions/ad-100.md)).
80
+ * hazard: the in-place guard compared `resolve(source)` with `resolve(dest)`, and `resolve` does not follow a
81
+ * symlink. An operator who installed with `--link` has a runtime home that *is* a link to their checkout, so the
82
+ * two paths differed lexically while naming the same directory — the guard missed, `rmSync` followed the link, and
83
+ * the first entry of the payload deleted the checkout's own `bin/`. Measured on this repository: the gate ate its
84
+ * own `bin/` ([/decisions/ad-100.md](/decisions/ad-100.md)).
80
85
  *
81
- * why the fallback: a destination that does not exist yet has no real path, and a first install is exactly that
82
- * case. Then the lexical answer is the only one there is, and it is correct — nothing is there to alias.
86
+ * invariant: one comparison, in `platform/`, so every caller that has to ask "is this the same place" gets the same
87
+ * answer ([/decisions/ad-101.md](/decisions/ad-101.md)).
83
88
  */
84
- function samePlace(a: string, b: string): boolean {
85
- const real = (path: string): string => {
86
- try {
87
- return realpathSync(path);
88
- } catch {
89
- return resolve(path);
90
- }
91
- };
92
- return real(a) === real(b);
93
- }
94
-
95
89
  export function installRuntime(source: string, dest: string): InstallReport {
96
- if (samePlace(source, dest)) {
90
+ if (sameLocation(source, dest)) {
97
91
  // why: the git route already has the code at the destination. Copying a directory onto itself is the one
98
92
  // input that turns a sync into data loss.
99
93
  return { kind: "in-place", source, dest, entries: [], missing: [] };