@tech-leads-club/harness-toolkit 0.12.0 → 0.12.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 +75 -75
- package/dist/doctor.mjs +76 -76
- package/dist/init-project.mjs +79 -79
- package/dist/install-runtime.mjs +75 -75
- package/dist/lessons-cli.mjs +76 -76
- package/dist/obs-cli.mjs +78 -78
- package/dist/prompt-submit.mjs +75 -75
- package/dist/refresh-model-prices.mjs +75 -75
- package/dist/response-after.mjs +75 -75
- package/dist/run.mjs +75 -75
- package/dist/session-end.mjs +80 -80
- package/dist/session-start.mjs +82 -82
- package/dist/shim.mjs +74 -74
- package/dist/ship-gate.mjs +79 -79
- package/dist/stop.mjs +80 -80
- package/dist/subagent-start.mjs +75 -75
- package/dist/subagent-stop.mjs +76 -76
- package/dist/support.mjs +79 -79
- package/dist/tlc-cli.mjs +91 -91
- package/dist/tool-after.mjs +76 -76
- package/dist/tool-before.mjs +81 -81
- package/dist/tool-failure.mjs +75 -75
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/concepts.md +6 -4
- package/docs/log.md +5 -0
- package/package.json +1 -1
- package/src/core/core.facade.ts +2 -1
- package/src/core/rules/rules.trigger.ts +35 -3
- package/src/entrypoints/ship-gate.ts +26 -10
- package/src/entrypoints/stop.ts +8 -8
- package/src/entrypoints/support.ts +8 -3
- package/src/entrypoints/tool-before.ts +26 -10
- package/src/platform/git.ts +25 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as F$}from"node:module";var l=F$(import.meta.url);import{existsSync as B,lstatSync as h8,readFileSync as S,readlinkSync as p8,realpathSync as K$,rmSync as c8,statSync as d8,unlinkSync as l8,writeFileSync as i8}from"node:fs";import{homedir as n8}from"node:os";import{basename as a8,dirname as M$,isAbsolute as O$,join as G,relative as s8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as MZ,dirname as OZ,join as D,resolve as CZ}from"node:path";function i(){return D(L(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:D(L(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:D(L(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||D(L(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function L$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var D$=L$(s);var T$=D$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var N5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",L5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var h9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function F($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
|
|
1
|
+
import{createRequire as I$}from"node:module";var l=I$(import.meta.url);import{existsSync as B,lstatSync as h8,readFileSync as S,readlinkSync as p8,realpathSync as K$,rmSync as c8,statSync as d8,unlinkSync as l8,writeFileSync as i8}from"node:fs";import{homedir as n8}from"node:os";import{basename as a8,dirname as M$,isAbsolute as O$,join as G,relative as s8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as MZ,dirname as OZ,join as D,resolve as CZ}from"node:path";function i(){return D(j(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:D(j(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:D(j(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||D(j(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function j$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var D$=j$(s);var T$=D$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var N5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",j5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var h9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function I($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
|
|
2
2
|
`)}var H={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var Q8=["No harness decision in this window.","Whatever you just saw was the model, not a rail — the harness allowed everything it was asked about."].join(`
|
|
3
|
-
`);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},secrets:{redactOutput:!0},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as
|
|
4
|
-
`}}import{join as Y$,relative as
|
|
3
|
+
`);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},secrets:{redactOutput:!0},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as kq,readFileSync as y8,realpathSync as xq,writeFileSync as v8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function m8($){return z$($)}var g8="tlc-exec.mjs";function u8($){return JSON.stringify($??null).includes(g8)}function g($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=m8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((_)=>!u8(_));if(U.length!==W.length)q=!0;if(U.length>0)Q[z]=U}let X={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")X[z]=W;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function W$($){if(!f8($))return{ok:!0,settingsText:"",changed:!1};let Z=g(y8($,"utf8"));if(Z.ok&&Z.changed)v8($,Z.settingsText,"utf8");return Z}function u($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let J;try{J=JSON.parse($)}catch{return{kind:"unparsed"}}if(J===null||typeof J!=="object"||Array.isArray(J))return{kind:"unparsed"};let Q=J,q=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},X={},z=0,W=0;for(let[U,_]of Object.entries(q)){let d=Array.isArray(_)?_:[],L=d.filter((F$)=>!JSON.stringify(F$??null).includes(Z));if(z+=d.length-L.length,L.length>0)X[U]=L,W+=L.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:X},null,2)}
|
|
4
|
+
`}}import{join as Y$,relative as mq,resolve as gq,sep as uq}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],aq=[Y$("tools","dev"),Y$("tools","__test__")];function r8($=process.env){let Z=n8(),J=b($);return{home:C($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(A(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(A(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return h8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(a8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=p8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function o8($,Z,J={relative:s8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return o8(x($),c(Z))}var t8="tlc-exec.mjs";function e8($){try{return d8($).size<4096&&S($,"utf8").includes(t8)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(e8(Z)){$.push({action:"remove",target:Z,detail:`${Q}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${Q} is a real file the installer did not write`});return}if(q==="target"&&!B$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!B$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function $Z($,Z){if(!B(Z))return;let J=g(S(Z,"utf8"));if(!J.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${J.error}`});return}if(!J.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function ZZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=u(J);switch(Q.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(Q.removed>0)$.push({action:"remove",target:Z,detail:`${Q.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${Q.removed} harness entries, keep the rest`})}}function QZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let X of V$){let z=G(Z,X);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=G(Z,k);if(B(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of G$){let z=G(Z,X);if(!B(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function JZ($,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 qZ($,Z={}){let J=Z.purge===!0,Q=[];$Z(Q,$.claudeSettings),ZZ(Q,$.cursorHooks);for(let X of $.skillLinks)U$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)U$(Q,X,$.home,"the tlc launcher on PATH","target");let q=QZ(Q,$.home,J);return JZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function XZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=u(S(q.target,"utf8"));if(X.kind==="rewritten")i8(q.target,X.text,"utf8")}else if(q.action==="unlink")l8(q.target);else c8(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 zZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function WZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:H$($)}],footer:`already clean · reinstall with npm i -g ${N}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:zZ[W.action]})),X=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:H$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[Q?`${Z.applied.length} applied`:`${J.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:Q?`reinstall any time with npm i -g ${N}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function H$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function YZ($,Z,J=O){return I(WZ($,Z),J)}function VZ($=process.argv.slice(2)){let Z=r8(),J=qZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?XZ(J,Z):null;return console.log(YZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=VZ();export{r8 as uninstallTargets,WZ as uninstallScreen,YZ as uninstallReportText,qZ as planUninstall,C$ as pendingItems,VZ as main,o8 as isInsideRoot,c as canonicalise,XZ as applyUninstall};
|
package/docs/concepts.md
CHANGED
|
@@ -409,10 +409,12 @@ Posture reaches `ask` and nothing else: it interrupts under `paired` and hardens
|
|
|
409
409
|
A pattern trigger is policy rather than containment. `pr-open` and `push` recognize their `gh api` REST
|
|
410
410
|
equivalent (`POST` to a path ending in `pulls`; `POST`/`PATCH` to a path touching `git/refs`) alongside the CLI
|
|
411
411
|
shape, and `pr-merge` recognizes `gh pr merge` and `PUT` to a path ending in `merge` under `pulls`
|
|
412
|
-
([/decisions/ad-128.md](/decisions/ad-128.md)) —
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
([/decisions/ad-128.md](/decisions/ad-128.md)) — scoped to the local checkout's own remote, so a `gh api` push
|
|
413
|
+
to an unrelated repository (a template repo, a sibling project) is never gated by this project's own local
|
|
414
|
+
state ([/decisions/ad-130.md](/decisions/ad-130.md)) — but a script written to disk and executed later, a
|
|
415
|
+
command name built at runtime, a GraphQL mutation, `curl` direct to the API, a `gh api` call that flips a pull
|
|
416
|
+
request to ready without going through `gh pr ready`, or a pull request opened, approved, or merged in a
|
|
417
|
+
browser all still escape every trigger this project has. The rule covers the agent's shell path
|
|
416
418
|
([/decisions/ad-100.md](/decisions/ad-100.md)); it was never meant to cover more than that.
|
|
417
419
|
|
|
418
420
|
**The durable, non-bypassable boundary is GitHub's own branch protection, not this mechanism.** A required
|
package/docs/log.md
CHANGED
|
@@ -13,9 +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-10
|
|
17
|
+
|
|
18
|
+
- **AD-130** — gh api pr-open/push/pr-merge detection is scoped to the local checkout's own remote ([/decisions/ad-130.md](/decisions/ad-130.md))
|
|
19
|
+
|
|
16
20
|
## 2026-09-09
|
|
17
21
|
|
|
18
22
|
- **AD-128** — pr-open and push recognize their gh api equivalent, and pr-merge exists ([/decisions/ad-128.md](/decisions/ad-128.md))
|
|
23
|
+
- **AD-129** — Turn-scope git diffing runs at the event's own working directory, not the project root ([/decisions/ad-129.md](/decisions/ad-129.md))
|
|
19
24
|
|
|
20
25
|
## 2026-09-08
|
|
21
26
|
|
package/package.json
CHANGED
package/src/core/core.facade.ts
CHANGED
|
@@ -178,7 +178,7 @@ import {
|
|
|
178
178
|
readRuleSources,
|
|
179
179
|
recordObservation,
|
|
180
180
|
} from "./rules/rules.store.ts";
|
|
181
|
-
import { firingRules, triggerMatches } from "./rules/rules.trigger.ts";
|
|
181
|
+
import { firingRules, mentionsGhApi, triggerMatches } from "./rules/rules.trigger.ts";
|
|
182
182
|
import { scanForSecrets } from "./secret-scan/secret-scan.service.ts";
|
|
183
183
|
import { placeholderFor } from "./secret-scan/secret-scan.store.ts";
|
|
184
184
|
import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
|
|
@@ -500,6 +500,7 @@ export const coreFacade = {
|
|
|
500
500
|
build: buildRuleSet,
|
|
501
501
|
firing: firingRules,
|
|
502
502
|
triggerMatches,
|
|
503
|
+
mentionsGhApi,
|
|
503
504
|
observations: readObservations,
|
|
504
505
|
record: recordObservation,
|
|
505
506
|
observationFrom,
|
|
@@ -20,6 +20,13 @@ export type TriggerContext = {
|
|
|
20
20
|
event: string;
|
|
21
21
|
toolName?: string;
|
|
22
22
|
command?: string;
|
|
23
|
+
/**
|
|
24
|
+
* why optional, and why `null` differs from `undefined`: unset means the caller never resolved this
|
|
25
|
+
* dimension (every existing caller, unaffected). `null` means it tried and found no confirmable local
|
|
26
|
+
* repo, which fails an API-shaped match rather than skipping the check
|
|
27
|
+
* ([/decisions/ad-130.md](/decisions/ad-130.md)).
|
|
28
|
+
*/
|
|
29
|
+
repoRemote?: { owner: string; repo: string } | null;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
/**
|
|
@@ -176,12 +183,24 @@ function inferredApiMethod(words: readonly string[]): string {
|
|
|
176
183
|
return words.some((word) => API_BODY_FLAGS.has(word)) ? "POST" : "GET";
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
function matchesApiShape(
|
|
186
|
+
function matchesApiShape(
|
|
187
|
+
words: readonly string[],
|
|
188
|
+
shape: ApiShape,
|
|
189
|
+
repoRemote?: { owner: string; repo: string } | null,
|
|
190
|
+
): boolean {
|
|
180
191
|
const path = apiPathArgument(words);
|
|
181
192
|
if (path === undefined) {
|
|
182
193
|
return false;
|
|
183
194
|
}
|
|
184
195
|
const segments = apiPathSegments(path);
|
|
196
|
+
if (repoRemote !== undefined) {
|
|
197
|
+
if (repoRemote === null) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (segments[0] !== "repos" || segments[1] !== repoRemote.owner || segments[2] !== repoRemote.repo) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
185
204
|
const last = segments[segments.length - 1];
|
|
186
205
|
if (shape.lastSegment !== undefined && last !== shape.lastSegment) {
|
|
187
206
|
return false;
|
|
@@ -228,12 +247,13 @@ const API_SHAPES: Partial<Record<"pr-open" | "commit" | "push" | "pr-merge", rea
|
|
|
228
247
|
function matchesAnyShape(
|
|
229
248
|
words: readonly string[],
|
|
230
249
|
kind: "pr-open" | "commit" | "push" | "pr-merge",
|
|
250
|
+
repoRemote?: { owner: string; repo: string } | null,
|
|
231
251
|
): boolean {
|
|
232
252
|
if (SHELL_SHAPES[kind].some((shape) => matchesShape(words, shape))) {
|
|
233
253
|
return true;
|
|
234
254
|
}
|
|
235
255
|
const apiShapes = API_SHAPES[kind] ?? [];
|
|
236
|
-
return apiShapes.some((shape) => matchesApiShape(words, shape));
|
|
256
|
+
return apiShapes.some((shape) => matchesApiShape(words, shape, repoRemote));
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): boolean {
|
|
@@ -249,7 +269,9 @@ export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): b
|
|
|
249
269
|
if (context.command === undefined) {
|
|
250
270
|
return false;
|
|
251
271
|
}
|
|
252
|
-
return subCommands(context.command).some((words) =>
|
|
272
|
+
return subCommands(context.command).some((words) =>
|
|
273
|
+
matchesAnyShape(words, trigger.kind, context.repoRemote),
|
|
274
|
+
);
|
|
253
275
|
}
|
|
254
276
|
default: {
|
|
255
277
|
if (context.command === undefined) {
|
|
@@ -264,3 +286,13 @@ export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): b
|
|
|
264
286
|
export function firingRules(rules: readonly Rule[], context: TriggerContext): Rule[] {
|
|
265
287
|
return rules.filter((rule) => rule.enabled && triggerMatches(rule.on, context));
|
|
266
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* why exported: a caller deciding whether `repoRemote` is worth a process spawn only needs to know whether
|
|
292
|
+
* the API-shaped path (the only one that ever reads it) could possibly be in play — a CLI shape never
|
|
293
|
+
* matches this phrase, so resolving the remote first would spend a spawn no verdict can use
|
|
294
|
+
* ([/decisions/ad-130.md](/decisions/ad-130.md)).
|
|
295
|
+
*/
|
|
296
|
+
export function mentionsGhApi(command: string): boolean {
|
|
297
|
+
return subCommands(command).some((words) => findPhraseIndex(words, ["gh", "api"]) !== -1);
|
|
298
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
6
6
|
import { coreFacade } from "../core/index.ts";
|
|
7
|
-
import { listAddedLines, listTrackedFiles } from "../platform/git.ts";
|
|
7
|
+
import { listAddedLines, listTrackedFiles, localRepoRemote } from "../platform/git.ts";
|
|
8
8
|
import type { HandlerContext } from "./run.ts";
|
|
9
9
|
import { runLockedGate } from "./stop.ts";
|
|
10
10
|
import {
|
|
@@ -20,12 +20,28 @@ import {
|
|
|
20
20
|
// ([/decisions/ad-128.md](/decisions/ad-128.md)).
|
|
21
21
|
const FULL_BATTERY_KINDS = ["push", "pr-open", "pr-merge"] as const;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* why a cheap pass first: `localRepoRemote` is a process spawn, and this runs on every shell command. The
|
|
25
|
+
* gh-api shape only needs it to disambiguate an unrelated repository's push from this project's own
|
|
26
|
+
* ([/decisions/ad-130.md](/decisions/ad-130.md)); a CLI-shape match or no match at all never pays for it.
|
|
27
|
+
*/
|
|
28
|
+
async function isShipCommand(event: HarnessEvent, gitRoot: string): Promise<boolean> {
|
|
24
29
|
if (event.command === undefined) {
|
|
25
30
|
return false;
|
|
26
31
|
}
|
|
27
32
|
const context = { event: event.event, command: event.command };
|
|
28
|
-
|
|
33
|
+
if (!FULL_BATTERY_KINDS.some((kind) => coreFacade.rules.triggerMatches({ kind }, context))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// why: a CLI shape (`gh pr create`, `git push`) never reads `repoRemote` — only a `gh api` call can, so a
|
|
37
|
+
// command that never mentions one already has its final answer.
|
|
38
|
+
if (!coreFacade.rules.mentionsGhApi(event.command)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const repoRemote = await localRepoRemote(gitRoot);
|
|
42
|
+
return FULL_BATTERY_KINDS.some((kind) =>
|
|
43
|
+
coreFacade.rules.triggerMatches({ kind }, { ...context, repoRemote }),
|
|
44
|
+
);
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
function gateFailureMessage(gate: string, command: readonly string[], outputTail: string): string {
|
|
@@ -44,7 +60,8 @@ function gateFailureMessage(gate: string, command: readonly string[], outputTail
|
|
|
44
60
|
* once something fired, one `git rev-parse`.
|
|
45
61
|
*/
|
|
46
62
|
export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
|
|
47
|
-
|
|
63
|
+
const shaRoot = shaScopeRoot(event);
|
|
64
|
+
if (!(await isShipCommand(event, shaRoot))) {
|
|
48
65
|
return { kind: "abstain" };
|
|
49
66
|
}
|
|
50
67
|
const { policy } = ctx;
|
|
@@ -52,10 +69,9 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
|
|
|
52
69
|
const provider = event.provider;
|
|
53
70
|
const sessionKey = event.sessionKey;
|
|
54
71
|
const session = sessionIdFromKey(event);
|
|
55
|
-
const scope = await computeTurnScope(root, provider, sessionKey, policy);
|
|
56
|
-
const shaRoot = shaScopeRoot(event);
|
|
72
|
+
const scope = await computeTurnScope(root, shaRoot, provider, sessionKey, policy);
|
|
57
73
|
|
|
58
|
-
const commentHits = await pendingCommentViolations(root, provider, sessionKey, policy);
|
|
74
|
+
const commentHits = await pendingCommentViolations(root, shaRoot, provider, sessionKey, policy);
|
|
59
75
|
if (commentHits.length > 0) {
|
|
60
76
|
return {
|
|
61
77
|
kind: "deny",
|
|
@@ -143,13 +159,13 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
|
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
if (policy.duplication.enabled && scope.codeTargets.length > 0) {
|
|
146
|
-
const added = await listAddedLines(
|
|
147
|
-
const tracked = await listTrackedFiles(
|
|
162
|
+
const added = await listAddedLines(shaRoot, scope.codeTargets, scope.turnBase);
|
|
163
|
+
const tracked = await listTrackedFiles(shaRoot);
|
|
148
164
|
const scan = coreFacade.duplication.scanProject(
|
|
149
165
|
tracked,
|
|
150
166
|
(relativePath) => {
|
|
151
167
|
try {
|
|
152
|
-
return readFileSync(`${
|
|
168
|
+
return readFileSync(`${shaRoot}/${relativePath}`, "utf8");
|
|
153
169
|
} catch {
|
|
154
170
|
return null;
|
|
155
171
|
}
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -490,7 +490,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
490
490
|
* says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
491
491
|
*/
|
|
492
492
|
const deferred: string[] = [];
|
|
493
|
-
const changedFiles = await listChangedRepoFiles(
|
|
493
|
+
const changedFiles = await listChangedRepoFiles(shaRoot, turnBase);
|
|
494
494
|
const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
|
|
495
495
|
const testTargets = filterTestTargets(changedFiles);
|
|
496
496
|
// why: the comment rail scopes by the syntax catalog (40+ languages), not `codeTargets`'s nine-extension
|
|
@@ -653,7 +653,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
653
653
|
coreFacade.observe.shouldObserve(policy.observe, "comments", policy.comments.enabled)
|
|
654
654
|
) {
|
|
655
655
|
const hits = await coreFacade.commentPolicy.scanAddedComments(
|
|
656
|
-
|
|
656
|
+
shaRoot,
|
|
657
657
|
commentTargets,
|
|
658
658
|
policy.comments.mode,
|
|
659
659
|
turnBase,
|
|
@@ -680,7 +680,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
680
680
|
|
|
681
681
|
if (policy.comments.enabled && policy.comments.onViolation === "followup" && commentTargets.length > 0) {
|
|
682
682
|
const hits = await coreFacade.commentPolicy.scanAddedComments(
|
|
683
|
-
|
|
683
|
+
shaRoot,
|
|
684
684
|
commentTargets,
|
|
685
685
|
policy.comments.mode,
|
|
686
686
|
turnBase,
|
|
@@ -708,13 +708,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
708
708
|
if (policy.supplyChain.enabled && changedFiles.length > 0) {
|
|
709
709
|
const manifests = changedFiles.filter((path) => coreFacade.supplyChain.isManifest(path));
|
|
710
710
|
if (manifests.length > 0) {
|
|
711
|
-
const added = await listAddedLines(
|
|
711
|
+
const added = await listAddedLines(shaRoot, manifests, turnBase);
|
|
712
712
|
const outcome = coreFacade.supplyChain.inspectSupplyChain({
|
|
713
713
|
changedFiles,
|
|
714
714
|
added,
|
|
715
715
|
readManifest: (relativePath) => {
|
|
716
716
|
try {
|
|
717
|
-
return readFileSync(join(
|
|
717
|
+
return readFileSync(join(shaRoot, relativePath), "utf8");
|
|
718
718
|
} catch {
|
|
719
719
|
return null;
|
|
720
720
|
}
|
|
@@ -742,13 +742,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
742
742
|
* ([/decisions/ad-071.md](/decisions/ad-071.md)).
|
|
743
743
|
*/
|
|
744
744
|
if (policy.duplication.enabled && codeTargets.length > 0) {
|
|
745
|
-
const added = await listAddedLines(
|
|
746
|
-
const tracked = await listTrackedFiles(
|
|
745
|
+
const added = await listAddedLines(shaRoot, codeTargets, turnBase);
|
|
746
|
+
const tracked = await listTrackedFiles(shaRoot);
|
|
747
747
|
const scan = coreFacade.duplication.scanProject(
|
|
748
748
|
tracked,
|
|
749
749
|
(relativePath) => {
|
|
750
750
|
try {
|
|
751
|
-
return readFileSync(join(
|
|
751
|
+
return readFileSync(join(shaRoot, relativePath), "utf8");
|
|
752
752
|
} catch {
|
|
753
753
|
return null;
|
|
754
754
|
}
|
|
@@ -94,9 +94,13 @@ export type TurnScope = {
|
|
|
94
94
|
*
|
|
95
95
|
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent or its seal diverged —
|
|
96
96
|
* identical to `stop.ts`'s own fallback.
|
|
97
|
+
* why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
|
|
98
|
+
* (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
|
|
99
|
+
* branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
|
|
97
100
|
*/
|
|
98
101
|
export async function computeTurnScope(
|
|
99
102
|
root: string,
|
|
103
|
+
gitRoot: string,
|
|
100
104
|
provider: string,
|
|
101
105
|
sessionKey: string,
|
|
102
106
|
policy: Pick<Policy, "codePaths">,
|
|
@@ -104,7 +108,7 @@ export async function computeTurnScope(
|
|
|
104
108
|
const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
|
|
105
109
|
const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
|
|
106
110
|
const turnBase = handoff?.turn_base_sha ?? "HEAD";
|
|
107
|
-
const changedFiles = await listChangedRepoFiles(
|
|
111
|
+
const changedFiles = await listChangedRepoFiles(gitRoot, turnBase);
|
|
108
112
|
const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
|
|
109
113
|
const testTargets = filterTestTargets(changedFiles);
|
|
110
114
|
const commentScope = changedFiles.filter((file) =>
|
|
@@ -128,6 +132,7 @@ export async function computeTurnScope(
|
|
|
128
132
|
*/
|
|
129
133
|
export async function pendingCommentViolations(
|
|
130
134
|
root: string,
|
|
135
|
+
gitRoot: string,
|
|
131
136
|
provider: string,
|
|
132
137
|
sessionKey: string,
|
|
133
138
|
policy: Pick<Policy, "comments" | "codePaths">,
|
|
@@ -135,12 +140,12 @@ export async function pendingCommentViolations(
|
|
|
135
140
|
if (!policy.comments.enabled || policy.comments.onViolation !== "followup") {
|
|
136
141
|
return [];
|
|
137
142
|
}
|
|
138
|
-
const scope = await computeTurnScope(root, provider, sessionKey, policy);
|
|
143
|
+
const scope = await computeTurnScope(root, gitRoot, provider, sessionKey, policy);
|
|
139
144
|
if (scope.commentTargets.length === 0) {
|
|
140
145
|
return [];
|
|
141
146
|
}
|
|
142
147
|
return coreFacade.commentPolicy.scanAddedComments(
|
|
143
|
-
|
|
148
|
+
gitRoot,
|
|
144
149
|
scope.commentTargets,
|
|
145
150
|
policy.comments.mode,
|
|
146
151
|
scope.turnBase,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
2
2
|
import { coreFacade } from "../core/index.ts";
|
|
3
|
+
import { localRepoRemote } from "../platform/git.ts";
|
|
3
4
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
5
|
import { main } from "./run.ts";
|
|
5
6
|
import { shipGateVerdict } from "./ship-gate.ts";
|
|
@@ -26,7 +27,13 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
|
|
|
26
27
|
if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
|
|
27
28
|
return { kind: "abstain" };
|
|
28
29
|
}
|
|
29
|
-
const hits = await pendingCommentViolations(
|
|
30
|
+
const hits = await pendingCommentViolations(
|
|
31
|
+
event.projectDir,
|
|
32
|
+
shaScopeRoot(event),
|
|
33
|
+
event.provider,
|
|
34
|
+
event.sessionKey,
|
|
35
|
+
ctx.policy,
|
|
36
|
+
);
|
|
30
37
|
if (hits.length === 0) {
|
|
31
38
|
return { kind: "abstain" };
|
|
32
39
|
}
|
|
@@ -94,15 +101,24 @@ async function rulesDecision(event: HarnessEvent, ctx: HandlerContext): Promise<
|
|
|
94
101
|
if (dryRun.outcomes.length === 0) {
|
|
95
102
|
return { kind: "abstain" };
|
|
96
103
|
}
|
|
97
|
-
// why
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// why: the first pass answers whether any rule fired at all, which costs no git. The remote is a second
|
|
105
|
+
// process, spent only when the command could possibly be a gh api call naming a different repository
|
|
106
|
+
// ([/decisions/ad-130.md](/decisions/ad-130.md)) — a CLI shape or an unrelated trigger never reads it.
|
|
107
|
+
const [sha, repoRemote] = await Promise.all([
|
|
108
|
+
currentGitSha(shaRoot),
|
|
109
|
+
event.command && coreFacade.rules.mentionsGhApi(event.command) ? localRepoRemote(shaRoot) : undefined,
|
|
110
|
+
]);
|
|
111
|
+
const verdict = coreFacade.rules.decideAction(
|
|
112
|
+
event.projectDir,
|
|
113
|
+
config,
|
|
114
|
+
{ ...trigger, repoRemote },
|
|
115
|
+
{
|
|
116
|
+
sha,
|
|
117
|
+
sessionKey: event.sessionKey,
|
|
118
|
+
mode: ctx.policy.mode,
|
|
119
|
+
shaRoot,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
106
122
|
return verdict.decision;
|
|
107
123
|
}
|
|
108
124
|
|
package/src/platform/git.ts
CHANGED
|
@@ -185,3 +185,28 @@ export async function listTrackedFiles(projectDir: string): Promise<string[]> {
|
|
|
185
185
|
}
|
|
186
186
|
return result.output.split("\0").filter((path) => path !== "");
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
export type RepoRef = { owner: string; repo: string };
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* why: both SSH (`git@host:owner/repo.git`) and HTTPS (`https://host/owner/repo(.git)?`) forms share the same
|
|
193
|
+
* tail shape — a `/` or `:` before the owner, a `/` before the repo, an optional `.git` and trailing slash.
|
|
194
|
+
* One pattern reads both without a URL parser this project has no other use for.
|
|
195
|
+
*/
|
|
196
|
+
export function parseOwnerRepo(url: string): RepoRef | null {
|
|
197
|
+
const match = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim());
|
|
198
|
+
return match ? { owner: match[1] as string, repo: match[2] as string } : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* why a fixed remote name: every repository this project has touched, including this one, names its own
|
|
203
|
+
* remote `origin` — a configurable name is generality nobody has asked for yet
|
|
204
|
+
* ([/decisions/ad-130.md](/decisions/ad-130.md)).
|
|
205
|
+
*/
|
|
206
|
+
export async function localRepoRemote(projectDir: string, remoteName = "origin"): Promise<RepoRef | null> {
|
|
207
|
+
if (!existsSync(join(projectDir, ".git"))) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const result = await runProcess({ command: ["git", "remote", "get-url", remoteName], cwd: projectDir });
|
|
211
|
+
return result.exitCode === 0 ? parseOwnerRepo(result.stdout.trim()) : null;
|
|
212
|
+
}
|