@tech-leads-club/harness-toolkit 0.15.4 → 0.16.0

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.
Files changed (37) hide show
  1. package/dist/compact-before.mjs +87 -87
  2. package/dist/doctor.mjs +88 -88
  3. package/dist/init-project.mjs +85 -85
  4. package/dist/install-runtime.mjs +81 -81
  5. package/dist/lessons-cli.mjs +82 -82
  6. package/dist/obs-cli.mjs +82 -82
  7. package/dist/prompt-submit.mjs +87 -87
  8. package/dist/refresh-model-prices.mjs +81 -81
  9. package/dist/response-after.mjs +87 -87
  10. package/dist/run.mjs +87 -87
  11. package/dist/session-end.mjs +87 -87
  12. package/dist/session-start.mjs +89 -89
  13. package/dist/shim.mjs +80 -80
  14. package/dist/ship-gate.mjs +83 -83
  15. package/dist/stop.mjs +92 -92
  16. package/dist/subagent-start.mjs +87 -87
  17. package/dist/subagent-stop.mjs +88 -88
  18. package/dist/support.mjs +87 -87
  19. package/dist/tlc-cli.mjs +98 -98
  20. package/dist/tool-after.mjs +87 -87
  21. package/dist/tool-before.mjs +98 -93
  22. package/dist/tool-failure.mjs +87 -87
  23. package/dist/uninstall-runtime.mjs +3 -3
  24. package/docs/log.md +3 -0
  25. package/docs/providers/cursor.md +1 -1
  26. package/package.json +1 -1
  27. package/src/contracts/harness-event.ts +9 -0
  28. package/src/core/comment-policy/comment-policy.service.ts +2 -2
  29. package/src/core/core.facade.ts +2 -0
  30. package/src/core/observability/observability.report.ts +25 -8
  31. package/src/core/observability/observability.service.ts +3 -0
  32. package/src/core/observability/observability.store.ts +8 -0
  33. package/src/entrypoints/tool-before.ts +112 -1
  34. package/src/platform/git.ts +76 -16
  35. package/src/providers/claude/claude.inbound.ts +16 -0
  36. package/src/providers/cursor/cursor.capabilities.ts +5 -1
  37. package/src/providers/cursor/cursor.inbound.ts +12 -1
@@ -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 C$,join as G,relative as o8,resolve as p}from"node:path";import{homedir as L}from"node:os";import{delimiter as OZ,dirname as FZ,join as D,resolve as IZ}from"node:path";function n(){return D(L(),".tlc","harness")}function F($=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 C($){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=[C({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}),C({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}),C({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}),C({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}),C({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}),C({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 O=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 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(`
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 sX,readFileSync as m8,realpathSync as rX,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((F$)=>!JSON.stringify(F$??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 J4,resolve as q4,sep as X4}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],H4=[Y$("tools","dev"),Y$("tools","__test__")];function t8($=process.env){let Z=s8(),J=b($);return{home:F($),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(C$(Z)?Z:p(M$($),Z))}catch{return d($)}}}function e8($,Z,J={relative:o8,isAbsolute:C$}){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 O$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function WZ($,Z){let J=[],Q=[];for(let q of O$($))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=O$($),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=O){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,O$ 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},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};
package/docs/log.md CHANGED
@@ -17,6 +17,9 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
17
17
 
18
18
  - **AD-139** — lessonsView joins ProviderPort, closing the one dispatcher that bypassed it ([/decisions/ad-139.md](/decisions/ad-139.md))
19
19
  - **AD-140** — an unrecognized shell wrapper can no longer hide a forced push ([/decisions/ad-140.md](/decisions/ad-140.md))
20
+ - **AD-141** — Cursor's empty-string cwd no longer outranks the real project root ([/decisions/ad-141.md](/decisions/ad-141.md))
21
+ - **AD-142** — Cursor's usageInPayload was never true ([/decisions/ad-142.md](/decisions/ad-142.md))
22
+ - **AD-143** — a session with no usage source reads as unavailable, not as $0.0000 ([/decisions/ad-143.md](/decisions/ad-143.md))
20
23
 
21
24
  ## 2026-09-11
22
25
 
@@ -35,7 +35,7 @@ Source: `src/providers/cursor/`.
35
35
  | `contextAtStop` | `false` |
36
36
  | `sessionStartContextReliable` | `false` |
37
37
  | `toolOutputAtAfter` | `true` |
38
- | `usageInPayload` | `true` |
38
+ | `usageInPayload` | `false` |
39
39
  | `effortSignal` | `false` |
40
40
  | `thoughtEvent` | `true` |
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.15.4",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -91,6 +91,15 @@ export type HarnessEvent = {
91
91
  * that assumed presence would be blind on the majority of one host's traffic.
92
92
  */
93
93
  toolOutput?: string;
94
+ /**
95
+ * The new text an Edit/Write-shaped tool call is about to introduce, when this project has confirmed the
96
+ * shape: full file content for a whole-file write, or the replacement fragment for a targeted edit. Absent
97
+ * for every other tool_name — the signal a downstream check uses to abstain rather than guess a field name
98
+ * (same per-adapter-translation pattern as `toolOutput` above, [/decisions/ad-077.md](/decisions/ad-077.md)).
99
+ */
100
+ proposedContent?: string;
101
+ /** Targeted-edit shape only: the text being replaced, needed to locate the edit's real position in the on-disk file. */
102
+ proposedOldContent?: string;
94
103
  /** Adapter-only escape hatch — core must not read this. */
95
104
  raw: Record<string, unknown>;
96
105
  };
@@ -278,7 +278,7 @@ export function findAddedComments(
278
278
 
279
279
  // hazard: documenting an existing export touches only the comment, so the declaration it attaches to is
280
280
  // absent from the diff and has to be read from disk.
281
- function diskLineReader(gitRoot: string): NextCodeLine {
281
+ export function diskLineReader(gitRoot: string): NextCodeLine {
282
282
  const cache = new Map<string, string[]>();
283
283
  return (file, line) => {
284
284
  let lines = cache.get(file);
@@ -327,7 +327,7 @@ export function commentViolationMessage(hits: CommentFinding[], mode: CommentMod
327
327
  ];
328
328
  return [
329
329
  `BLOCKED: this turn added ${hits.length} comment(s).`,
330
- "TRIED: compared the lines this turn added against the commit it started from; pre-existing",
330
+ "TRIED: compared the proposed lines against what already exists there; pre-existing",
331
331
  "comments are never counted.",
332
332
  "Each entry is one comment, reported at its first line.",
333
333
  ...need,
@@ -25,6 +25,7 @@ import {
25
25
  commentEditAdvisory,
26
26
  commentViolationMessage,
27
27
  declaresReason,
28
+ diskLineReader,
28
29
  filterCommentTargets,
29
30
  findAddedComments,
30
31
  isCommentLine,
@@ -491,6 +492,7 @@ export const coreFacade = {
491
492
  filterCommentTargets,
492
493
  unknownExtensions,
493
494
  KNOWN_EXTENSION_COUNT,
495
+ diskLineReader,
494
496
  },
495
497
  ship: {
496
498
  detectShipClaim,
@@ -42,6 +42,23 @@ export function groupByProvider(events: ObsEvent[]): Record<string, ProviderTota
42
42
  return groups;
43
43
  }
44
44
 
45
+ // why: `$0.0000` and "no usage source ever reported" render identically without this — one is a real
46
+ // answer, the other is a host this project cannot get a number from at all
47
+ // ([/decisions/ad-142.md](/decisions/ad-142.md)). `cost_incomplete` is a different, narrower reason
48
+ // (a reading arrived, its model had no catalog rate) and is still checked once this one clears.
49
+
50
+ // hazard: `=== false`, not a falsy check — a rollup a build before this field existed wrote has
51
+ // `usage_reported: undefined`, and its `estimated_cost_usd` is a real number from before this flag
52
+ // existed to doubt it. A falsy check would relabel that real answer as unavailable, the exact
53
+ // confusion this field exists to remove, just pointed the other way.
54
+ function costLabel(rollup: SessionRollup, prefix = ""): string {
55
+ if (rollup.usage_reported === false) {
56
+ return "not available (this host reports no usage data)";
57
+ }
58
+ const amount = `${prefix}${rollup.estimated_cost_usd.toFixed(4)}`;
59
+ return rollup.cost_incomplete ? `${amount} (incomplete — some models lacked catalog rates)` : amount;
60
+ }
61
+
45
62
  /**
46
63
  * why: the count alone is not actionable — "seven interruptions" names no switch. The breakdown renders only when
47
64
  * there is something in it, so a session that was never interrupted reads exactly as it did before.
@@ -173,9 +190,7 @@ export function sessionReportMarkdown(rollup: SessionRollup, activeRules: readon
173
190
  const subs = Object.entries(rollup.subagents)
174
191
  .map(([t, s]) => `| ${t} | ${s.count} | ${JSON.stringify(s.models)} |`)
175
192
  .join("\n");
176
- const costLabel = rollup.cost_incomplete
177
- ? `${rollup.estimated_cost_usd.toFixed(4)} (incomplete — some models lacked catalog rates)`
178
- : rollup.estimated_cost_usd.toFixed(4);
193
+ const cost = costLabel(rollup);
179
194
 
180
195
  return `# Harness session report
181
196
 
@@ -188,7 +203,7 @@ export function sessionReportMarkdown(rollup: SessionRollup, activeRules: readon
188
203
 
189
204
  | Metric | Value |
190
205
  |--------|-------|
191
- | Estimated USD | ${costLabel} |
206
+ | Estimated USD | ${cost} |
192
207
  | Input tokens | ${rollup.input_tokens} |
193
208
  | Output tokens | ${rollup.output_tokens} |
194
209
  | Cost alert sent | ${rollup.cost_alert_sent} |
@@ -249,9 +264,7 @@ export function sessionReportScreen(rollup: SessionRollup): Screen {
249
264
  .slice(0, limit)
250
265
  .map(([name, count]) => `${name} ${count}`);
251
266
 
252
- const costLabel = rollup.cost_incomplete
253
- ? `$${rollup.estimated_cost_usd.toFixed(4)} (incomplete — some models lacked catalog rates)`
254
- : `$${rollup.estimated_cost_usd.toFixed(4)}`;
267
+ const cost = costLabel(rollup, "$");
255
268
 
256
269
  const shell = rollup.shell;
257
270
  // hazard: `rollup.tools` is fed only by `tool.start|end|fail`. A successful shell call is `shell.end` and a
@@ -279,7 +292,11 @@ export function sessionReportScreen(rollup: SessionRollup): Screen {
279
292
  {
280
293
  title: "Cost",
281
294
  rows: [
282
- { label: "estimated", value: costLabel, level: rollup.cost_incomplete ? "warn" : "info" },
295
+ {
296
+ label: "estimated",
297
+ value: cost,
298
+ level: rollup.usage_reported !== false && rollup.cost_incomplete ? "warn" : "info",
299
+ },
283
300
  {
284
301
  // why: the reading is the transcript tail's total, not the session's. Labelled for what it is, because
285
302
  // "Input tokens" invited the reading that produced 102.7M ([/decisions/ad-064.md](/decisions/ad-064.md)).
@@ -233,6 +233,9 @@ function updateRollup(root: string, config: ObservabilityConfig, event: ObsEvent
233
233
  */
234
234
  const inTok = event.gen_ai?.input_tokens ?? 0;
235
235
  const outTok = event.gen_ai?.output_tokens ?? 0;
236
+ if (event.gen_ai) {
237
+ rollup.usage_reported = true;
238
+ }
236
239
  if (inTok || outTok) {
237
240
  rollup.input_tokens = inTok;
238
241
  rollup.output_tokens = outTok;
@@ -51,6 +51,13 @@ export type SessionRollup = {
51
51
  mcp: Record<string, number>;
52
52
  estimated_cost_usd: number;
53
53
  cost_incomplete: boolean;
54
+ /**
55
+ * why a separate flag from `cost_incomplete`: that one means a reading arrived and its model had no
56
+ * catalog rate. `$0.0000` and no reading ever arriving at all — a provider with no usage source at
57
+ * all ([/decisions/ad-142.md](/decisions/ad-142.md)) — render identically without this, and only one
58
+ * of those is actually zero.
59
+ */
60
+ usage_reported: boolean;
54
61
  input_tokens: number;
55
62
  output_tokens: number;
56
63
  cost_alert_sent: boolean;
@@ -217,6 +224,7 @@ export function newRollup(sessionKey: string, provider: string): SessionRollup {
217
224
  mcp: {},
218
225
  estimated_cost_usd: 0,
219
226
  cost_incomplete: false,
227
+ usage_reported: false,
220
228
  input_tokens: 0,
221
229
  output_tokens: 0,
222
230
  cost_alert_sent: false,
@@ -1,6 +1,10 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
1
3
  import type { Decision, HarnessEvent } from "../contracts/index.ts";
2
4
  import { coreFacade } from "../core/index.ts";
3
- import { localRepoRemote } from "../platform/git.ts";
5
+ import type { AddedLine } from "../platform/git.ts";
6
+ import { diffProposedAgainstDisk, diffTwoStrings, localRepoRemote } from "../platform/git.ts";
7
+ import { normalizeSeparators } from "../platform/sanitize.ts";
4
8
  import type { Handler, HandlerContext } from "./run.ts";
5
9
  import { main } from "./run.ts";
6
10
  import { shipGateVerdict } from "./ship-gate.ts";
@@ -47,6 +51,108 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
47
51
  };
48
52
  }
49
53
 
54
+ /**
55
+ * hazard: `new_string` routinely carries lines copied verbatim from `old_string` — Claude's own Edit tool
56
+ * asks for surrounding context to make `old_string` unique. Treating every `new_string` line as added
57
+ * flagged a pre-existing comment the model never touched. Diffing `oldContent` against `newContent` (the
58
+ * same real-diff machinery `diffProposedAgainstDisk` already uses) isolates only what actually changed.
59
+ *
60
+ * why: `old_string`'s own start line, located on disk, offsets the diff's line numbers onto the real file —
61
+ * `new_string` is not at line 1, and `diskLineReader`'s JSDoc-identifier lookup needs the real line to read
62
+ * the right context after it.
63
+ */
64
+ async function editAddedLines(
65
+ currentContent: string,
66
+ oldContent: string,
67
+ newContent: string,
68
+ file: string,
69
+ ): Promise<AddedLine[] | null> {
70
+ const idx = currentContent.indexOf(oldContent);
71
+ if (idx === -1) {
72
+ return null;
73
+ }
74
+ const startLine = currentContent.slice(0, idx).split("\n").length;
75
+ const diff = await diffTwoStrings(oldContent, newContent, file);
76
+ return diff.map((line) => ({ ...line, line: line.line + startLine - 1 }));
77
+ }
78
+
79
+ /**
80
+ * why after `commentGateBeforeCommit`, before `shipGuard`: same tier — a built-in capability check, reads
81
+ * policy, not operator-authored ([/decisions/ad-115.md](/decisions/ad-115.md)).
82
+ */
83
+ async function commentGateBeforeEdit(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
84
+ const { policy } = ctx;
85
+ if (!policy.comments.enabled || policy.comments.onViolation !== "followup") {
86
+ return { kind: "abstain" };
87
+ }
88
+ if (event.proposedContent === undefined || event.proposedContent === "") {
89
+ return { kind: "abstain" };
90
+ }
91
+ const filePath = filePathOf(event);
92
+ if (!filePath) {
93
+ return { kind: "abstain" };
94
+ }
95
+ // why: a Write/Edit `tool_input.file_path` arrives relative to the project root, not to this process's own
96
+ // cwd — resolving it first is `resolveTarget`'s own approach (`src/core/floor/floor.paths.ts`); `relative()`
97
+ // alone treats a bare relative path as relative to `process.cwd()`, which only happens to equal `projectDir`
98
+ // when a real hook process's cwd was set there.
99
+ const absoluteFilePath = isAbsolute(filePath) ? filePath : resolve(event.projectDir, filePath);
100
+ const relativePath = normalizeSeparators(relative(event.projectDir, absoluteFilePath));
101
+ if (relativePath.startsWith("..") || !coreFacade.policy.isUnderCodePaths(relativePath, policy.codePaths)) {
102
+ return { kind: "abstain" };
103
+ }
104
+ if (coreFacade.commentPolicy.filterCommentTargets([relativePath]).length === 0) {
105
+ return { kind: "abstain" };
106
+ }
107
+
108
+ // why: `relativePath` was just computed against `event.projectDir`, not any resolved git root — a
109
+ // monorepo package whose `projectDir` sits below the repo's real root joined it onto the wrong base and
110
+ // either read the wrong file or, worse, read nothing and silently treated an existing file as brand new.
111
+ let added: AddedLine[];
112
+ let nextCodeLine: ((file: string, line: number) => string | undefined) | undefined;
113
+
114
+ if (event.toolName === "Write") {
115
+ added = await diffProposedAgainstDisk(event.projectDir, relativePath, event.proposedContent);
116
+ const proposedLines = event.proposedContent.split("\n");
117
+ nextCodeLine = (_file, line) => proposedLines[line - 1];
118
+ } else if (event.toolName === "Edit" && event.proposedOldContent !== undefined) {
119
+ let currentContent: string;
120
+ try {
121
+ currentContent = readFileSync(join(event.projectDir, relativePath), "utf8");
122
+ } catch {
123
+ return { kind: "abstain" };
124
+ }
125
+ const edited = await editAddedLines(
126
+ currentContent,
127
+ event.proposedOldContent,
128
+ event.proposedContent,
129
+ relativePath,
130
+ );
131
+ if (edited === null) {
132
+ return { kind: "abstain" };
133
+ }
134
+ added = edited;
135
+ nextCodeLine = coreFacade.commentPolicy.diskLineReader(event.projectDir);
136
+ } else {
137
+ return { kind: "abstain" };
138
+ }
139
+
140
+ const hits = coreFacade.commentPolicy.findAddedComments(added, policy.comments.mode, nextCodeLine);
141
+ if (hits.length === 0) {
142
+ return { kind: "abstain" };
143
+ }
144
+ // why: every deny in this battery names what it checked and points at `tlc harness why`
145
+ // ([/decisions/ad-131.md](/decisions/ad-131.md)) — this one has no commit sha to reproduce against, so
146
+ // it names the directory it checked, the same minimal form `rootDiagnostic` already gives a gate failure.
147
+ const diag = coreFacade.diagnostics.rootDiagnostic(event.projectDir);
148
+ return {
149
+ kind: "deny",
150
+ reason: `${coreFacade.commentPolicy.commentViolationMessage(hits, policy.comments.mode)}\n\n${diag.footer}`,
151
+ rule: "comment-policy-before-edit",
152
+ diagnostic: diag.summary,
153
+ };
154
+ }
155
+
50
156
  const READONLY_BLOCKED_TOOLS = new Set(["Write", "Delete", "Shell"]);
51
157
 
52
158
  /**
@@ -276,6 +382,11 @@ export const toolBeforeHandler: Handler = async (
276
382
  return commitGuard;
277
383
  }
278
384
 
385
+ const editGuard = await commentGateBeforeEdit(event, ctx);
386
+ if (editGuard.kind !== "abstain") {
387
+ return editGuard;
388
+ }
389
+
279
390
  /**
280
391
  * why the full battery only for push/pr-open: that is the moment something leaves the machine —
281
392
  * a shared branch, a PR, a deploy. A bare commit does not, so it keeps the cheap check above
@@ -1,4 +1,5 @@
1
- import { readFileSync } from "node:fs";
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { runProcess } from "./process.ts";
4
5
  import { normalizeSeparators } from "./sanitize.ts";
@@ -120,6 +121,28 @@ export type AddedLine = {
120
121
  text: string;
121
122
  };
122
123
 
124
+ // why: shared by `listAddedLines` (a real git-history diff) and `diffProposedAgainstDisk` (a diff of two
125
+ // in-memory strings never committed anywhere) — one parser for one diff format, not two.
126
+ function parseUnifiedAdded(diffLines: string[], file: string): AddedLine[] {
127
+ const out: AddedLine[] = [];
128
+ let lineNo = 0;
129
+ for (const row of diffLines) {
130
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(row);
131
+ if (hunk) {
132
+ lineNo = Number(hunk[1]);
133
+ continue;
134
+ }
135
+ if (row.startsWith("+++")) {
136
+ continue;
137
+ }
138
+ if (row.startsWith("+")) {
139
+ out.push({ file, line: lineNo, text: row.slice(1) });
140
+ lineNo += 1;
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+
123
146
  export async function listAddedLines(
124
147
  projectDir: string,
125
148
  relativePaths: string[],
@@ -149,25 +172,62 @@ export async function listAddedLines(
149
172
  continue;
150
173
  }
151
174
  const diff = await gitLines(root, ["diff", "--unified=0", base, "--", file]);
152
- let lineNo = 0;
153
- for (const row of diff) {
154
- const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(row);
155
- if (hunk) {
156
- lineNo = Number(hunk[1]);
157
- continue;
158
- }
159
- if (row.startsWith("+++")) {
160
- continue;
161
- }
162
- if (row.startsWith("+")) {
163
- out.push({ file, line: lineNo, text: row.slice(1) });
164
- lineNo += 1;
165
- }
166
- }
175
+ out.push(...parseUnifiedAdded(diff, file));
167
176
  }
168
177
  return out;
169
178
  }
170
179
 
180
+ // hazard: exit 1 means "diff found" here, not failure like every other git call in this file — reusing
181
+ // `gitLines` would read every real diff as empty.
182
+ async function noIndexDiffLines(fileA: string, fileB: string): Promise<string[]> {
183
+ const result = await runProcess({ command: ["git", "diff", "--no-index", "--unified=0", fileA, fileB] });
184
+ if (result.exitCode >= 2) {
185
+ return [];
186
+ }
187
+ return result.stdout
188
+ .split("\n")
189
+ .map((line) => line.trim())
190
+ .filter(Boolean);
191
+ }
192
+
193
+ // invariant: never writes to `.git/objects` or the working tree — two temp files carry both sides,
194
+ // removed in `finally`.
195
+ export async function diffTwoStrings(
196
+ currentContent: string,
197
+ proposedContent: string,
198
+ file: string,
199
+ ): Promise<AddedLine[]> {
200
+ if (currentContent === proposedContent) {
201
+ return [];
202
+ }
203
+
204
+ const scratch = mkdtempSync(join(tmpdir(), "tlc-comment-gate-"));
205
+ try {
206
+ const currentPath = join(scratch, "current");
207
+ const proposedPath = join(scratch, "proposed");
208
+ writeFileSync(currentPath, currentContent, "utf8");
209
+ writeFileSync(proposedPath, proposedContent, "utf8");
210
+ const diff = await noIndexDiffLines(currentPath, proposedPath);
211
+ return parseUnifiedAdded(diff, file);
212
+ } finally {
213
+ rmSync(scratch, { recursive: true, force: true });
214
+ }
215
+ }
216
+
217
+ export async function diffProposedAgainstDisk(
218
+ gitRoot: string,
219
+ file: string,
220
+ proposedContent: string,
221
+ ): Promise<AddedLine[]> {
222
+ let currentContent = "";
223
+ try {
224
+ currentContent = readFileSync(join(gitRoot, file), "utf8");
225
+ } catch {
226
+ currentContent = "";
227
+ }
228
+ return diffTwoStrings(currentContent, proposedContent, file);
229
+ }
230
+
171
231
  function isUnderPrefixes(relativePath: string, prefixes: string[]): boolean {
172
232
  const normalized = normalizeSeparators(relativePath);
173
233
  return prefixes.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
@@ -250,6 +250,22 @@ export function claudeToEvent(raw: Record<string, unknown>): HarnessEvent | null
250
250
  if (spawnModel) {
251
251
  event.spawnModel = spawnModel;
252
252
  }
253
+ if (toolName === "Write") {
254
+ const proposedContent = toolInput ? asString(toolInput.content) : undefined;
255
+ if (proposedContent !== undefined) {
256
+ event.proposedContent = proposedContent;
257
+ }
258
+ }
259
+ if (toolName === "Edit") {
260
+ const proposedContent = toolInput ? asString(toolInput.new_string) : undefined;
261
+ if (proposedContent !== undefined) {
262
+ event.proposedContent = proposedContent;
263
+ }
264
+ const proposedOldContent = toolInput ? asString(toolInput.old_string) : undefined;
265
+ if (proposedOldContent !== undefined) {
266
+ event.proposedOldContent = proposedOldContent;
267
+ }
268
+ }
253
269
  break;
254
270
  }
255
271
  case "subagent.start":
@@ -23,7 +23,11 @@ export function cursorCapabilities(): ProviderCapabilities {
23
23
  // code path ([/decisions/ad-050.md](/decisions/ad-050.md)).
24
24
  sessionStartContextReliable: false,
25
25
  toolOutputAtAfter: true,
26
- usageInPayload: true,
26
+ // hazard: found live — a real session's cost report read $0.0000 throughout. No hook payload this
27
+ // adapter has observed carries a token field, and `transcript_path` is a plain message/tool-call
28
+ // log with no usage field either ([/decisions/ad-142.md](/decisions/ad-142.md)). `true` made
29
+ // `usageGenAi` skip its one real fallback for a host with nothing to read there either.
30
+ usageInPayload: false,
27
31
  effortSignal: false,
28
32
  thoughtEvent: true,
29
33
  };
@@ -151,6 +151,12 @@ export function cursorToEvent(raw: Record<string, unknown>): HarnessEvent | null
151
151
  if (toolSpawnType) {
152
152
  event.spawnSubagentType = toolSpawnType;
153
153
  }
154
+ if (toolName === "Write") {
155
+ const proposedContent = toolInput ? asString(toolInput.content) : undefined;
156
+ if (proposedContent !== undefined) {
157
+ event.proposedContent = proposedContent;
158
+ }
159
+ }
154
160
  break;
155
161
  }
156
162
  case "shell.before":
@@ -167,9 +173,14 @@ export function cursorToEvent(raw: Record<string, unknown>): HarnessEvent | null
167
173
  // (this event's `shell.before` half), not on `afterShellExecution` or any other event this
168
174
  // adapter maps. Never guessed onto an event kind the host does not report it for
169
175
  // ([/decisions/ad-114.md](/decisions/ad-114.md)).
176
+
177
+ // hazard: found live — Cursor sends `cwd: ""` on a command that never entered a worktree, not an
178
+ // absent field. `shaScopeRoot` treats any non-nullish `event.cwd` as an override, so `""` beat the
179
+ // real `projectDir` and resolved a rule proof's sha against the wrong directory entirely
180
+ // ([/decisions/ad-141.md](/decisions/ad-141.md)) — a truthy check, like `claude.inbound.ts` already uses.
170
181
  if (eventKind === "shell.before") {
171
182
  const cwd = asString(raw.cwd);
172
- if (cwd !== undefined) {
183
+ if (cwd) {
173
184
  event.cwd = cwd;
174
185
  }
175
186
  }