@tech-leads-club/harness-toolkit 0.10.6 → 0.11.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.
Files changed (64) hide show
  1. package/README.md +6 -4
  2. package/bin/tlc-cli.ts +60 -1
  3. package/capabilities/catalog.json +18 -1
  4. package/dist/compact-before.mjs +79 -78
  5. package/dist/doctor.mjs +83 -82
  6. package/dist/init-project.mjs +86 -85
  7. package/dist/install-runtime.mjs +80 -79
  8. package/dist/lessons-cli.mjs +82 -81
  9. package/dist/new-provider.mjs +126 -0
  10. package/dist/obs-cli.mjs +83 -82
  11. package/dist/prompt-submit.mjs +79 -78
  12. package/dist/refresh-model-prices.mjs +80 -79
  13. package/dist/response-after.mjs +80 -79
  14. package/dist/run.mjs +79 -78
  15. package/dist/session-end.mjs +85 -84
  16. package/dist/session-start.mjs +87 -86
  17. package/dist/shim.mjs +81 -80
  18. package/dist/ship-gate.mjs +79 -78
  19. package/dist/stop.mjs +86 -85
  20. package/dist/subagent-start.mjs +79 -78
  21. package/dist/subagent-stop.mjs +80 -79
  22. package/dist/support.mjs +84 -83
  23. package/dist/tlc-cli.mjs +225 -97
  24. package/dist/tool-after.mjs +80 -79
  25. package/dist/tool-before.mjs +82 -81
  26. package/dist/tool-failure.mjs +79 -78
  27. package/dist/uninstall-runtime.mjs +3 -3
  28. package/docs/architecture.md +16 -3
  29. package/docs/concepts.md +11 -0
  30. package/docs/log.md +4 -0
  31. package/docs/providers/claude-code.md +42 -21
  32. package/docs/providers/cursor.md +23 -7
  33. package/docs/troubleshooting.md +1 -0
  34. package/package.json +1 -1
  35. package/schema.json +12 -0
  36. package/skills/harness-init/references/capabilities.md +1 -0
  37. package/src/contracts/capabilities.ts +8 -1
  38. package/src/contracts/decision.ts +2 -1
  39. package/src/core/core.facade.ts +6 -0
  40. package/src/core/floor/floor.catalog.ts +5 -0
  41. package/src/core/floor/floor.paths.ts +17 -1
  42. package/src/core/floor/floor.policy-surface.ts +26 -9
  43. package/src/core/floor/floor.service.ts +37 -5
  44. package/src/core/policy/policy.defaults.ts +3 -0
  45. package/src/core/policy/policy.loader.ts +1 -0
  46. package/src/core/policy/policy.types.ts +4 -0
  47. package/src/core/secret-scan/secret-scan.service.ts +96 -0
  48. package/src/core/secret-scan/secret-scan.store.ts +56 -0
  49. package/src/entrypoints/run.ts +3 -1
  50. package/src/entrypoints/tool-after.ts +20 -0
  51. package/src/entrypoints/tool-before.ts +1 -0
  52. package/src/providers/claude/claude.capabilities.ts +3 -1
  53. package/src/providers/claude/claude.inbound.ts +37 -22
  54. package/src/providers/claude/claude.outbound.ts +4 -0
  55. package/src/providers/claude/claude.wiring.ts +4 -0
  56. package/src/providers/claude/index.ts +2 -1
  57. package/src/providers/cursor/cursor.capabilities.ts +3 -1
  58. package/src/providers/cursor/cursor.inbound.ts +1 -1
  59. package/src/providers/cursor/cursor.outbound.ts +9 -0
  60. package/src/providers/cursor/cursor.wiring.ts +9 -1
  61. package/src/providers/cursor/index.ts +2 -1
  62. package/src/providers/provider.degrade.ts +19 -0
  63. package/src/providers/provider.port.ts +1 -0
  64. package/tools/new-provider.ts +251 -0
@@ -1,4 +1,4 @@
1
- import{createRequire as F$}from"node:module";var l=F$(import.meta.url);import{existsSync as B,lstatSync as h8,readFileSync as S,readlinkSync as p8,realpathSync as K$,rmSync as c8,statSync as d8,unlinkSync as l8,writeFileSync as i8}from"node:fs";import{homedir as n8}from"node:os";import{basename as a8,dirname as M$,isAbsolute as O$,join as G,relative as s8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as MZ,dirname as OZ,join as T,resolve as CZ}from"node:path";function i(){return T(j(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function D(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:T(j(),".claude")}function P(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:T(j(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||T(j(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:[]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:[]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function j$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var T$=j$(s);var D$=T$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var F5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",L5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var u9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=m)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function F($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(m,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
1
+ import{createRequire as I$}from"node:module";var l=I$(import.meta.url);import{existsSync as B,lstatSync as d8,readFileSync as S,readlinkSync as l8,realpathSync as K$,rmSync as i8,statSync as n8,unlinkSync as a8,writeFileSync as s8}from"node:fs";import{homedir as r8}from"node:os";import{basename as o8,dirname as M$,isAbsolute as O$,join as G,relative as t8,resolve as h}from"node:path";import{homedir as D}from"node:os";import{delimiter as FZ,dirname as IZ,join as j,resolve as NZ}from"node:path";function i(){return j(D(),".tlc","harness")}function C($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:j(D(),".claude")}function A(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:j(D(),".cursor")}function b($=process.env){return $.TLC_BIN_DIR?.trim()||j(D(),".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 D$($){let Z=new Map;for(let J of $){let Q={line:J.line,block:J.block,middle:J.middle};for(let q of J.extensions)Z.set(q.toLowerCase(),Q)}return Z}var j$=D$(s);var T$=j$.size;var y=6;var x$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var D5=new Set([...x$,"nc","ncat","socat","telnet","lwp-request"]);var S$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",A5=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${S$}))(?=[:)\\s,'"\`]|$)`,"g");var R="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:R,lastSeenAt:R,lastAccessedAt:R,updatedAt:R}}var d9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var i$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function n$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((J)=>Number.parseInt(J,16)).join(";")}var q$=String.fromCharCode(27),a$=`${q$}[0m`;function s$($=process.env,Z=process.argv,J=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return J}var r$={ok:"success",warn:"warning",fail:"error",info:"info"},o$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},g=16;function E($=s$()){let Z=(Q,q)=>$?`${q$}[${Q}m${q}${a$}`:q,J=(Q,q)=>Z(`38;2;${n$(i$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${V.rule} ${Q} ${V.rule}`),footer:(Q)=>J("textDim",`${V.dash} ${Q} ${V.dash}`),kv:(Q,q,X=g)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(r$[Q],o$[Q])} ${q}`}}var O=E(!1);var t$=2;function e$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function $8($,Z){if($==="")return[""];let J=[],Q="";for(let q of $.split(/\s+/).filter((X)=>X.length>0)){if(Q===""){Q=q;continue}if(Q.length+1+q.length<=Z){Q=`${Q} ${q}`;continue}J.push(Q),Q=q}if(Q!=="")J.push(Q);return J.length===0?[""]:J}function I($,Z,J=e$()){let Q=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)Q.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let q=Math.max(g,...$.sections.flatMap((X)=>(X.rows??[]).map((z)=>z.label.length+1)));for(let X of $.sections){if(Q.push(""),X.title)Q.push(Z.paint("accent",X.title));for(let z of X.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;Q.push(Z.kv(z.label,W,q))}for(let z of X.lines??[]){if(z===""){Q.push("");continue}let W=X.wrap?$8(z,J-t$):[z];for(let U of W)Q.push(` ${U}`)}}if($.footer)Q.push("",Z.footer($.footer));return Q.join(`
2
2
  `)}var H={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var Q8=["No harness decision in this window.","Whatever you just saw was the model, not a rail — the harness allowed everything it was asked about."].join(`
3
- `);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as f8,mkdirSync as _q,readFileSync as y8,realpathSync as Lq,writeFileSync as v8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function m8($){return z$($)}var g8="tlc-exec.mjs";function u8($){return JSON.stringify($??null).includes(g8)}function g($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=m8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((_)=>!u8(_));if(U.length!==W.length)q=!0;if(U.length>0)Q[z]=U}let X={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")X[z]=W;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function W$($){if(!f8($))return{ok:!0,settingsText:"",changed:!1};let Z=g(y8($,"utf8"));if(Z.ok&&Z.changed)v8($,Z.settingsText,"utf8");return Z}function u($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let J;try{J=JSON.parse($)}catch{return{kind:"unparsed"}}if(J===null||typeof J!=="object"||Array.isArray(J))return{kind:"unparsed"};let Q=J,q=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},X={},z=0,W=0;for(let[U,_]of Object.entries(q)){let d=Array.isArray(_)?_:[],L=d.filter((I$)=>!JSON.stringify(I$??null).includes(Z));if(z+=d.length-L.length,L.length>0)X[U]=L,W+=L.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:X},null,2)}
4
- `}}import{join as Y$,relative as Eq,resolve as kq,sep as xq}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],gq=[Y$("tools","dev"),Y$("tools","__test__")];function r8($=process.env){let Z=n8(),J=b($);return{home:C($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(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 h8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(a8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=p8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function o8($,Z,J={relative:s8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return o8(x($),c(Z))}var t8="tlc-exec.mjs";function e8($){try{return d8($).size<4096&&S($,"utf8").includes(t8)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(e8(Z)){$.push({action:"remove",target:Z,detail:`${Q}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${Q} is a real file the installer did not write`});return}if(q==="target"&&!B$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!B$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function $Z($,Z){if(!B(Z))return;let J=g(S(Z,"utf8"));if(!J.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${J.error}`});return}if(!J.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function ZZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=u(J);switch(Q.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(Q.removed>0)$.push({action:"remove",target:Z,detail:`${Q.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${Q.removed} harness entries, keep the rest`})}}function QZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let X of V$){let z=G(Z,X);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=G(Z,k);if(B(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of G$){let z=G(Z,X);if(!B(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function JZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${N}`,detail:"the global package is reported, never removed for you"});$.push({action:"manual",target:"rm -rf .tlc/ in each repository",detail:"per-project config and state — this command does not search your disk for them"})}function qZ($,Z={}){let J=Z.purge===!0,Q=[];$Z(Q,$.claudeSettings),ZZ(Q,$.cursorHooks);for(let X of $.skillLinks)U$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)U$(Q,X,$.home,"the tlc launcher on PATH","target");let q=QZ(Q,$.home,J);return JZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function XZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=u(S(q.target,"utf8"));if(X.kind==="rewritten")i8(q.target,X.text,"utf8")}else if(q.action==="unlink")l8(q.target);else c8(q.target,{recursive:!0,force:!0});J.push(q)}catch(X){Q.push({item:q,reason:X instanceof Error?X.message:String(X)})}return{applied:J,failed:Q}}var zZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function WZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:H$($)}],footer:`already clean · reinstall with npm i -g ${N}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:zZ[W.action]})),X=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:H$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[Q?`${Z.applied.length} applied`:`${J.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:Q?`reinstall any time with npm i -g ${N}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function H$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function YZ($,Z,J=O){return F(WZ($,Z),J)}function VZ($=process.argv.slice(2)){let Z=r8(),J=qZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?XZ(J,Z):null;return console.log(YZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=VZ();export{r8 as uninstallTargets,WZ as uninstallScreen,YZ as uninstallReportText,qZ as planUninstall,C$ as pendingItems,VZ as main,o8 as isInsideRoot,c as canonicalise,XZ as applyUninstall};
3
+ `);var J8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},X$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:H.includePayloads,maxAttrChars:H.maxAttrChars,sessionCostAlertUsd:H.sessionCostAlertUsd,retentionDays:H.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},secrets:{redactOutput:!0},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...J8}},mcpPrime:[],bootstrapExtra:[]};import{dirname as b8,join as w8}from"node:path";import{fileURLToPath as f8}from"node:url";var Mq=w8(b8(f8(import.meta.url)),"..");var N="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as g8,mkdirSync as gq,readFileSync as u8,realpathSync as uq,writeFileSync as m8}from"node:fs";function z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function h8($){return z$($)}var p8="tlc-exec.mjs";function c8($){return JSON.stringify($??null).includes(p8)}function u($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!z$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let J=h8(Z.hooks)?Z.hooks:{},Q={},q=!1;for(let[z,W]of Object.entries(J)){let U=W.filter((_)=>!c8(_));if(U.length!==W.length)q=!0;if(U.length>0)Q[z]=U}let X={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")X[z]=W;else if(Object.keys(Q).length>0)X.hooks=Q;return{ok:!0,settingsText:JSON.stringify(X,null,2),changed:q}}function W$($){if(!g8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(u8($,"utf8"));if(Z.ok&&Z.changed)m8($,Z.settingsText,"utf8");return Z}function m($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let J;try{J=JSON.parse($)}catch{return{kind:"unparsed"}}if(J===null||typeof J!=="object"||Array.isArray(J))return{kind:"unparsed"};let Q=J,q=Q.hooks!==null&&typeof Q.hooks==="object"&&!Array.isArray(Q.hooks)?Q.hooks:{},X={},z=0,W=0;for(let[U,_]of Object.entries(q)){let d=Array.isArray(_)?_:[],L=d.filter((F$)=>!JSON.stringify(F$??null).includes(Z));if(z+=d.length-L.length,L.length>0)X[U]=L,W+=L.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...Q,hooks:X},null,2)}
4
+ `}}import{join as Y$,relative as iq,resolve as nq,sep as aq}from"node:path";var V$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],G$=["config.json","state","flags","rules"],QX=[Y$("tools","dev"),Y$("tools","__test__")];function e8($=process.env){let Z=r8(),J=b($);return{home:C($),binLinks:n().map((Q)=>G(J,Q)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(A(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(A(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return d8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),J=[];for(;;)try{return G(K$(Z),...J)}catch{let Q=M$(Z);if(Q===Z)return h($);J.unshift(o8(Z)),Z=Q}}function x($){try{return K$($)}catch{try{let Z=l8($);return c(O$(Z)?Z:h(M$($),Z))}catch{return c($)}}}function $Z($,Z,J={relative:t8,isAbsolute:O$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function B$($,Z){return $Z(x($),c(Z))}var ZZ="tlc-exec.mjs";function QZ($){try{return n8($).size<4096&&S($,"utf8").includes(ZZ)}catch{return!1}}function U$($,Z,J,Q,q){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(QZ(Z)){$.push({action:"remove",target:Z,detail:`${Q}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${Q} is a real file the installer did not write`});return}if(q==="target"&&!B$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let X=q==="location"&&!B$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${x(Z)}`:Q})}function JZ($,Z){if(!B(Z))return;let J=u(S(Z,"utf8"));if(!J.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${J.error}`});return}if(!J.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function qZ($,Z){let J=B(Z)?S(Z,"utf8"):null,Q=m(J);switch(Q.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(Q.removed>0)$.push({action:"remove",target:Z,detail:`${Q.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${Q.removed} harness entries, keep the rest`})}}function XZ($,Z,J){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let X of V$){let z=G(Z,X);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=G(Z,k);if(B(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of G$){let z=G(Z,X);if(!B(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function zZ($,Z){if(B(G(Z,k)))$.push({action:"manual",target:`npm uninstall -g ${N}`,detail:"the global package is reported, never removed for you"});$.push({action:"manual",target:"rm -rf .tlc/ in each repository",detail:"per-project config and state — this command does not search your disk for them"})}function WZ($,Z={}){let J=Z.purge===!0,Q=[];JZ(Q,$.claudeSettings),qZ(Q,$.cursorHooks);for(let X of $.skillLinks)U$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)U$(Q,X,$.home,"the tlc launcher on PATH","target");let q=XZ(Q,$.home,J);return zZ(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function C$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function YZ($,Z){let J=[],Q=[];for(let q of C$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)W$(q.target);else if(q.action==="unmerge"){let X=m(S(q.target,"utf8"));if(X.kind==="rewritten")s8(q.target,X.text,"utf8")}else if(q.action==="unlink")a8(q.target);else i8(q.target,{recursive:!0,force:!0});J.push(q)}catch(X){Q.push({item:q,reason:X instanceof Error?X.message:String(X)})}return{applied:J,failed:Q}}var VZ={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function GZ($,Z){let J=C$($),Q=Z!==null;if(J.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:H$($)}],footer:`already clean · reinstall with npm i -g ${N}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:VZ[W.action]})),X=$.items.filter((W)=>W.action==="keep"),z=[{title:Q?"REMOVED":"WOULD REMOVE",rows:q},...X.length>0?[{title:"KEPT",rows:X.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:H$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[Q?`${Z.applied.length} applied`:`${J.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:Q?`reinstall any time with npm i -g ${N}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function H$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function BZ($,Z,J=O){return I(GZ($,Z),J)}function UZ($=process.argv.slice(2)){let Z=e8(),J=WZ(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?YZ(J,Z):null;return console.log(BZ(J,Q,E())),Q!==null&&Q.failed.length>0?1:0}if(l.main==l.module)process.exitCode=UZ();export{e8 as uninstallTargets,GZ as uninstallScreen,BZ as uninstallReportText,WZ as planUninstall,C$ as pendingItems,UZ as main,$Z as isInsideRoot,c as canonicalise,YZ as applyUninstall};
@@ -122,15 +122,19 @@ loaded, and `degrade` guarantees the answer fits what the provider can actually
122
122
 
123
123
  ## Degradation, not detection
124
124
 
125
- When a core `Decision` (`allow | deny | ask | context | continue | rewriteInput | abstain`) cannot be
126
- expressed on a given provider, `src/providers/provider.degrade.ts` degrades it based on the capability
127
- descriptor:
125
+ When a core `Decision` (`allow | deny | ask | context | continue | rewriteInput | rewriteOutput | abstain`)
126
+ cannot be expressed on a given provider, `src/providers/provider.degrade.ts` degrades it based on the
127
+ capability descriptor:
128
128
 
129
129
  - Provider cannot enforce hooks at all → any enforcing decision becomes an `ADVISORY —` context message.
130
130
  - `ask` where `askSupportedOn` does not include the current event → becomes `deny` (a provider that cannot
131
131
  ask must not silently allow).
132
132
  - `rewriteInput` where `toolInputRewrite` is false → becomes `ask`, carrying the proposed input in the
133
133
  reason text.
134
+ - `rewriteOutput` where the current event is absent from `toolOutputRewriteOn` → becomes a `context` notice
135
+ naming the masking constraint, never the masked value itself — the model already saw the original output on
136
+ a host with no real rewrite for this event, so the notice states the limit instead of pretending it erased
137
+ anything.
134
138
  - `context` truncates to a caller-supplied character budget, dropping `env` if `sessionEnv` is false.
135
139
 
136
140
  This is what lets a hookless or partially-capable provider be a new adapter file rather than a core
@@ -216,6 +220,7 @@ and not described there fails the typecheck.
216
220
  | `machine-control` | `shutdown`, `reboot`, `halt`, `poweroff` | — |
217
221
  | `unprovable-execution` | a program fetched over the network and handed to a shell — piped, process-substituted, or inside a shell's `-c`/`eval` substitution. The gate cannot read what would run | a fetch with no shell downstream, and a shell fed a local file the gate can read |
218
222
  | `policy-surface-write` | every route an agent has to harness policy and state — a shell redirect, an interpreter, a heredoc program, or a write tool — in the project and under the runtime home, plus the mutating `tlc harness` subcommands from inside a session | reading them with a proven reader (`cat`, `head`, `grep`, `jq`, `ls`, `stat`, `test`), and `tlc harness handoff` for the handoff state |
223
+ | `wiring-tamper` | a shell redirect, in-place edit, or delete into a registered provider's wiring target — the document its own editor reads to register the harness's hooks — and a direct `Edit`/`Write`/`MultiEdit` tool call against the same path — overwriting it silences every hook the harness has for that host | reading the same path with a proven reader or a read tool |
219
224
 
220
225
  <!-- /generated -->
221
226
 
@@ -258,12 +263,20 @@ detects a condition that a config field could otherwise switch off:
258
263
  | Plan gate (declared scope vs diff) | Blocks the stop when the turn changed files the declared HARNESS_PLAN did not name, so scope creep fails like a failing test instead of surviving as a review comment. | `planGate.enabled` |
259
264
  | Observation mode (measure a rail with its rule off) | Runs a rail's checker while that rail is not enforcing, so the record says whether the property held with the rule injected or without it. That is the reading that tells you a rail is unnecessary rather than merely quiet, and it is what makes deleting one a decision instead of a guess. | `observe.enabled` |
260
265
  | Operator rules (your trigger, your proof) | Turns a standing instruction into a gate. A rule names when it applies, what the harness must have observed, and what to do when it has not — so 'no pull request without a review' stops depending on the model remembering it. | `rules.enabled` |
266
+ | Secret redaction (tool/shell output) | Masks an AWS access key, GitHub/Slack/Stripe token, PEM private-key block, JWT, or an unlabelled high-entropy string with a deterministic placeholder — the same value always yields the same placeholder within a session — instead of blocking the command that produced it. | `secrets.redactOutput` |
261
267
 
262
268
  <!-- /generated -->
263
269
 
264
270
  Operator posture (`mode`: `paired`, `solo`, `focus`) governs surfacing rather than capability, so it is not in
265
271
  the generated table.
266
272
 
273
+ ### Policy stays declarative
274
+
275
+ The floor and the rails above grow by adding a `FloorRule` member, a `Policy` field, or an operator-rules
276
+ trigger/proof pair — never by running operator- or third-party-supplied code inside the decision path. Custom
277
+ policy functions and remote code-as-policy packs were evaluated and rejected; see
278
+ [/decisions/ad-123.md](/decisions/ad-123.md) for the reasoning and the declarative alternative.
279
+
267
280
  ## See also
268
281
 
269
282
  - [/concepts.md](/concepts.md) — the same rails described from the operator's side
package/docs/concepts.md CHANGED
@@ -285,6 +285,17 @@ are not signal events.
285
285
  `shell.stallDetection`. When enabled, repeating the same shell command N times (`stallRepeatThreshold`) is denied with a
286
286
  change-approach follow-up. Trade-off: stops loops; can block intentional retries.
287
287
 
288
+ ## secret redaction
289
+
290
+ `secrets.redactOutput`, on by default. After a tool or shell command runs, its output is scanned for a
291
+ recognised secret signature (AWS access key, GitHub/Slack/Stripe token, PEM private-key block, JWT) or an
292
+ unlabelled high-entropy string, and any match is replaced with a deterministic placeholder
293
+ (`[REDACTED:<kind>:<hash>]`) before it reaches the model — the same value always yields the same placeholder
294
+ within a session. On a host or event where a real rewrite is unsupported, the model instead gets a context
295
+ notice that a secret was masked and must not be repeated; the raw value already reached it there, which is a
296
+ stated structural limit, not a silent gap. Trade-off: a synchronous regex-plus-entropy pass per output, with the
297
+ usual false-positive/false-negative risk of that detector shape.
298
+
288
299
  ## intelligence (rails)
289
300
 
290
301
  | Key | Effect |
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-09-06
17
+
18
+ - **AD-123** — Policy stays declarative; no custom executable policy code, ever ([/decisions/ad-123.md](/decisions/ad-123.md))
19
+
16
20
  ## 2026-09-01
17
21
 
18
22
  - **AD-122** — Handoff continuity is scoped per session, and a live neighbour is never a source ([/decisions/ad-122.md](/decisions/ad-122.md))
@@ -19,23 +19,39 @@ Source: `src/providers/claude/`.
19
19
 
20
20
  `claude.capabilities.ts`:
21
21
 
22
+ <!-- generated:capabilities -->
23
+
22
24
  | Capability | Value |
23
- | --- | --- |
25
+ |---|---|
24
26
  | `enforcesHooks` | `true` |
25
- | `askSupportedOn` | `["tool.before", "shell.before", "mcp.before", "read.before"]` — includes `tool.before`, unlike Cursor (see [/decisions/ad-009.md](/decisions/ad-009.md), note) |
27
+ | `askSupportedOn` | `["tool.before","shell.before","mcp.before","read.before"]` |
26
28
  | `sessionEnv` | `false` |
27
- | `nativeLoopCounter` | `false` — `Stop` carries no loop counter; `core/turn` supplies it instead (see [/decisions/ad-014.md](/decisions/ad-014.md)) |
28
- | `dedicatedShellEvent` | `false` — shell is `PreToolUse`/`PostToolUse` with `tool_name: "Bash"` |
29
+ | `nativeLoopCounter` | `false` |
30
+ | `dedicatedShellEvent` | `false` |
29
31
  | `toolInputRewrite` | `true` |
30
- | `toolOutputRewrite` | `true` |
32
+ | `toolOutputRewriteOn` | `["tool.after","tool.failure"]` |
31
33
  | `contextAtToolBefore` | `true` |
32
34
  | `contextAtToolAfter` | `true` |
33
- | `contextAtStop` | `true` — `Stop` accepts `hookSpecificOutput.additionalContext` for feedback that continues the turn |
34
- | `sessionStartContextReliable` | `true` — `SessionStart` delivers `hookSpecificOutput.additionalContext`, capped at 10,000 characters |
35
- | `usageInPayload` | `false` — cost comes from the transcript, not the hook payload |
36
- | `effortSignal` | `true` — `effort.level` (`low\|medium\|high\|xhigh\|max`) |
35
+ | `contextAtStop` | `true` |
36
+ | `sessionStartContextReliable` | `true` |
37
+ | `toolOutputAtAfter` | `true` |
38
+ | `usageInPayload` | `false` |
39
+ | `effortSignal` | `true` |
37
40
  | `thoughtEvent` | `false` |
38
41
 
42
+ <!-- /generated -->
43
+
44
+ `askSupportedOn` includes `tool.before`, unlike Cursor (see [/decisions/ad-009.md](/decisions/ad-009.md), note).
45
+ `nativeLoopCounter` is `false` because `Stop` carries no loop counter — `core/turn` supplies it instead (see
46
+ [/decisions/ad-014.md](/decisions/ad-014.md)). `dedicatedShellEvent` is `false` because shell is
47
+ `PreToolUse`/`PostToolUse` with `tool_name: "Bash"`. `toolOutputRewriteOn` covers `tool.after`/`tool.failure`
48
+ because `updatedToolOutput` is documented on every `PostToolUse`/`PostToolUseFailure` event this adapter fires.
49
+ `contextAtStop` is `true` because `Stop` accepts `hookSpecificOutput.additionalContext` for feedback that
50
+ continues the turn. `sessionStartContextReliable` is `true` because `SessionStart` delivers
51
+ `hookSpecificOutput.additionalContext`, capped at 10,000 characters. `usageInPayload` is `false` — cost comes
52
+ from the transcript, not the hook payload. `effortSignal` is `true` — `effort.level`
53
+ (`low|medium|high|xhigh|max`).
54
+
39
55
  ## Policy defaults
40
56
 
41
57
  `claude.policy-defaults.ts` supplies no model allowlist and no blocked patterns — see
@@ -51,27 +67,32 @@ enforces nothing and `doctor` says so ([/decisions/ad-053.md](/decisions/ad-053.
51
67
  `claude.inbound.ts` maps Claude's PascalCase hook names to `HarnessEventKind`. Unlike Cursor, `PreToolUse`
52
68
  and `PostToolUse` are single dispatcher hooks that fan out by `tool_name`:
53
69
 
54
- | Claude hook | Fan-out rule | `HarnessEventKind` |
55
- | --- | --- | --- |
70
+ <!-- generated:event-mapping -->
71
+
72
+ | Hook | Fan-out rule | HarnessEventKind |
73
+ |---|---|---|
56
74
  | `SessionStart` | — | `session.start` |
57
75
  | `SessionEnd` | — | `session.end` |
58
76
  | `UserPromptSubmit` | — | `prompt.submit` |
59
- | `PreToolUse` | `tool_name === "Bash"` | `shell.before` |
60
- | `PreToolUse` | `tool_name` matches `mcp__*` | `mcp.before` |
61
- | `PreToolUse` | `tool_name === "Read"` | `read.before` |
62
- | `PreToolUse` | otherwise | `tool.before` |
63
- | `PostToolUse` | `tool_name === "Bash"` | `shell.after` |
64
- | `PostToolUse` | `tool_name` matches `mcp__*` | `mcp.after` |
65
- | `PostToolUse` | `tool_name` is `Edit`/`Write` | `edit.after` |
66
- | `PostToolUse` | otherwise | `tool.after` |
67
77
  | `PostToolUseFailure` | — | `tool.failure` |
68
78
  | `SubagentStart` | — | `subagent.start` |
69
79
  | `SubagentStop` | — | `subagent.stop` |
70
80
  | `Stop` | — | `stop` |
71
81
  | `PreCompact` | — | `compact.before` |
72
82
  | `MessageDisplay` | — | `response.after` |
73
-
74
- Claude has no `thought.after` equivalent (`thoughtEvent: false`).
83
+ | `PreToolUse` | tool_name === "Bash" | `shell.before` |
84
+ | `PreToolUse` | tool_name matches `^mcp__` | `mcp.before` |
85
+ | `PreToolUse` | tool_name === "Read" | `read.before` |
86
+ | `PostToolUse` | tool_name === "Bash" | `shell.after` |
87
+ | `PostToolUse` | tool_name matches `^mcp__` | `mcp.after` |
88
+ | `PostToolUse` | tool_name === "Edit" | `edit.after` |
89
+ | `PostToolUse` | tool_name === "Write" | `edit.after` |
90
+
91
+ <!-- /generated -->
92
+
93
+ Neither fan-out table carries the "otherwise" fallback (`tool.before`/`tool.after`) — that default lives in
94
+ the adapter's own dispatch code, not in an exported table. Claude has no `thought.after` equivalent
95
+ (`thoughtEvent: false`).
75
96
 
76
97
  ## Field paths
77
98
 
@@ -19,23 +19,35 @@ Source: `src/providers/cursor/`.
19
19
 
20
20
  `cursor.capabilities.ts`:
21
21
 
22
+ <!-- generated:capabilities -->
23
+
22
24
  | Capability | Value |
23
- | --- | --- |
25
+ |---|---|
24
26
  | `enforcesHooks` | `true` |
25
- | `askSupportedOn` | `["shell.before", "mcp.before"]` — **not** `tool.before` (see [/decisions/ad-009.md](/decisions/ad-009.md), note) |
27
+ | `askSupportedOn` | `["shell.before","mcp.before"]` |
26
28
  | `sessionEnv` | `true` |
27
29
  | `nativeLoopCounter` | `true` |
28
30
  | `dedicatedShellEvent` | `true` |
29
31
  | `toolInputRewrite` | `true` |
30
- | `toolOutputRewrite` | `true` |
32
+ | `toolOutputRewriteOn` | `["mcp.after"]` |
31
33
  | `contextAtToolBefore` | `false` |
32
34
  | `contextAtToolAfter` | `true` |
33
- | `contextAtStop` | `false` — the `stop` output schema carries `followup_message` and nothing else |
34
- | `sessionStartContextReliable` | `false` — Cursor accepts `additional_context` at `sessionStart`, logs it as merged, and drops it (see [Lessons view](#lessons-view)) |
35
+ | `contextAtStop` | `false` |
36
+ | `sessionStartContextReliable` | `false` |
37
+ | `toolOutputAtAfter` | `true` |
35
38
  | `usageInPayload` | `true` |
36
39
  | `effortSignal` | `false` |
37
40
  | `thoughtEvent` | `true` |
38
41
 
42
+ <!-- /generated -->
43
+
44
+ `askSupportedOn` is **not** `tool.before` (see [/decisions/ad-009.md](/decisions/ad-009.md), note).
45
+ `toolOutputRewriteOn` is **not** `shell.after`/`edit.after`: `afterShellExecution`/`afterFileEdit` are
46
+ documented as observation-only, with no `updated_mcp_tool_output` equivalent. `contextAtStop` is `false`
47
+ because the `stop` output schema carries `followup_message` and nothing else. `sessionStartContextReliable`
48
+ is `false` because Cursor accepts `additional_context` at `sessionStart`, logs it as merged, and drops it
49
+ (see [Lessons view](#lessons-view)).
50
+
39
51
  ## Policy defaults
40
52
 
41
53
  `cursor.policy-defaults.ts` supplies the blocked-pattern list (`-fast(?:$|[^a-z0-9])`, `/fast(?:$|[^a-z0-9])`,
@@ -51,8 +63,10 @@ project's rather than replacing them.
51
63
 
52
64
  `cursor.inbound.ts` maps Cursor's own camelCase hook names to the shared `HarnessEventKind`:
53
65
 
54
- | Cursor hook | `HarnessEventKind` |
55
- | --- | --- |
66
+ <!-- generated:event-mapping -->
67
+
68
+ | Hook | HarnessEventKind |
69
+ |---|---|
56
70
  | `sessionStart` | `session.start` |
57
71
  | `sessionEnd` | `session.end` |
58
72
  | `beforeSubmitPrompt` | `prompt.submit` |
@@ -72,6 +86,8 @@ project's rather than replacing them.
72
86
  | `afterAgentResponse` | `response.after` |
73
87
  | `afterAgentThought` | `thought.after` |
74
88
 
89
+ <!-- /generated -->
90
+
75
91
  Cursor has a dedicated event per tool class (`beforeShellExecution`, `beforeMCPExecution`,
76
92
  `beforeReadFile`), unlike Claude's single `PreToolUse`/`PostToolUse` fan-out.
77
93
 
@@ -57,6 +57,7 @@ Every refusal names one. The name is the pointer — the reasoning lives in the
57
57
  | `history-rewrite` | `git push --force`. `--force-with-lease` is allowed |
58
58
  | `machine-control` | `shutdown`, `reboot`, `halt`, `poweroff` |
59
59
  | `policy-surface-write` | any route an agent has to harness policy or state |
60
+ | `wiring-tamper` | a shell redirect, in-place edit, or delete into a registered provider's wiring target, or a direct `Edit`/`Write`/`MultiEdit` call against it |
60
61
  | `policy-baseline-divergence` | a policy file changed mid-session with no `tlc harness` command behind it |
61
62
  | `untrusted-command` | a shell command that appears verbatim in content this session fetched — put to you before it runs |
62
63
  | `edit-collision` | another live session touched this file recently |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
package/schema.json CHANGED
@@ -106,6 +106,9 @@
106
106
  "shell": {
107
107
  "$ref": "#/definitions/Partial%3C%7BcatastrophicAsk%3Aboolean%3BstallDetection%3Aboolean%3BstallRepeatThreshold%3Anumber%3B%7D%3E"
108
108
  },
109
+ "secrets": {
110
+ "$ref": "#/definitions/Partial%3C%7BredactOutput%3Aboolean%3B%7D%3E"
111
+ },
109
112
  "intelligence": {
110
113
  "additionalProperties": false,
111
114
  "type": "object",
@@ -498,6 +501,15 @@
498
501
  },
499
502
  "additionalProperties": false
500
503
  },
504
+ "Partial<{redactOutput:boolean;}>": {
505
+ "type": "object",
506
+ "properties": {
507
+ "redactOutput": {
508
+ "type": "boolean"
509
+ }
510
+ },
511
+ "additionalProperties": false
512
+ },
501
513
  "Partial<import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).LessonsPolicyConfig>": {
502
514
  "type": "object",
503
515
  "properties": {
@@ -43,6 +43,7 @@ assume Cursor — check what Step 1 detected).
43
43
  | 22 | Plan gate (declared scope vs diff) | `planGate.enabled` | off | Blocks the stop when the turn changed files the declared HARNESS_PLAN did not name, so scope creep fails like a failing test instead of surviving as a review comment. | Requires the agent to declare HARNESS_PLAN before editing, and each honest deviation to state a reason. A turn with no declaration is not gated at all. | `windowMinutes` |
44
44
  | 23 | Observation mode (measure a rail with its rule off) | `observe.enabled` | off | Runs a rail's checker while that rail is not enforcing, so the record says whether the property held with the rule injected or without it. That is the reading that tells you a rail is unnecessary rather than merely quiet, and it is what makes deleting one a decision instead of a guess. | Costs one diff scan per turn per observed rail, and answers a question only an operator who is asking it needs answered. It never blocks and never changes a decision, so it buys information and nothing else. | `rails — the rails to observe, chosen from the observable set (today: comments). A name with no checker records nothing and doctor reports it` |
45
45
  | 24 | Operator rules (your trigger, your proof) | `rules.enabled` | off | Turns a standing instruction into a gate. A rule names when it applies, what the harness must have observed, and what to do when it has not — so 'no pull request without a review' stops depending on the model remembering it. | The proof must be something the harness observed: a subagent that ran, a command that completed, a gate that passed, a file that changed. It cannot judge whether the review was good, and a pattern trigger is policy rather than containment — a script written to disk and run later, or a pull request opened in a browser, escape it. | — |
46
+ | 25 | Secret redaction (tool/shell output) | `secrets.redactOutput` | **on** | Masks an AWS access key, GitHub/Slack/Stripe token, PEM private-key block, JWT, or an unlabelled high-entropy string with a deterministic placeholder — the same value always yields the same placeholder within a session — instead of blocking the command that produced it. | A synchronous regex plus Shannon-entropy pass on every tool/shell output; a high-diversity non-secret string can be masked as a false positive, and an unlabelled low-entropy secret can be missed. On a host with no rewrite mechanism for the firing event, the mask degrades to a context warning instead — the model is told a secret was masked but the raw value already reached it. | recommend **on** |
46
47
 
47
48
  <!-- /generated -->
48
49
 
@@ -7,7 +7,14 @@ export type ProviderCapabilities = {
7
7
  nativeLoopCounter: boolean;
8
8
  dedicatedShellEvent: boolean;
9
9
  toolInputRewrite: boolean;
10
- toolOutputRewrite: boolean;
10
+ /**
11
+ * The events on which a rewritten tool output actually reaches the model. Measured as per-event rather than
12
+ * per-host: one host documents its rewrite field for every after-event it fires, while another scopes its
13
+ * equivalent to a single after-event and leaves the rest observation-only
14
+ * ([/decisions/ad-009.md](/decisions/ad-009.md)'s `askSupportedOn` correction, applied to the same false
15
+ * per-host boolean here).
16
+ */
17
+ toolOutputRewriteOn: HarnessEventKind[];
11
18
  contextAtToolBefore: boolean;
12
19
  contextAtToolAfter: boolean;
13
20
  contextAtStop: boolean;
@@ -11,7 +11,8 @@ export type Decision =
11
11
  | { kind: "ask"; reason: string; userNote?: string; rule: string }
12
12
  | { kind: "context"; text: string; env?: Record<string, string> }
13
13
  | { kind: "continue"; text: string }
14
- | { kind: "rewriteInput"; input: Record<string, unknown>; reason: string };
14
+ | { kind: "rewriteInput"; input: Record<string, unknown>; reason: string }
15
+ | { kind: "rewriteOutput"; output: string };
15
16
 
16
17
  export type Rendered = {
17
18
  // invariant: null means write nothing; a provider whose abstain is a literal "{}" sets it explicitly.
@@ -179,6 +179,8 @@ import {
179
179
  recordObservation,
180
180
  } from "./rules/rules.store.ts";
181
181
  import { firingRules, triggerMatches } from "./rules/rules.trigger.ts";
182
+ import { scanForSecrets } from "./secret-scan/secret-scan.service.ts";
183
+ import { placeholderFor } from "./secret-scan/secret-scan.store.ts";
182
184
  import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
183
185
  import { clearShellStall } from "./shell-policy/shell-policy.stall.ts";
184
186
  import { coversHandler, decideShim } from "./shim/shim.precedence.ts";
@@ -519,6 +521,10 @@ export const coreFacade = {
519
521
  floor: {
520
522
  evaluateFloor,
521
523
  },
524
+ secretScan: {
525
+ scanForSecrets,
526
+ placeholderFor,
527
+ },
522
528
  observe: {
523
529
  shouldObserve,
524
530
  observeAttrs,
@@ -49,6 +49,11 @@ export const FLOOR_RULES: Record<FloorRule, FloorRuleDoc> = {
49
49
  allows:
50
50
  "reading them with a proven reader (`cat`, `head`, `grep`, `jq`, `ls`, `stat`, `test`), and `tlc harness handoff` for the handoff state",
51
51
  },
52
+ "wiring-tamper": {
53
+ denies:
54
+ "a shell redirect, in-place edit, or delete into a registered provider's wiring target — the document its own editor reads to register the harness's hooks — and a direct `Edit`/`Write`/`MultiEdit` tool call against the same path — overwriting it silences every hook the harness has for that host",
55
+ allows: "reading the same path with a proven reader or a read tool",
56
+ },
52
57
  };
53
58
 
54
59
  export const FLOOR_RULE_IDS = Object.keys(FLOOR_RULES) as FloorRule[];
@@ -40,13 +40,29 @@ export function isRuntimePolicySurface(filePath: string): boolean {
40
40
  return target === resolve(machineConfigPath()) || isInside(runtimeStateDir(), target);
41
41
  }
42
42
 
43
+ // why: a provider's wiring target (its own editor's user-level hook/settings document) lives outside the
44
+ // project and outside the runtime home, so neither existing branch of isPolicySurface would ever match it.
45
+ // A protected path is matched by containment, not equality, so a provider that ever declares a directory
46
+ // target covers every file written under it.
47
+ export function isProtectedWiringTarget(target: string, protectedPaths: readonly string[]): boolean {
48
+ const resolved = resolve(target);
49
+ return protectedPaths.some((path) => isInside(resolve(path), resolved));
50
+ }
51
+
43
52
  // invariant: the policy surface is defined here, next to the floor's other path predicates, because the
44
53
  // floor decides before any policy is read. Defining it inside the policy module would point the dependency
45
54
  // backwards — against the order the two actually run in.
46
- export function isPolicySurface(projectDir: string, filePath: string): boolean {
55
+ export function isPolicySurface(
56
+ projectDir: string,
57
+ filePath: string,
58
+ extraSurfacePaths: readonly string[] = [],
59
+ ): boolean {
47
60
  if (isRuntimePolicySurface(filePath)) {
48
61
  return true;
49
62
  }
63
+ if (isProtectedWiringTarget(filePath, extraSurfacePaths)) {
64
+ return true;
65
+ }
50
66
  const target = normalizeSeparators(relative(projectDir, filePath) || filePath);
51
67
  const config = normalizeSeparators(relative(projectDir, projectConfigPath(projectDir)));
52
68
  const flags = normalizeSeparators(relative(projectDir, flagsDir(projectDir)));
@@ -141,11 +141,15 @@ function namesSurface(projectDir: string, segment: ShellSegment): boolean {
141
141
  // direction is what catches `rm -rf .tlc/harness/state`, which removes the flags without ever naming one.
142
142
  // hazard: the project root also contains the surface. Counting it would deny `find .` and `grep -r x .`,
143
143
  // so the root is excluded and destruction of the whole project stays the concern of the existing rules.
144
- function overlapsSurface(projectDir: string, resolved: string): boolean {
144
+ function overlapsSurface(
145
+ projectDir: string,
146
+ resolved: string,
147
+ extraSurfacePaths: readonly string[],
148
+ ): boolean {
145
149
  if (resolved === resolve(projectDir)) {
146
150
  return false;
147
151
  }
148
- if (isPolicySurface(projectDir, resolved)) {
152
+ if (isPolicySurface(projectDir, resolved, extraSurfacePaths)) {
149
153
  return true;
150
154
  }
151
155
  return [projectConfigPath(projectDir), projectStateDir(projectDir)].some(
@@ -153,7 +157,11 @@ function overlapsSurface(projectDir: string, resolved: string): boolean {
153
157
  );
154
158
  }
155
159
 
156
- function referencesSurface(projectDir: string, word: ShellWord): boolean {
160
+ function referencesSurface(
161
+ projectDir: string,
162
+ word: ShellWord,
163
+ extraSurfacePaths: readonly string[],
164
+ ): boolean {
157
165
  if (word.text === "") {
158
166
  return false;
159
167
  }
@@ -161,9 +169,13 @@ function referencesSurface(projectDir: string, word: ShellWord): boolean {
161
169
  // literal portion catches `> .tlc/harness/$f`; a fully computed path stays out of reach and is what the
162
170
  // per-session integrity baseline exists to catch.
163
171
  if (word.unresolved) {
164
- return normalizeSeparators(word.text).includes(harnessPrefix(projectDir));
172
+ const normalized = normalizeSeparators(word.text);
173
+ if (normalized.includes(harnessPrefix(projectDir))) {
174
+ return true;
175
+ }
176
+ return extraSurfacePaths.some((path) => normalized.includes(normalizeSeparators(path)));
165
177
  }
166
- return overlapsSurface(projectDir, resolveTarget(projectDir, word.text));
178
+ return overlapsSurface(projectDir, resolveTarget(projectDir, word.text), extraSurfacePaths);
167
179
  }
168
180
 
169
181
  // why: a redirect target is not an argument of the head verb, so argument scanning alone would allow
@@ -206,9 +218,13 @@ function harnessSubcommand(args: ShellWord[]): string | null {
206
218
  return (operands[1]?.text ?? "status").toLowerCase();
207
219
  }
208
220
 
209
- function checkSegment(projectDir: string, segment: ShellSegment): PolicySurfaceVerdict {
221
+ function checkSegment(
222
+ projectDir: string,
223
+ segment: ShellSegment,
224
+ extraSurfacePaths: readonly string[],
225
+ ): PolicySurfaceVerdict {
210
226
  for (const target of redirectTargets(segment.words)) {
211
- if (referencesSurface(projectDir, target)) {
227
+ if (referencesSurface(projectDir, target, extraSurfacePaths)) {
212
228
  return deny(
213
229
  "a redirect in this command writes into the harness policy surface.",
214
230
  "redirect into the policy surface",
@@ -228,7 +244,7 @@ function checkSegment(projectDir: string, segment: ShellSegment): PolicySurfaceV
228
244
  }
229
245
  }
230
246
 
231
- const references = segment.words.filter((word) => referencesSurface(projectDir, word));
247
+ const references = segment.words.filter((word) => referencesSurface(projectDir, word, extraSurfacePaths));
232
248
  if (references.length === 0 && !namesSurface(projectDir, segment)) {
233
249
  return ALLOW;
234
250
  }
@@ -305,9 +321,10 @@ export function checkPolicySurface(
305
321
  projectDir: string,
306
322
  command: string,
307
323
  segments: ShellSegment[],
324
+ extraSurfacePaths: readonly string[] = [],
308
325
  ): PolicySurfaceVerdict {
309
326
  for (const segment of segments) {
310
- const verdict = checkSegment(projectDir, segment);
327
+ const verdict = checkSegment(projectDir, segment, extraSurfacePaths);
311
328
  if (verdict.kind === "deny") {
312
329
  return verdict;
313
330
  }