@tech-leads-club/harness-toolkit 0.16.1 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compact-before.mjs +69 -69
- package/dist/doctor.mjs +74 -74
- package/dist/init-project.mjs +71 -71
- package/dist/install-runtime.mjs +67 -67
- package/dist/lessons-cli.mjs +69 -69
- package/dist/obs-cli.mjs +68 -68
- package/dist/prompt-submit.mjs +86 -86
- package/dist/refresh-model-prices.mjs +69 -69
- package/dist/response-after.mjs +70 -70
- package/dist/run.mjs +69 -69
- package/dist/session-end.mjs +69 -69
- package/dist/session-start.mjs +71 -71
- package/dist/shim.mjs +66 -66
- package/dist/ship-gate.mjs +83 -83
- package/dist/stop.mjs +91 -91
- package/dist/subagent-start.mjs +70 -70
- package/dist/subagent-stop.mjs +88 -88
- package/dist/support.mjs +85 -85
- package/dist/tlc-cli.mjs +83 -83
- package/dist/tool-after.mjs +86 -86
- package/dist/tool-before.mjs +94 -94
- package/dist/tool-failure.mjs +69 -69
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +4 -0
- package/package.json +1 -1
- package/src/core/handoff/handoff.types.ts +4 -0
- package/src/core/skill/skill.link.ts +4 -2
- package/src/core/supply-chain/supply-chain.catalog.ts +8 -1
- package/src/entrypoints/prompt-submit.ts +1 -1
- package/src/entrypoints/ship-gate.ts +1 -1
- package/src/entrypoints/stop.ts +7 -3
- package/src/entrypoints/support.ts +37 -13
- package/src/entrypoints/tool-before.ts +22 -2
- package/src/platform/git.ts +27 -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 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(`
|
|
1
|
+
import{createRequire as _$}from"node:module";var i=_$(import.meta.url);import{existsSync as B,lstatSync as c8,readFileSync as S,readlinkSync as d8,realpathSync as M$,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 O$,isAbsolute as F$,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 D$($){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$=D$(s);var A$=T$.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 X$=String.fromCharCode(27),s$=`${X$}[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)=>$?`${X$}[${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},
|
|
4
|
-
`}}import{join 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},z$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun: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 oX,readFileSync as m8,realpathSync as tX,writeFileSync as g8}from"node:fs";function W$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function u8($){return W$($)}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(!W$(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 Y$($){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((I$)=>!JSON.stringify(I$??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 V$,relative as X4,resolve as z4,sep as W4}from"node:path";var G$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],B$=["config.json","state","flags","rules"],M4=[V$("tools","dev"),V$("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(M$(Z),...J)}catch{let Q=O$(Z);if(Q===Z)return p($);J.unshift(r8(Z)),Z=Q}}function x($){try{return M$($)}catch{try{let Z=d8($);return d(F$(Z)?Z:p(O$($),Z))}catch{return d($)}}}function e8($,Z,J={relative:o8,isAbsolute:F$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function U$($,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 H$($,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"&&!U$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!U$(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 G$){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 B$){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)H$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)H$(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 C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function WZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)Y$(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=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:K$($)}],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:K$($)},...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 K$($){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,C$ as pendingItems,BZ as main,e8 as isInsideRoot,d as canonicalise,WZ as applyUninstall};
|
package/docs/log.md
CHANGED
|
@@ -13,6 +13,10 @@ Generated from `docs/decisions/` — do not edit by hand. Run `node tools/render
|
|
|
13
13
|
A reserved file of the [OKF v0.1](/decisions/ad-013.md) bundle: entries grouped under ISO 8601 headings,
|
|
14
14
|
newest first. For what landed in which npm release, see `CHANGELOG.md` at the repository root.
|
|
15
15
|
|
|
16
|
+
## 2026-09-19
|
|
17
|
+
|
|
18
|
+
- **AD-145** — a session's last real shell cwd is recalled wherever the host reports none ([/decisions/ad-145.md](/decisions/ad-145.md))
|
|
19
|
+
|
|
16
20
|
## 2026-09-16
|
|
17
21
|
|
|
18
22
|
- **AD-144** — turn_base_sha records the git root it came from, and a mismatch falls back to HEAD ([/decisions/ad-144.md](/decisions/ad-144.md))
|
package/package.json
CHANGED
|
@@ -43,6 +43,10 @@ export type HandoffProviderSlice = {
|
|
|
43
43
|
* history as "added this turn" ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
44
44
|
*/
|
|
45
45
|
turn_base_root?: string;
|
|
46
|
+
// why: the last real cwd a shell-execution event reported for this session, recalled at an event kind
|
|
47
|
+
// that reports none. Without it, that event scopes every gate to the shared workspace root even when this
|
|
48
|
+
// session's own shell moved into a worktree ([/decisions/ad-145.md](/decisions/ad-145.md)).
|
|
49
|
+
last_shell_cwd?: string;
|
|
46
50
|
plan_paths?: string[];
|
|
47
51
|
plan_at?: string;
|
|
48
52
|
plan_snippet?: string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isInside } from "../floor/floor.paths.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Where the init skill has to be linked, and what counts as a link that works.
|
|
3
5
|
*
|
|
@@ -72,8 +74,8 @@ export function linkHealth(
|
|
|
72
74
|
return { state: "dangling", target, resolved };
|
|
73
75
|
}
|
|
74
76
|
const resolveHome = probe.realpath ?? ((path: string) => path);
|
|
75
|
-
const home = resolveHome(runtimeHome)
|
|
76
|
-
return
|
|
77
|
+
const home = resolveHome(runtimeHome);
|
|
78
|
+
return isInside(home, resolved)
|
|
77
79
|
? { state: "ok", target, resolved }
|
|
78
80
|
: { state: "outside-runtime", target, resolved };
|
|
79
81
|
}
|
|
@@ -42,7 +42,14 @@ export function manifestFor(relativePath: string): ManifestEntry | null {
|
|
|
42
42
|
|
|
43
43
|
/** why: the alternative lockfiles an ecosystem accepts. A project using pnpm has locked just as firmly as one using npm. */
|
|
44
44
|
export const ALTERNATE_LOCKFILES: Readonly<Record<string, readonly string[]>> = {
|
|
45
|
-
"package.json": [
|
|
45
|
+
"package.json": [
|
|
46
|
+
"package-lock.json",
|
|
47
|
+
"pnpm-lock.yaml",
|
|
48
|
+
"yarn.lock",
|
|
49
|
+
"npm-shrinkwrap.json",
|
|
50
|
+
"bun.lock",
|
|
51
|
+
"bun.lockb",
|
|
52
|
+
],
|
|
46
53
|
"pyproject.toml": ["poetry.lock", "pdm.lock", "uv.lock"],
|
|
47
54
|
};
|
|
48
55
|
|
|
@@ -15,7 +15,7 @@ export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: Han
|
|
|
15
15
|
// why: the exact AD-114 divergence, now against turn_base_sha instead of a rule's proof sha — a worktree
|
|
16
16
|
// session recorded the main checkout's HEAD as the turn's start, so every gate diffing against it saw a
|
|
17
17
|
// whole untouched file read as "added this turn" ([/decisions/ad-117.md](/decisions/ad-117.md)).
|
|
18
|
-
const shaRoot = shaScopeRoot(event);
|
|
18
|
+
const shaRoot = await shaScopeRoot(event);
|
|
19
19
|
const [sha, gitRoot] = await Promise.all([currentGitSha(shaRoot), gitRootOf(shaRoot)]);
|
|
20
20
|
// why: a sha with no recorded root cannot later be told apart from one captured in a different git root
|
|
21
21
|
// entirely — `resolveTurnBase` treats that ambiguity as unsafe, so both are written together or neither is
|
|
@@ -68,7 +68,7 @@ function gateFailureMessage(
|
|
|
68
68
|
* once something fired, one `git rev-parse`.
|
|
69
69
|
*/
|
|
70
70
|
export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
|
|
71
|
-
const shaRoot = shaScopeRoot(event);
|
|
71
|
+
const shaRoot = await shaScopeRoot(event);
|
|
72
72
|
if (!(await isShipCommand(event, shaRoot))) {
|
|
73
73
|
return { kind: "abstain" };
|
|
74
74
|
}
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -205,7 +205,11 @@ export async function runLockedGate(args: {
|
|
|
205
205
|
pendingCredit: PendingLessonCredit | undefined;
|
|
206
206
|
}): Promise<GateRun> {
|
|
207
207
|
const command = [...args.command, ...args.argvFiles];
|
|
208
|
-
|
|
208
|
+
// why: `args.recordFiles`/`args.argvFiles` are the turn's changed files, resolved against `shaRoot` — the
|
|
209
|
+
// same split AD-129 already applied to diffing. Hashing or running against `args.root` instead reads a
|
|
210
|
+
// worktree session's own changed file from the unrelated main checkout that happens to share its path
|
|
211
|
+
// ([/decisions/ad-145.md](/decisions/ad-145.md)).
|
|
212
|
+
const inputs = coreFacade.gate.computeInputsHash(args.shaRoot, args.recordFiles, command);
|
|
209
213
|
const cached = coreFacade.gate.cachedVerdict(
|
|
210
214
|
coreFacade.gate.readLastGate(args.root, args.sessionKey),
|
|
211
215
|
args.gate,
|
|
@@ -222,7 +226,7 @@ export async function runLockedGate(args: {
|
|
|
222
226
|
args.provider,
|
|
223
227
|
args.session,
|
|
224
228
|
async () => {
|
|
225
|
-
const result = await runCommand(args.
|
|
229
|
+
const result = await runCommand(args.shaRoot, args.command, args.argvFiles);
|
|
226
230
|
return coreFacade.gate.writeLastGate({
|
|
227
231
|
root: args.root,
|
|
228
232
|
sessionKey: args.sessionKey,
|
|
@@ -467,7 +471,7 @@ async function decideStopRules(
|
|
|
467
471
|
export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerContext): Promise<Decision> => {
|
|
468
472
|
const { policy, capabilities } = ctx;
|
|
469
473
|
const root = event.projectDir;
|
|
470
|
-
const shaRoot = shaScopeRoot(event);
|
|
474
|
+
const shaRoot = await shaScopeRoot(event);
|
|
471
475
|
const provider = event.provider;
|
|
472
476
|
const sessionKey = event.sessionKey;
|
|
473
477
|
const session = sessionIdFromKey(event);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import type { EffortLevel, HarnessEvent } from "../contracts/index.ts";
|
|
3
3
|
import {
|
|
4
4
|
type CommentFinding,
|
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
type PendingLessonCredit,
|
|
9
9
|
type Policy,
|
|
10
10
|
} from "../core/index.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
filterCodeTargets,
|
|
13
|
+
filterTestTargets,
|
|
14
|
+
gitCommonDirOf,
|
|
15
|
+
gitRootOf,
|
|
16
|
+
listChangedRepoFiles,
|
|
17
|
+
} from "../platform/git.ts";
|
|
12
18
|
import { runProcess } from "../platform/process.ts";
|
|
13
19
|
import { type ProviderPort, providers } from "../providers/index.ts";
|
|
14
20
|
|
|
@@ -66,16 +72,34 @@ export async function currentGitBranch(root: string): Promise<string | null> {
|
|
|
66
72
|
return branch.length > 0 ? branch : null;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
// why: `event.projectDir` prefers `CLAUDE_PROJECT_DIR`, kept pointed at the session's original root on
|
|
76
|
+
// purpose, including inside a worktree. `event.cwd` is the field a host actually moves — present on every
|
|
77
|
+
// Claude Code hook, but on Cursor only `beforeShellExecution` carries it ([/decisions/ad-114.md](/decisions/ad-114.md)).
|
|
78
|
+
// Absent, `recallSessionCwd` is the next best answer for an event, like `stop`, that never gets one.
|
|
79
|
+
export async function shaScopeRoot(event: HarnessEvent): Promise<string> {
|
|
80
|
+
if (event.cwd) {
|
|
81
|
+
return event.cwd;
|
|
82
|
+
}
|
|
83
|
+
const recalled = coreFacade.handoff.readHandoff(
|
|
84
|
+
event.projectDir,
|
|
85
|
+
event.provider,
|
|
86
|
+
event.sessionKey,
|
|
87
|
+
).last_shell_cwd;
|
|
88
|
+
return recallSessionCwd(recalled, event.projectDir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// why: trusted only once confirmed against `projectDir`'s own git object database — a deleted worktree, or a
|
|
92
|
+
// stale value a different repository entirely once wrote to this same session key, must not redirect a gate
|
|
93
|
+
// into the wrong directory ([/decisions/ad-145.md](/decisions/ad-145.md)).
|
|
94
|
+
async function recallSessionCwd(recalled: string | undefined, projectDir: string): Promise<string> {
|
|
95
|
+
if (!recalled || recalled === projectDir || !existsSync(recalled)) {
|
|
96
|
+
return projectDir;
|
|
97
|
+
}
|
|
98
|
+
const [recalledCommon, projectCommon] = await Promise.all([
|
|
99
|
+
gitCommonDirOf(recalled),
|
|
100
|
+
gitCommonDirOf(projectDir),
|
|
101
|
+
]);
|
|
102
|
+
return recalledCommon !== null && recalledCommon === projectCommon ? recalled : projectDir;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
export type TurnScope = {
|
|
@@ -331,7 +355,7 @@ export async function observeForRules(
|
|
|
331
355
|
if (!coreFacade.rules.wants(event.projectDir, config, event)) {
|
|
332
356
|
return;
|
|
333
357
|
}
|
|
334
|
-
const sha = await currentGitSha(shaScopeRoot(event));
|
|
358
|
+
const sha = await currentGitSha(await shaScopeRoot(event));
|
|
335
359
|
coreFacade.rules.observe(event.projectDir, config, event, {
|
|
336
360
|
sha,
|
|
337
361
|
sessionKey: event.sessionKey,
|
|
@@ -31,7 +31,7 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
|
|
|
31
31
|
if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
|
|
32
32
|
return { kind: "abstain" };
|
|
33
33
|
}
|
|
34
|
-
const shaRoot = shaScopeRoot(event);
|
|
34
|
+
const shaRoot = await shaScopeRoot(event);
|
|
35
35
|
const hits = await pendingCommentViolations(
|
|
36
36
|
event.projectDir,
|
|
37
37
|
shaRoot,
|
|
@@ -206,7 +206,7 @@ async function rulesDecision(event: HarnessEvent, ctx: HandlerContext): Promise<
|
|
|
206
206
|
command: event.command,
|
|
207
207
|
toolInput: event.toolInput,
|
|
208
208
|
};
|
|
209
|
-
const shaRoot = shaScopeRoot(event);
|
|
209
|
+
const shaRoot = await shaScopeRoot(event);
|
|
210
210
|
const dryRun = coreFacade.rules.decideAction(event.projectDir, config, trigger, {
|
|
211
211
|
sha: null,
|
|
212
212
|
sessionKey: event.sessionKey,
|
|
@@ -305,10 +305,30 @@ async function handleToolBefore(event: HarnessEvent, ctx: HandlerContext): Promi
|
|
|
305
305
|
return { kind: "allow" };
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// why: the one event a Cursor session ever reports a real cwd on — read back by `shaScopeRoot` at `stop`,
|
|
309
|
+
// which gets none at all ([/decisions/ad-145.md](/decisions/ad-145.md)). A no-op once the value already
|
|
310
|
+
// matches, so a session sitting in one directory pays this exactly once, not on every command.
|
|
311
|
+
async function recordShellCwd(event: HarnessEvent): Promise<void> {
|
|
312
|
+
if (event.event !== "shell.before" || !event.cwd) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const current = coreFacade.handoff.readHandoff(event.projectDir, event.provider, event.sessionKey);
|
|
316
|
+
if (current.last_shell_cwd === event.cwd) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, event.sessionKey, {
|
|
320
|
+
slice: { last_shell_cwd: event.cwd },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
308
324
|
export const toolBeforeHandler: Handler = async (
|
|
309
325
|
event: HarnessEvent,
|
|
310
326
|
ctx: HandlerContext,
|
|
311
327
|
): Promise<Decision> => {
|
|
328
|
+
// why: an observation, not a decision — recorded before the floor so it is captured regardless of what
|
|
329
|
+
// this specific command is ultimately allowed to do ([/decisions/ad-145.md](/decisions/ad-145.md)).
|
|
330
|
+
await recordShellCwd(event);
|
|
331
|
+
|
|
312
332
|
// invariant: the floor runs first and reads no policy, so no config value and no agent edit can
|
|
313
333
|
// reach a decision before it.
|
|
314
334
|
const floor = coreFacade.floor.evaluateFloor({
|
package/src/platform/git.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
4
5
|
import { runProcess } from "./process.ts";
|
|
5
6
|
import { normalizeSeparators } from "./sanitize.ts";
|
|
6
7
|
|
|
@@ -32,6 +33,31 @@ export async function gitRootOf(dir: string): Promise<string | null> {
|
|
|
32
33
|
return root.length > 0 ? root : null;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// why: every worktree of one repository shares this path, unlike `gitRootOf`'s own toplevel, which is
|
|
37
|
+
// worktree-specific by design — this is what tells "two worktrees of one repo" apart from "two unrelated repos".
|
|
38
|
+
export async function gitCommonDirOf(dir: string): Promise<string | null> {
|
|
39
|
+
let result: { exitCode: number; stdout: string };
|
|
40
|
+
try {
|
|
41
|
+
result = await runProcess({ command: ["git", "rev-parse", "--git-common-dir"], cwd: dir });
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (result.exitCode !== 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const raw = result.stdout.trim();
|
|
49
|
+
if (raw.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// why: `/tmp` and macOS's `/var/folders` are themselves symlinks, so two paths naming the same real
|
|
53
|
+
// directory can disagree as strings unless both are resolved through the same filesystem call.
|
|
54
|
+
try {
|
|
55
|
+
return await realpath(resolve(dir, raw));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
async function gitLines(projectDir: string, args: string[]): Promise<string[]> {
|
|
36
62
|
const result = await runProcess({ command: ["git", ...args], cwd: projectDir });
|
|
37
63
|
if (result.exitCode !== 0) {
|