@tech-leads-club/harness-toolkit 0.6.0 → 0.7.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/bin/tlc-cli.ts +43 -11
- 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 +75 -75
- package/dist/lessons-cli.mjs +74 -74
- package/dist/obs-cli.mjs +76 -76
- 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 +76 -76
- package/dist/session-start.mjs +79 -79
- package/dist/shim.mjs +74 -74
- package/dist/stop.mjs +79 -79
- package/dist/subagent-start.mjs +74 -74
- package/dist/subagent-stop.mjs +75 -75
- package/dist/support.mjs +75 -75
- package/dist/tlc-cli.mjs +98 -97
- package/dist/tool-after.mjs +76 -76
- package/dist/tool-before.mjs +79 -79
- package/dist/tool-failure.mjs +74 -74
- package/dist/uninstall-runtime.mjs +4 -4
- package/docs/concepts.md +4 -1
- package/docs/log.md +8 -0
- package/package.json +1 -1
- package/src/core/core.facade.ts +2 -0
- package/src/core/floor/floor.policy-surface.ts +1 -1
- package/src/core/handoff/handoff.service.ts +30 -0
- package/src/core/handoff/handoff.types.ts +0 -2
- package/src/core/lesson/lesson.types.ts +5 -0
- package/src/core/turn/turn.failure-signals.ts +3 -1
- package/src/entrypoints/stop.ts +11 -7
- package/src/entrypoints/subagent-stop.ts +8 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as M$}from"node:module";var l=M$(import.meta.url);import{existsSync as B,lstatSync as m8,readFileSync as S,readlinkSync as u8,realpathSync as G$,rmSync as g8,statSync as h8,unlinkSync as p8,writeFileSync as c8}from"node:fs";import{homedir as d8}from"node:os";import{basename as l8,dirname as B$,isAbsolute as H$,join as G,relative as i8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as H2,dirname as K2,join as T,resolve as U2}from"node:path";function i(){return T(L(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function D(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:T(L(),".claude")}function j(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:T(L(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||T(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 OZ=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",CZ=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 z5=[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
|
-
`)}var
|
|
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:
|
|
4
|
-
`}}import{join as X$,relative as
|
|
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 K$,join as G,relative as n8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as H2,dirname as U2,join as T,resolve as M2}from"node:path";function i(){return T(L(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function j(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:T(L(),".claude")}function D(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:T(L(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||T(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 K of W)Q.push(` ${K}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
|
|
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 $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: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},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",x="installed-from-npm";import{existsSync as f8,mkdirSync as sX,readFileSync as b8,realpathSync as rX,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 K=W.filter((_)=>!m8(_));if(K.length!==W.length)X=!0;if(K.length>0)Q[z]=K}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[K,_]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[K]=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 J4,resolve as X4,sep as q4}from"node:path";var q$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],z$=["config.json","state","flags","rules"],H4=[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(j(),"settings.json"),cursorHooks:G(D(),"hooks.json"),skillLinks:[G(j(),"skills","harness-init"),G(D(),"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 k($){try{return G$($)}catch{try{let Z=g8($);return c(K$(Z)?Z:h(B$($),Z))}catch{return c($)}}}function s8($,Z,J={relative:n8,isAbsolute:K$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function W$($,Z){return s8(k($),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 ${k(Z)} — not ours`});return}let q=X==="location"&&!W$(Z,J);$.push({action:"unlink",target:Z,detail:q?`${Q}, stale — points at ${k(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 ${k(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,x);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,x)))$.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 H$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function J2($,Z){let J=[],Q=[];for(let X of H$($))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=H$($),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,H$ as pendingItems,W2 as main,s8 as isInsideRoot,c as canonicalise,J2 as applyUninstall};
|
package/docs/concepts.md
CHANGED
|
@@ -178,7 +178,10 @@ with BLOCKED.
|
|
|
178
178
|
Outcomes append to `.tlc/harness/state/ship-ledger.jsonl` (`claim` / `challenge` / `pass`), each row tagged
|
|
179
179
|
with the resolved `provider`.
|
|
180
180
|
|
|
181
|
-
Default excludes: `.tlc/`,
|
|
181
|
+
Default excludes: `.tlc/`, `**/node_modules/`, `**/.git/`. `src/core` stays provider-neutral by design
|
|
182
|
+
([/decisions/ad-011.md](/decisions/ad-011.md)), so a provider's own directory (`.cursor/`, `.claude/`) is
|
|
183
|
+
not a core default — add it to your project's `shipGate.runtimePathExcludes` if `runtimePathPrefixes` is
|
|
184
|
+
customised broadly enough to reach one.
|
|
182
185
|
|
|
183
186
|
## emptyDiffAntiShip
|
|
184
187
|
|
package/docs/log.md
CHANGED
|
@@ -13,6 +13,14 @@ Generated from `docs/decisions/` — do not edit by hand. Run `node tools/render
|
|
|
13
13
|
A reserved file of the [OKF v0.1](/decisions/ad-013.md) bundle: entries grouped under ISO 8601 headings,
|
|
14
14
|
newest first. For what landed in which npm release, see `CHANGELOG.md` at the repository root.
|
|
15
15
|
|
|
16
|
+
## 2026-08-26
|
|
17
|
+
|
|
18
|
+
- **AD-106** — A build step's own exit code cannot be the publish guarantee when it is also a recovery path ([/decisions/ad-106.md](/decisions/ad-106.md))
|
|
19
|
+
- **AD-107** — A subagent's own budget running out is not evidence about the tree, and a stuck handoff now has an exit ([/decisions/ad-107.md](/decisions/ad-107.md))
|
|
20
|
+
- **AD-108** — Two fields in the unfinished-work formula had no writer, and the carried-gap merge dropped this turn's own result ([/decisions/ad-108.md](/decisions/ad-108.md))
|
|
21
|
+
- **AD-109** — A lesson's credit is graded by the session that earned it, not by whichever session runs the gate next ([/decisions/ad-109.md](/decisions/ad-109.md))
|
|
22
|
+
- **AD-110** — The documented ship-gate excludes promised two paths core is not allowed to name ([/decisions/ad-110.md](/decisions/ad-110.md))
|
|
23
|
+
|
|
16
24
|
## 2026-08-22
|
|
17
25
|
|
|
18
26
|
- **AD-104** — A subagent proof names the type the spawn declared, never the name the spawning agent chose ([/decisions/ad-104.md](/decisions/ad-104.md))
|
package/package.json
CHANGED
package/src/core/core.facade.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { cachedVerdict, computeInputsHash, isCacheHit } from "./gate/gate.inputs
|
|
|
49
49
|
import { describeHolder, GATE_LOCK_WAIT_MS, GateLockTimeoutError, withGateLock } from "./gate/gate.lock.ts";
|
|
50
50
|
import { gapsFromArtifact } from "./gate/gate.service.ts";
|
|
51
51
|
import {
|
|
52
|
+
clearStuckSignals,
|
|
52
53
|
handoffInjectable,
|
|
53
54
|
patchHandoff,
|
|
54
55
|
readForeignSlices,
|
|
@@ -308,6 +309,7 @@ export const coreFacade = {
|
|
|
308
309
|
readHandoff,
|
|
309
310
|
readHandoffFile,
|
|
310
311
|
readForeignSlices,
|
|
312
|
+
clearStuckSignals,
|
|
311
313
|
},
|
|
312
314
|
shim: {
|
|
313
315
|
coversHandler,
|
|
@@ -99,7 +99,7 @@ const HARNESS_BINS = new Set(["tlc", "tlc.cmd"]);
|
|
|
99
99
|
// why: `policy` is here for the sharpest reason of any of them — `tlc harness policy accept` exists to clear a
|
|
100
100
|
// tampering signal, so an agent that could reach it would make the whole integrity rail decorative. It is refused
|
|
101
101
|
// by a floor rule with no config switch, exactly like the rest ([/decisions/ad-030.md](/decisions/ad-030.md)).
|
|
102
|
-
const MUTATING_SUBCOMMANDS = new Set(["pause", "resume", "grind", "mode", "init", "gate", "policy"]);
|
|
102
|
+
const MUTATING_SUBCOMMANDS = new Set(["pause", "resume", "reset", "grind", "mode", "init", "gate", "policy"]);
|
|
103
103
|
|
|
104
104
|
function deny(detail: string, note: string, remedy?: string): PolicySurfaceVerdict {
|
|
105
105
|
return { kind: "deny", detail, note, ...(remedy ? { remedy } : {}) };
|
|
@@ -25,6 +25,36 @@ export function readHandoff(root: string, provider: string): ResolvedHandoff {
|
|
|
25
25
|
return { ...file.shared, ...slice };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const CLEARED_SLICE: Partial<HandoffProviderSlice> = {
|
|
29
|
+
blockers: undefined,
|
|
30
|
+
previous_gaps: undefined,
|
|
31
|
+
last_failure_category: undefined,
|
|
32
|
+
next_action: undefined,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function hasStuckSignal(slice: HandoffProviderSlice): boolean {
|
|
36
|
+
return (Object.keys(CLEARED_SLICE) as (keyof HandoffProviderSlice)[]).some(
|
|
37
|
+
(field) => slice[field] !== undefined,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* why: an operator's escape hatch. These are the exact fields `subagent-stop.ts` reads as "unfinished
|
|
43
|
+
* work" — clearing anything wider would erase state no gate is stuck on.
|
|
44
|
+
*/
|
|
45
|
+
export async function clearStuckSignals(root: string): Promise<string[]> {
|
|
46
|
+
const file = readHandoffFile(root);
|
|
47
|
+
const cleared: string[] = [];
|
|
48
|
+
for (const [provider, slice] of Object.entries(file.by_provider)) {
|
|
49
|
+
if (!hasStuckSignal(slice)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
await patchHandoff(root, provider, { slice: CLEARED_SLICE });
|
|
53
|
+
cleared.push(provider);
|
|
54
|
+
}
|
|
55
|
+
return cleared;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
export function readForeignSlices(root: string, provider: string): ForeignSlice[] {
|
|
29
59
|
const file = readHandoffFile(root);
|
|
30
60
|
const foreign: ForeignSlice[] = [];
|
|
@@ -18,8 +18,6 @@ export type HandoffProviderSlice = {
|
|
|
18
18
|
session_key?: string;
|
|
19
19
|
session_narrative?: string;
|
|
20
20
|
completed?: string[];
|
|
21
|
-
in_progress?: string[];
|
|
22
|
-
pending?: string[];
|
|
23
21
|
last_ship_claim_at?: string;
|
|
24
22
|
last_ship_claim_snippet?: string;
|
|
25
23
|
last_ship_claim_kind?: "structured";
|
|
@@ -89,4 +89,9 @@ export type PendingLessonCredit = {
|
|
|
89
89
|
gate: string;
|
|
90
90
|
ids: string[];
|
|
91
91
|
at: string;
|
|
92
|
+
// why: the handoff is per-project, not per-session, so without this a lesson injected for session A's
|
|
93
|
+
// failing gate gets credited by whichever session B next happens to pass the same gate — helped/neutral
|
|
94
|
+
// it never earned. Optional so an on-disk record from before this field existed still credits once,
|
|
95
|
+
// the same legacy fallback AD-038 used for pre-existing lesson records.
|
|
96
|
+
sessionKey?: string;
|
|
92
97
|
};
|
|
@@ -104,10 +104,12 @@ export function formatCarriedGaps(gaps: readonly GateGap[], limit = CARRIED_GAP_
|
|
|
104
104
|
return lines.join("\n");
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// why: `current` goes first — it is this turn's fresh result, `prior` is carried history. Iterating
|
|
108
|
+
// prior-first let a saturated 12-item carry silently drop every fresh gap this turn found.
|
|
107
109
|
export function mergeGaps(prior: GateGap[] | undefined, current: GateGap[], max = 12): GateGap[] {
|
|
108
110
|
const seen = new Set<string>();
|
|
109
111
|
const out: GateGap[] = [];
|
|
110
|
-
for (const gap of [...(prior ?? [])
|
|
112
|
+
for (const gap of [...current, ...(prior ?? [])]) {
|
|
111
113
|
const key = `${gap.gate}|${gap.summary}`;
|
|
112
114
|
if (seen.has(key)) {
|
|
113
115
|
continue;
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -151,20 +151,26 @@ export function stopLockWaitMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
|
151
151
|
* invariant: consumed exactly once. The pending credit is cleared whether or not any lesson matched, so a single
|
|
152
152
|
* injection cannot be graded twice by two later runs of the same gate.
|
|
153
153
|
*
|
|
154
|
-
* hazard: the gate name is compared
|
|
155
|
-
*
|
|
154
|
+
* hazard: the gate name is compared, and the session that earned the credit is compared — without either,
|
|
155
|
+
* lessons injected for `lint` would be credited by whichever gate ran next, or by whichever session next
|
|
156
|
+
* happened to run this gate on the same per-project handoff `blockers` lives on, crediting help a session
|
|
157
|
+
* that never saw the lesson did not give ([/decisions/ad-107.md](/decisions/ad-107.md) is the same family).
|
|
156
158
|
*/
|
|
157
159
|
async function creditPendingLessons(args: {
|
|
158
160
|
root: string;
|
|
159
161
|
provider: string;
|
|
160
162
|
pending: PendingLessonCredit | undefined;
|
|
161
163
|
gate: string;
|
|
164
|
+
sessionKey: string;
|
|
162
165
|
passed: boolean;
|
|
163
166
|
}): Promise<void> {
|
|
164
167
|
const { pending } = args;
|
|
165
168
|
if (!pending || pending.gate !== args.gate || pending.ids.length === 0) {
|
|
166
169
|
return;
|
|
167
170
|
}
|
|
171
|
+
if (pending.sessionKey !== undefined && pending.sessionKey !== args.sessionKey) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
168
174
|
await coreFacade.lesson.creditLessons(args.root, pending.ids, args.passed ? "helped" : "neutral");
|
|
169
175
|
await coreFacade.handoff.patchHandoff(args.root, args.provider, {
|
|
170
176
|
slice: { pending_lesson_credit: undefined },
|
|
@@ -231,6 +237,7 @@ async function runLockedGate(args: {
|
|
|
231
237
|
provider: args.provider,
|
|
232
238
|
pending: args.pendingCredit,
|
|
233
239
|
gate: args.gate,
|
|
240
|
+
sessionKey: args.sessionKey,
|
|
234
241
|
passed: artifact.passed,
|
|
235
242
|
});
|
|
236
243
|
return { kind: "ran", artifact, reused: cached !== null };
|
|
@@ -334,6 +341,7 @@ async function failGate(args: {
|
|
|
334
341
|
gate: args.gate,
|
|
335
342
|
ids: selected.usedIds,
|
|
336
343
|
at: new Date().toISOString(),
|
|
344
|
+
sessionKey: args.sessionKey,
|
|
337
345
|
},
|
|
338
346
|
},
|
|
339
347
|
});
|
|
@@ -504,11 +512,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
504
512
|
}
|
|
505
513
|
|
|
506
514
|
const intel = policy.intelligence;
|
|
507
|
-
const unfinishedWork =
|
|
508
|
-
Boolean(handoff.blockers) ||
|
|
509
|
-
Boolean(handoff.previous_gaps?.length) ||
|
|
510
|
-
Boolean(handoff.pending?.length) ||
|
|
511
|
-
Boolean(handoff.in_progress?.length);
|
|
515
|
+
const unfinishedWork = Boolean(handoff.blockers) || Boolean(handoff.previous_gaps?.length);
|
|
512
516
|
if (
|
|
513
517
|
intel.idleTurnGate &&
|
|
514
518
|
coreFacade.turn.endedWithoutActing({
|
|
@@ -4,8 +4,8 @@ import type { Handler, HandlerContext } from "./run.ts";
|
|
|
4
4
|
import { main } from "./run.ts";
|
|
5
5
|
import { observeForRules } from "./support.ts";
|
|
6
6
|
|
|
7
|
-
// why: no legacy predecessor covers subagent.stop verification — this reuses the
|
|
8
|
-
// signal
|
|
7
|
+
// why: no legacy predecessor covers subagent.stop verification — this reuses the blockers/previous_gaps
|
|
8
|
+
// signal already carried on the handoff slice.
|
|
9
9
|
export const subagentStopHandler: Handler = async (
|
|
10
10
|
event: HarnessEvent,
|
|
11
11
|
ctx: HandlerContext,
|
|
@@ -15,11 +15,13 @@ export const subagentStopHandler: Handler = async (
|
|
|
15
15
|
await observeForRules(event, ctx);
|
|
16
16
|
|
|
17
17
|
const handoff = coreFacade.handoff.readHandoff(event.projectDir, event.provider);
|
|
18
|
+
// why: `blockers` is per-project, and every Task subagent shares the parent's session_id
|
|
19
|
+
// (anthropics/claude-code#7881), so this cannot tell its own subagent apart from another. A "budget"
|
|
20
|
+
// category means the session's turn budget ran out, not that the tree is broken — unlike a gate
|
|
21
|
+
// failure, it is not valid evidence here ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
22
|
+
const isBudgetBlocker = handoff.last_failure_category === "budget";
|
|
18
23
|
const unfinishedWork =
|
|
19
|
-
Boolean(handoff.blockers) ||
|
|
20
|
-
Boolean(handoff.previous_gaps?.length) ||
|
|
21
|
-
Boolean(handoff.pending?.length) ||
|
|
22
|
-
Boolean(handoff.in_progress?.length);
|
|
24
|
+
(Boolean(handoff.blockers) && !isBudgetBlocker) || Boolean(handoff.previous_gaps?.length);
|
|
23
25
|
|
|
24
26
|
if (!unfinishedWork) {
|
|
25
27
|
return { kind: "abstain" };
|