@tech-leads-club/harness-toolkit 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compact-before.mjs +74 -74
- package/dist/doctor.mjs +75 -75
- package/dist/init-project.mjs +77 -77
- package/dist/install-runtime.mjs +74 -74
- package/dist/lessons-cli.mjs +74 -74
- package/dist/obs-cli.mjs +74 -74
- package/dist/prompt-submit.mjs +74 -74
- package/dist/refresh-model-prices.mjs +74 -74
- package/dist/response-after.mjs +74 -74
- package/dist/run.mjs +74 -74
- package/dist/session-end.mjs +79 -79
- package/dist/session-start.mjs +81 -81
- package/dist/shim.mjs +73 -73
- package/dist/ship-gate.mjs +143 -0
- package/dist/stop.mjs +80 -80
- package/dist/subagent-start.mjs +74 -74
- package/dist/subagent-stop.mjs +75 -75
- package/dist/support.mjs +83 -83
- package/dist/tlc-cli.mjs +81 -81
- package/dist/tool-after.mjs +74 -74
- package/dist/tool-before.mjs +83 -81
- package/dist/tool-failure.mjs +74 -74
- package/dist/uninstall-runtime.mjs +2 -2
- package/docs/log.md +2 -0
- package/package.json +1 -1
- package/src/core/core.facade.ts +2 -1
- package/src/entrypoints/ship-gate.ts +174 -0
- package/src/entrypoints/stop.ts +7 -2
- package/src/entrypoints/support.ts +76 -1
- package/src/entrypoints/tool-before.ts +49 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import{createRequire as M$}from"node:module";var l=M$(import.meta.url);import{existsSync as B,lstatSync as u8,readFileSync as S,readlinkSync as g8,realpathSync as G$,rmSync as h8,statSync as p8,unlinkSync as c8,writeFileSync as d8}from"node:fs";import{homedir as l8}from"node:os";import{basename as i8,dirname as B$,isAbsolute as H$,join as G,relative as n8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as K2,dirname as U2,join as j,resolve as M2}from"node:path";function i(){return j(L(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function P(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".claude")}function T(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||j(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 C$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let X of J.extensions)Z.set(X.toLowerCase(),Q)}return Z}var _$=C$(s);var N$=_$.size;var y=6;var A$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var FZ=new Set([...A$,"nc","ncat","socat","telnet","lwp-request"]);var R$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",_Z=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${R$}))(?=[:)\\s,'"\`]|$)`,"g");var A="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:A,lastSeenAt:A,lastAccessedAt:A,updatedAt:A}}var W5=[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 d$={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 l$($){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 $$=String.fromCharCode(27),i$=`${$$}[0m`;function n$($=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 a$={ok:"success",warn:"warning",fail:"error",info:"info"},s$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function R($=n$()){let Z=(Q,X)=>$?`${$$}[${Q}m${X}${i$}`:X,J=(Q,X)=>Z(`38;2;${l$(d$[Q])}`,X);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,X,q=m)=>` ${J("textMuted",`${Q}:`.padEnd(q))} ${X}`,status:(Q,X)=>`${J(a$[Q],s$[Q])} ${X}`}}var O=R(!1);var r$=2;function o$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function t$($,Z){if($==="")return[""];let J=[],Q="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(Q===""){Q=X;continue}if(Q.length+1+X.length<=Z){Q=`${Q} ${X}`;continue}J.push(Q),Q=X}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function I($,Z,J=o$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let X=Math.max(m,...$.sections.flatMap((q)=>(q.rows??[]).map((z)=>z.label.length+1)));for(let q of $.sections){if(Q.push(""),q.title)Q.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){Q.push("");continue}let W=q.wrap?t$(z,J-r$):[z];for(let H of W)Q.push(` ${H}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
|
|
2
2
|
`)}var K={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var $8=["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 Z8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},Z$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:K.includePayloads,maxAttrChars:K.maxAttrChars,sessionCostAlertUsd:K.sessionCostAlertUsd,retentionDays:K.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...Z8}},mcpPrime:[],bootstrapExtra:[]};var C="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as
|
|
4
|
-
`}}import{join as X$,relative as
|
|
3
|
+
`);var Z8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},Z$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:K.includePayloads,maxAttrChars:K.maxAttrChars,sessionCostAlertUsd:K.sessionCostAlertUsd,retentionDays:K.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...Z8}},mcpPrime:[],bootstrapExtra:[]};var C="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as oX,readFileSync as b8,realpathSync as tX,writeFileSync as w8}from"node:fs";function Q$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function y8($){return Q$($)}var v8="tlc-exec.mjs";function m8($){return JSON.stringify($??null).includes(v8)}function u($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!Q$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=y8(Z.hooks)?Z.hooks:{},Q={},X=!1;for(let[z,W]of Object.entries(J)){let H=W.filter((_)=>!m8(_));if(H.length!==W.length)X=!0;if(H.length>0)Q[z]=H}let q={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")q[z]=W;else if(Object.keys(Q).length>0)q.hooks=Q;return{ok:!0,settingsText:JSON.stringify(q,null,2),changed:X}}function J$($){if(!f8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(b8($,"utf8"));if(Z.ok&&Z.changed)w8($,Z.settingsText,"utf8");return Z}function g($,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,X=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},q={},z=0,W=0;for(let[H,_]of Object.entries(X)){let d=Array.isArray(_)?_:[],N=d.filter((U$)=>!JSON.stringify(U$??null).includes(Z));if(z+=d.length-N.length,N.length>0)q[H]=N,W+=N.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:q},null,2)}
|
|
4
|
+
`}}import{join as X$,relative as q4,resolve as z4,sep as W4}from"node:path";var q$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],z$=["config.json","state","flags","rules"],M4=[X$("tools","dev"),X$("tools","__test__")];function a8($=process.env){let Z=l8(),J=f($);return{home:F($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(P(),"settings.json"),cursorHooks:G(T(),"hooks.json"),skillLinks:[G(P(),"skills","harness-init"),G(T(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return u8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(G$(Z),...J)}catch{let Q=B$(Z);if(Q===Z)return h($);J.unshift(i8(Z)),Z=Q}}function x($){try{return G$($)}catch{try{let Z=g8($);return c(H$(Z)?Z:h(B$($),Z))}catch{return c($)}}}function s8($,Z,J={relative:n8,isAbsolute:H$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function W$($,Z){return s8(x($),c(Z))}var r8="tlc-exec.mjs";function o8($){try{return p8($).size<4096&&S($,"utf8").includes(r8)}catch{return!1}}function Y$($,Z,J,Q,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(o8(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(X==="target"&&!W$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let q=X==="location"&&!W$(Z,J);$.push({action:"unlink",target:Z,detail:q?`${Q}, stale — points at ${x(Z)}`:Q})}function t8($,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 e8($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=g(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 $2($,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 q of q$){let z=G(Z,q);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let X=G(Z,k);if(B(X))$.push({action:"remove",target:X,detail:"install marker"});for(let q of z$){let z=G(Z,q);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 Z2($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${C}`,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 Q2($,Z={}){let J=Z.purge===!0,Q=[];t8(Q,$.claudeSettings),e8(Q,$.cursorHooks);for(let q of $.skillLinks)Y$(Q,q,$.home,"skill link","location");for(let q of $.binLinks)Y$(Q,q,$.home,"the tlc launcher on PATH","target");let X=$2(Q,$.home,J);return Z2(Q,$.home),{items:Q,purge:J,homeIsLink:X}}function K$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function J2($,Z){let J=[],Q=[];for(let X of K$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)J$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")d8(X.target,q.text,"utf8")}else if(X.action==="unlink")c8(X.target);else h8(X.target,{recursive:!0,force:!0});J.push(X)}catch(q){Q.push({item:X,reason:q instanceof Error?q.message:String(q)})}return{applied:J,failed:Q}}var X2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function q2($,Z){let J=K$($),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:V$($)}],footer:`already clean · reinstall with npm i -g ${C}, then \`tlc harness install\``};let X=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:X2[W.action]})),q=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:X},...q.length>0?[{title:"KEPT",rows:q.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:V$($)},...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 ${C}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function V$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function z2($,Z,J=O){return I(q2($,Z),J)}function W2($=process.argv.slice(2)){let Z=a8(),J=Q2(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?J2(J,Z):null;return console.log(z2(J,Q,R())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=W2();export{a8 as uninstallTargets,q2 as uninstallScreen,z2 as uninstallReportText,Q2 as planUninstall,K$ as pendingItems,W2 as main,s8 as isInsideRoot,c as canonicalise,J2 as applyUninstall};
|
package/docs/log.md
CHANGED
|
@@ -16,6 +16,8 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
|
|
|
16
16
|
## 2026-08-29
|
|
17
17
|
|
|
18
18
|
- **AD-114** — Rule proof `since HEAD` resolves sha from the event's own working directory, not the project root ([/decisions/ad-114.md](/decisions/ad-114.md))
|
|
19
|
+
- **AD-115** — A comment violation refuses commit, push and gh pr create, not only the stop ([/decisions/ad-115.md](/decisions/ad-115.md))
|
|
20
|
+
- **AD-116** — push and gh pr create run the stop-time battery before shipping, identically on every provider ([/decisions/ad-116.md](/decisions/ad-116.md))
|
|
19
21
|
|
|
20
22
|
## 2026-08-26
|
|
21
23
|
|
package/package.json
CHANGED
package/src/core/core.facade.ts
CHANGED
|
@@ -174,7 +174,7 @@ import {
|
|
|
174
174
|
readRuleSources,
|
|
175
175
|
recordObservation,
|
|
176
176
|
} from "./rules/rules.store.ts";
|
|
177
|
-
import { firingRules } from "./rules/rules.trigger.ts";
|
|
177
|
+
import { firingRules, triggerMatches } from "./rules/rules.trigger.ts";
|
|
178
178
|
import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
|
|
179
179
|
import { clearShellStall } from "./shell-policy/shell-policy.stall.ts";
|
|
180
180
|
import { coversHandler, decideShim } from "./shim/shim.precedence.ts";
|
|
@@ -489,6 +489,7 @@ export const coreFacade = {
|
|
|
489
489
|
read: readRuleSources,
|
|
490
490
|
build: buildRuleSet,
|
|
491
491
|
firing: firingRules,
|
|
492
|
+
triggerMatches,
|
|
492
493
|
observations: readObservations,
|
|
493
494
|
record: recordObservation,
|
|
494
495
|
observationFrom,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// why: `stop` was the only place these checks could refuse a turn, so `push`/`gh pr create` mid-turn
|
|
2
|
+
// always shipped first and learned about the violation afterward. Same checks, before ship, identical
|
|
3
|
+
// on Claude and Cursor — every input read here is host-neutral ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
6
|
+
import { coreFacade } from "../core/index.ts";
|
|
7
|
+
import { listAddedLines, listTrackedFiles } from "../platform/git.ts";
|
|
8
|
+
import type { HandlerContext } from "./run.ts";
|
|
9
|
+
import { runLockedGate } from "./stop.ts";
|
|
10
|
+
import {
|
|
11
|
+
computeTurnScope,
|
|
12
|
+
currentGitSha,
|
|
13
|
+
pendingCommentViolations,
|
|
14
|
+
sessionIdFromKey,
|
|
15
|
+
shaScopeRoot,
|
|
16
|
+
} from "./support.ts";
|
|
17
|
+
|
|
18
|
+
const FULL_BATTERY_KINDS = ["push", "pr-open"] as const;
|
|
19
|
+
|
|
20
|
+
function isShipCommand(event: HarnessEvent): boolean {
|
|
21
|
+
if (event.command === undefined) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const context = { event: event.event, command: event.command };
|
|
25
|
+
return FULL_BATTERY_KINDS.some((kind) => coreFacade.rules.triggerMatches({ kind }, context));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function gateFailureMessage(gate: string, command: readonly string[], outputTail: string): string {
|
|
29
|
+
return [
|
|
30
|
+
`BLOCKED: ${gate} failed before shipping — a review or a deploy would have seen this next.`,
|
|
31
|
+
`TRIED: ${command.join(" ")}`,
|
|
32
|
+
`NEED: fix the ${gate} findings, then retry the commit/push/PR.`,
|
|
33
|
+
"",
|
|
34
|
+
outputTail,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* why after comments and before the stop-rules check: cheapest first. A comment violation costs a
|
|
40
|
+
* regex scan; a gate command is a real process; the stop-rules check is a directory read plus, only
|
|
41
|
+
* once something fired, one `git rev-parse`.
|
|
42
|
+
*/
|
|
43
|
+
export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
|
|
44
|
+
if (!isShipCommand(event)) {
|
|
45
|
+
return { kind: "abstain" };
|
|
46
|
+
}
|
|
47
|
+
const { policy } = ctx;
|
|
48
|
+
const root = event.projectDir;
|
|
49
|
+
const provider = event.provider;
|
|
50
|
+
const sessionKey = event.sessionKey;
|
|
51
|
+
const session = sessionIdFromKey(event);
|
|
52
|
+
const scope = await computeTurnScope(root, provider, policy);
|
|
53
|
+
const shaRoot = shaScopeRoot(event);
|
|
54
|
+
|
|
55
|
+
const commentHits = await pendingCommentViolations(root, provider, policy);
|
|
56
|
+
if (commentHits.length > 0) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "deny",
|
|
59
|
+
reason: coreFacade.commentPolicy.commentViolationMessage(commentHits, policy.comments.mode),
|
|
60
|
+
rule: "ship-gate-comments",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (policy.grind.enabled && policy.grind.lintCommand && scope.codeTargets.length > 0) {
|
|
65
|
+
const run = await runLockedGate({
|
|
66
|
+
root,
|
|
67
|
+
shaRoot,
|
|
68
|
+
provider,
|
|
69
|
+
session,
|
|
70
|
+
pendingCredit: scope.pendingCredit,
|
|
71
|
+
sessionKey,
|
|
72
|
+
policy,
|
|
73
|
+
gate: "lint",
|
|
74
|
+
command: policy.grind.lintCommand,
|
|
75
|
+
argvFiles: coreFacade.gate.shouldAppendFiles(policy.grind.lintCommand, policy.grind.appendFiles)
|
|
76
|
+
? scope.codeTargets
|
|
77
|
+
: [],
|
|
78
|
+
recordFiles: scope.codeTargets,
|
|
79
|
+
});
|
|
80
|
+
if (run.kind === "ran" && !run.artifact.passed) {
|
|
81
|
+
return {
|
|
82
|
+
kind: "deny",
|
|
83
|
+
reason: gateFailureMessage("lint", run.artifact.command, run.artifact.outputTail),
|
|
84
|
+
rule: "ship-gate-lint",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
policy.grind.enabled &&
|
|
91
|
+
policy.grind.testCommand &&
|
|
92
|
+
(scope.testTargets.length > 0 || scope.codeTargets.length > 0)
|
|
93
|
+
) {
|
|
94
|
+
const recordFiles = scope.testTargets.length > 0 ? scope.testTargets : scope.codeTargets;
|
|
95
|
+
const run = await runLockedGate({
|
|
96
|
+
root,
|
|
97
|
+
shaRoot,
|
|
98
|
+
provider,
|
|
99
|
+
session,
|
|
100
|
+
pendingCredit: scope.pendingCredit,
|
|
101
|
+
sessionKey,
|
|
102
|
+
policy,
|
|
103
|
+
gate: "test",
|
|
104
|
+
command: policy.grind.testCommand,
|
|
105
|
+
argvFiles: coreFacade.gate.shouldAppendFiles(policy.grind.testCommand, policy.grind.appendFiles)
|
|
106
|
+
? recordFiles
|
|
107
|
+
: [],
|
|
108
|
+
recordFiles,
|
|
109
|
+
});
|
|
110
|
+
if (run.kind === "ran" && !run.artifact.passed) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "deny",
|
|
113
|
+
reason: gateFailureMessage("test", run.artifact.command, run.artifact.outputTail),
|
|
114
|
+
rule: "ship-gate-test",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (policy.docs.command && policy.docs.command.length > 0 && policy.docs.severity === "deny") {
|
|
120
|
+
const run = await runLockedGate({
|
|
121
|
+
root,
|
|
122
|
+
shaRoot,
|
|
123
|
+
provider,
|
|
124
|
+
session,
|
|
125
|
+
pendingCredit: scope.pendingCredit,
|
|
126
|
+
sessionKey,
|
|
127
|
+
policy,
|
|
128
|
+
gate: "docs",
|
|
129
|
+
command: policy.docs.command,
|
|
130
|
+
argvFiles: [],
|
|
131
|
+
recordFiles: scope.changedFiles,
|
|
132
|
+
});
|
|
133
|
+
if (run.kind === "ran" && !run.artifact.passed) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "deny",
|
|
136
|
+
reason: gateFailureMessage("docs", run.artifact.command, run.artifact.outputTail),
|
|
137
|
+
rule: "ship-gate-docs",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (policy.duplication.enabled && scope.codeTargets.length > 0) {
|
|
143
|
+
const added = await listAddedLines(root, scope.codeTargets, scope.turnBase);
|
|
144
|
+
const tracked = await listTrackedFiles(root);
|
|
145
|
+
const scan = coreFacade.duplication.scanProject(
|
|
146
|
+
tracked,
|
|
147
|
+
(relativePath) => {
|
|
148
|
+
try {
|
|
149
|
+
return readFileSync(`${root}/${relativePath}`, "utf8");
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
policy.duplication.minRun,
|
|
155
|
+
);
|
|
156
|
+
const hits = coreFacade.duplication.findDuplications(added, scan.index, policy.duplication.minRun);
|
|
157
|
+
if (hits.length > 0) {
|
|
158
|
+
return {
|
|
159
|
+
kind: "deny",
|
|
160
|
+
reason: coreFacade.duplication.duplicationMessage(hits),
|
|
161
|
+
rule: "ship-gate-duplication",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const sha = await currentGitSha(shaRoot);
|
|
167
|
+
const stopRules = coreFacade.rules.decideStop(root, policy.rules, { sessionKey, mode: policy.mode, sha });
|
|
168
|
+
const worst = coreFacade.rules.strictest(stopRules.outcomes);
|
|
169
|
+
if (worst && worst.verdict !== "warn") {
|
|
170
|
+
return { kind: "deny", reason: worst.message, rule: `rule:${worst.rule.name}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { kind: "abstain" };
|
|
174
|
+
}
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -130,7 +130,7 @@ function recordGateDeferred(args: {
|
|
|
130
130
|
* would have let `undefined` mean "fine" at three call sites that reach straight for `artifact.passed`
|
|
131
131
|
* ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
132
132
|
*/
|
|
133
|
-
type GateRun =
|
|
133
|
+
export type GateRun =
|
|
134
134
|
| { kind: "ran"; artifact: LastGateArtifact; reused: boolean }
|
|
135
135
|
| { kind: "deferred"; holder: string };
|
|
136
136
|
|
|
@@ -184,7 +184,12 @@ async function creditPendingLessons(args: {
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
/**
|
|
188
|
+
* why exported: this is the one place a `commit`/`push`/`pr-open` ship-time gate
|
|
189
|
+
* ([/decisions/ad-116.md](/decisions/ad-116.md)) can ask the identical question `stop` asks — same lock, same
|
|
190
|
+
* content-hash cache, same observability. A second implementation is the drift AD-071 already named once.
|
|
191
|
+
*/
|
|
192
|
+
export async function runLockedGate(args: {
|
|
188
193
|
root: string;
|
|
189
194
|
shaRoot: string;
|
|
190
195
|
provider: string;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { EffortLevel, HarnessEvent } from "../contracts/index.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type CommentFinding,
|
|
6
|
+
coreFacade,
|
|
7
|
+
type HarnessLesson,
|
|
8
|
+
type ObservabilityConfig,
|
|
9
|
+
type PendingLessonCredit,
|
|
10
|
+
type Policy,
|
|
11
|
+
} from "../core/index.ts";
|
|
12
|
+
import { filterCodeTargets, filterTestTargets, listChangedRepoFiles } from "../platform/git.ts";
|
|
5
13
|
import { runProcess } from "../platform/process.ts";
|
|
6
14
|
import { type ProviderPort, renderClaudeLessonsView, renderCursorLessonsView } from "../providers/index.ts";
|
|
7
15
|
|
|
@@ -70,6 +78,73 @@ export function shaScopeRoot(event: HarnessEvent): string {
|
|
|
70
78
|
return event.cwd ?? event.projectDir;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
export type TurnScope = {
|
|
82
|
+
turnBase: string;
|
|
83
|
+
changedFiles: string[];
|
|
84
|
+
codeTargets: string[];
|
|
85
|
+
testTargets: string[];
|
|
86
|
+
commentTargets: string[];
|
|
87
|
+
pendingCredit: PendingLessonCredit | undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* "What did this turn add" — the one answer every gate that scopes by turn shares, whether it runs
|
|
92
|
+
* at `stop` or, since AD-116, before `commit`/`push`/`pr-open` ships it. A second, differently-scoped
|
|
93
|
+
* answer to the same question is the drift AD-071 already named once ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
94
|
+
*
|
|
95
|
+
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent or its seal diverged —
|
|
96
|
+
* identical to `stop.ts`'s own fallback.
|
|
97
|
+
*/
|
|
98
|
+
export async function computeTurnScope(
|
|
99
|
+
root: string,
|
|
100
|
+
provider: string,
|
|
101
|
+
policy: Pick<Policy, "codePaths">,
|
|
102
|
+
): Promise<TurnScope> {
|
|
103
|
+
const seal = coreFacade.handoff.handoffInjectable(root);
|
|
104
|
+
const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider) : undefined;
|
|
105
|
+
const turnBase = handoff?.turn_base_sha ?? "HEAD";
|
|
106
|
+
const changedFiles = await listChangedRepoFiles(root, turnBase);
|
|
107
|
+
const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
|
|
108
|
+
const testTargets = filterTestTargets(changedFiles);
|
|
109
|
+
const commentScope = changedFiles.filter((file) =>
|
|
110
|
+
coreFacade.policy.isUnderCodePaths(file, policy.codePaths),
|
|
111
|
+
);
|
|
112
|
+
const commentTargets = coreFacade.commentPolicy.filterCommentTargets(commentScope);
|
|
113
|
+
return {
|
|
114
|
+
turnBase,
|
|
115
|
+
changedFiles,
|
|
116
|
+
codeTargets,
|
|
117
|
+
testTargets,
|
|
118
|
+
commentTargets,
|
|
119
|
+
pendingCredit: handoff?.pending_lesson_credit,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The same unresolved-comment check `stop.ts` already runs at the end of the turn, exposed so an
|
|
125
|
+
* action-time rail can ask the identical question before a `commit`/`push`/`pr-open` ships it
|
|
126
|
+
* ([/decisions/ad-115.md](/decisions/ad-115.md)).
|
|
127
|
+
*/
|
|
128
|
+
export async function pendingCommentViolations(
|
|
129
|
+
root: string,
|
|
130
|
+
provider: string,
|
|
131
|
+
policy: Pick<Policy, "comments" | "codePaths">,
|
|
132
|
+
): Promise<CommentFinding[]> {
|
|
133
|
+
if (!policy.comments.enabled || policy.comments.onViolation !== "followup") {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const scope = await computeTurnScope(root, provider, policy);
|
|
137
|
+
if (scope.commentTargets.length === 0) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return coreFacade.commentPolicy.scanAddedComments(
|
|
141
|
+
root,
|
|
142
|
+
scope.commentTargets,
|
|
143
|
+
policy.comments.mode,
|
|
144
|
+
scope.turnBase,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
73
148
|
export async function currentGitSha(root: string): Promise<string | null> {
|
|
74
149
|
if (!existsSync(join(root, ".git"))) {
|
|
75
150
|
return null;
|
|
@@ -2,14 +2,41 @@ import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
|
2
2
|
import { coreFacade } from "../core/index.ts";
|
|
3
3
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
4
|
import { main } from "./run.ts";
|
|
5
|
+
import { shipGateVerdict } from "./ship-gate.ts";
|
|
5
6
|
import {
|
|
6
7
|
currentGitSha,
|
|
7
8
|
obsConfigFor,
|
|
9
|
+
pendingCommentViolations,
|
|
8
10
|
readModelFromToolInput,
|
|
9
11
|
shaScopeRoot,
|
|
10
12
|
subagentSpawnInput,
|
|
11
13
|
} from "./support.ts";
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* why `commit` gets only the cheap check and `push`/`pr-open` get the full battery
|
|
17
|
+
* (`shipGateVerdict`): measured in this repo, lint+test run ~9.4s cold — paying that on every local
|
|
18
|
+
* commit inside one turn is the edit-time cost AD-111 already declined, moved to a higher frequency.
|
|
19
|
+
* A bare commit is local and reversible; nothing shares it yet ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
20
|
+
*/
|
|
21
|
+
async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext): Promise<Decision> {
|
|
22
|
+
if (event.command === undefined) {
|
|
23
|
+
return { kind: "abstain" };
|
|
24
|
+
}
|
|
25
|
+
const context = { event: event.event, command: event.command };
|
|
26
|
+
if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
|
|
27
|
+
return { kind: "abstain" };
|
|
28
|
+
}
|
|
29
|
+
const hits = await pendingCommentViolations(event.projectDir, event.provider, ctx.policy);
|
|
30
|
+
if (hits.length === 0) {
|
|
31
|
+
return { kind: "abstain" };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: "deny",
|
|
35
|
+
reason: coreFacade.commentPolicy.commentViolationMessage(hits, ctx.policy.comments.mode),
|
|
36
|
+
rule: "comment-policy-before-ship",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
const READONLY_BLOCKED_TOOLS = new Set(["Write", "Delete", "Shell"]);
|
|
14
41
|
|
|
15
42
|
/**
|
|
@@ -206,6 +233,28 @@ export const toolBeforeHandler: Handler = async (
|
|
|
206
233
|
return rulesVerdict;
|
|
207
234
|
}
|
|
208
235
|
|
|
236
|
+
/**
|
|
237
|
+
* why after the operator rules and before the shell fallback: a built-in capability check, not
|
|
238
|
+
* operator-authored, so it is not a `.tlc/harness/rules/` rule — but it reads policy, so it is
|
|
239
|
+
* not floor-tier either ([/decisions/ad-115.md](/decisions/ad-115.md)).
|
|
240
|
+
*/
|
|
241
|
+
const commitGuard = await commentGateBeforeCommit(event, ctx);
|
|
242
|
+
if (commitGuard.kind !== "abstain") {
|
|
243
|
+
recordShellDecisionIfShell(event, ctx, commitGuard);
|
|
244
|
+
return commitGuard;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* why the full battery only for push/pr-open: that is the moment something leaves the machine —
|
|
249
|
+
* a shared branch, a PR, a deploy. A bare commit does not, so it keeps the cheap check above
|
|
250
|
+
* ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
251
|
+
*/
|
|
252
|
+
const shipGuard = await shipGateVerdict(event, ctx);
|
|
253
|
+
if (shipGuard.kind !== "abstain") {
|
|
254
|
+
recordShellDecisionIfShell(event, ctx, shipGuard);
|
|
255
|
+
return shipGuard;
|
|
256
|
+
}
|
|
257
|
+
|
|
209
258
|
switch (event.event) {
|
|
210
259
|
case "shell.before":
|
|
211
260
|
return handleShellBefore(event, ctx);
|