@tech-leads-club/harness-toolkit 0.13.0 → 0.14.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 +61 -61
- package/dist/doctor.mjs +56 -56
- package/dist/init-project.mjs +64 -64
- package/dist/install-runtime.mjs +58 -58
- package/dist/lessons-cli.mjs +53 -53
- package/dist/obs-cli.mjs +56 -56
- package/dist/prompt-submit.mjs +61 -61
- package/dist/refresh-model-prices.mjs +59 -59
- package/dist/response-after.mjs +61 -61
- package/dist/run.mjs +64 -64
- package/dist/session-end.mjs +64 -64
- package/dist/session-start.mjs +87 -87
- package/dist/shim.mjs +54 -54
- package/dist/ship-gate.mjs +76 -76
- package/dist/stop.mjs +84 -84
- package/dist/subagent-start.mjs +61 -61
- package/dist/subagent-stop.mjs +62 -62
- package/dist/support.mjs +82 -82
- package/dist/tlc-cli.mjs +57 -57
- package/dist/tool-after.mjs +60 -60
- package/dist/tool-before.mjs +80 -80
- package/dist/tool-failure.mjs +61 -61
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +1 -0
- package/package.json +1 -1
- package/src/core/comment-policy/comment-policy.service.ts +5 -4
- package/src/entrypoints/ship-gate.ts +4 -2
- package/src/entrypoints/stop.ts +5 -2
- package/src/entrypoints/support.ts +8 -7
- package/src/platform/git.ts +56 -20
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as I$}from"node:module";var l=I$(import.meta.url);import{existsSync as B,lstatSync as p8,readFileSync as S,readlinkSync as c8,realpathSync as K$,rmSync as d8,statSync as l8,unlinkSync as i8,writeFileSync as n8}from"node:fs";import{homedir as a8}from"node:os";import{basename as s8,dirname as M$,isAbsolute as O$,join as G,relative as r8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as OZ,dirname as CZ,join as T,resolve as FZ}from"node:path";function i(){return T(j(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function D(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:T(j(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:T(j(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||T(j(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function j$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var T$=j$(s);var D$=T$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var L5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",D5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var c9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={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 n$($){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),a$=`${q$}[0m`;function s$($=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 r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[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=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,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=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.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?$8(z,J-t$):[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 l=I$(import.meta.url);import{existsSync as B,lstatSync as p8,readFileSync as S,readlinkSync as c8,realpathSync as K$,rmSync as d8,statSync as l8,unlinkSync as i8,writeFileSync as n8}from"node:fs";import{homedir as a8}from"node:os";import{basename as s8,dirname as M$,isAbsolute as O$,join as G,relative as r8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as OZ,dirname as CZ,join as D,resolve as FZ}from"node:path";function i(){return D(L(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}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 n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function 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 y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var L5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="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-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var d9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={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 n$($){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),a$=`${q$}[0m`;function s$($=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 r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[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=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,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=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.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?$8(z,J-t$):[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 Q8=["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 J8={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:y},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:{...J8}},mcpPrime:[],bootstrapExtra:[]};var _="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as y8,mkdirSync as
|
|
4
|
-
`}}import{join as Y$,relative as
|
|
3
|
+
`);var J8={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:y},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:{...J8}},mcpPrime:[],bootstrapExtra:[]};var _="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as y8,mkdirSync as v4,readFileSync as v8,realpathSync as m4,writeFileSync as m8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function g8($){return z$($)}var u8="tlc-exec.mjs";function h8($){return JSON.stringify($??null).includes(u8)}function g($){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=g8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((N)=>!h8(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(!y8($))return{ok:!0,settingsText:"",changed:!1};let Z=g(v8($,"utf8"));if(Z.ok&&Z.changed)m8($,Z.settingsText,"utf8");return Z}function u($,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 d=Array.isArray(N)?N:[],j=d.filter((F$)=>!JSON.stringify(F$??null).includes(Z));if(z+=d.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 l4,resolve as i4,sep as n4}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],ZX=[Y$("tools","dev"),Y$("tools","__test__")];function o8($=process.env){let Z=a8(),J=b($);return{home:C($),binLinks:n().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 p($){try{return p8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(s8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=c8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function t8($,Z,J={relative:r8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return t8(x($),c(Z))}var e8="tlc-exec.mjs";function $Z($){try{return l8($).size<4096&&S($,"utf8").includes(e8)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if($Z(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 ZZ($,Z){if(!B(Z))return;let J=g(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 QZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=u(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 JZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let 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 qZ($,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 XZ($,Z={}){let J=Z.purge===!0,Q=[];ZZ(Q,$.claudeSettings),QZ(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=JZ(Q,$.home,J);return qZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function zZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=u(S(q.target,"utf8"));if(X.kind==="rewritten")n8(q.target,X.text,"utf8")}else if(q.action==="unlink")i8(q.target);else d8(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 WZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function YZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows: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:WZ[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 VZ($,Z,J=O){return I(YZ($,Z),J)}function GZ($=process.argv.slice(2)){let Z=o8(),J=XZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?zZ(J,Z):null;return console.log(VZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=GZ();export{o8 as uninstallTargets,YZ as uninstallScreen,VZ as uninstallReportText,XZ as planUninstall,C$ as pendingItems,GZ as main,t8 as isInsideRoot,c as canonicalise,zZ as applyUninstall};
|
package/docs/log.md
CHANGED
|
@@ -17,6 +17,7 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
|
|
|
17
17
|
|
|
18
18
|
- **AD-130** — gh api pr-open/push/pr-merge detection is scoped to the local checkout's own remote ([/decisions/ad-130.md](/decisions/ad-130.md))
|
|
19
19
|
- **AD-131** — A ship-gate denial names what it checked and points at tlc harness why ([/decisions/ad-131.md](/decisions/ad-131.md))
|
|
20
|
+
- **AD-132** — Git-root discovery replaces an exact-match guard, in the six functions and their consumers ([/decisions/ad-132.md](/decisions/ad-132.md))
|
|
20
21
|
|
|
21
22
|
## 2026-09-09
|
|
22
23
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { AddedLine } from "../../platform/git.ts";
|
|
4
|
-
import { listAddedLines } from "../../platform/git.ts";
|
|
4
|
+
import { gitRootOf, listAddedLines } from "../../platform/git.ts";
|
|
5
5
|
import type { CommentMode } from "../policy/policy.types.ts";
|
|
6
6
|
import type { CommentFinding } from "./comment-policy.types.ts";
|
|
7
7
|
import { firstLeak, leakReason } from "./comment-resolvability.ts";
|
|
@@ -278,13 +278,13 @@ 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(
|
|
281
|
+
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);
|
|
285
285
|
if (lines === undefined) {
|
|
286
286
|
try {
|
|
287
|
-
lines = readFileSync(join(
|
|
287
|
+
lines = readFileSync(join(gitRoot, file), "utf8").split("\n");
|
|
288
288
|
} catch {
|
|
289
289
|
lines = [];
|
|
290
290
|
}
|
|
@@ -301,7 +301,8 @@ export async function scanAddedComments(
|
|
|
301
301
|
base = "HEAD",
|
|
302
302
|
): Promise<CommentFinding[]> {
|
|
303
303
|
const added = await listAddedLines(projectDir, relativePaths, base);
|
|
304
|
-
|
|
304
|
+
const gitRoot = (await gitRootOf(projectDir)) ?? projectDir;
|
|
305
|
+
return findAddedComments(added, mode, diskLineReader(gitRoot));
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
export function commentViolationMessage(hits: CommentFinding[], mode: CommentMode = "declared"): string {
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// always shipped first and learned about the violation afterward. Same checks, before ship, identical
|
|
3
3
|
// on Claude and Cursor — every input read here is host-neutral ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
6
7
|
import { coreFacade } from "../core/index.ts";
|
|
7
|
-
import { listAddedLines, listTrackedFiles, localRepoRemote } from "../platform/git.ts";
|
|
8
|
+
import { gitRootOf, listAddedLines, listTrackedFiles, localRepoRemote } from "../platform/git.ts";
|
|
8
9
|
import type { HandlerContext } from "./run.ts";
|
|
9
10
|
import { runLockedGate } from "./stop.ts";
|
|
10
11
|
import {
|
|
@@ -174,11 +175,12 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
|
|
|
174
175
|
if (policy.duplication.enabled && scope.codeTargets.length > 0) {
|
|
175
176
|
const added = await listAddedLines(shaRoot, scope.codeTargets, scope.turnBase);
|
|
176
177
|
const tracked = await listTrackedFiles(shaRoot);
|
|
178
|
+
const gitRoot = (await gitRootOf(shaRoot)) ?? shaRoot;
|
|
177
179
|
const scan = coreFacade.duplication.scanProject(
|
|
178
180
|
tracked,
|
|
179
181
|
(relativePath) => {
|
|
180
182
|
try {
|
|
181
|
-
return readFileSync(
|
|
183
|
+
return readFileSync(join(gitRoot, relativePath), "utf8");
|
|
182
184
|
} catch {
|
|
183
185
|
return null;
|
|
184
186
|
}
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { coreFacade, type LastGateArtifact, type PendingLessonCredit, type Polic
|
|
|
5
5
|
import {
|
|
6
6
|
filterCodeTargets,
|
|
7
7
|
filterTestTargets,
|
|
8
|
+
gitRootOf,
|
|
8
9
|
listAddedLines,
|
|
9
10
|
listChangedRepoFiles,
|
|
10
11
|
listTrackedFiles,
|
|
@@ -709,12 +710,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
709
710
|
const manifests = changedFiles.filter((path) => coreFacade.supplyChain.isManifest(path));
|
|
710
711
|
if (manifests.length > 0) {
|
|
711
712
|
const added = await listAddedLines(shaRoot, manifests, turnBase);
|
|
713
|
+
const gitRoot = (await gitRootOf(shaRoot)) ?? shaRoot;
|
|
712
714
|
const outcome = coreFacade.supplyChain.inspectSupplyChain({
|
|
713
715
|
changedFiles,
|
|
714
716
|
added,
|
|
715
717
|
readManifest: (relativePath) => {
|
|
716
718
|
try {
|
|
717
|
-
return readFileSync(join(
|
|
719
|
+
return readFileSync(join(gitRoot, relativePath), "utf8");
|
|
718
720
|
} catch {
|
|
719
721
|
return null;
|
|
720
722
|
}
|
|
@@ -744,11 +746,12 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
744
746
|
if (policy.duplication.enabled && codeTargets.length > 0) {
|
|
745
747
|
const added = await listAddedLines(shaRoot, codeTargets, turnBase);
|
|
746
748
|
const tracked = await listTrackedFiles(shaRoot);
|
|
749
|
+
const gitRoot = (await gitRootOf(shaRoot)) ?? shaRoot;
|
|
747
750
|
const scan = coreFacade.duplication.scanProject(
|
|
748
751
|
tracked,
|
|
749
752
|
(relativePath) => {
|
|
750
753
|
try {
|
|
751
|
-
return readFileSync(join(
|
|
754
|
+
return readFileSync(join(gitRoot, relativePath), "utf8");
|
|
752
755
|
} catch {
|
|
753
756
|
return null;
|
|
754
757
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { statSync } from "node:fs";
|
|
3
2
|
import type { EffortLevel, HarnessEvent } from "../contracts/index.ts";
|
|
4
3
|
import {
|
|
5
4
|
type CommentFinding,
|
|
@@ -9,7 +8,7 @@ import {
|
|
|
9
8
|
type PendingLessonCredit,
|
|
10
9
|
type Policy,
|
|
11
10
|
} from "../core/index.ts";
|
|
12
|
-
import { filterCodeTargets, filterTestTargets, listChangedRepoFiles } from "../platform/git.ts";
|
|
11
|
+
import { filterCodeTargets, filterTestTargets, gitRootOf, listChangedRepoFiles } from "../platform/git.ts";
|
|
13
12
|
import { runProcess } from "../platform/process.ts";
|
|
14
13
|
import { type ProviderPort, renderClaudeLessonsView, renderCursorLessonsView } from "../providers/index.ts";
|
|
15
14
|
|
|
@@ -55,10 +54,11 @@ export function sessionIdFromKey(event: HarnessEvent): string {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export async function currentGitBranch(root: string): Promise<string | null> {
|
|
58
|
-
|
|
57
|
+
const gitRoot = await gitRootOf(root);
|
|
58
|
+
if (gitRoot === null) {
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
|
-
const result = await runProcess({ command: ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd:
|
|
61
|
+
const result = await runProcess({ command: ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd: gitRoot });
|
|
62
62
|
if (result.exitCode !== 0) {
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
@@ -153,10 +153,11 @@ export async function pendingCommentViolations(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
export async function currentGitSha(root: string): Promise<string | null> {
|
|
156
|
-
|
|
156
|
+
const gitRoot = await gitRootOf(root);
|
|
157
|
+
if (gitRoot === null) {
|
|
157
158
|
return null;
|
|
158
159
|
}
|
|
159
|
-
const result = await runProcess({ command: ["git", "rev-parse", "--short", "HEAD"], cwd:
|
|
160
|
+
const result = await runProcess({ command: ["git", "rev-parse", "--short", "HEAD"], cwd: gitRoot });
|
|
160
161
|
if (result.exitCode !== 0) {
|
|
161
162
|
return null;
|
|
162
163
|
}
|
package/src/platform/git.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { runProcess } from "./process.ts";
|
|
4
4
|
import { normalizeSeparators } from "./sanitize.ts";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* why: `existsSync(join(dir, ".git"))` requires `.git` to exist *exactly* at `dir` — it does not discover a
|
|
8
|
+
* repository root the way git itself does, walking upward from any subdirectory. A directory genuinely inside
|
|
9
|
+
* a real repository read as "not a repository" by every caller that used that check, silently
|
|
10
|
+
* ([/decisions/ad-132.md](/decisions/ad-132.md)).
|
|
11
|
+
*
|
|
12
|
+
* invariant: `git rev-parse --show-toplevel` already handles every case this project would otherwise have to
|
|
13
|
+
* reimplement — a subdirectory, a worktree's own subdirectory, a missing path, a genuinely absent repository —
|
|
14
|
+
* so nothing here special-cases any of them.
|
|
15
|
+
*/
|
|
16
|
+
export async function gitRootOf(dir: string): Promise<string | null> {
|
|
17
|
+
// why: every other caller in this file used to check `existsSync` first, which absorbed a missing directory
|
|
18
|
+
// silently. This function is now the first thing that runs when the directory does not exist at all —
|
|
19
|
+
// `spawn` rejects instead of resolving a non-zero exit code for that case, a different failure shape than
|
|
20
|
+
// "not a repository" ([/decisions/ad-132.md](/decisions/ad-132.md)).
|
|
21
|
+
let result: { exitCode: number; stdout: string };
|
|
22
|
+
try {
|
|
23
|
+
result = await runProcess({ command: ["git", "rev-parse", "--show-toplevel"], cwd: dir });
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const root = result.stdout.trim();
|
|
31
|
+
return root.length > 0 ? root : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
6
34
|
async function gitLines(projectDir: string, args: string[]): Promise<string[]> {
|
|
7
35
|
const result = await runProcess({ command: ["git", ...args], cwd: projectDir });
|
|
8
36
|
if (result.exitCode !== 0) {
|
|
@@ -22,14 +50,15 @@ async function gitLines(projectDir: string, args: string[]): Promise<string[]> {
|
|
|
22
50
|
* invariant: `HEAD` stays the default, so a caller with no recorded base behaves exactly as before.
|
|
23
51
|
*/
|
|
24
52
|
export async function listChangedRepoFiles(projectDir: string, base = "HEAD"): Promise<string[]> {
|
|
25
|
-
|
|
53
|
+
const root = await gitRootOf(projectDir);
|
|
54
|
+
if (root === null) {
|
|
26
55
|
return [];
|
|
27
56
|
}
|
|
28
57
|
|
|
29
58
|
const batches = await Promise.all([
|
|
30
|
-
gitLines(
|
|
31
|
-
gitLines(
|
|
32
|
-
gitLines(
|
|
59
|
+
gitLines(root, ["diff", "--name-only", base]),
|
|
60
|
+
gitLines(root, ["diff", "--name-only", "--cached"]),
|
|
61
|
+
gitLines(root, ["ls-files", "--others", "--exclude-standard"]),
|
|
33
62
|
]);
|
|
34
63
|
|
|
35
64
|
const paths = new Set<string>();
|
|
@@ -46,18 +75,16 @@ export async function listChangedRepoFiles(projectDir: string, base = "HEAD"): P
|
|
|
46
75
|
* over history without knowing how git formats anything.
|
|
47
76
|
*/
|
|
48
77
|
export async function listCommitFileSets(projectDir: string, limit: number): Promise<string[][]> {
|
|
49
|
-
if (
|
|
78
|
+
if (limit <= 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const root = await gitRootOf(projectDir);
|
|
82
|
+
if (root === null) {
|
|
50
83
|
return [];
|
|
51
84
|
}
|
|
52
85
|
// why: one git call for all commits. A separate call per commit is the obvious shape and is an order of
|
|
53
86
|
// magnitude slower on the history sizes this is used for.
|
|
54
|
-
const lines = await gitLines(
|
|
55
|
-
"log",
|
|
56
|
-
`-${limit}`,
|
|
57
|
-
"--name-only",
|
|
58
|
-
"--no-renames",
|
|
59
|
-
"--format=%x00",
|
|
60
|
-
]);
|
|
87
|
+
const lines = await gitLines(root, ["log", `-${limit}`, "--name-only", "--no-renames", "--format=%x00"]);
|
|
61
88
|
|
|
62
89
|
const commits: string[][] = [];
|
|
63
90
|
let current: string[] | null = null;
|
|
@@ -83,17 +110,21 @@ export async function listAddedLines(
|
|
|
83
110
|
relativePaths: string[],
|
|
84
111
|
base = "HEAD",
|
|
85
112
|
): Promise<AddedLine[]> {
|
|
86
|
-
if (
|
|
113
|
+
if (relativePaths.length === 0) {
|
|
87
114
|
return [];
|
|
88
115
|
}
|
|
89
|
-
const
|
|
116
|
+
const root = await gitRootOf(projectDir);
|
|
117
|
+
if (root === null) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const tracked = new Set(await gitLines(root, ["ls-files", "--", ...relativePaths]));
|
|
90
121
|
const out: AddedLine[] = [];
|
|
91
122
|
|
|
92
123
|
for (const file of relativePaths) {
|
|
93
124
|
if (!tracked.has(file)) {
|
|
94
125
|
let raw = "";
|
|
95
126
|
try {
|
|
96
|
-
raw = readFileSync(join(
|
|
127
|
+
raw = readFileSync(join(root, file), "utf8");
|
|
97
128
|
} catch {
|
|
98
129
|
continue;
|
|
99
130
|
}
|
|
@@ -102,7 +133,7 @@ export async function listAddedLines(
|
|
|
102
133
|
});
|
|
103
134
|
continue;
|
|
104
135
|
}
|
|
105
|
-
const diff = await gitLines(
|
|
136
|
+
const diff = await gitLines(root, ["diff", "--unified=0", base, "--", file]);
|
|
106
137
|
let lineNo = 0;
|
|
107
138
|
for (const row of diff) {
|
|
108
139
|
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(row);
|
|
@@ -179,7 +210,11 @@ export async function runCommand(
|
|
|
179
210
|
* and no second ignore list has to be kept in step ([/decisions/ad-071.md](/decisions/ad-071.md)).
|
|
180
211
|
*/
|
|
181
212
|
export async function listTrackedFiles(projectDir: string): Promise<string[]> {
|
|
182
|
-
const
|
|
213
|
+
const root = await gitRootOf(projectDir);
|
|
214
|
+
if (root === null) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const result = await runCommand(root, ["git", "ls-files", "-z"]);
|
|
183
218
|
if (result.exitCode !== 0) {
|
|
184
219
|
return [];
|
|
185
220
|
}
|
|
@@ -204,9 +239,10 @@ export function parseOwnerRepo(url: string): RepoRef | null {
|
|
|
204
239
|
* ([/decisions/ad-130.md](/decisions/ad-130.md)).
|
|
205
240
|
*/
|
|
206
241
|
export async function localRepoRemote(projectDir: string, remoteName = "origin"): Promise<RepoRef | null> {
|
|
207
|
-
|
|
242
|
+
const root = await gitRootOf(projectDir);
|
|
243
|
+
if (root === null) {
|
|
208
244
|
return null;
|
|
209
245
|
}
|
|
210
|
-
const result = await runProcess({ command: ["git", "remote", "get-url", remoteName], cwd:
|
|
246
|
+
const result = await runProcess({ command: ["git", "remote", "get-url", remoteName], cwd: root });
|
|
211
247
|
return result.exitCode === 0 ? parseOwnerRepo(result.stdout.trim()) : null;
|
|
212
248
|
}
|