@tech-leads-club/harness-toolkit 0.16.2 → 0.16.4
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/README.md +10 -6
- package/bin/tlc-cli.ts +3 -3
- package/bin/tlc-exec.mjs +14 -2
- package/dist/compact-before.mjs +89 -89
- package/dist/doctor.mjs +88 -88
- package/dist/init-project.mjs +84 -84
- package/dist/install-runtime.mjs +87 -87
- package/dist/lessons-cli.mjs +81 -81
- package/dist/obs-cli.mjs +82 -82
- package/dist/prompt-submit.mjs +89 -89
- package/dist/refresh-model-prices.mjs +81 -81
- package/dist/response-after.mjs +89 -89
- package/dist/run.mjs +89 -89
- package/dist/session-end.mjs +89 -89
- package/dist/session-start.mjs +91 -91
- package/dist/shim.mjs +80 -80
- package/dist/ship-gate.mjs +83 -83
- package/dist/stop.mjs +95 -95
- package/dist/subagent-start.mjs +89 -89
- package/dist/subagent-stop.mjs +90 -90
- package/dist/support.mjs +85 -85
- package/dist/tlc-cli.mjs +97 -97
- package/dist/tool-after.mjs +89 -89
- package/dist/tool-before.mjs +98 -98
- package/dist/tool-failure.mjs +89 -89
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/architecture.md +20 -10
- package/docs/concepts.md +4 -2
- package/docs/diagnose.md +3 -2
- package/docs/providers/cursor.md +2 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +1 -1
- package/src/core/core.facade.ts +3 -0
- package/src/core/policy/policy.pause.ts +21 -0
- package/src/entrypoints/run.ts +38 -4
- package/src/entrypoints/tool-before.ts +13 -10
- package/src/platform/git.ts +15 -0
- package/src/providers/cursor/cursor.detect.ts +1 -4
|
@@ -1,4 +1,4 @@
|
|
|
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(`
|
|
1
|
+
import{createRequire as _$}from"node:module";var i=_$(import.meta.url);import{existsSync as B,lstatSync as d8,readFileSync as S,readlinkSync as l8,realpathSync as M$,rmSync as i8,statSync as n8,unlinkSync as a8,writeFileSync as s8}from"node:fs";import{homedir as r8}from"node:os";import{basename as o8,dirname as O$,isAbsolute as F$,join as G,relative as t8,resolve as p}from"node:path";import{homedir as D}from"node:os";import{delimiter as CZ,dirname as IZ,join as T,resolve as _Z}from"node:path";function n(){return T(D(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??n()}function A(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:T(D(),".claude")}function P(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:T(D(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||T(D(),".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 D5=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",P5=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 o9=[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 _($,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((W)=>W.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let W of X.rows??[]){let z=W.level?Z.status(W.level,W.value):W.value;Q.push(Z.kv(W.label,z,q))}for(let W of X.lines??[]){if(W===""){Q.push("");continue}let z=X.wrap?Z8(W,J-e$):[W];for(let U of z)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 V$,relative as
|
|
3
|
+
`);var q8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},W$={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 N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as m8,mkdirSync as X4,readFileSync as g8,realpathSync as W4,writeFileSync as u8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function h8($){return z$($)}var p8="tlc-exec.mjs";function c8($){return JSON.stringify($??null).includes(p8)}function u($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(W){return{ok:!1,error:W instanceof Error?W.message:String(W),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=h8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[W,z]of Object.entries(J)){let U=z.filter((j)=>!c8(j));if(U.length!==z.length)q=!0;if(U.length>0)Q[W]=U}let X={};for(let[W,z]of Object.entries(Z))if(W!=="hooks")X[W]=z;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function Y$($){if(!m8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(g8($,"utf8"));if(Z.ok&&Z.changed)u8($,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={},W=0,z=0;for(let[U,j]of Object.entries(q)){let l=Array.isArray(j)?j:[],L=l.filter((I$)=>!JSON.stringify(I$??null).includes(Z));if(W+=l.length-L.length,L.length>0)X[U]=L,z+=L.length}if(z===0)return{kind:"empty",removed:W};return{kind:"rewritten",removed:W,text:`${JSON.stringify({...Q,hooks:X},null,2)}
|
|
4
|
+
`}}import{join as V$,relative as H4,resolve as K4,sep as M4}from"node:path";var G$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],B$=["config.json","state","flags","rules"],L4=[V$("tools","dev"),V$("tools","__test__")];function e8($=process.env){let Z=r8(),J=b($);return{home:C($),binLinks:a().map((Q)=>G(J,Q)),claudeSettings:G(A(),"settings.json"),cursorHooks:G(P(),"hooks.json"),skillLinks:[G(A(),"skills","harness-init"),G(P(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function c($){try{return d8($).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(o8(Z)),Z=Q}}function x($){try{return M$($)}catch{try{let Z=l8($);return d(F$(Z)?Z:p(O$($),Z))}catch{return d($)}}}function $Z($,Z,J={relative:t8,isAbsolute:F$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function U$($,Z){return $Z(x($),d(Z))}var ZZ="tlc-exec.mjs";function QZ($){try{return n8($).size<4096&&S($,"utf8").includes(ZZ)}catch{return!1}}function H$($,Z,J,Q,q){if(!B(Z)&&!c(Z))return;if(!c(Z)){if(QZ(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 JZ($,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 qZ($,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 XZ($,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 W=G(Z,X);if(B(W))$.push({action:"remove",target:W,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 W=G(Z,X);if(!B(W))continue;$.push(J?{action:"remove",target:W,detail:"--purge"}:{action:"keep",target:W,detail:"yours — add --purge to remove it"})}return!1}function WZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${N}`,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=[];JZ(Q,$.claudeSettings),qZ(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=XZ(Q,$.home,J);return WZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function YZ($,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")s8(q.target,X.text,"utf8")}else if(q.action==="unlink")a8(q.target);else i8(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 VZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function GZ($,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 ${N}, then \`tlc harness install\``};let q=J.map((z)=>({label:z.action,value:`${z.target} — ${z.detail}`,level:VZ[z.action]})),X=$.items.filter((z)=>z.action==="keep"),W=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((z)=>({label:"keep",value:`${z.target} — ${z.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:K$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((z)=>({label:"error",value:`${z.item.target} — ${z.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:W,footer:Q?`reinstall any time with npm i -g ${N}, 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 BZ($,Z,J=F){return _(GZ($,Z),J)}function UZ($=process.argv.slice(2)){let Z=e8(),J=zZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?YZ(J,Z):null;return console.log(BZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(i.main==i.module)process.exitCode=UZ();export{e8 as uninstallTargets,GZ as uninstallScreen,BZ as uninstallReportText,zZ as planUninstall,C$ as pendingItems,UZ as main,$Z as isInsideRoot,d as canonicalise,YZ as applyUninstall};
|
package/docs/architecture.md
CHANGED
|
@@ -20,13 +20,18 @@ Global does the work. Project only configures *this* repo. Do not copy `~/.tlc/h
|
|
|
20
20
|
```mermaid
|
|
21
21
|
graph LR
|
|
22
22
|
subgraph one["One machine"]
|
|
23
|
-
R["
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
R["`~/.tlc/harness
|
|
24
|
+
*runtime, CLI, hooks*`"]
|
|
25
|
+
CU["`cursor config
|
|
26
|
+
*hooks.json*`"]
|
|
27
|
+
CL["`claude config
|
|
28
|
+
*settings.json*`"]
|
|
26
29
|
end
|
|
27
30
|
subgraph repo["One repository, shared by both"]
|
|
28
|
-
PC["config.json
|
|
29
|
-
|
|
31
|
+
PC["`config.json
|
|
32
|
+
*policy, tracked*`"]
|
|
33
|
+
ST["`state/
|
|
34
|
+
*handoff, lessons, obs*`"]
|
|
30
35
|
end
|
|
31
36
|
CU --> R
|
|
32
37
|
CL --> R
|
|
@@ -55,11 +60,16 @@ Arrows are imports, and every one of them is checked mechanically:
|
|
|
55
60
|
|
|
56
61
|
```mermaid
|
|
57
62
|
graph TD
|
|
58
|
-
E["entrypoints
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
E["`entrypoints/
|
|
64
|
+
*one file per hook handler*`"]
|
|
65
|
+
C["`core/
|
|
66
|
+
*steering logic, provider-agnostic*`"]
|
|
67
|
+
P["`providers/
|
|
68
|
+
*one ACL adapter per provider*`"]
|
|
69
|
+
K["`contracts/
|
|
70
|
+
*shared vocabulary*`"]
|
|
71
|
+
L["`platform/
|
|
72
|
+
*OS primitives*`"]
|
|
63
73
|
|
|
64
74
|
E --> C
|
|
65
75
|
E --> P
|
package/docs/concepts.md
CHANGED
|
@@ -132,8 +132,10 @@ findings without deleting tests, which on a malformed command sends it to edit h
|
|
|
132
132
|
|
|
133
133
|
## pause / resume
|
|
134
134
|
|
|
135
|
-
`tlc harness pause`
|
|
136
|
-
|
|
135
|
+
`tlc harness pause` is the circuit breaker: every hook steps aside and answers at once, reading no policy and
|
|
136
|
+
writing no state. Only the floor still runs, so a paused session still cannot rewrite the policy it will resume
|
|
137
|
+
under. Pausing a repository also pauses the agents working in its linked worktrees. `tlc harness resume` turns
|
|
138
|
+
the harness back on.
|
|
137
139
|
|
|
138
140
|
Run both from your own terminal. Inside an agent session they are denied by the floor rule
|
|
139
141
|
`policy-surface-write`: policy is the operator's to change, and a stop check the agent can switch off is not a
|
package/docs/diagnose.md
CHANGED
|
@@ -51,8 +51,9 @@ a green light nobody earned. So a silent harness shows up as a healthy-looking s
|
|
|
51
51
|
tlc harness doctor
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
and the
|
|
54
|
+
`tlc harness pause` is the operator's circuit breaker: every hook steps aside without reading policy or writing
|
|
55
|
+
state, and only the floor keeps running, because a limit an agent can turn off is not a limit. A pause flag that
|
|
56
|
+
appears without the CLI is a policy change, not a pause, and is ignored.
|
|
56
57
|
|
|
57
58
|
## Node vs Bun
|
|
58
59
|
|
package/docs/providers/cursor.md
CHANGED
|
@@ -13,7 +13,8 @@ Source: `src/providers/cursor/`.
|
|
|
13
13
|
## Detection
|
|
14
14
|
|
|
15
15
|
`cursor.detect.ts`: a raw hook payload is Cursor's when `hook_event_name` is camelCase (e.g.
|
|
16
|
-
`beforeShellExecution`)
|
|
16
|
+
`beforeShellExecution`). `workspace_roots` is read when present but not required: a payload without it used to
|
|
17
|
+
match no provider and be answered with nothing, which Cursor's fail-closed hooks treat as a deny.
|
|
17
18
|
|
|
18
19
|
## Capability descriptor
|
|
19
20
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -113,7 +113,7 @@ For a rail, the operator changes it from their own terminal, outside the agent s
|
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
115
|
tlc harness mode solo # leave the paired posture
|
|
116
|
-
tlc harness pause #
|
|
116
|
+
tlc harness pause # circuit breaker: every hook steps aside, only the floor runs
|
|
117
117
|
tlc harness policy accept --all # after you edited config mid-session
|
|
118
118
|
```
|
|
119
119
|
|
package/package.json
CHANGED
package/src/core/core.facade.ts
CHANGED
|
@@ -146,6 +146,7 @@ import {
|
|
|
146
146
|
resolveProjectSyncMode,
|
|
147
147
|
} from "./policy/policy.loader.ts";
|
|
148
148
|
import { operatorBootstrapLines } from "./policy/policy.operator.ts";
|
|
149
|
+
import { isPaused, pauseFlagPath } from "./policy/policy.pause.ts";
|
|
149
150
|
import { isOperatorMode, OPERATOR_MODES } from "./policy/policy.posture.ts";
|
|
150
151
|
import { activeRails } from "./policy/policy.rails.ts";
|
|
151
152
|
import { pruneShadowed, shadowedKeys, typeMismatches, unknownKeys } from "./policy/policy.shadow.ts";
|
|
@@ -438,6 +439,8 @@ export const coreFacade = {
|
|
|
438
439
|
acceptPolicySources,
|
|
439
440
|
divergedPaths,
|
|
440
441
|
allDivergedPaths,
|
|
442
|
+
isPaused,
|
|
443
|
+
pauseFlagPath,
|
|
441
444
|
activeRails,
|
|
442
445
|
operatorBootstrapLines,
|
|
443
446
|
loadPolicy,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { linkedWorktreeMainRoot } from "../../platform/git.ts";
|
|
4
|
+
import { flagsDir } from "../../platform/paths.ts";
|
|
5
|
+
import { divergedPaths } from "./policy.integrity.ts";
|
|
6
|
+
|
|
7
|
+
export const PAUSE_FLAG = "skip-verify";
|
|
8
|
+
|
|
9
|
+
export function pauseFlagPath(root: string): string {
|
|
10
|
+
return join(flagsDir(root), PAUSE_FLAG);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isPaused(root: string, sessionKey: string): boolean {
|
|
14
|
+
const roots = [root, linkedWorktreeMainRoot(root)].filter(
|
|
15
|
+
(candidate): candidate is string => candidate !== null,
|
|
16
|
+
);
|
|
17
|
+
return roots.some((candidate) => {
|
|
18
|
+
const flag = pauseFlagPath(candidate);
|
|
19
|
+
return existsSync(flag) && !divergedPaths(candidate, sessionKey).includes(flag);
|
|
20
|
+
});
|
|
21
|
+
}
|
package/src/entrypoints/run.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type HandlerContext = {
|
|
|
23
23
|
|
|
24
24
|
export type Handler = (event: HarnessEvent, ctx: HandlerContext) => Decision | Promise<Decision>;
|
|
25
25
|
|
|
26
|
+
export type PausedHandler = (event: HarnessEvent, protectedPaths: string[]) => Decision;
|
|
27
|
+
|
|
26
28
|
export type RunIo = {
|
|
27
29
|
readStdin?: () => Promise<string>;
|
|
28
30
|
now?: () => Date;
|
|
@@ -151,10 +153,36 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
151
153
|
: {};
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
function isPausedSafely(event: HarnessEvent): boolean {
|
|
157
|
+
try {
|
|
158
|
+
return coreFacade.policy.isPaused(event.projectDir, event.sessionKey);
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pausedDecision(event: HarnessEvent, whilePaused: PausedHandler | undefined): Decision {
|
|
165
|
+
if (!whilePaused) {
|
|
166
|
+
return { kind: "abstain" };
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return whilePaused(
|
|
170
|
+
event,
|
|
171
|
+
providerRegistry.flatMap((p) => p.wiringTargets()),
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
return { kind: "abstain" };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function runHandler(
|
|
179
|
+
handler: Handler,
|
|
180
|
+
io: RunIo = {},
|
|
181
|
+
whilePaused?: PausedHandler,
|
|
182
|
+
): Promise<RunOutcome> {
|
|
155
183
|
const readStdin = io.readStdin ?? readStdinText;
|
|
156
184
|
const now = io.now ? io.now() : new Date();
|
|
157
|
-
const abstainRendered: Rendered = { stdout:
|
|
185
|
+
const abstainRendered: Rendered = { stdout: "{}", exitCode: 0 };
|
|
158
186
|
|
|
159
187
|
const text = await readStdin();
|
|
160
188
|
const trimmed = text.trim();
|
|
@@ -191,6 +219,12 @@ export async function runHandler(handler: Handler, io: RunIo = {}): Promise<RunO
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
const capabilities = provider.capabilities();
|
|
222
|
+
if (isPausedSafely(event)) {
|
|
223
|
+
const decision = degrade(pausedDecision(event, whilePaused), event, capabilities, {
|
|
224
|
+
contextBudgetChars: CONTEXT_BUDGET_CHARS,
|
|
225
|
+
});
|
|
226
|
+
return { event, decision, rendered: provider.render(decision, event) };
|
|
227
|
+
}
|
|
194
228
|
recordHookEnter(event);
|
|
195
229
|
|
|
196
230
|
try {
|
|
@@ -239,8 +273,8 @@ export async function runHandler(handler: Handler, io: RunIo = {}): Promise<RunO
|
|
|
239
273
|
}
|
|
240
274
|
}
|
|
241
275
|
|
|
242
|
-
export async function main(handler: Handler): Promise<void> {
|
|
243
|
-
const outcome = await runHandler(handler);
|
|
276
|
+
export async function main(handler: Handler, whilePaused?: PausedHandler): Promise<void> {
|
|
277
|
+
const outcome = await runHandler(handler, {}, whilePaused);
|
|
244
278
|
if (outcome.rendered.stdout !== null) {
|
|
245
279
|
const text = outcome.rendered.stdout;
|
|
246
280
|
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
@@ -5,7 +5,7 @@ import { coreFacade } from "../core/index.ts";
|
|
|
5
5
|
import type { AddedLine } from "../platform/git.ts";
|
|
6
6
|
import { diffProposedAgainstDisk, diffTwoStrings, localRepoRemote } from "../platform/git.ts";
|
|
7
7
|
import { normalizeSeparators } from "../platform/sanitize.ts";
|
|
8
|
-
import type { Handler, HandlerContext } from "./run.ts";
|
|
8
|
+
import type { Handler, HandlerContext, PausedHandler } from "./run.ts";
|
|
9
9
|
import { main } from "./run.ts";
|
|
10
10
|
import { shipGateVerdict } from "./ship-gate.ts";
|
|
11
11
|
import {
|
|
@@ -321,6 +321,16 @@ async function recordShellCwd(event: HarnessEvent): Promise<void> {
|
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
export const floorVerdict: PausedHandler = (event, protectedPaths) =>
|
|
325
|
+
coreFacade.floor.evaluateFloor({
|
|
326
|
+
projectDir: event.projectDir,
|
|
327
|
+
toolName: event.toolName,
|
|
328
|
+
filePath: filePathOf(event),
|
|
329
|
+
command: event.command,
|
|
330
|
+
isReadEvent: event.event === "read.before",
|
|
331
|
+
protectedPaths,
|
|
332
|
+
});
|
|
333
|
+
|
|
324
334
|
export const toolBeforeHandler: Handler = async (
|
|
325
335
|
event: HarnessEvent,
|
|
326
336
|
ctx: HandlerContext,
|
|
@@ -331,14 +341,7 @@ export const toolBeforeHandler: Handler = async (
|
|
|
331
341
|
|
|
332
342
|
// invariant: the floor runs first and reads no policy, so no config value and no agent edit can
|
|
333
343
|
// reach a decision before it.
|
|
334
|
-
const floor =
|
|
335
|
-
projectDir: event.projectDir,
|
|
336
|
-
toolName: event.toolName,
|
|
337
|
-
filePath: filePathOf(event),
|
|
338
|
-
command: event.command,
|
|
339
|
-
isReadEvent: event.event === "read.before",
|
|
340
|
-
protectedPaths: ctx.protectedPaths,
|
|
341
|
-
});
|
|
344
|
+
const floor = floorVerdict(event, ctx.protectedPaths);
|
|
342
345
|
if (floor.kind !== "allow") {
|
|
343
346
|
// invariant: one rail owns the record of every shell decision. The floor short-circuits before the shell
|
|
344
347
|
// rail runs, so without this a floor denial of a shell command was recorded by nothing at all — and the
|
|
@@ -441,5 +444,5 @@ export const toolBeforeHandler: Handler = async (
|
|
|
441
444
|
};
|
|
442
445
|
|
|
443
446
|
if (import.meta.main) {
|
|
444
|
-
await main(toolBeforeHandler);
|
|
447
|
+
await main(toolBeforeHandler, floorVerdict);
|
|
445
448
|
}
|
package/src/platform/git.ts
CHANGED
|
@@ -58,6 +58,21 @@ export async function gitCommonDirOf(dir: string): Promise<string | null> {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function linkedWorktreeMainRoot(root: string): string | null {
|
|
62
|
+
let text: string;
|
|
63
|
+
try {
|
|
64
|
+
text = readFileSync(join(root, ".git"), "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
|
|
69
|
+
if (!pointer) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const main = /^(.*)[\\/]\.git[\\/]worktrees[\\/][^\\/]+[\\/]?$/.exec(resolve(root, pointer))?.[1];
|
|
73
|
+
return main ? main : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
async function gitLines(projectDir: string, args: string[]): Promise<string[]> {
|
|
62
77
|
const result = await runProcess({ command: ["git", ...args], cwd: projectDir });
|
|
63
78
|
if (result.exitCode !== 0) {
|
|
@@ -7,8 +7,5 @@ export function detectCursor(raw: unknown): boolean {
|
|
|
7
7
|
}
|
|
8
8
|
const value = raw as Record<string, unknown>;
|
|
9
9
|
const eventName = value.hook_event_name;
|
|
10
|
-
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
return Array.isArray(value.workspace_roots);
|
|
10
|
+
return typeof eventName === "string" && CAMEL_CASE_EVENT_NAME.test(eventName);
|
|
14
11
|
}
|