@tech-leads-club/harness-toolkit 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compact-before.mjs +72 -72
- package/dist/doctor.mjs +66 -66
- package/dist/install-runtime.mjs +65 -65
- package/dist/lessons-cli.mjs +65 -65
- package/dist/obs-cli.mjs +66 -66
- package/dist/prompt-submit.mjs +71 -71
- package/dist/refresh-model-prices.mjs +65 -65
- package/dist/response-after.mjs +72 -72
- package/dist/run.mjs +73 -73
- package/dist/session-end.mjs +77 -77
- package/dist/session-start.mjs +79 -79
- package/dist/shim.mjs +64 -64
- package/dist/stop.mjs +76 -76
- package/dist/subagent-start.mjs +72 -72
- package/dist/subagent-stop.mjs +73 -73
- package/dist/support.mjs +67 -67
- package/dist/tlc-cli.mjs +80 -80
- package/dist/tool-after.mjs +72 -72
- package/dist/tool-before.mjs +72 -72
- package/dist/tool-failure.mjs +70 -70
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +1 -0
- package/package.json +1 -1
- package/src/contracts/index.ts +1 -0
- package/src/contracts/tool-names.ts +25 -0
- package/src/core/policy/policy.guard.ts +2 -3
- package/src/core/presence/presence.service.ts +10 -2
- package/src/entrypoints/run.ts +31 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as B$}from"node:module";var h=B$(import.meta.url);import{existsSync as K,lstatSync as C8,readFileSync as x,readlinkSync as I8,realpathSync as
|
|
1
|
+
import{createRequire as B$}from"node:module";var h=B$(import.meta.url);import{existsSync as K,lstatSync as C8,readFileSync as x,readlinkSync as I8,realpathSync as z$,rmSync as _8,statSync as N8,unlinkSync as j8,writeFileSync as L8}from"node:fs";import{homedir as T8}from"node:os";import{basename as D8,dirname as W$,isAbsolute as Y$,join as V,relative as P8,resolve as v}from"node:path";import{homedir as k}from"node:os";import{join as f}from"node:path";function p(){return f(k(),".tlc","harness")}function B($=process.env){return $.TLC_HOME??p()}function j(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:f(k(),".claude")}function L(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:f(k(),".cursor")}var i=[{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 M$($){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 O$=M$(i);var F$=O$.size;var S=6;var L$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var n2=new Set([...L$,"nc","ncat","socat","telnet","lwp-request"]);var T$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",s2=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${T$}))(?=[:)\\s,'"\`]|$)`,"g");var D="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:D,lastSeenAt:D,lastAccessedAt:D,updatedAt:D}}var vZ=[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 g$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},G={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function h$($){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 o=String.fromCharCode(27),p$=`${o}[0m`;function c$($=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 d$={ok:"success",warn:"warning",fail:"error",info:"info"},l$={ok:G.check,warn:G.warning,fail:G.cross,info:G.arrowRight},b=16;function P($=c$()){let Z=(Q,q)=>$?`${o}[${Q}m${q}${p$}`:q,J=(Q,q)=>Z(`38;2;${h$(g$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${G.rule} ${Q} ${G.rule}`),footer:(Q)=>J("textDim",`${G.dash} ${Q} ${G.dash}`),kv:(Q,q,X=b)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(d$[Q],l$[Q])} ${q}`}}var O=P(!1);function C($,Z){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.push(` ${$.summary.join(Z.dim(` ${G.bar} `))}`);let Q=Math.max(b,...$.sections.flatMap((q)=>(q.rows??[]).map((X)=>X.label.length+1)));for(let q of $.sections){if(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let X of q.rows??[]){let z=X.level?Z.status(X.level,X.value):X.value;J.push(Z.kv(X.label,z,Q))}for(let X of q.lines??[])J.push(X===""?"":` ${X}`)}if($.footer)J.push("",Z.footer($.footer));return J.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 i$=["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 n$={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},a$={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:S},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},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:{...n$}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",A="installed-from-npm";import{existsSync as K8,mkdirSync as
|
|
4
|
-
`}}import{join as $$,relative as
|
|
3
|
+
`);var n$={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},a$={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:S},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},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:{...n$}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",A="installed-from-npm";import{existsSync as K8,mkdirSync as Y3,readFileSync as H8,realpathSync as V3,writeFileSync as U8}from"node:fs";function t($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function M8($){return t($)}var O8="tlc-exec.mjs";function F8($){return JSON.stringify($??null).includes(O8)}function w($){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(!t(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=M8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let F=W.filter((_)=>!F8(_));if(F.length!==W.length)q=!0;if(F.length>0)Q[z]=F}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 e($){if(!K8($))return{ok:!0,settingsText:"",changed:!1};let Z=w(H8($,"utf8"));if(Z.ok&&Z.changed)U8($,Z.settingsText,"utf8");return Z}function y($,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[F,_]of Object.entries(q)){let g=Array.isArray(_)?_:[],N=g.filter((G$)=>!JSON.stringify(G$??null).includes(Z));if(z+=g.length-N.length,N.length>0)X[F]=N,W+=N.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 $$,relative as O3,resolve as F3,sep as C3}from"node:path";var Z$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],Q$=["config.json","state","flags"],P3=[$$("tools","dev"),$$("tools","__test__")];function R8($=process.env){let Z=T8(),J=$.TLC_BIN_DIR?.trim()||V(Z,".local","bin");return{home:B($),binLinks:[V(J,"tlc"),V(J,"tlc.cmd")],claudeSettings:V(j(),"settings.json"),cursorHooks:V(L(),"hooks.json"),skillLinks:[V(j(),"skills","harness-init"),V(L(),"skills","harness-init"),V(Z,".tlc","skills","harness-init")]}}function u($){try{return C8($).isSymbolicLink()}catch{return!1}}function m($){let Z=v($),J=[];for(;;)try{return V(z$(Z),...J)}catch{let Q=W$(Z);if(Q===Z)return v($);J.unshift(D8(Z)),Z=Q}}function E($){try{return z$($)}catch{try{let Z=I8($);return m(Y$(Z)?Z:v(W$($),Z))}catch{return m($)}}}function A8($,Z,J={relative:P8,isAbsolute:Y$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function J$($,Z){return A8(E($),m(Z))}var E8="tlc-exec.mjs";function x8($){try{return N8($).size<4096&&x($,"utf8").includes(E8)}catch{return!1}}function q$($,Z,J,Q,q){if(!K(Z)&&!u(Z))return;if(!u(Z)){if(x8(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"&&!J$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${E(Z)} — not ours`});return}let X=q==="location"&&!J$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${E(Z)}`:Q})}function k8($,Z){if(!K(Z))return;let J=w(x(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 f8($,Z){let J=K(Z)?x(Z,"utf8"):null,Q=y(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 S8($,Z,J){if(u(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${E(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!K(Z))return!1;for(let X of Z$){let z=V(Z,X);if(K(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=V(Z,A);if(K(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of Q$){let z=V(Z,X);if(!K(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function b8($,Z){if(K(V(Z,A)))$.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 w8($,Z={}){let J=Z.purge===!0,Q=[];k8(Q,$.claudeSettings),f8(Q,$.cursorHooks);for(let X of $.skillLinks)q$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)q$(Q,X,$.home,"the tlc launcher on PATH","target");let q=S8(Q,$.home,J);return b8(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function V$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function y8($,Z){let J=[],Q=[];for(let q of V$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)e(q.target);else if(q.action==="unmerge"){let X=y(x(q.target,"utf8"));if(X.kind==="rewritten")L8(q.target,X.text,"utf8")}else if(q.action==="unlink")j8(q.target);else _8(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 v8={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function u8($,Z){let J=V$($),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:X$($)}],footer:`already clean · reinstall with npm i -g ${I}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:v8[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:X$($)},...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 ${I}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function X$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function m8($,Z,J=O){return C(u8($,Z),J)}function g8($=process.argv.slice(2)){let Z=R8(),J=w8(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?y8(J,Z):null;return console.log(m8(J,Q,P())),Q!==null&&Q.failed.length>0?1:0}if(h.main==h.module)process.exitCode=g8();export{R8 as uninstallTargets,u8 as uninstallScreen,m8 as uninstallReportText,w8 as planUninstall,V$ as pendingItems,g8 as main,A8 as isInsideRoot,m as canonicalise,y8 as applyUninstall};
|
package/docs/log.md
CHANGED
|
@@ -16,6 +16,7 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
|
|
|
16
16
|
## 2026-08-20
|
|
17
17
|
|
|
18
18
|
- **AD-098** — Splitting is reverted: a shared chunk ran the CLI's main ([/decisions/ad-098.md](/decisions/ad-098.md))
|
|
19
|
+
- **AD-099** — Reading a file claimed it, so a review agent locked the operator out of writing ([/decisions/ad-099.md](/decisions/ad-099.md))
|
|
19
20
|
|
|
20
21
|
## 2026-08-19
|
|
21
22
|
|
package/package.json
CHANGED
package/src/contracts/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host tool names that write to a file.
|
|
3
|
+
*
|
|
4
|
+
* why here: two rails need the same answer — the policy-surface guard, which refuses an agent write to policy, and
|
|
5
|
+
* the presence claim, which decides whether another session may lose work. A second copy of this list is a second
|
|
6
|
+
* thing to forget when a host adds a tool ([/decisions/ad-010.md](/decisions/ad-010.md)).
|
|
7
|
+
*
|
|
8
|
+
* hazard: the presence claim did not consult any list. It recorded whatever file the event carried, and
|
|
9
|
+
* `read.before` carries one — so reading a file claimed it for ten minutes and blocked every other session from
|
|
10
|
+
* writing it, under a rule called `edit-collision` and a message that said the file had been edited. Measured on a
|
|
11
|
+
* real machine: a review agent that only read poisoned two files, and the operator's own `git status` showed one
|
|
12
|
+
* modification, theirs ([/decisions/ad-099.md](/decisions/ad-099.md)).
|
|
13
|
+
*/
|
|
14
|
+
export const WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
15
|
+
"Edit",
|
|
16
|
+
"Write",
|
|
17
|
+
"Delete",
|
|
18
|
+
"MultiEdit",
|
|
19
|
+
"NotebookEdit",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/** invariant: an absent tool name is not a write. A read carries a path too, and only writes may claim one. */
|
|
23
|
+
export function isWriteTool(toolName: string | undefined): boolean {
|
|
24
|
+
return toolName !== undefined && WRITE_TOOLS.has(toolName);
|
|
25
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { Decision } from "../../contracts/decision.ts";
|
|
2
|
+
import { isWriteTool } from "../../contracts/tool-names.ts";
|
|
2
3
|
import { isPolicySurface } from "../floor/floor.paths.ts";
|
|
3
4
|
|
|
4
5
|
export { isPolicySurface };
|
|
5
6
|
|
|
6
|
-
const WRITE_TOOLS = new Set(["Edit", "Write", "Delete", "MultiEdit", "NotebookEdit"]);
|
|
7
|
-
|
|
8
7
|
export function guardPolicySurface(args: {
|
|
9
8
|
projectDir: string;
|
|
10
9
|
toolName: string | undefined;
|
|
11
10
|
filePath: string | undefined;
|
|
12
11
|
}): Decision {
|
|
13
|
-
if (!
|
|
12
|
+
if (!isWriteTool(args.toolName) || !args.filePath) {
|
|
14
13
|
return { kind: "allow" };
|
|
15
14
|
}
|
|
16
15
|
if (!isPolicySurface(args.projectDir, args.filePath)) {
|
|
@@ -81,10 +81,18 @@ export function checkCollision(
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
const elapsed = elapsedLabel(record, nowMs);
|
|
84
|
+
/**
|
|
85
|
+
* hazard: this said "touched" and "edited this file" for a claim that a *read* could create, so the message
|
|
86
|
+
* asserted an edit that had not happened — and an operator checking `git status` found one modification, their
|
|
87
|
+
* own. Only a write claims now, and the words say so ([/decisions/ad-099.md](/decisions/ad-099.md)).
|
|
88
|
+
*
|
|
89
|
+
* invariant: the way out is named. `ask` becomes a refusal wherever no operator can answer the prompt, and a
|
|
90
|
+
* refusal with no exit is a lock-out.
|
|
91
|
+
*/
|
|
84
92
|
return {
|
|
85
93
|
kind: "ask",
|
|
86
|
-
reason: `${record.provider} session ${record.session}
|
|
87
|
-
userNote: `Another agent (${record.provider}, session ${record.session})
|
|
94
|
+
reason: `${record.provider} session ${record.session} wrote ${file} ${elapsed}.`,
|
|
95
|
+
userNote: `Another agent (${record.provider}, session ${record.session}) wrote this file ${elapsed}. Coordinate before proceeding: end that session, or wait for its claim to go stale (10 minutes without a heartbeat). \`tlc harness status\` lists the live sessions.`,
|
|
88
96
|
// why: this asks unconditionally, like the floor does, and carried no rule — so an operator reading a rate of
|
|
89
97
|
// interruptions could see the count and not the cause.
|
|
90
98
|
rule: "edit-collision",
|
package/src/entrypoints/run.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { Decision, HarnessEvent, ProviderCapabilities, Rendered } from "../contracts/index.ts";
|
|
3
|
+
import { isWriteTool } from "../contracts/tool-names.ts";
|
|
3
4
|
import { coreFacade, type Policy } from "../core/index.ts";
|
|
4
5
|
import { appendRecord } from "../platform/fs-jsonl.ts";
|
|
5
6
|
import { projectStateDir } from "../platform/paths.ts";
|
|
@@ -36,6 +37,25 @@ export type RunOutcome = {
|
|
|
36
37
|
// (lessons.maxCharsSession) — reusing that here truncated the operator posture and handoff.
|
|
37
38
|
export const CONTEXT_BUDGET_CHARS = 6000;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Whether this event may claim its file against other sessions.
|
|
42
|
+
*
|
|
43
|
+
* invariant: a claim exists so that two writers do not lose each other's work. A reader loses nothing, so a read
|
|
44
|
+
* carries no claim however many files it opens ([/decisions/ad-099.md](/decisions/ad-099.md)).
|
|
45
|
+
*
|
|
46
|
+
* why `edit.after` with no tool name still claims: the write already happened, and the host does not always name
|
|
47
|
+
* the tool on that event. An event that reports a completed edit is a writer by definition.
|
|
48
|
+
*/
|
|
49
|
+
export function claimsFile(event: HarnessEvent): boolean {
|
|
50
|
+
if (event.filePath === undefined) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (event.event === "edit.after") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return isWriteTool(event.toolName);
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
function errorMessage(error: unknown): string {
|
|
40
60
|
return error instanceof Error ? error.message : String(error);
|
|
41
61
|
}
|
|
@@ -144,10 +164,20 @@ export async function runHandler(handler: Handler, io: RunIo = {}): Promise<RunO
|
|
|
144
164
|
effectiveBlockedPatterns(policy.subagents.blockedPatterns, provider),
|
|
145
165
|
);
|
|
146
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* hazard: this passed `event.filePath` for every event, and `read.before` carries one. So reading a file
|
|
169
|
+
* claimed it for ten minutes, and the next session to write it was refused — under a rule called
|
|
170
|
+
* `edit-collision`, with a message saying the file had been edited. Measured on a real machine: a review agent
|
|
171
|
+
* that only read blocked the operator's own writes to two files, while their `git status` showed a single
|
|
172
|
+
* modification, theirs ([/decisions/ad-099.md](/decisions/ad-099.md)).
|
|
173
|
+
*
|
|
174
|
+
* invariant: the heartbeat is unconditional — a reading session is still a live session, and staleness is what
|
|
175
|
+
* expires a claim. Only the *claim* is write-only, because only a writer can lose somebody's work.
|
|
176
|
+
*/
|
|
147
177
|
coreFacade.presence.heartbeat(event.projectDir, {
|
|
148
178
|
provider: event.provider,
|
|
149
179
|
session: sessionIdFromKey(event),
|
|
150
|
-
file: event.filePath,
|
|
180
|
+
...(claimsFile(event) ? { file: event.filePath } : {}),
|
|
151
181
|
now,
|
|
152
182
|
});
|
|
153
183
|
const context: HandlerContext = { policy, capabilities, provider, now };
|