@tech-leads-club/harness-toolkit 0.3.6 → 0.4.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 (73) hide show
  1. package/README.md +44 -8
  2. package/bin/tlc-cli.ts +40 -1
  3. package/bin/tlc-exec.d.mts +1 -0
  4. package/bin/tlc-exec.mjs +47 -2
  5. package/capabilities/catalog.json +47 -30
  6. package/dist/compact-before.mjs +84 -78
  7. package/dist/doctor.mjs +87 -81
  8. package/dist/help-topic.mjs +6 -5
  9. package/dist/init-project.mjs +147 -9
  10. package/dist/install-runtime.mjs +85 -79
  11. package/dist/lessons-cli.mjs +87 -81
  12. package/dist/obs-cli.mjs +83 -77
  13. package/dist/price-lookup.mjs +2 -2
  14. package/dist/prompt-submit.mjs +84 -78
  15. package/dist/refresh-model-prices.mjs +85 -79
  16. package/dist/response-after.mjs +84 -78
  17. package/dist/run.mjs +84 -78
  18. package/dist/session-end.mjs +90 -84
  19. package/dist/session-start.mjs +92 -86
  20. package/dist/shim.mjs +82 -76
  21. package/dist/stop.mjs +90 -84
  22. package/dist/subagent-start.mjs +84 -78
  23. package/dist/subagent-stop.mjs +85 -79
  24. package/dist/support.mjs +88 -82
  25. package/dist/tlc-cli.mjs +104 -98
  26. package/dist/tool-after.mjs +84 -78
  27. package/dist/tool-before.mjs +84 -78
  28. package/dist/tool-failure.mjs +84 -78
  29. package/dist/uninstall-runtime.mjs +4 -4
  30. package/docs/architecture.md +1 -0
  31. package/docs/concepts.md +86 -0
  32. package/docs/diagnose.md +20 -0
  33. package/docs/init.md +10 -2
  34. package/docs/lessons.md +12 -0
  35. package/docs/log.md +5 -0
  36. package/package.json +1 -1
  37. package/skills/harness-init/references/capabilities.md +54 -0
  38. package/src/core/core.facade.ts +54 -0
  39. package/src/core/floor/floor.paths.ts +2 -2
  40. package/src/core/floor/floor.policy-surface.ts +6 -1
  41. package/src/core/lesson/lesson.select.ts +30 -7
  42. package/src/core/policy/policy.defaults.ts +3 -0
  43. package/src/core/policy/policy.integrity.ts +2 -2
  44. package/src/core/policy/policy.loader.ts +14 -3
  45. package/src/core/policy/policy.shadow.ts +97 -0
  46. package/src/core/policy/policy.types.ts +8 -0
  47. package/src/core/release/release.decisions.ts +3 -13
  48. package/src/core/rules/rules.decide.ts +123 -0
  49. package/src/core/rules/rules.observe.ts +76 -0
  50. package/src/core/rules/rules.parse.ts +142 -0
  51. package/src/core/rules/rules.proof.ts +130 -0
  52. package/src/core/rules/rules.service.ts +141 -0
  53. package/src/core/rules/rules.store.ts +77 -0
  54. package/src/core/rules/rules.trigger.ts +101 -0
  55. package/src/core/rules/rules.types.ts +64 -0
  56. package/src/entrypoints/shim.ts +9 -1
  57. package/src/entrypoints/stop.ts +75 -1
  58. package/src/entrypoints/subagent-stop.ts +9 -1
  59. package/src/entrypoints/support.ts +32 -0
  60. package/src/entrypoints/tool-after.ts +7 -2
  61. package/src/entrypoints/tool-before.ts +44 -3
  62. package/src/platform/frontmatter.ts +142 -0
  63. package/src/platform/links.ts +32 -0
  64. package/src/platform/paths.ts +58 -4
  65. package/src/platform/pricing.ts +3 -3
  66. package/src/platform/screen.ts +62 -3
  67. package/tools/doctor.ts +162 -2
  68. package/tools/help-topic.ts +39 -23
  69. package/tools/init-project.ts +51 -6
  70. package/tools/install-runtime.ts +23 -2
  71. package/tools/lessons-cli.ts +4 -1
  72. package/tools/refresh-model-prices.ts +2 -2
  73. package/tools/uninstall-runtime.ts +11 -3
@@ -1,4 +1,4 @@
1
- import{createRequire as B$}from"node:module";var h=B$(import.meta.url);import{existsSync as K,lstatSync as C8,readFileSync as x,readlinkSync as I8,realpathSync as z$,rmSync as _8,statSync as N8,unlinkSync as j8,writeFileSync as L8}from"node:fs";import{homedir as T8}from"node:os";import{basename as D8,dirname as W$,isAbsolute as Y$,join as V,relative as P8,resolve as v}from"node:path";import{homedir as k}from"node:os";import{join as f}from"node:path";function p(){return f(k(),".tlc","harness")}function B($=process.env){return $.TLC_HOME??p()}function j(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:f(k(),".claude")}function L(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:f(k(),".cursor")}var i=[{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 M$($){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 O$=M$(i);var F$=O$.size;var S=6;var L$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var n2=new Set([...L$,"nc","ncat","socat","telnet","lwp-request"]);var T$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",s2=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${T$}))(?=[:)\\s,'"\`]|$)`,"g");var D="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:D,lastSeenAt:D,lastAccessedAt:D,updatedAt:D}}var vZ=[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 g$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},G={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function h$($){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 o=String.fromCharCode(27),p$=`${o}[0m`;function c$($=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 d$={ok:"success",warn:"warning",fail:"error",info:"info"},l$={ok:G.check,warn:G.warning,fail:G.cross,info:G.arrowRight},b=16;function P($=c$()){let Z=(Q,q)=>$?`${o}[${Q}m${q}${p$}`:q,J=(Q,q)=>Z(`38;2;${h$(g$[Q])}`,q);return{enabled:$,paint:J,bold:(Q)=>Z("1",Q),dim:(Q)=>J("textDim",Q),heading:(Q)=>J("accent",`${G.rule} ${Q} ${G.rule}`),footer:(Q)=>J("textDim",`${G.dash} ${Q} ${G.dash}`),kv:(Q,q,X=b)=>` ${J("textMuted",`${Q}:`.padEnd(X))} ${q}`,status:(Q,q)=>`${J(d$[Q],l$[Q])} ${q}`}}var O=P(!1);function C($,Z){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.push(` ${$.summary.join(Z.dim(` ${G.bar} `))}`);let Q=Math.max(b,...$.sections.flatMap((q)=>(q.rows??[]).map((X)=>X.label.length+1)));for(let q of $.sections){if(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let X of q.rows??[]){let z=X.level?Z.status(X.level,X.value):X.value;J.push(Z.kv(X.label,z,Q))}for(let X of q.lines??[])J.push(X===""?"":` ${X}`)}if($.footer)J.push("",Z.footer($.footer));return J.join(`
2
- `)}var H={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var i$=["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 n$={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},a$={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:S},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},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:{...n$}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",A="installed-from-npm";import{existsSync as K8,mkdirSync as Y3,readFileSync as H8,realpathSync as V3,writeFileSync as U8}from"node:fs";function t($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function M8($){return t($)}var O8="tlc-exec.mjs";function F8($){return JSON.stringify($??null).includes(O8)}function w($){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(!t(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 F=W.filter((_)=>!F8(_));if(F.length!==W.length)q=!0;if(F.length>0)Q[z]=F}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 e($){if(!K8($))return{ok:!0,settingsText:"",changed:!1};let Z=w(H8($,"utf8"));if(Z.ok&&Z.changed)U8($,Z.settingsText,"utf8");return Z}function y($,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[F,_]of Object.entries(q)){let g=Array.isArray(_)?_:[],N=g.filter((G$)=>!JSON.stringify(G$??null).includes(Z));if(z+=g.length-N.length,N.length>0)X[F]=N,W+=N.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 $$,relative as O3,resolve as F3,sep as C3}from"node:path";var Z$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],Q$=["config.json","state","flags"],P3=[$$("tools","dev"),$$("tools","__test__")];function R8($=process.env){let Z=T8(),J=$.TLC_BIN_DIR?.trim()||V(Z,".local","bin");return{home:B($),binLinks:[V(J,"tlc"),V(J,"tlc.cmd")],claudeSettings:V(j(),"settings.json"),cursorHooks:V(L(),"hooks.json"),skillLinks:[V(j(),"skills","harness-init"),V(L(),"skills","harness-init"),V(Z,".tlc","skills","harness-init")]}}function u($){try{return C8($).isSymbolicLink()}catch{return!1}}function m($){let Z=v($),J=[];for(;;)try{return V(z$(Z),...J)}catch{let Q=W$(Z);if(Q===Z)return v($);J.unshift(D8(Z)),Z=Q}}function E($){try{return z$($)}catch{try{let Z=I8($);return m(Y$(Z)?Z:v(W$($),Z))}catch{return m($)}}}function A8($,Z,J={relative:P8,isAbsolute:Y$}){if($===Z)return!0;let Q=J.relative(Z,$);return Q!==""&&!Q.startsWith("..")&&!J.isAbsolute(Q)}function J$($,Z){return A8(E($),m(Z))}var E8="tlc-exec.mjs";function x8($){try{return N8($).size<4096&&x($,"utf8").includes(E8)}catch{return!1}}function q$($,Z,J,Q,q){if(!K(Z)&&!u(Z))return;if(!u(Z)){if(x8(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"&&!J$(Z,J)){$.push({action:"keep",target:Z,detail:`points at ${E(Z)} — not ours`});return}let X=q==="location"&&!J$(Z,J);$.push({action:"unlink",target:Z,detail:X?`${Q}, stale — points at ${E(Z)}`:Q})}function k8($,Z){if(!K(Z))return;let J=w(x(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 f8($,Z){let J=K(Z)?x(Z,"utf8"):null,Q=y(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 S8($,Z,J){if(u(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${E(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!K(Z))return!1;for(let X of Z$){let z=V(Z,X);if(K(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let q=V(Z,A);if(K(q))$.push({action:"remove",target:q,detail:"install marker"});for(let X of Q$){let z=V(Z,X);if(!K(z))continue;$.push(J?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function b8($,Z){if(K(V(Z,A)))$.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 w8($,Z={}){let J=Z.purge===!0,Q=[];k8(Q,$.claudeSettings),f8(Q,$.cursorHooks);for(let X of $.skillLinks)q$(Q,X,$.home,"skill link","location");for(let X of $.binLinks)q$(Q,X,$.home,"the tlc launcher on PATH","target");let q=S8(Q,$.home,J);return b8(Q,$.home),{items:Q,purge:J,homeIsLink:q}}function V$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function y8($,Z){let J=[],Q=[];for(let q of V$($))try{if(q.action==="unmerge"&&q.target===Z.claudeSettings)e(q.target);else if(q.action==="unmerge"){let X=y(x(q.target,"utf8"));if(X.kind==="rewritten")L8(q.target,X.text,"utf8")}else if(q.action==="unlink")j8(q.target);else _8(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 v8={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function u8($,Z){let J=V$($),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:X$($)}],footer:`already clean · reinstall with npm i -g ${I}, then \`tlc harness install\``};let q=J.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:v8[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:X$($)},...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 ${I}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function X$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function m8($,Z,J=O){return C(u8($,Z),J)}function g8($=process.argv.slice(2)){let Z=R8(),J=w8(Z,{purge:$.includes("--purge")}),Q=$.includes("--yes")?y8(J,Z):null;return console.log(m8(J,Q,P())),Q!==null&&Q.failed.length>0?1:0}if(h.main==h.module)process.exitCode=g8();export{R8 as uninstallTargets,u8 as uninstallScreen,m8 as uninstallReportText,w8 as planUninstall,V$ as pendingItems,g8 as main,A8 as isInsideRoot,m as canonicalise,y8 as applyUninstall};
1
+ import{createRequire as U$}from"node:module";var l=U$(import.meta.url);import{existsSync as B,lstatSync as w8,readFileSync as S,readlinkSync as y8,realpathSync as V$,rmSync as v8,statSync as m8,unlinkSync as u8,writeFileSync as g8}from"node:fs";import{homedir as h8}from"node:os";import{basename as p8,dirname as G$,isAbsolute as B$,join as G,relative as c8,resolve as h}from"node:path";import{homedir as L}from"node:os";import{delimiter as V2,join as j,resolve as G2}from"node:path";function i(){return j(L(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function T(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".claude")}function D(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:j(L(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||j(L(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:["*"]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function C$($){let Z=new Map;for(let 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$=C$(s);var _$=I$.size;var y=6;var P$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var HZ=new Set([...P$,"nc","ncat","socat","telnet","lwp-request"]);var A$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",MZ=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${A$}))(?=[:)\\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 J9=[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 c$={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 d$($){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),l$=`${$$}[0m`;function i$($=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 n$={ok:"success",warn:"warning",fail:"error",info:"info"},a$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function R($=i$()){let Z=(J,X)=>$?`${$$}[${J}m${X}${l$}`:X,Q=(J,X)=>Z(`38;2;${d$(c$[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(n$[J],a$[J])} ${X}`}}var O=R(!1);var s$=2;function r$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function o$($,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=r$()){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?o$(z,Q-s$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.join(`
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 t$=["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 e$={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},$8={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:{...e$}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as E8,mkdirSync as xX,readFileSync as k8,realpathSync as SX,writeFileSync as x8}from"node:fs";function Z$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function S8($){return Z$($)}var f8="tlc-exec.mjs";function b8($){return JSON.stringify($??null).includes(f8)}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 Q=S8(Z.hooks)?Z.hooks:{},J={},X=!1;for(let[z,W]of Object.entries(Q)){let H=W.filter((_)=>!b8(_));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 J$($){if(!E8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(k8($,"utf8"));if(Z.ok&&Z.changed)x8($,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((K$)=>!JSON.stringify(K$??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 Q$,relative as uX,resolve as gX,sep as hX}from"node:path";var X$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],q$=["config.json","state","flags"],sX=[Q$("tools","dev"),Q$("tools","__test__")];function d8($=process.env){let Z=h8(),Q=f($);return{home:F($),binLinks:n().map((J)=>G(Q,J)),claudeSettings:G(T(),"settings.json"),cursorHooks:G(D(),"hooks.json"),skillLinks:[G(T(),"skills","harness-init"),G(D(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return w8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),Q=[];for(;;)try{return G(V$(Z),...Q)}catch{let J=G$(Z);if(J===Z)return h($);Q.unshift(p8(Z)),Z=J}}function x($){try{return V$($)}catch{try{let Z=y8($);return c(B$(Z)?Z:h(G$($),Z))}catch{return c($)}}}function l8($,Z,Q={relative:c8,isAbsolute:B$}){if($===Z)return!0;let J=Q.relative(Z,$);return J!==""&&!J.startsWith("..")&&!Q.isAbsolute(J)}function z$($,Z){return l8(x($),c(Z))}var i8="tlc-exec.mjs";function n8($){try{return m8($).size<4096&&S($,"utf8").includes(i8)}catch{return!1}}function W$($,Z,Q,J,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(n8(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"&&!z$(Z,Q)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let q=X==="location"&&!z$(Z,Q);$.push({action:"unlink",target:Z,detail:q?`${J}, stale — points at ${x(Z)}`:J})}function a8($,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 s8($,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 r8($,Z,Q){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let q of X$){let z=G(Z,q);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let X=G(Z,k);if(B(X))$.push({action:"remove",target:X,detail:"install marker"});for(let q of q$){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 o8($,Z){if(B(G(Z,k)))$.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 t8($,Z={}){let Q=Z.purge===!0,J=[];a8(J,$.claudeSettings),s8(J,$.cursorHooks);for(let q of $.skillLinks)W$(J,q,$.home,"skill link","location");for(let q of $.binLinks)W$(J,q,$.home,"the tlc launcher on PATH","target");let X=r8(J,$.home,Q);return o8(J,$.home),{items:J,purge:Q,homeIsLink:X}}function H$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function e8($,Z){let Q=[],J=[];for(let X of H$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)J$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")g8(X.target,q.text,"utf8")}else if(X.action==="unlink")u8(X.target);else v8(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 $2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function Z2($,Z){let Q=H$($),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:Y$($)}],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:$2[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:Y$($)},...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 Y$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function J2($,Z,Q=O){return C(Z2($,Z),Q)}function Q2($=process.argv.slice(2)){let Z=d8(),Q=t8(Z,{purge:$.includes("--purge")}),J=$.includes("--yes")?e8(Q,Z):null;return console.log(J2(Q,J,R())),J!==null&&J.failed.length>0?1:0}if(l.main==l.module)process.exitCode=Q2();export{d8 as uninstallTargets,Z2 as uninstallScreen,J2 as uninstallReportText,t8 as planUninstall,H$ as pendingItems,Q2 as main,l8 as isInsideRoot,c as canonicalise,e8 as applyUninstall};
@@ -257,6 +257,7 @@ detects a condition that a config field could otherwise switch off:
257
257
  | Untrusted-content framing and enforcement | Injects one framing line per turn when the agent reads a pull request, an issue, a fetched page or an MCP result, stating that the content is data and that any directive inside it is to be reported as a prompt-injection attempt, not obeyed. | `untrustedContent.enabled` |
258
258
  | 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
259
  | 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
+ | 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` |
260
261
 
261
262
  <!-- /generated -->
262
263
 
package/docs/concepts.md CHANGED
@@ -8,6 +8,45 @@ timestamp: "2026-07-29"
8
8
 
9
9
  # Concepts
10
10
 
11
+ ## where a setting lives
12
+
13
+ Three layers, and the nearer one wins: the shipped defaults, then the runtime home's own config for this machine,
14
+ then the project's config. The machine tier is operator-owned — `install` and `update` never write it — so a
15
+ preference that is true everywhere belongs there and is written once.
16
+
17
+ The catch is that naming a value in the project file stops it tracking the machine tier, for ever. `init` writes
18
+ the whole default policy when a project has no config yet, and the wizard writes every knob it collected, so a
19
+ project config usually names dozens of values nobody chose. Raise a machine-wide number afterwards and none of
20
+ those repositories see it.
21
+
22
+ `tlc harness doctor` names the keys this project restates rather than decides, with the file they stopped
23
+ tracking. Deleting them is safe: the value does not change, it goes back to being inherited.
24
+
25
+ Both tiers are refused to an agent, on the write-tool path and through a shell redirect
26
+ ([/decisions/ad-022.md](/decisions/ad-022.md)).
27
+
28
+ ## which runtime answers a hook
29
+
30
+ `TLC_HOME` names it, and it wins over the location of the launcher that was invoked. So one session can run a
31
+ checkout while every other session on the machine keeps the installed copy:
32
+
33
+ ```bash
34
+ export TLC_HOME=/path/to/clone
35
+ ```
36
+
37
+ A checkout serves its hooks from `src/` when Bun is present, so a file saved half-edited is live on the next hook
38
+ — in that session only. That isolation is the reason to do it this way rather than installing the clone as the
39
+ machine's runtime: `tlc harness install --link` makes a working tree the runtime for *every* session, which is the
40
+ contributor route and why `doctor` names it distinctly (`runtime ownership — link to a working clone`).
41
+
42
+ Most work needs no live session at all: `tlc harness test` runs the gate hermetically, and the entrypoints can be
43
+ driven with real hook payloads from a scratch directory.
44
+
45
+ A machine with both — an installed copy and a checkout — is fine, but only one `tlc` should be on `PATH`, or which
46
+ runtime you get depends on which one the shell resolved. `doctor` reports the resolved runtime and its ownership on
47
+ every run, which is where a split shows up: the skill links and the wired hooks are checked against the runtime
48
+ that answered, so pointing at the other one reads as a fault.
49
+
11
50
  ## operator posture
12
51
 
13
52
  `mode`, or `tlc harness mode <paired|solo|focus>`. It sets how much the agent surfaces and what earns an
@@ -250,6 +289,53 @@ change-approach follow-up. Trade-off: stops loops; can block intentional retries
250
289
  | `intelligence.budgetContinue` | Under loop/context pressure **and** unfinished handoff work, follow-up says keep working — do not summarize |
251
290
  | `intelligence.idleTurnGate` | Blocks a turn that ends with open handoff work, zero recorded tool calls and zero file changes. It counts events the harness recorded rather than reading the reply, so no wording satisfies it |
252
291
 
292
+ ## operator rules
293
+
294
+ `rules.enabled` (off by default). The operator writes a rule per markdown file, and the harness enforces it. Two
295
+ tiers apply together, the way the lesson tiers do — one in the runtime home for every repository on this machine,
296
+ one in the project for the team — deduplicated by file name with the project winning. A project rule carrying
297
+ `enabled: false` switches a global one off there, and its body is where the reason goes.
298
+
299
+ ```markdown
300
+ ---
301
+ on: pr-open # pr-open | commit | push | stop | tool(<name>) | command(<pattern>)
302
+ require:
303
+ - subagent(the-jury) since HEAD # subagent | command | gate | file, since HEAD or since session
304
+ otherwise: deny # deny | ask | follow-up | warn
305
+ ---
306
+
307
+ Convene the jury on this branch. Checklist: docs/review-checklist.md
308
+ ```
309
+
310
+ The frontmatter is what the harness enforces; the body is the operator's own text, injected verbatim when the rule
311
+ fires. A proof is satisfied only by something the harness observed: a subagent of that type finishing, a command
312
+ that ran and did not fail, a gate that passed, a file that changed. The agent cannot create one — the observation
313
+ store is under the project state directory, which the floor refuses to an agent, and the mutating `tlc harness`
314
+ subcommands are refused from inside a session.
315
+
316
+ `since HEAD` compares the sha the observation was made against with the current one, so a review followed by
317
+ another commit is stale. A project with no git checkout cannot satisfy `since HEAD` at all.
318
+
319
+ The four verdicts land differently at the two moments a rule can fire:
320
+
321
+ | verdict | on an action | at the stop |
322
+ |---|---|---|
323
+ | `deny` | refuses the action | refuses the stop |
324
+ | `ask` | asks under `paired`, refuses otherwise | refuses the stop — no host offers an ask there |
325
+ | `follow-up` | allows | refuses the stop, framed as the next action |
326
+ | `warn` | allows | advisory text, and the stop is allowed |
327
+
328
+ `on: stop` rules are read only at the stop, and they are read after the lint, test and docs gates have run — so a
329
+ rule asking for `gate(test) since HEAD` can be satisfied by the gate this turn.
330
+
331
+ Posture reaches `ask` and nothing else: it interrupts under `paired` and hardens to `deny` under `solo` and
332
+ `focus`. `deny`, `follow-up` and `warn` are verification and are identical at all three
333
+ ([/decisions/ad-025.md](/decisions/ad-025.md)).
334
+
335
+ A pattern trigger is policy rather than containment. A script written to disk and executed later, a command name
336
+ built at runtime, `gh api` instead of `gh pr create`, or a pull request opened in a browser all escape it — the
337
+ rule covers the agent's shell path ([/decisions/ad-100.md](/decisions/ad-100.md)).
338
+
253
339
  ## plan gate
254
340
 
255
341
  `planGate.enabled` (off by default), with `planGate.windowMinutes` (default 120). The turn declares the paths
package/docs/diagnose.md CHANGED
@@ -34,6 +34,26 @@ Run `tlc harness doctor` first. Then walk this checklist.
34
34
  On Windows, Cursor hooks use `cmd /c node "…\tlc-exec.mjs" …`; Claude Code hooks stay exec-form
35
35
  (`node …`) on every platform.
36
36
 
37
+ ## A broken runtime does not block the agent
38
+
39
+ A hook that cannot run answers `{}` and exits 0 — no opinion, both hosts carry on — and prints why on stderr.
40
+ That covers a missing `dist/`, an interpreter below the minimum, a runtime home that is not there, and a project
41
+ shim whose handler is gone.
42
+
43
+ The reason is not politeness: a harness that cannot run was not checking anything, so it must not be the thing that
44
+ stops the turn. It happened the other way once, and the only route out was editing provider hooks by hand
45
+ ([/decisions/ad-101.md](/decisions/ad-101.md)).
46
+
47
+ A **command** is the opposite and still fails loudly — `tlc harness doctor` exiting 0 on a broken runtime would be
48
+ a green light nobody earned. So a silent harness shows up as a healthy-looking session and a failing `doctor`:
49
+
50
+ ```bash
51
+ tlc harness doctor
52
+ ```
53
+
54
+ There is deliberately no switch that turns the harness off. `tlc harness pause` disables the stop checks; the floor
55
+ and the action-time rails keep running, because a limit an agent can turn off is not a limit.
56
+
37
57
  ## Node vs Bun
38
58
 
39
59
  - Preferred: **Bun** on PATH — every hook runs the TypeScript source directly, ~1 ms per invocation.
package/docs/init.md CHANGED
@@ -82,8 +82,16 @@ tlc harness init --write [--stdin-json] [--force]
82
82
  tlc harness init --minimal
83
83
  ```
84
84
 
85
- `--minimal` writes a safe, provider-agnostic stub (grind/ship off) plus whichever provider shims are
86
- detected. Prefer the harness-init skill for full discovery.
85
+ `--minimal` writes a config that decides nothing — `{"version": 1}` — plus whichever provider shims are
86
+ detected. Everything else is inherited: the shipped defaults, then this machine's own config. Naming a value the
87
+ tiers below already resolve to would stop it tracking them, so a machine-wide change would never reach the project
88
+ ([/decisions/ad-101.md](/decisions/ad-101.md)).
89
+
90
+ The wizard's answers go in through `--stdin-json`, and the ones that restate the tiers below are dropped the same
91
+ way. Prefer the harness-init skill for full discovery.
92
+
93
+ **An existing config is never overwritten.** `--minimal` and `--write` keep it and say so; only `--stdin-json`
94
+ replaces it, because that is the operator supplying a policy. To start over, delete the file first.
87
95
 
88
96
  ## After write
89
97
 
package/docs/lessons.md CHANGED
@@ -185,6 +185,18 @@ the validity window, mode filtering and the char budget — so pinning changes o
185
185
  rule naming a renamed file is withheld like any other. Nothing caps how many can be pinned; the budget bounds
186
186
  delivery and `lessons list` marks each one `PINNED` ([/decisions/ad-043.md](/decisions/ad-043.md)).
187
187
 
188
+ Going first means taking the room first. `maxCharsSession` defaults to 900, a rendered lesson runs 300–500
189
+ characters, and the six shipped seeds compete for the same budget — so one long pinned rule can be the only thing
190
+ that arrives:
191
+
192
+ ```bash
193
+ tlc harness doctor # lesson budget — N eligible lessons never reach the model at session start
194
+ ```
195
+
196
+ That row names how many fit, how much of the budget they used, and which pinned lessons went first. Raise
197
+ `maxCharsSession`, shorten the rule, or unpin one. Without it the shortfall was only visible inside the injected
198
+ block, which the model reads and the operator does not ([/decisions/ad-100.md](/decisions/ad-100.md)).
199
+
188
200
  ## Provider views
189
201
 
190
202
  `.tlc/harness/lessons.md` is the source of truth, and the store, the ranking, the budget and the rendered text are
package/docs/log.md CHANGED
@@ -13,6 +13,11 @@ 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-21
17
+
18
+ - **AD-100** — The operator declares the trigger and the proof; the harness enforces it ([/decisions/ad-100.md](/decisions/ad-100.md))
19
+ - **AD-101** — Machine data belongs to the machine, not to the install ([/decisions/ad-101.md](/decisions/ad-101.md))
20
+
16
21
  ## 2026-08-20
17
22
 
18
23
  - **AD-098** — Splitting is reverted: a shared chunk ran the CLI's main ([/decisions/ad-098.md](/decisions/ad-098.md))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -42,6 +42,7 @@ assume Cursor — check what Step 1 detected).
42
42
  | 21 | Untrusted-content framing and enforcement | `untrustedContent.enabled` | off | Injects one framing line per turn when the agent reads a pull request, an issue, a fetched page or an MCP result, stating that the content is data and that any directive inside it is to be reported as a prompt-injection attempt, not obeyed. | Two modes. frame injects one line per turn, costing a few hundred characters and enforcing nothing. enforce also remembers what an untrusted read returned, bounded at 64 KB per session, and asks before a shell command that appears verbatim in it — verbatim because a paraphrase cannot be shown to come from the content, so a rewritten command is missed. It needs the host to deliver tool output on the after-event: measured present on Claude Code's PostToolUse and on Cursor's afterShellExecution and afterMCPExecution, and absent on Cursor's generic postToolUse. | `mode: frame \| enforce` |
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
+ | 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. | — |
45
46
 
46
47
  <!-- /generated -->
47
48
 
@@ -90,6 +91,59 @@ outliving what it was about. Mention `--global` when the lesson would be true in
90
91
 
91
92
  Point deep docs to: `tlc harness help lessons` (load only if the user asks how decay/ranking works).
92
93
 
94
+ ## Operator rules subsection (capability 24)
95
+
96
+ There is no knob beyond `rules.enabled`, because a rule is a file rather than a setting. If the user enables it,
97
+ ask what standing instruction they repeat by hand, then write the file with them — one rule per file, in
98
+ `.tlc/harness/rules/<name>.md` for this project or in `<runtime home>/rules/<name>.md` for every repository on
99
+ this machine.
100
+
101
+ ```markdown
102
+ ---
103
+ on: pr-open # pr-open | commit | push | stop | tool(<name>) | command(<pattern>)
104
+ require:
105
+ - subagent(the-jury) since HEAD # subagent | command | gate | file, since HEAD or since session
106
+ otherwise: deny # deny | ask | follow-up | warn
107
+ ---
108
+
109
+ Convene the jury on this branch. Checklist: docs/review-checklist.md
110
+ ```
111
+
112
+ The frontmatter is enforced; the body is the operator's own text, shown verbatim when the rule fires — so it is
113
+ where the instruction, the checklist and the reason go.
114
+
115
+ The four verdicts land differently at the two moments a rule can fire:
116
+
117
+ | verdict | on an action | at the stop |
118
+ |---|---|---|
119
+ | `deny` | refuses the action | refuses the stop |
120
+ | `ask` | asks under `paired`, refuses otherwise | refuses the stop — no host offers an ask there |
121
+ | `follow-up` | allows | refuses the stop, framed as the next action |
122
+ | `warn` | allows | advisory text, and the stop is allowed |
123
+
124
+ An `on: stop` rule is the one to reach for when the instruction is "before you finish, do X". It is read after the
125
+ lint, test and docs gates have run, so a rule asking for `gate(test) since HEAD` can be satisfied by this turn's
126
+ gate.
127
+
128
+ Five things to say while writing it:
129
+
130
+ - **The proof must be observable.** A subagent of that type finishing, a command that ran and did not fail, a gate
131
+ that passed, a file that changed. There is no proof that the review was any good, and asking for one would be
132
+ the same guesswork the rule exists to remove.
133
+ - **`since HEAD` means this code.** A review followed by another commit is stale and the rule fires again. Use
134
+ `since session` for something that only needs doing once per session.
135
+ - **Both tiers apply**, deduplicated by file name with the project winning. A standing rule belongs in the runtime
136
+ home so it is not retyped per repository; a project file with `enabled: false` and a body is how one repository
137
+ opts out, with the reason recorded.
138
+ - **Posture changes only `ask`** — it interrupts under `paired` and hardens to `deny` under `solo` and `focus`.
139
+ `deny`, `follow-up` and `warn` are identical at all three.
140
+
141
+ `tlc harness doctor` lists every active rule with the tier it came from, every one switched off here, and every
142
+ rule whose proof kind this project has never recorded — which is how a rule that enforces nothing satisfiable is
143
+ found before it is trusted.
144
+
145
+ Point deep docs to: `tlc harness help rules`.
146
+
93
147
  ## Not configurable — state this once, before discovery
94
148
 
95
149
  A floor tier runs ahead of every setting and reads no configuration, so nothing below can switch it off.
@@ -61,6 +61,7 @@ import { formatLessonLink, isStaleLesson, lessonLinkVerdict, parseLessonLink } f
61
61
  import {
62
62
  appliesHere,
63
63
  isInjectable,
64
+ previewLessonSelection,
64
65
  renderLessonBlock,
65
66
  selectLessons as selectLessonsInner,
66
67
  } from "./lesson/lesson.select.ts";
@@ -122,12 +123,14 @@ import {
122
123
  import {
123
124
  isUnderCodePaths,
124
125
  loadPolicy,
126
+ resolvedWithoutProjectTier,
125
127
  resolveProjectPosture,
126
128
  resolveProjectSyncMode,
127
129
  } from "./policy/policy.loader.ts";
128
130
  import { operatorBootstrapLines } from "./policy/policy.operator.ts";
129
131
  import { isOperatorMode, OPERATOR_MODES } from "./policy/policy.posture.ts";
130
132
  import { activeRails } from "./policy/policy.rails.ts";
133
+ import { pruneShadowed, shadowedKeys } from "./policy/policy.shadow.ts";
131
134
  import { forProvider } from "./policy/policy.types.ts";
132
135
  import { checkCollision, heartbeat, register, release, sweepStale } from "./presence/presence.service.ts";
133
136
  import { freshness, freshnessMessage, mayReplace, shouldRefetch } from "./pricing/pricing.freshness.ts";
@@ -139,6 +142,27 @@ import {
139
142
  readDecisions,
140
143
  } from "./release/release.decisions.ts";
141
144
  import { readReleaseSeen, writeReleaseSeen } from "./release/release.seen.ts";
145
+ import {
146
+ actionDecision,
147
+ effectiveVerdict,
148
+ evaluateRules,
149
+ stopDecision,
150
+ strictest,
151
+ } from "./rules/rules.decide.ts";
152
+ import { gateObservation, observationFrom } from "./rules/rules.observe.ts";
153
+ import { buildRuleSet } from "./rules/rules.parse.ts";
154
+ import { unobservedKinds } from "./rules/rules.proof.ts";
155
+ import {
156
+ decideAction,
157
+ decideStop,
158
+ loadRules,
159
+ observe,
160
+ observeGate,
161
+ wantsGateObservation,
162
+ wantsObservation,
163
+ } from "./rules/rules.service.ts";
164
+ import { readObservations, readRuleSources, recordObservation } from "./rules/rules.store.ts";
165
+ import { firingRules } from "./rules/rules.trigger.ts";
142
166
  import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
143
167
  import { clearShellStall } from "./shell-policy/shell-policy.stall.ts";
144
168
  import { coversHandler, decideShim } from "./shim/shim.precedence.ts";
@@ -295,6 +319,7 @@ export const coreFacade = {
295
319
  buildAuthoredLesson,
296
320
  authoredLessonId,
297
321
  selectLessons,
322
+ previewLessonSelection,
298
323
  touchAccessed,
299
324
  upsertProjectLesson,
300
325
  upsertLesson,
@@ -374,6 +399,9 @@ export const coreFacade = {
374
399
  activeRails,
375
400
  operatorBootstrapLines,
376
401
  loadPolicy,
402
+ resolvedWithoutProjectTier,
403
+ shadowedKeys,
404
+ pruneShadowed,
377
405
  resolveProjectPosture,
378
406
  resolveProjectSyncMode,
379
407
  OPERATOR_MODES,
@@ -421,6 +449,32 @@ export const coreFacade = {
421
449
  hasRecentEvidence,
422
450
  newestChangeMs,
423
451
  },
452
+ rules: {
453
+ // invariant: the single door. No entrypoint imports core/rules directly
454
+ // ([/decisions/ad-016.md](/decisions/ad-016.md)).
455
+ // what the entrypoints call
456
+ decideAction,
457
+ decideStop,
458
+ observe,
459
+ observeGate,
460
+ wants: wantsObservation,
461
+ wantsGate: wantsGateObservation,
462
+ load: loadRules,
463
+ // the pieces, for doctor and for tests
464
+ read: readRuleSources,
465
+ build: buildRuleSet,
466
+ firing: firingRules,
467
+ observations: readObservations,
468
+ record: recordObservation,
469
+ observationFrom,
470
+ gateObservation,
471
+ evaluate: evaluateRules,
472
+ strictest,
473
+ actionDecision,
474
+ stopDecision,
475
+ effectiveVerdict,
476
+ unobservedKinds,
477
+ },
424
478
  presence: {
425
479
  register,
426
480
  heartbeat,
@@ -2,9 +2,9 @@ import { homedir, tmpdir } from "node:os";
2
2
  import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import {
4
4
  flagsDir,
5
+ machineConfigPath,
5
6
  projectConfigPath,
6
7
  projectStateDir,
7
- runtimeHome,
8
8
  runtimeStateDir,
9
9
  } from "../../platform/paths.ts";
10
10
  import { normalizeSeparators } from "../../platform/sanitize.ts";
@@ -37,7 +37,7 @@ export function isScratch(target: string, tmp = tmpdir()): boolean {
37
37
  // `echo '{}' > ~/.tlc/harness/config.json` allowed, which is the same defect one directory up.
38
38
  export function isRuntimePolicySurface(filePath: string): boolean {
39
39
  const target = resolve(filePath);
40
- return target === resolve(runtimeHome(), "config.json") || isInside(runtimeStateDir(), target);
40
+ return target === resolve(machineConfigPath()) || isInside(runtimeStateDir(), target);
41
41
  }
42
42
 
43
43
  // invariant: the policy surface is defined here, next to the floor's other path predicates, because the
@@ -113,7 +113,12 @@ function deny(detail: string, note: string, remedy?: string): PolicySurfaceVerdi
113
113
  * all — an instruction that points at a path the floor guards is three layers disagreeing.
114
114
  */
115
115
  export const READ_REMEDY =
116
- "Reading is allowed: run `tlc harness handoff` for handoff state, `tlc harness policy` for the resolved policy, or use a proven reader (cat, head, jq, grep, ls, stat, test) on the path.";
116
+ // hazard: this said "`tlc harness policy` for the resolved policy". That command reports a policy that changed
117
+ // out of band and prints nothing about the resolved values, so an agent that followed the advice learned nothing
118
+ // and had no other route — measured, it cost a working session. `status` answers posture and gates; the resolved
119
+ // values are read from the file, which the proven readers are allowed to do
120
+ // ([/decisions/ad-100.md](/decisions/ad-100.md)).
121
+ "Reading is allowed: run `tlc harness handoff` for handoff state, `tlc harness status` for posture and gates, `tlc harness doctor` for what the settings amount to, or use a proven reader (cat, head, jq, grep, ls, stat, test) on the path.";
117
122
 
118
123
  // why: the harness directory prefix is derived from the path module rather than written as a literal, so a
119
124
  // change to the on-disk layout cannot leave this rule matching a path that no longer exists. The relative
@@ -139,14 +139,28 @@ export function rankLessonsForSync(lessons: HarnessLesson[]): HarnessLesson[] {
139
139
  );
140
140
  }
141
141
 
142
- export async function selectLessons(args: {
142
+ export type LessonSelectionArgs = {
143
143
  projectDir: string;
144
144
  config: LessonsPolicyConfig;
145
145
  mode: SelectMode;
146
146
  gate?: string;
147
147
  text?: string;
148
148
  now?: Date;
149
- }): Promise<{ lessons: HarnessLesson[]; usedIds: string[]; omitted: number }> {
149
+ };
150
+
151
+ export type LessonSelection = { lessons: HarnessLesson[]; usedIds: string[]; omitted: number };
152
+
153
+ /**
154
+ * The selection with no side effect, so a reader can ask what would be injected without becoming an injection.
155
+ *
156
+ * why this is separate: `selectLessons` marks the picked lessons as accessed, which is correct at an injection and
157
+ * wrong anywhere else. `doctor` needs the same answer and must not move the accessed timestamps — a measurement
158
+ * that changes what it measures is not a measurement ([/decisions/ad-027.md](/decisions/ad-027.md)).
159
+ *
160
+ * invariant: one selector. Re-deriving the budget arithmetic for the reporting path would be a second answer that
161
+ * drifts from the first ([/decisions/ad-100.md](/decisions/ad-100.md)).
162
+ */
163
+ export function previewLessonSelection(args: LessonSelectionArgs): LessonSelection {
150
164
  if (!args.config.enabled) {
151
165
  return { lessons: [], usedIds: [], omitted: 0 };
152
166
  }
@@ -200,10 +214,19 @@ export async function selectLessons(args: {
200
214
  chars += block.length;
201
215
  }
202
216
 
203
- const usedIds = picked.filter((l) => l.source !== "core").map((l) => l.id);
204
- await touchAccessed(args.projectDir, usedIds, now);
205
217
  // hazard: `maxInjectSession` defaults to 5 and `maxCharsSession` to 900, which fits about two rendered blocks —
206
- // so the count promises five and delivers two, silently. Whoever reads the injected block has to be able to tell
207
- // that eligible lessons were dropped ([/decisions/ad-043.md](/decisions/ad-043.md)).
208
- return { lessons: picked, usedIds: picked.map((l) => l.id), omitted: ordered.length - picked.length };
218
+ // so the count promises five and delivers two. Whoever reads the injected block has to be able to tell that
219
+ // eligible lessons were dropped, and so does the operator ([/decisions/ad-043.md](/decisions/ad-043.md)).
220
+ return {
221
+ lessons: picked,
222
+ usedIds: picked.map((lesson) => lesson.id),
223
+ omitted: ordered.length - picked.length,
224
+ };
225
+ }
226
+
227
+ export async function selectLessons(args: LessonSelectionArgs): Promise<LessonSelection> {
228
+ const selection = previewLessonSelection(args);
229
+ const accessed = selection.lessons.filter((lesson) => lesson.source !== "core").map((lesson) => lesson.id);
230
+ await touchAccessed(args.projectDir, accessed, args.now ?? new Date());
231
+ return selection;
209
232
  }
@@ -82,6 +82,9 @@ export const DEFAULTS: Policy = {
82
82
  enabled: false,
83
83
  windowMinutes: 120,
84
84
  },
85
+ rules: {
86
+ enabled: false,
87
+ },
85
88
  shell: {
86
89
  catastrophicAsk: true,
87
90
  stallDetection: false,
@@ -4,10 +4,10 @@ import { join } from "node:path";
4
4
  import type { Decision } from "../../contracts/decision.ts";
5
5
  import {
6
6
  flagsDir,
7
+ machineConfigPath,
7
8
  policyBaselineDir,
8
9
  projectConfigPath,
9
10
  projectStateDir,
10
- runtimeHome,
11
11
  } from "../../platform/paths.ts";
12
12
  import { sanitizeSegment } from "../../platform/sanitize.ts";
13
13
 
@@ -39,7 +39,7 @@ function hashOf(path: string): string {
39
39
  export function policySourceFingerprint(root: string): PolicySource[] {
40
40
  const paths = [
41
41
  projectConfigPath(root),
42
- join(runtimeHome(), "config.json"),
42
+ machineConfigPath(),
43
43
  join(projectStateDir(root), MODE_FILE),
44
44
  ...FLAG_FILES.map((flag) => join(flagsDir(root), flag)),
45
45
  ];