@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.
- package/dist/compact-before.mjs +87 -87
- package/dist/doctor.mjs +88 -88
- package/dist/init-project.mjs +85 -85
- package/dist/install-runtime.mjs +81 -81
- package/dist/lessons-cli.mjs +82 -82
- package/dist/obs-cli.mjs +82 -82
- package/dist/prompt-submit.mjs +87 -87
- package/dist/refresh-model-prices.mjs +81 -81
- package/dist/response-after.mjs +87 -87
- package/dist/run.mjs +87 -87
- package/dist/session-end.mjs +87 -87
- package/dist/session-start.mjs +89 -89
- package/dist/shim.mjs +80 -80
- package/dist/ship-gate.mjs +83 -83
- package/dist/stop.mjs +92 -92
- package/dist/subagent-start.mjs +87 -87
- package/dist/subagent-stop.mjs +88 -88
- package/dist/support.mjs +87 -87
- package/dist/tlc-cli.mjs +98 -98
- package/dist/tool-after.mjs +87 -87
- package/dist/tool-before.mjs +98 -93
- package/dist/tool-failure.mjs +87 -87
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +3 -0
- package/docs/providers/cursor.md +1 -1
- package/package.json +1 -1
- package/src/contracts/harness-event.ts +9 -0
- package/src/core/comment-policy/comment-policy.service.ts +2 -2
- package/src/core/core.facade.ts +2 -0
- package/src/core/observability/observability.report.ts +25 -8
- package/src/core/observability/observability.service.ts +3 -0
- package/src/core/observability/observability.store.ts +8 -0
- package/src/entrypoints/tool-before.ts +112 -1
- package/src/platform/git.ts +76 -16
- package/src/providers/claude/claude.inbound.ts +16 -0
- package/src/providers/cursor/cursor.capabilities.ts +5 -1
- 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
|
|
4
|
-
`}}import{join as Y$,relative as
|
|
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
|
|
package/docs/providers/cursor.md
CHANGED
|
@@ -35,7 +35,7 @@ Source: `src/providers/cursor/`.
|
|
|
35
35
|
| `contextAtStop` | `false` |
|
|
36
36
|
| `sessionStartContextReliable` | `false` |
|
|
37
37
|
| `toolOutputAtAfter` | `true` |
|
|
38
|
-
| `usageInPayload` | `
|
|
38
|
+
| `usageInPayload` | `false` |
|
|
39
39
|
| `effortSignal` | `false` |
|
|
40
40
|
| `thoughtEvent` | `true` |
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -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
|
|
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,
|
package/src/core/core.facade.ts
CHANGED
|
@@ -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
|
|
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 | ${
|
|
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
|
|
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
|
-
{
|
|
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 {
|
|
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
|
package/src/platform/git.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
183
|
+
if (cwd) {
|
|
173
184
|
event.cwd = cwd;
|
|
174
185
|
}
|
|
175
186
|
}
|