@tech-leads-club/harness-toolkit 0.4.4 → 0.4.6
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 +9 -3
- package/dist/compact-before.mjs +77 -77
- package/dist/doctor.mjs +78 -78
- package/dist/init-project.mjs +87 -87
- package/dist/install-runtime.mjs +77 -77
- package/dist/lessons-cli.mjs +77 -77
- package/dist/obs-cli.mjs +78 -78
- package/dist/prompt-submit.mjs +77 -77
- package/dist/refresh-model-prices.mjs +77 -77
- package/dist/response-after.mjs +77 -77
- package/dist/run.mjs +78 -78
- package/dist/session-end.mjs +81 -81
- package/dist/session-start.mjs +85 -85
- package/dist/shim.mjs +76 -76
- package/dist/stop.mjs +84 -84
- package/dist/subagent-start.mjs +78 -78
- package/dist/subagent-stop.mjs +79 -79
- package/dist/support.mjs +81 -81
- package/dist/tlc-cli.mjs +99 -99
- package/dist/tool-after.mjs +77 -77
- package/dist/tool-before.mjs +76 -76
- package/dist/tool-failure.mjs +77 -77
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/log.md +4 -0
- package/package.json +1 -1
- package/src/contracts/harness-event.ts +2 -0
- package/src/core/capability/capability.service.ts +15 -7
- package/src/core/rules/rules.observe.ts +59 -3
- package/src/core/rules/rules.service.ts +25 -3
- package/src/core/rules/rules.store.ts +30 -0
- package/src/providers/claude/claude.inbound.ts +22 -3
- package/tools/doctor.ts +9 -3
|
@@ -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 j}from"node:os";import{delimiter as H2,dirname as K2,join as L,resolve as U2}from"node:path";function i(){return L(j(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function P(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".claude")}function T(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||L(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 I$($){let Z=new Map;for(let Q of $){let J={line:Q.line,block:Q.block,middle:Q.middle};for(let X of Q.extensions)Z.set(X.toLowerCase(),J)}return Z}var _$=I$(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",IZ=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 z9=[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((Q)=>Number.parseInt(Q,16)).join(";")}var $$=String.fromCharCode(27),i$=`${$$}[0m`;function n$($=process.env,Z=process.argv,Q=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return Q}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=(J,X)=>$?`${$$}[${J}m${X}${i$}`:X,Q=(J,X)=>Z(`38;2;${l$(d$[J])}`,X);return{enabled:$,paint:Q,bold:(J)=>Z("1",J),dim:(J)=>Q("textDim",J),heading:(J)=>Q("accent",`${V.rule} ${J} ${V.rule}`),footer:(J)=>Q("textDim",`${V.dash} ${J} ${V.dash}`),kv:(J,X,q=m)=>` ${Q("textMuted",`${J}:`.padEnd(q))} ${X}`,status:(J,X)=>`${Q(a$[J],s$[J])} ${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 Q=[],J="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(J===""){J=X;continue}if(J.length+1+X.length<=Z){J=`${J} ${X}`;continue}Q.push(J),J=X}if(J!=="")Q.push(J);return Q.length===0?[""]:Q}function C($,Z,Q=o$()){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.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(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;J.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){J.push("");continue}let W=q.wrap?t$(z,Q-r$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.join(`
|
|
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 j}from"node:os";import{delimiter as H2,dirname as K2,join as L,resolve as U2}from"node:path";function i(){return L(j(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function D(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".claude")}function P(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||L(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 I$($){let Z=new Map;for(let Q of $){let J={line:Q.line,block:Q.block,middle:Q.middle};for(let X of Q.extensions)Z.set(X.toLowerCase(),J)}return Z}var _$=I$(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",IZ=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 z9=[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((Q)=>Number.parseInt(Q,16)).join(";")}var $$=String.fromCharCode(27),i$=`${$$}[0m`;function n$($=process.env,Z=process.argv,Q=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return Q}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=(J,X)=>$?`${$$}[${J}m${X}${i$}`:X,Q=(J,X)=>Z(`38;2;${l$(d$[J])}`,X);return{enabled:$,paint:Q,bold:(J)=>Z("1",J),dim:(J)=>Q("textDim",J),heading:(J)=>Q("accent",`${V.rule} ${J} ${V.rule}`),footer:(J)=>Q("textDim",`${V.dash} ${J} ${V.dash}`),kv:(J,X,q=m)=>` ${Q("textMuted",`${J}:`.padEnd(q))} ${X}`,status:(J,X)=>`${Q(a$[J],s$[J])} ${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 Q=[],J="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(J===""){J=X;continue}if(J.length+1+X.length<=Z){J=`${J} ${X}`;continue}Q.push(J),J=X}if(J!=="")Q.push(J);return Q.length===0?[""]:Q}function C($,Z,Q=o$()){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.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(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;J.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){J.push("");continue}let W=q.wrap?t$(z,Q-r$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.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 I="@tech-leads-club/harness-toolkit",
|
|
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 I="@tech-leads-club/harness-toolkit",x="installed-from-npm";import{existsSync as S8,mkdirSync as dX,readFileSync as f8,realpathSync as lX,writeFileSync as b8}from"node:fs";function J$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function w8($){return J$($)}var y8="tlc-exec.mjs";function v8($){return JSON.stringify($??null).includes(y8)}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(!J$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let Q=w8(Z.hooks)?Z.hooks:{},J={},X=!1;for(let[z,W]of Object.entries(Q)){let H=W.filter((_)=>!v8(_));if(H.length!==W.length)X=!0;if(H.length>0)J[z]=H}let q={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")q[z]=W;else if(Object.keys(J).length>0)q.hooks=J;return{ok:!0,settingsText:JSON.stringify(q,null,2),changed:X}}function Q$($){if(!S8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(f8($,"utf8"));if(Z.ok&&Z.changed)b8($,Z.settingsText,"utf8");return Z}function g($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let Q;try{Q=JSON.parse($)}catch{return{kind:"unparsed"}}if(Q===null||typeof Q!=="object"||Array.isArray(Q))return{kind:"unparsed"};let J=Q,X=J.hooks!==null&&typeof J.hooks==="object"&&!Array.isArray(J.hooks)?J.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({...J,hooks:q},null,2)}
|
|
4
|
+
`}}import{join as X$,relative as tX,resolve as eX,sep as $4}from"node:path";var q$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],z$=["config.json","state","flags","rules"],Y4=[X$("tools","dev"),X$("tools","__test__")];function n8($=process.env){let Z=d8(),Q=f($);return{home:F($),binLinks:n().map((J)=>G(Q,J)),claudeSettings:G(D(),"settings.json"),cursorHooks:G(P(),"hooks.json"),skillLinks:[G(D(),"skills","harness-init"),G(P(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return m8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),Q=[];for(;;)try{return G(G$(Z),...Q)}catch{let J=B$(Z);if(J===Z)return h($);Q.unshift(l8(Z)),Z=J}}function k($){try{return G$($)}catch{try{let Z=u8($);return c(H$(Z)?Z:h(B$($),Z))}catch{return c($)}}}function a8($,Z,Q={relative:i8,isAbsolute:H$}){if($===Z)return!0;let J=Q.relative(Z,$);return J!==""&&!J.startsWith("..")&&!Q.isAbsolute(J)}function W$($,Z){return a8(k($),c(Z))}var s8="tlc-exec.mjs";function r8($){try{return h8($).size<4096&&S($,"utf8").includes(s8)}catch{return!1}}function Y$($,Z,Q,J,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(r8(Z)){$.push({action:"remove",target:Z,detail:`${J}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${J} is a real file the installer did not write`});return}if(X==="target"&&!W$(Z,Q)){$.push({action:"keep",target:Z,detail:`points at ${k(Z)} — not ours`});return}let q=X==="location"&&!W$(Z,Q);$.push({action:"unlink",target:Z,detail:q?`${J}, stale — points at ${k(Z)}`:J})}function o8($,Z){if(!B(Z))return;let Q=u(S(Z,"utf8"));if(!Q.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${Q.error}`});return}if(!Q.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function t8($,Z){let Q=B(Z)?S(Z,"utf8"):null,J=g(Q);switch(J.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(J.removed>0)$.push({action:"remove",target:Z,detail:`${J.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${J.removed} harness entries, keep the rest`})}}function e8($,Z,Q){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(Q?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function $2($,Z){if(B(G(Z,x)))$.push({action:"manual",target:`npm uninstall -g ${I}`,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 Z2($,Z={}){let Q=Z.purge===!0,J=[];o8(J,$.claudeSettings),t8(J,$.cursorHooks);for(let q of $.skillLinks)Y$(J,q,$.home,"skill link","location");for(let q of $.binLinks)Y$(J,q,$.home,"the tlc launcher on PATH","target");let X=e8(J,$.home,Q);return $2(J,$.home),{items:J,purge:Q,homeIsLink:X}}function K$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function J2($,Z){let Q=[],J=[];for(let X of K$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)Q$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")c8(X.target,q.text,"utf8")}else if(X.action==="unlink")p8(X.target);else g8(X.target,{recursive:!0,force:!0});Q.push(X)}catch(q){J.push({item:X,reason:q instanceof Error?q.message:String(q)})}return{applied:Q,failed:J}}var Q2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function X2($,Z){let Q=K$($),J=Z!==null;if(Q.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 ${I}, then \`tlc harness install\``};let X=Q.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:Q2[W.action]})),q=$.items.filter((W)=>W.action==="keep"),z=[{title:J?"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:[J?`${Z.applied.length} applied`:`${Q.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:J?`reinstall any time with npm i -g ${I}, 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 q2($,Z,Q=O){return C(X2($,Z),Q)}function z2($=process.argv.slice(2)){let Z=n8(),Q=Z2(Z,{purge:$.includes("--purge")}),J=$.includes("--yes")?J2(Q,Z):null;return console.log(q2(Q,J,R())),J!==null&&J.failed.length>0?1:0}if(l.main==l.module)process.exitCode=z2();export{n8 as uninstallTargets,X2 as uninstallScreen,q2 as uninstallReportText,Z2 as planUninstall,K$ as pendingItems,z2 as main,a8 as isInsideRoot,c as canonicalise,J2 as applyUninstall};
|
package/docs/log.md
CHANGED
|
@@ -13,6 +13,10 @@ 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-22
|
|
17
|
+
|
|
18
|
+
- **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))
|
|
19
|
+
|
|
16
20
|
## 2026-08-21
|
|
17
21
|
|
|
18
22
|
- **AD-100** — The operator declares the trigger and the proof; the harness enforces it ([/decisions/ad-100.md](/decisions/ad-100.md))
|
package/package.json
CHANGED
|
@@ -64,6 +64,8 @@ export type HarnessEvent = {
|
|
|
64
64
|
filePath?: string;
|
|
65
65
|
subagentType?: string;
|
|
66
66
|
spawnSubagentType?: string;
|
|
67
|
+
/** What the host calls a spawn — its name, which the spawning agent chooses. Never a type to match against. */
|
|
68
|
+
spawnAgentLabel?: string;
|
|
67
69
|
status?: "completed" | "aborted" | "error";
|
|
68
70
|
loopCount?: number;
|
|
69
71
|
/** Provider's own permission posture, when it exposes one. Absent means unknown, not permissive. */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type CapabilityCatalog, type CatalogCapability, ENABLE_HINT } from "./capability.types.ts";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* why `object` and not `Record<string, unknown>`: the callers hand this the *effective* policy, which is a typed
|
|
5
|
+
* `Policy` rather than a bag of unknowns. Narrowing the parameter forced a cast at every call site, and a cast is
|
|
6
|
+
* where a wrong argument stops being a type error ([/decisions/ad-103.md](/decisions/ad-103.md)).
|
|
7
|
+
*/
|
|
8
|
+
export function resolveConfigPath(policy: object, configPath: string): unknown {
|
|
4
9
|
let current: unknown = policy;
|
|
5
10
|
for (const part of configPath.split(".").filter(Boolean)) {
|
|
6
11
|
if (!current || typeof current !== "object") {
|
|
@@ -11,20 +16,23 @@ export function resolveConfigPath(policy: Record<string, unknown>, configPath: s
|
|
|
11
16
|
return current;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
export function isAvailableNotEnabled(policy:
|
|
19
|
+
export function isAvailableNotEnabled(policy: object, cap: CatalogCapability): boolean {
|
|
15
20
|
const value = resolveConfigPath(policy, cap.configPath);
|
|
16
21
|
return cap.defaultOn ? value === false : value !== true;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* hazard: this used to be handed the project's config file alone, so a capability switched on in the machine tier
|
|
26
|
+
* was reported as "available and not enabled" while it was being enforced — and `doctor`'s neighbouring row tells
|
|
27
|
+
* the operator to delete restatements, which is what produces exactly that state. A row that says a rail is off
|
|
28
|
+
* while the rail is on is worse than no row ([/decisions/ad-103.md](/decisions/ad-103.md)).
|
|
29
|
+
*/
|
|
30
|
+
export function listAvailableNotEnabled(policy: object, catalog: CapabilityCatalog): CatalogCapability[] {
|
|
23
31
|
return catalog.capabilities.filter((cap) => isAvailableNotEnabled(policy, cap));
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function listNewlyAnnounceable(
|
|
27
|
-
policy:
|
|
35
|
+
policy: object,
|
|
28
36
|
catalog: CapabilityCatalog,
|
|
29
37
|
seenCatalogVersion: number,
|
|
30
38
|
): CatalogCapability[] {
|
|
@@ -28,6 +28,16 @@ export type ObservableEvent = {
|
|
|
28
28
|
* field name it was asserting about ([/decisions/ad-100.md](/decisions/ad-100.md)).
|
|
29
29
|
*/
|
|
30
30
|
spawnSubagentType?: string;
|
|
31
|
+
/**
|
|
32
|
+
* What the host calls this spawn, which is not what the spawn declared.
|
|
33
|
+
*
|
|
34
|
+
* hazard: the documented `agent_type` field of `SubagentStart`/`SubagentStop` is described as the agent's name,
|
|
35
|
+
* and when a spawn is given a `name` the host puts that name here — so a value the gated agent chose was being
|
|
36
|
+
* recorded as the type it must prove. That made a legitimate review fail to satisfy the rule *and* made the
|
|
37
|
+
* proof forgeable: a `general-purpose` subagent named `the-judge` would have satisfied it
|
|
38
|
+
* ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
39
|
+
*/
|
|
40
|
+
spawnAgentLabel?: string;
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
export type ObserveContext = { sha: string | null; sessionKey: string; at: string };
|
|
@@ -46,9 +56,12 @@ export type ObservedFact = { kind: Observation["kind"]; value: string };
|
|
|
46
56
|
export function observedFact(event: ObservableEvent): ObservedFact | null {
|
|
47
57
|
if (event.event === "subagent.stop") {
|
|
48
58
|
// why the type and not the id: a rule says "the jury reviewed", not "agent 7f3a reviewed".
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
//
|
|
60
|
+
// why the label is a fallback and not the first choice: it is what the host calls the spawn, which the gated
|
|
61
|
+
// agent chooses when it names one. A host that sends the declared type wins; a host that sends only a label
|
|
62
|
+
// degrades to it, which is visible rather than silent ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
63
|
+
const value = event.spawnSubagentType ?? event.spawnAgentLabel;
|
|
64
|
+
return value === undefined ? null : { kind: "subagent", value };
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
if (event.event === "tool.after" || event.event === "shell.after") {
|
|
@@ -74,3 +87,46 @@ export function observationFrom(event: ObservableEvent, context: ObserveContext)
|
|
|
74
87
|
export function gateObservation(gate: string, context: ObserveContext): Observation {
|
|
75
88
|
return { kind: "gate", value: gate, sha: context.sha, sessionKey: context.sessionKey, at: context.at };
|
|
76
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The elo a spawn leaves behind: the type it declared, under the label the host will echo back at the stop.
|
|
93
|
+
*
|
|
94
|
+
* why `tool.after` and not `tool.before`: after means the host accepted the spawn. A spawn refused by the
|
|
95
|
+
* allowlist rail must not leave a link that a later stop could resolve against
|
|
96
|
+
* ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
97
|
+
*
|
|
98
|
+
* invariant: only a spawn that declared a type and carries a label is worth linking. Without a label there is
|
|
99
|
+
* nothing to resolve at the stop, and without a declared type there is nothing to resolve *to*.
|
|
100
|
+
*/
|
|
101
|
+
export function spawnLinkFrom(event: ObservableEvent): { label: string; type: string } | null {
|
|
102
|
+
if (event.event !== "tool.after" || !SPAWN_TOOLS.has(event.toolName ?? "")) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const { spawnAgentLabel: label, spawnSubagentType: type } = event;
|
|
106
|
+
return label === undefined || type === undefined || label === type ? null : { label, type };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const SPAWN_TOOLS = new Set(["Task", "Agent"]);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The stop, with the label resolved back to the type the spawn declared.
|
|
113
|
+
*
|
|
114
|
+
* invariant: the newest link wins, because a label can be reused across a session and the proof is about now.
|
|
115
|
+
* A stop that already carries a declared type is returned untouched — a host that answers correctly is never
|
|
116
|
+
* second-guessed ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
117
|
+
*/
|
|
118
|
+
export function resolveSpawnType(
|
|
119
|
+
event: ObservableEvent,
|
|
120
|
+
links: () => readonly { label: string; type: string }[],
|
|
121
|
+
): ObservableEvent {
|
|
122
|
+
if (event.event !== "subagent.stop" || event.spawnSubagentType !== undefined) {
|
|
123
|
+
return event;
|
|
124
|
+
}
|
|
125
|
+
const label = event.spawnAgentLabel;
|
|
126
|
+
if (label === undefined) {
|
|
127
|
+
return event;
|
|
128
|
+
}
|
|
129
|
+
const matches = links().filter((link) => link.label === label);
|
|
130
|
+
const newest = matches[matches.length - 1];
|
|
131
|
+
return newest === undefined ? event : { ...event, spawnSubagentType: newest.type };
|
|
132
|
+
}
|
|
@@ -16,10 +16,18 @@ import {
|
|
|
16
16
|
type ObserveContext,
|
|
17
17
|
observationFrom,
|
|
18
18
|
observedFact,
|
|
19
|
+
resolveSpawnType,
|
|
20
|
+
spawnLinkFrom,
|
|
19
21
|
} from "./rules.observe.ts";
|
|
20
22
|
import { buildRuleSet } from "./rules.parse.ts";
|
|
21
23
|
import { kindIsRequired } from "./rules.proof.ts";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
readObservations,
|
|
26
|
+
readRuleSources,
|
|
27
|
+
readSpawnLinks,
|
|
28
|
+
recordObservation,
|
|
29
|
+
recordSpawnLink,
|
|
30
|
+
} from "./rules.store.ts";
|
|
23
31
|
import { firingRules, type TriggerContext } from "./rules.trigger.ts";
|
|
24
32
|
import type { RuleError, RuleSet } from "./rules.types.ts";
|
|
25
33
|
|
|
@@ -45,8 +53,14 @@ export function loadRules(root: string, config: RulesConfig): RuleSet {
|
|
|
45
53
|
* and no git at all.
|
|
46
54
|
*/
|
|
47
55
|
export function wantsObservation(root: string, config: RulesConfig, event: ObservableEvent): boolean {
|
|
56
|
+
const rules = loadRules(root, config).rules;
|
|
57
|
+
// why the spawn counts as wanted: the link it leaves is what makes the stop resolvable, so a rule asking for
|
|
58
|
+
// subagent proof has to make the spawn worth observing too ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
59
|
+
if (spawnLinkFrom(event) !== null) {
|
|
60
|
+
return kindIsRequired(rules, "subagent");
|
|
61
|
+
}
|
|
48
62
|
const fact = observedFact(event);
|
|
49
|
-
return fact !== null && kindIsRequired(
|
|
63
|
+
return fact !== null && kindIsRequired(rules, fact.kind);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/** invariant: with the capability off nothing is written, so a machine that never opted in carries no new file. */
|
|
@@ -59,7 +73,15 @@ export function observe(
|
|
|
59
73
|
if (!config.enabled) {
|
|
60
74
|
return;
|
|
61
75
|
}
|
|
62
|
-
const
|
|
76
|
+
const link = spawnLinkFrom(event);
|
|
77
|
+
if (link !== null) {
|
|
78
|
+
recordSpawnLink(root, { ...link, at: context.at });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const observation = observationFrom(
|
|
82
|
+
resolveSpawnType(event, () => readSpawnLinks(root)),
|
|
83
|
+
context,
|
|
84
|
+
);
|
|
63
85
|
if (observation !== null) {
|
|
64
86
|
recordObservation(root, observation);
|
|
65
87
|
}
|
|
@@ -59,6 +59,36 @@ export function readRuleSources(root: string): RuleSource[] {
|
|
|
59
59
|
return [...readDir(globalRulesDir(), "global"), ...readDir(projectRulesDir(root), "project")];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* What the host will call a spawn, against the type the spawn actually declared.
|
|
64
|
+
*
|
|
65
|
+
* why a second file and not a second record shape in the proof store: that store answers "what was proven", and
|
|
66
|
+
* every reader of it treats a record as proof. A link is not proof — the subagent has not finished yet — and a
|
|
67
|
+
* reader that had to skip records would be one refactor away from counting one ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
68
|
+
*/
|
|
69
|
+
export function spawnLinksPath(root: string): string {
|
|
70
|
+
return join(projectStateDir(root), "rule-spawn-links.jsonl");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SpawnLink = { label: string; type: string; at: string };
|
|
74
|
+
|
|
75
|
+
export function recordSpawnLink(root: string, link: SpawnLink): void {
|
|
76
|
+
try {
|
|
77
|
+
appendRecord(spawnLinksPath(root), link);
|
|
78
|
+
} catch {
|
|
79
|
+
// why swallowed: the same reason the proof store swallows. A link that cannot be written costs a proof that
|
|
80
|
+
// reads as missing, which the gate already reports as missing.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readSpawnLinks(root: string): SpawnLink[] {
|
|
85
|
+
try {
|
|
86
|
+
return readTail<SpawnLink>(spawnLinksPath(root), OBSERVATION_TAIL);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
export function recordObservation(root: string, observation: Observation): void {
|
|
63
93
|
try {
|
|
64
94
|
appendRecord(observationsPath(root), observation);
|
|
@@ -216,6 +216,13 @@ export function claudeToEvent(raw: Record<string, unknown>): HarnessEvent | null
|
|
|
216
216
|
if (spawnSubagentType) {
|
|
217
217
|
event.spawnSubagentType = spawnSubagentType;
|
|
218
218
|
}
|
|
219
|
+
// why the name too: an addressable spawn carries one, and it is the value the host echoes back as
|
|
220
|
+
// `agent_type` at the stop. Without it here, the stop cannot be resolved to the type the spawn declared
|
|
221
|
+
// ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
222
|
+
const spawnAgentLabel = toolInput ? asString(toolInput.name) : undefined;
|
|
223
|
+
if (spawnAgentLabel) {
|
|
224
|
+
event.spawnAgentLabel = spawnAgentLabel;
|
|
225
|
+
}
|
|
219
226
|
const spawnModel = toolInput ? asString(toolInput.model) : undefined;
|
|
220
227
|
if (spawnModel) {
|
|
221
228
|
event.spawnModel = spawnModel;
|
|
@@ -224,13 +231,25 @@ export function claudeToEvent(raw: Record<string, unknown>): HarnessEvent | null
|
|
|
224
231
|
}
|
|
225
232
|
case "subagent.start":
|
|
226
233
|
case "subagent.stop": {
|
|
234
|
+
/**
|
|
235
|
+
* hazard: `agent_type` was read here as the type. The hooks reference describes it as the agent's *name*,
|
|
236
|
+
* and when a spawn is given a `name` the host puts that name in it — measured on a real payload:
|
|
237
|
+
* `subagent_type: "the-judge"` at the spawn, `agent_type: "judge-harness-rule"` inside the child. So the
|
|
238
|
+
* value a rule had to match was the one the gated agent chose, which broke a legitimate review and made the
|
|
239
|
+
* proof forgeable ([/decisions/ad-104.md](/decisions/ad-104.md)).
|
|
240
|
+
*
|
|
241
|
+
* invariant: only a field that means the declared type lands in `spawnSubagentType`. The host's label goes
|
|
242
|
+
* to `spawnAgentLabel`, and the correlation with the spawn resolves it.
|
|
243
|
+
*/
|
|
227
244
|
const spawnSubagentType =
|
|
228
|
-
asString(raw.
|
|
229
|
-
asString(raw.subagent_type) ??
|
|
230
|
-
(toolInput ? asString(toolInput.subagent_type) : undefined);
|
|
245
|
+
asString(raw.subagent_type) ?? (toolInput ? asString(toolInput.subagent_type) : undefined);
|
|
231
246
|
if (spawnSubagentType) {
|
|
232
247
|
event.spawnSubagentType = spawnSubagentType;
|
|
233
248
|
}
|
|
249
|
+
const spawnAgentLabel = asString(raw.agent_type) ?? (toolInput ? asString(toolInput.name) : undefined);
|
|
250
|
+
if (spawnAgentLabel) {
|
|
251
|
+
event.spawnAgentLabel = spawnAgentLabel;
|
|
252
|
+
}
|
|
234
253
|
const spawnModel = (toolInput ? asString(toolInput.model) : undefined) ?? asString(raw.model);
|
|
235
254
|
if (spawnModel) {
|
|
236
255
|
event.spawnModel = spawnModel;
|
package/tools/doctor.ts
CHANGED
|
@@ -418,11 +418,17 @@ export function checkProviders(registry: readonly ProviderPort[], home: string):
|
|
|
418
418
|
*/
|
|
419
419
|
export function checkCapabilities(root: string, runtimeRoot: string): Check[] {
|
|
420
420
|
const catalog = coreFacade.capability.loadCatalog(runtimeRoot);
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
// why the project file is still read: its absence means this repository is not connected, and inventory about a
|
|
422
|
+
// repository nobody wired is noise. What decides *enabled* is the effective policy, which includes the machine
|
|
423
|
+
// tier ([/decisions/ad-103.md](/decisions/ad-103.md)).
|
|
424
|
+
const connected = coreFacade.capability.readProjectPolicyRaw(root);
|
|
425
|
+
if (!catalog || !connected) {
|
|
423
426
|
return [];
|
|
424
427
|
}
|
|
425
|
-
const available = coreFacade.capability.listAvailableNotEnabled(
|
|
428
|
+
const available = coreFacade.capability.listAvailableNotEnabled(
|
|
429
|
+
coreFacade.policy.loadPolicy(root),
|
|
430
|
+
catalog,
|
|
431
|
+
);
|
|
426
432
|
if (available.length === 0) {
|
|
427
433
|
return [];
|
|
428
434
|
}
|