@tech-leads-club/harness-toolkit 0.16.0 → 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 +85 -85
- package/dist/stop.mjs +93 -93
- 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 +95 -95
- package/dist/tool-failure.mjs +69 -69
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +8 -0
- package/package.json +1 -1
- package/src/core/handoff/handoff.types.ts +11 -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 +8 -3
- package/src/entrypoints/ship-gate.ts +1 -1
- package/src/entrypoints/stop.ts +11 -5
- package/src/entrypoints/support.ts +62 -16
- package/src/entrypoints/tool-after.ts +2 -2
- package/src/entrypoints/tool-before.ts +22 -2
- package/src/platform/git.ts +27 -1
- package/src/providers/cursor/cursor.wiring.ts +1 -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,14 @@ 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
|
+
|
|
20
|
+
## 2026-09-16
|
|
21
|
+
|
|
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))
|
|
23
|
+
|
|
16
24
|
## 2026-09-12
|
|
17
25
|
|
|
18
26
|
- **AD-139** — lessonsView joins ProviderPort, closing the one dispatcher that bypassed it ([/decisions/ad-139.md](/decisions/ad-139.md))
|
package/package.json
CHANGED
|
@@ -36,6 +36,17 @@ export type HandoffProviderSlice = {
|
|
|
36
36
|
* ([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
37
37
|
*/
|
|
38
38
|
turn_base_sha?: string;
|
|
39
|
+
/**
|
|
40
|
+
* why: the git root `turn_base_sha` was captured from. A session that moves between independent
|
|
41
|
+
* worktrees can submit one turn's prompt from one root and stop it from another; without this, the
|
|
42
|
+
* stale sha from the first root gets diffed against the second, reading months of someone else's
|
|
43
|
+
* history as "added this turn" ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
44
|
+
*/
|
|
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;
|
|
39
50
|
plan_paths?: string[];
|
|
40
51
|
plan_at?: string;
|
|
41
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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HarnessEvent } from "../contracts/index.ts";
|
|
2
2
|
import { coreFacade } from "../core/index.ts";
|
|
3
|
+
import { gitRootOf } from "../platform/git.ts";
|
|
3
4
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
5
|
import { main } from "./run.ts";
|
|
5
6
|
import { currentGitSha, obsConfigFor, shaScopeRoot } from "./support.ts";
|
|
@@ -14,10 +15,14 @@ export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: Han
|
|
|
14
15
|
// why: the exact AD-114 divergence, now against turn_base_sha instead of a rule's proof sha — a worktree
|
|
15
16
|
// session recorded the main checkout's HEAD as the turn's start, so every gate diffing against it saw a
|
|
16
17
|
// whole untouched file read as "added this turn" ([/decisions/ad-117.md](/decisions/ad-117.md)).
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const shaRoot = await shaScopeRoot(event);
|
|
19
|
+
const [sha, gitRoot] = await Promise.all([currentGitSha(shaRoot), gitRootOf(shaRoot)]);
|
|
20
|
+
// why: a sha with no recorded root cannot later be told apart from one captured in a different git root
|
|
21
|
+
// entirely — `resolveTurnBase` treats that ambiguity as unsafe, so both are written together or neither is
|
|
22
|
+
// ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
23
|
+
if (sha && gitRoot) {
|
|
19
24
|
await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, event.sessionKey, {
|
|
20
|
-
slice: { turn_base_sha: sha },
|
|
25
|
+
slice: { turn_base_sha: sha, turn_base_root: gitRoot },
|
|
21
26
|
});
|
|
22
27
|
}
|
|
23
28
|
return { kind: "abstain" };
|
|
@@ -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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
currentGitSha,
|
|
19
19
|
formatLessonsBlock,
|
|
20
20
|
obsConfigFor,
|
|
21
|
+
resolveTurnBase,
|
|
21
22
|
sessionIdFromKey,
|
|
22
23
|
shaScopeRoot,
|
|
23
24
|
} from "./support.ts";
|
|
@@ -204,7 +205,11 @@ export async function runLockedGate(args: {
|
|
|
204
205
|
pendingCredit: PendingLessonCredit | undefined;
|
|
205
206
|
}): Promise<GateRun> {
|
|
206
207
|
const command = [...args.command, ...args.argvFiles];
|
|
207
|
-
|
|
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);
|
|
208
213
|
const cached = coreFacade.gate.cachedVerdict(
|
|
209
214
|
coreFacade.gate.readLastGate(args.root, args.sessionKey),
|
|
210
215
|
args.gate,
|
|
@@ -221,7 +226,7 @@ export async function runLockedGate(args: {
|
|
|
221
226
|
args.provider,
|
|
222
227
|
args.session,
|
|
223
228
|
async () => {
|
|
224
|
-
const result = await runCommand(args.
|
|
229
|
+
const result = await runCommand(args.shaRoot, args.command, args.argvFiles);
|
|
225
230
|
return coreFacade.gate.writeLastGate({
|
|
226
231
|
root: args.root,
|
|
227
232
|
sessionKey: args.sessionKey,
|
|
@@ -466,7 +471,7 @@ async function decideStopRules(
|
|
|
466
471
|
export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerContext): Promise<Decision> => {
|
|
467
472
|
const { policy, capabilities } = ctx;
|
|
468
473
|
const root = event.projectDir;
|
|
469
|
-
const shaRoot = shaScopeRoot(event);
|
|
474
|
+
const shaRoot = await shaScopeRoot(event);
|
|
470
475
|
const provider = event.provider;
|
|
471
476
|
const sessionKey = event.sessionKey;
|
|
472
477
|
const session = sessionIdFromKey(event);
|
|
@@ -489,8 +494,9 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
489
494
|
// past its own changes, and every gate below then saw an empty diff and skipped — the comment gate in a repo
|
|
490
495
|
// whose task was "schema v2 + tests + commit" ([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
491
496
|
//
|
|
492
|
-
// invariant: absent,
|
|
493
|
-
|
|
497
|
+
// invariant: absent, or captured from a different git root than `shaRoot`, this is the string `HEAD`,
|
|
498
|
+
// which is exactly the previous behaviour for the absent case ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
499
|
+
const turnBase = await resolveTurnBase(handoff, shaRoot);
|
|
494
500
|
/**
|
|
495
501
|
* why: collected rather than returned. A turn may defer more than one gate to the same neighbour, and the reply
|
|
496
502
|
* says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
@@ -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 = {
|
|
@@ -92,8 +116,9 @@ export type TurnScope = {
|
|
|
92
116
|
* at `stop` or, since AD-116, before `commit`/`push`/`pr-open` ships it. A second, differently-scoped
|
|
93
117
|
* answer to the same question is the drift AD-071 already named once ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
94
118
|
*
|
|
95
|
-
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent
|
|
96
|
-
* identical to `stop.ts`'s own fallback
|
|
119
|
+
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent, its seal diverged, or
|
|
120
|
+
* `resolveTurnBase` finds a different git root — identical to `stop.ts`'s own fallback
|
|
121
|
+
* ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
97
122
|
* why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
|
|
98
123
|
* (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
|
|
99
124
|
* branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
|
|
@@ -107,7 +132,7 @@ export async function computeTurnScope(
|
|
|
107
132
|
): Promise<TurnScope> {
|
|
108
133
|
const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
|
|
109
134
|
const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
|
|
110
|
-
const turnBase = handoff
|
|
135
|
+
const turnBase = await resolveTurnBase(handoff, gitRoot);
|
|
111
136
|
const rawChangedFiles = await listChangedRepoFiles(gitRoot, turnBase);
|
|
112
137
|
const otherSessionFiles = await coreFacade.presence.filesClaimedByOtherLiveSessions(
|
|
113
138
|
root,
|
|
@@ -159,6 +184,27 @@ export async function pendingCommentViolations(
|
|
|
159
184
|
);
|
|
160
185
|
}
|
|
161
186
|
|
|
187
|
+
/**
|
|
188
|
+
* why: `turn_base_sha` alone cannot tell "this session's own HEAD" from "the HEAD of a worktree the shell
|
|
189
|
+
* was sitting in three turns ago" — both are just a sha. Comparing the git root it was captured from
|
|
190
|
+
* against the root being diffed against is what tells the two apart, the same way an absent
|
|
191
|
+
* `turn_base_sha` already falls back to `HEAD` rather than diffing against nothing, instead of diffing one
|
|
192
|
+
* repository's turn against an unrelated one's entire history ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
193
|
+
*
|
|
194
|
+
* invariant: a slice with a sha but no recorded root (state written before this existed) resolves to
|
|
195
|
+
* `HEAD`, the same as no slice at all — safe by default, and self-healing on the next `prompt.submit`.
|
|
196
|
+
*/
|
|
197
|
+
export async function resolveTurnBase(
|
|
198
|
+
slice: { turn_base_sha?: string; turn_base_root?: string } | undefined,
|
|
199
|
+
gitRoot: string,
|
|
200
|
+
): Promise<string> {
|
|
201
|
+
if (!slice?.turn_base_sha || !slice.turn_base_root) {
|
|
202
|
+
return "HEAD";
|
|
203
|
+
}
|
|
204
|
+
const currentRoot = await gitRootOf(gitRoot);
|
|
205
|
+
return currentRoot !== null && currentRoot === slice.turn_base_root ? slice.turn_base_sha : "HEAD";
|
|
206
|
+
}
|
|
207
|
+
|
|
162
208
|
export async function currentGitSha(root: string): Promise<string | null> {
|
|
163
209
|
const gitRoot = await gitRootOf(root);
|
|
164
210
|
if (gitRoot === null) {
|
|
@@ -309,7 +355,7 @@ export async function observeForRules(
|
|
|
309
355
|
if (!coreFacade.rules.wants(event.projectDir, config, event)) {
|
|
310
356
|
return;
|
|
311
357
|
}
|
|
312
|
-
const sha = await currentGitSha(shaScopeRoot(event));
|
|
358
|
+
const sha = await currentGitSha(await shaScopeRoot(event));
|
|
313
359
|
coreFacade.rules.observe(event.projectDir, config, event, {
|
|
314
360
|
sha,
|
|
315
361
|
sessionKey: event.sessionKey,
|
|
@@ -6,7 +6,7 @@ import { normalizeSeparators } from "../platform/sanitize.ts";
|
|
|
6
6
|
import { readClaudeUsage } from "../providers/index.ts";
|
|
7
7
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
8
8
|
import { main } from "./run.ts";
|
|
9
|
-
import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules } from "./support.ts";
|
|
9
|
+
import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules, resolveTurnBase } from "./support.ts";
|
|
10
10
|
|
|
11
11
|
const OBS_KIND_BY_EVENT: Partial<Record<HarnessEventKind, ObsKind>> = {
|
|
12
12
|
"tool.after": "tool.end",
|
|
@@ -73,7 +73,7 @@ async function commentEditAdvisory(event: HarnessEvent, ctx: HandlerContext): Pr
|
|
|
73
73
|
event.projectDir,
|
|
74
74
|
targets,
|
|
75
75
|
policy.comments.mode,
|
|
76
|
-
handoff.
|
|
76
|
+
await resolveTurnBase(handoff, event.projectDir),
|
|
77
77
|
);
|
|
78
78
|
if (hits.length === 0) {
|
|
79
79
|
return null;
|
|
@@ -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) {
|
|
@@ -28,7 +28,7 @@ const ENTRY_SPECS: readonly EntrySpec[] = [
|
|
|
28
28
|
{ hookEvent: "preCompact", handler: "compact-before", timeoutSeconds: 5 },
|
|
29
29
|
{ hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5, failClosed: true },
|
|
30
30
|
{ hookEvent: "subagentStop", handler: "subagent-stop", timeoutSeconds: 5 },
|
|
31
|
-
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds:
|
|
31
|
+
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|
|
32
32
|
{ hookEvent: "postToolUse", handler: "tool-after", timeoutSeconds: 5 },
|
|
33
33
|
{ hookEvent: "postToolUseFailure", handler: "tool-failure", timeoutSeconds: 5 },
|
|
34
34
|
{ hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|