@tech-leads-club/harness-toolkit 0.3.6 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/bin/tlc-cli.ts +90 -18
- package/bin/tlc-exec.d.mts +1 -0
- package/bin/tlc-exec.mjs +47 -2
- package/capabilities/catalog.json +47 -30
- package/dist/compact-before.mjs +84 -78
- package/dist/doctor.mjs +87 -81
- package/dist/help-topic.mjs +6 -5
- package/dist/init-project.mjs +147 -9
- package/dist/install-runtime.mjs +85 -79
- package/dist/lessons-cli.mjs +88 -82
- package/dist/obs-cli.mjs +84 -78
- package/dist/price-lookup.mjs +2 -2
- package/dist/prompt-submit.mjs +84 -78
- package/dist/refresh-model-prices.mjs +85 -79
- package/dist/response-after.mjs +84 -78
- package/dist/run.mjs +84 -78
- package/dist/session-end.mjs +90 -84
- package/dist/session-start.mjs +92 -86
- package/dist/shim.mjs +82 -76
- package/dist/stop.mjs +90 -84
- package/dist/subagent-start.mjs +84 -78
- package/dist/subagent-stop.mjs +85 -79
- package/dist/support.mjs +88 -82
- package/dist/tlc-cli.mjs +104 -98
- package/dist/tool-after.mjs +84 -78
- package/dist/tool-before.mjs +86 -80
- package/dist/tool-failure.mjs +84 -78
- package/dist/uninstall-runtime.mjs +4 -4
- package/docs/architecture.md +1 -0
- package/docs/concepts.md +86 -0
- package/docs/diagnose.md +20 -0
- package/docs/init.md +10 -2
- package/docs/lessons.md +12 -0
- package/docs/log.md +5 -0
- package/package.json +1 -1
- package/skills/harness-init/references/capabilities.md +54 -0
- package/src/core/core.facade.ts +61 -0
- package/src/core/floor/floor.paths.ts +2 -2
- package/src/core/floor/floor.policy-surface.ts +6 -1
- package/src/core/lesson/lesson.select.ts +30 -7
- package/src/core/policy/policy.defaults.ts +3 -0
- package/src/core/policy/policy.integrity.ts +2 -2
- package/src/core/policy/policy.loader.ts +14 -3
- package/src/core/policy/policy.shadow.ts +93 -0
- package/src/core/policy/policy.types.ts +8 -0
- package/src/core/release/release.decisions.ts +3 -13
- package/src/core/rules/rules.decide.ts +123 -0
- package/src/core/rules/rules.observe.ts +76 -0
- package/src/core/rules/rules.parse.ts +142 -0
- package/src/core/rules/rules.proof.ts +130 -0
- package/src/core/rules/rules.service.ts +141 -0
- package/src/core/rules/rules.store.ts +77 -0
- package/src/core/rules/rules.trigger.ts +101 -0
- package/src/core/rules/rules.types.ts +64 -0
- package/src/entrypoints/shim.ts +9 -1
- package/src/entrypoints/stop.ts +75 -1
- package/src/entrypoints/subagent-stop.ts +9 -1
- package/src/entrypoints/support.ts +32 -0
- package/src/entrypoints/tool-after.ts +7 -2
- package/src/entrypoints/tool-before.ts +44 -3
- package/src/platform/frontmatter.ts +142 -0
- package/src/platform/links.ts +32 -0
- package/src/platform/paths.ts +113 -4
- package/src/platform/pricing.ts +3 -3
- package/src/platform/screen.ts +62 -3
- package/src/providers/index.ts +1 -0
- package/tools/doctor.ts +166 -27
- package/tools/help-topic.ts +39 -23
- package/tools/init-project.ts +51 -6
- package/tools/install-runtime.ts +17 -2
- package/tools/lessons-cli.ts +4 -1
- package/tools/refresh-model-prices.ts +2 -2
- 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
|
|
3
|
-
`);var
|
|
4
|
-
`}}import{join as
|
|
1
|
+
import{createRequire as M$}from"node:module";var l=M$(import.meta.url);import{existsSync as B,lstatSync as m8,readFileSync as S,readlinkSync as u8,realpathSync as G$,rmSync as g8,statSync as h8,unlinkSync as p8,writeFileSync as c8}from"node:fs";import{homedir as d8}from"node:os";import{basename as l8,dirname as B$,isAbsolute as H$,join as G,relative as i8,resolve as h}from"node:path";import{homedir as j}from"node:os";import{delimiter as H2,join as L,resolve as K2}from"node:path";function i(){return L(j(),".tlc","harness")}function F($=process.env){return $.TLC_HOME??i()}function P(){let $=process.env.CLAUDE_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".claude")}function D(){let $=process.env.CURSOR_CONFIG_DIR?.trim();return $&&$.length>0?$:L(j(),".cursor")}function f($=process.env){return $.TLC_BIN_DIR?.trim()||L(j(),".local","bin")}function n(){return["tlc","tlc.cmd"]}var s=[{id:"typescript",extensions:[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"python",extensions:[".py",".pyi",".pyw"],line:["#"],block:[['"""','"""'],["'''","'''"]],middle:[]},{id:"ruby",extensions:[".rb",".rake",".gemspec"],line:["#"],block:[["=begin","=end"]],middle:[]},{id:"shell",extensions:[".sh",".bash",".zsh",".ksh",".fish"],line:["#"],block:[],middle:[]},{id:"go",extensions:[".go"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"rust",extensions:[".rs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"java",extensions:[".java"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"kotlin",extensions:[".kt",".kts"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"swift",extensions:[".swift"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"c",extensions:[".c",".h"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"cpp",extensions:[".cc",".cpp",".cxx",".hpp",".hh",".hxx"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"csharp",extensions:[".cs"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"php",extensions:[".php"],line:["//","#"],block:[["/*","*/"]],middle:["*"]},{id:"scala",extensions:[".scala",".sc"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"dart",extensions:[".dart"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"elixir",extensions:[".ex",".exs"],line:["#"],block:[],middle:[]},{id:"erlang",extensions:[".erl",".hrl"],line:["%"],block:[],middle:[]},{id:"haskell",extensions:[".hs"],line:["--"],block:[["{-","-}"]],middle:[]},{id:"lua",extensions:[".lua"],line:["--"],block:[["--[[","]]"]],middle:[]},{id:"sql",extensions:[".sql"],line:["--"],block:[["/*","*/"]],middle:["*"]},{id:"css",extensions:[".css",".scss",".sass",".less"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"yaml",extensions:[".yaml",".yml"],line:["#"],block:[],middle:[]},{id:"toml",extensions:[".toml"],line:["#"],block:[],middle:[]},{id:"ini",extensions:[".ini",".cfg",".conf",".properties"],line:[";","#"],block:[],middle:[]},{id:"dockerfile",extensions:[".dockerfile","dockerfile"],line:["#"],block:[],middle:[]},{id:"makefile",extensions:[".mk","makefile"],line:["#"],block:[],middle:[]},{id:"terraform",extensions:[".tf",".tfvars"],line:["#","//"],block:[["/*","*/"]],middle:["*"]},{id:"powershell",extensions:[".ps1",".psm1",".psd1"],line:["#"],block:[["<#","#>"]],middle:[]},{id:"perl",extensions:[".pl",".pm"],line:["#"],block:[],middle:[]},{id:"r",extensions:[".r"],line:["#"],block:[],middle:[]},{id:"julia",extensions:[".jl"],line:["#"],block:[["#=","=#"]],middle:[]},{id:"vue",extensions:[".vue",".svelte"],line:["//"],block:[["/*","*/"],["<!--","-->"]],middle:["*"]},{id:"html",extensions:[".html",".htm",".xml",".xhtml"],line:[],block:[["<!--","-->"]],middle:[]},{id:"graphql",extensions:[".graphql",".gql"],line:["#"],block:[],middle:[]},{id:"protobuf",extensions:[".proto"],line:["//"],block:[["/*","*/"]],middle:["*"]},{id:"zig",extensions:[".zig"],line:["//"],block:[],middle:[]},{id:"clojure",extensions:[".clj",".cljs",".cljc",".edn"],line:[";"],block:[],middle:[]},{id:"ocaml",extensions:[".ml",".mli"],line:[],block:[["(*","*)"]],middle:["*"]},{id:"fsharp",extensions:[".fs",".fsi",".fsx"],line:["//"],block:[["(*","*)"]],middle:["*"]},{id:"vim",extensions:[".vim"],line:['"'],block:[],middle:[]},{id:"tex",extensions:[".tex",".sty",".cls"],line:["%"],block:[],middle:[]}];function I$($){let Z=new Map;for(let Q of $){let J={line:Q.line,block:Q.block,middle:Q.middle};for(let X of Q.extensions)Z.set(X.toLowerCase(),J)}return Z}var _$=I$(s);var N$=_$.size;var y=6;var A$=new Set(["aria2c","curl","fetch","http","httpie","https","wget"]);var MZ=new Set([...A$,"nc","ncat","socat","telnet","lwp-request"]);var R$="ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|php|sh|sql",CZ=new RegExp(`(?:file://)?((?:[A-Za-z]:)?[\\w./~@+-]*[\\w-]\\.(?:${R$}))(?=[:)\\s,'"\`]|$)`,"g");var A="1970-01-01T00:00:00.000Z";function M($){return{...$,scope:"gate-execution",source:"core",tier:"core",status:"active",confidence:1,hitCount:1,pinned:!1,refs:[],sessionKeys:[],injectedCount:0,gradeableCount:0,helpedCount:0,neutralCount:0,firstSeenAt:A,lastSeenAt:A,lastAccessedAt:A,updatedAt:A}}var q9=[M({id:"core:gate:lint",failedGate:"lint",category:"verification",triggerTokens:["lint","biome","eslint","ruff","format"],instruction:"A lint gate failure means changed files still violate the project lint command. Fix the reported findings without suppressions.",avoid:"Do not add lint suppressions, disable comments, or delete failing files to silence the gate.",prefer:"Apply the smallest fix that clears each finding, then let the stop hook re-check.",preRetryCheck:"Confirm the lint command targets only the intended changed files and still fails for the same codes.",priority:90}),M({id:"core:gate:test",failedGate:"test",category:"verification",triggerTokens:["test","vitest","jest","pytest","failing"],instruction:"A test gate failure means assertions still fail. Fix the behavior or the test under the real contract — do not delete or skip tests.",avoid:"Do not delete failing tests, mark them skipped, or weaken assertions to force green.",prefer:"Reproduce the failure, fix root cause, re-run the same test target.",preRetryCheck:"Identify the failing test name/file from the gate output before editing.",priority:90}),M({id:"core:gate:comments",failedGate:"comments",category:"verification",triggerTokens:["junk comment","TODO","FIXME","banner"],instruction:"Junk-comment policy failed. Delete narrating comments, banners, TODO/FIXME, and commented-out code.",avoid:"Do not keep TODO markers or section banners 'for clarity'.",prefer:"Keep only comments that explain a non-obvious why (invariant, hazard, external constraint).",preRetryCheck:"Scan the listed file:line hits and remove each one.",priority:80}),M({id:"core:gate:ship",failedGate:"ship",category:"ship-evidence",triggerTokens:["ship","evidence","90-verdict","PASS"],instruction:"Ship claim without recent production PASS evidence. Produce real evidence before claiming done.",avoid:"Do not claim shipped based on unit tests alone when runtime paths changed.",prefer:"Run production E2E, write 90-verdict.txt PASS, cite the evidence path.",preRetryCheck:"Confirm evidenceDir and a recent PASS verdict exist for this change.",priority:95}),M({id:"core:gate:empty-diff",failedGate:"empty-diff",category:"ship-evidence",triggerTokens:["empty","diff","no changes","shipped"],instruction:"Done/shipped was claimed with zero file changes. Either implement the work or explain why zero-diff is correct — do not claim shipped on an empty tree.",avoid:"Do not restate 'done' without a real diff or an explicit zero-change justification.",prefer:"Make the missing change, or clearly document why no files should change.",preRetryCheck:"Inspect git status / changed files before the next stop.",priority:92}),M({id:"core:gate:stagnation",failedGate:"stagnation",category:"stagnation",triggerTokens:["stagnation","identical","fingerprint","same fail"],instruction:"Identical validation fingerprint repeated. Change approach — do not re-apply the same failing edit.",avoid:"Do not retry the exact same patch, command, or suppression.",prefer:"Diagnose root cause with a different path, or escalate with BLOCKED / TRIED / NEED.",preRetryCheck:"Diff your last edit against the gate output; ensure the next action is different.",priority:100})];var d$={structure:"#3d3a4a",accent:"#a78bfa",success:"#6ee7b7",warning:"#d4a574",error:"#f87171",info:"#93c5fd",textMain:"#f5f5f7",textMuted:"#9ca3af",textDim:"#6b7280"},V={check:"✔",cross:"✖",warning:"⚠",arrow:"→",arrowRight:"▸",dot:"•",bar:"│",rule:"══",dash:"──"};function l$($){let Z=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec($.trim());if(!Z)return"255;255;255";return[Z[1],Z[2],Z[3]].map((Q)=>Number.parseInt(Q,16)).join(";")}var $$=String.fromCharCode(27),i$=`${$$}[0m`;function n$($=process.env,Z=process.argv,Q=process.stdout.isTTY===!0){if("NO_COLOR"in $)return!1;if(Z.includes("--no-color"))return!1;return Q}var a$={ok:"success",warn:"warning",fail:"error",info:"info"},s$={ok:V.check,warn:V.warning,fail:V.cross,info:V.arrowRight},m=16;function R($=n$()){let Z=(J,X)=>$?`${$$}[${J}m${X}${i$}`:X,Q=(J,X)=>Z(`38;2;${l$(d$[J])}`,X);return{enabled:$,paint:Q,bold:(J)=>Z("1",J),dim:(J)=>Q("textDim",J),heading:(J)=>Q("accent",`${V.rule} ${J} ${V.rule}`),footer:(J)=>Q("textDim",`${V.dash} ${J} ${V.dash}`),kv:(J,X,q=m)=>` ${Q("textMuted",`${J}:`.padEnd(q))} ${X}`,status:(J,X)=>`${Q(a$[J],s$[J])} ${X}`}}var O=R(!1);var r$=2;function o$($=process.stdout.columns){return Math.min(110,Math.max(60,$??100))}function t$($,Z){if($==="")return[""];let Q=[],J="";for(let X of $.split(/\s+/).filter((q)=>q.length>0)){if(J===""){J=X;continue}if(J.length+1+X.length<=Z){J=`${J} ${X}`;continue}Q.push(J),J=X}if(J!=="")Q.push(J);return Q.length===0?[""]:Q}function C($,Z,Q=o$()){let J=[Z.heading($.title.toUpperCase())];if($.summary&&$.summary.length>0)J.push(` ${$.summary.join(Z.dim(` ${V.bar} `))}`);let X=Math.max(m,...$.sections.flatMap((q)=>(q.rows??[]).map((z)=>z.label.length+1)));for(let q of $.sections){if(J.push(""),q.title)J.push(Z.paint("accent",q.title));for(let z of q.rows??[]){let W=z.level?Z.status(z.level,z.value):z.value;J.push(Z.kv(z.label,W,X))}for(let z of q.lines??[]){if(z===""){J.push("");continue}let W=q.wrap?t$(z,Q-r$):[z];for(let H of W)J.push(` ${H}`)}}if($.footer)J.push("",Z.footer($.footer));return J.join(`
|
|
2
|
+
`)}var K={enabled:!0,signalPath:"obs.jsonl",debugPath:"debug.jsonl",debugEnabled:!1,includePayloads:!1,maxAttrChars:500,sessionCostAlertUsd:5,retentionDays:14,maxSignalEvents:50000,globalSpool:!1};var $8=["No harness decision in this window.","Whatever you just saw was the model, not a rail — the harness allowed everything it was asked about."].join(`
|
|
3
|
+
`);var Z8={enabled:!1,maxInjectSession:5,maxInjectRetry:8,maxCharsSession:900,maxCharsRetry:1400,promoteHitCount:2,decayLambda:0.02,projectBoost:1.5,syncRulesFile:"auto",gardenOnSessionEnd:!0},Z$={version:1,mode:"solo",codePaths:["src","apps","libs","packages"],grind:{enabled:!1,maxLoops:5,lintCommand:null,testCommand:null,appendFiles:"auto"},shipGate:{enabled:!1,runtimePathPrefixes:["src","apps","libs","packages","deploy","scripts"],runtimePathExcludes:[".tlc/","**/node_modules/","**/.git/"],evidenceDir:null,evidenceMaxAgeHours:48,emptyDiffAntiShip:!1,claimWindowMinutes:10},subagents:{enforceAllowlist:!1,requireModel:!1,allowedModels:[],blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])"],minEffort:null,blockParentFast:!1,blockMode:"deny",readOnlyTypes:["explore"]},docs:{command:null,severity:"warn"},observe:{enabled:!1,rails:[]},comments:{enabled:!1,onViolation:"followup",mode:"declared"},supplyChain:{enabled:!1},duplication:{enabled:!1,minRun:y},obs:{globalSpool:!1,includePayloads:K.includePayloads,maxAttrChars:K.maxAttrChars,sessionCostAlertUsd:K.sessionCostAlertUsd,retentionDays:K.retentionDays},untrustedContent:{mode:"frame",enabled:!1,extraTools:[],extraCommandPatterns:[]},planGate:{enabled:!1,windowMinutes:120},rules:{enabled:!1},shell:{catastrophicAsk:!0,stallDetection:!1,stallRepeatThreshold:3},intelligence:{gapFeedback:!0,failureClassification:!0,progressiveHandoff:!0,progressiveContext:!0,autopilot:!0,idleTurnGate:!1,budgetContinue:!1,budgetContinueAfterLoops:3,lessons:{...Z8}},mcpPrime:[],bootstrapExtra:[]};var I="@tech-leads-club/harness-toolkit",k="installed-from-npm";import{existsSync as S8,mkdirSync as yX,readFileSync as f8,realpathSync as vX,writeFileSync as b8}from"node:fs";function J$($){return $!==null&&typeof $==="object"&&!Array.isArray($)}function w8($){return J$($)}var y8="tlc-exec.mjs";function v8($){return JSON.stringify($??null).includes(y8)}function u($){if($===null||$.trim()==="")return{ok:!0,settingsText:"",changed:!1};let Z;try{Z=JSON.parse($)}catch(z){return{ok:!1,error:z instanceof Error?z.message:String(z),block:""}}if(!J$(Z))return{ok:!1,error:"settings.json root is not a JSON object",block:""};let Q=w8(Z.hooks)?Z.hooks:{},J={},X=!1;for(let[z,W]of Object.entries(Q)){let H=W.filter((_)=>!v8(_));if(H.length!==W.length)X=!0;if(H.length>0)J[z]=H}let q={};for(let[z,W]of Object.entries(Z))if(z!=="hooks")q[z]=W;else if(Object.keys(J).length>0)q.hooks=J;return{ok:!0,settingsText:JSON.stringify(q,null,2),changed:X}}function Q$($){if(!S8($))return{ok:!0,settingsText:"",changed:!1};let Z=u(f8($,"utf8"));if(Z.ok&&Z.changed)b8($,Z.settingsText,"utf8");return Z}function g($,Z="tlc-exec.mjs"){if($===null||$.trim()==="")return{kind:"absent"};let Q;try{Q=JSON.parse($)}catch{return{kind:"unparsed"}}if(Q===null||typeof Q!=="object"||Array.isArray(Q))return{kind:"unparsed"};let J=Q,X=J.hooks!==null&&typeof J.hooks==="object"&&!Array.isArray(J.hooks)?J.hooks:{},q={},z=0,W=0;for(let[H,_]of Object.entries(X)){let d=Array.isArray(_)?_:[],N=d.filter((U$)=>!JSON.stringify(U$??null).includes(Z));if(z+=d.length-N.length,N.length>0)q[H]=N,W+=N.length}if(W===0)return{kind:"empty",removed:z};return{kind:"rewritten",removed:z,text:`${JSON.stringify({...J,hooks:q},null,2)}
|
|
4
|
+
`}}import{join as X$,relative as dX,resolve as lX,sep as iX}from"node:path";var q$=["bin","capabilities","dist","docs","skills","src","tools","config.example.json","package.json"],z$=["config.json","state","flags"],$4=[X$("tools","dev"),X$("tools","__test__")];function n8($=process.env){let Z=d8(),Q=f($);return{home:F($),binLinks:n().map((J)=>G(Q,J)),claudeSettings:G(P(),"settings.json"),cursorHooks:G(D(),"hooks.json"),skillLinks:[G(P(),"skills","harness-init"),G(D(),"skills","harness-init"),G(Z,".tlc","skills","harness-init")]}}function p($){try{return m8($).isSymbolicLink()}catch{return!1}}function c($){let Z=h($),Q=[];for(;;)try{return G(G$(Z),...Q)}catch{let J=B$(Z);if(J===Z)return h($);Q.unshift(l8(Z)),Z=J}}function x($){try{return G$($)}catch{try{let Z=u8($);return c(H$(Z)?Z:h(B$($),Z))}catch{return c($)}}}function a8($,Z,Q={relative:i8,isAbsolute:H$}){if($===Z)return!0;let J=Q.relative(Z,$);return J!==""&&!J.startsWith("..")&&!Q.isAbsolute(J)}function W$($,Z){return a8(x($),c(Z))}var s8="tlc-exec.mjs";function r8($){try{return h8($).size<4096&&S($,"utf8").includes(s8)}catch{return!1}}function Y$($,Z,Q,J,X){if(!B(Z)&&!p(Z))return;if(!p(Z)){if(r8(Z)){$.push({action:"remove",target:Z,detail:`${J}, installed as a copy`});return}$.push({action:"keep",target:Z,detail:`${J} is a real file the installer did not write`});return}if(X==="target"&&!W$(Z,Q)){$.push({action:"keep",target:Z,detail:`points at ${x(Z)} — not ours`});return}let q=X==="location"&&!W$(Z,Q);$.push({action:"unlink",target:Z,detail:q?`${J}, stale — points at ${x(Z)}`:J})}function o8($,Z){if(!B(Z))return;let Q=u(S(Z,"utf8"));if(!Q.ok){$.push({action:"keep",target:Z,detail:`left untouched — it does not parse as JSON: ${Q.error}`});return}if(!Q.changed)return;$.push({action:"unmerge",target:Z,detail:"drop the harness hook groups, keep every other key and every foreign hook"})}function t8($,Z){let Q=B(Z)?S(Z,"utf8"):null,J=g(Q);switch(J.kind){case"absent":return;case"unparsed":$.push({action:"keep",target:Z,detail:"left untouched — it does not parse as JSON"});return;case"empty":if(J.removed>0)$.push({action:"remove",target:Z,detail:`${J.removed} entries, all ours`});return;default:$.push({action:"unmerge",target:Z,detail:`drop ${J.removed} harness entries, keep the rest`})}}function e8($,Z,Q){if(p(Z))return $.push({action:"unlink",target:Z,detail:`a link to ${x(Z)} — the checkout it points at is left exactly as it is`}),!0;if(!B(Z))return!1;for(let q of q$){let z=G(Z,q);if(B(z))$.push({action:"remove",target:z,detail:"runtime payload"})}let X=G(Z,k);if(B(X))$.push({action:"remove",target:X,detail:"install marker"});for(let q of z$){let z=G(Z,q);if(!B(z))continue;$.push(Q?{action:"remove",target:z,detail:"--purge"}:{action:"keep",target:z,detail:"yours — add --purge to remove it"})}return!1}function $2($,Z){if(B(G(Z,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 Z2($,Z={}){let Q=Z.purge===!0,J=[];o8(J,$.claudeSettings),t8(J,$.cursorHooks);for(let q of $.skillLinks)Y$(J,q,$.home,"skill link","location");for(let q of $.binLinks)Y$(J,q,$.home,"the tlc launcher on PATH","target");let X=e8(J,$.home,Q);return $2(J,$.home),{items:J,purge:Q,homeIsLink:X}}function K$($){return $.items.filter((Z)=>Z.action==="unmerge"||Z.action==="unlink"||Z.action==="remove")}function J2($,Z){let Q=[],J=[];for(let X of K$($))try{if(X.action==="unmerge"&&X.target===Z.claudeSettings)Q$(X.target);else if(X.action==="unmerge"){let q=g(S(X.target,"utf8"));if(q.kind==="rewritten")c8(X.target,q.text,"utf8")}else if(X.action==="unlink")p8(X.target);else g8(X.target,{recursive:!0,force:!0});Q.push(X)}catch(q){J.push({item:X,reason:q instanceof Error?q.message:String(q)})}return{applied:Q,failed:J}}var Q2={unmerge:"warn",unlink:"warn",remove:"warn",keep:"ok",manual:"info"};function X2($,Z){let Q=K$($),J=Z!==null;if(Q.length===0)return{title:"harness uninstall",summary:["nothing wired"],sections:[{rows:[{label:"state",value:"no harness artefact found — nothing to undo",level:"ok"}]},{title:"STILL YOURS TO DO",rows:V$($)}],footer:`already clean · reinstall with npm i -g ${I}, then \`tlc harness install\``};let X=Q.map((W)=>({label:W.action,value:`${W.target} — ${W.detail}`,level:Q2[W.action]})),q=$.items.filter((W)=>W.action==="keep"),z=[{title:J?"REMOVED":"WOULD REMOVE",rows:X},...q.length>0?[{title:"KEPT",rows:q.map((W)=>({label:"keep",value:`${W.target} — ${W.detail}`,level:"ok"}))}]:[],{title:"STILL YOURS TO DO",rows:V$($)},...Z!==null&&Z.failed.length>0?[{title:"FAILED",rows:Z.failed.map((W)=>({label:"error",value:`${W.item.target} — ${W.reason}`,level:"fail"}))}]:[]];return{title:"harness uninstall",summary:[J?`${Z.applied.length} applied`:`${Q.length} pending`,$.purge?"purge: state included":"purge: state kept",$.homeIsLink?"runtime: linked checkout, unlinked only":"runtime: owned by the installer"],sections:z,footer:J?`reinstall any time with npm i -g ${I}, then \`tlc harness install\``:"nothing was changed · re-run with --yes to apply this plan"}}function V$($){return $.items.filter((Z)=>Z.action==="manual").map((Z)=>({label:"run",value:`${Z.target} — ${Z.detail}`,level:"info"}))}function q2($,Z,Q=O){return C(X2($,Z),Q)}function z2($=process.argv.slice(2)){let Z=n8(),Q=Z2(Z,{purge:$.includes("--purge")}),J=$.includes("--yes")?J2(Q,Z):null;return console.log(q2(Q,J,R())),J!==null&&J.failed.length>0?1:0}if(l.main==l.module)process.exitCode=z2();export{n8 as uninstallTargets,X2 as uninstallScreen,q2 as uninstallReportText,Z2 as planUninstall,K$ as pendingItems,z2 as main,a8 as isInsideRoot,c as canonicalise,J2 as applyUninstall};
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
86
|
-
detected.
|
|
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
|
@@ -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.
|
package/src/core/core.facade.ts
CHANGED
|
@@ -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";
|
|
@@ -94,6 +95,7 @@ import {
|
|
|
94
95
|
recordObs,
|
|
95
96
|
} from "./observability/observability.service.ts";
|
|
96
97
|
import { getRollup, pruneObs, pruneSpool, readSignalEvents } from "./observability/observability.store.ts";
|
|
98
|
+
import { resolveObsLevel } from "./observability/observability.types.ts";
|
|
97
99
|
import {
|
|
98
100
|
decisionsFrom,
|
|
99
101
|
NOTHING_WAS_THE_HARNESS,
|
|
@@ -109,6 +111,7 @@ import {
|
|
|
109
111
|
} from "./observe/observe.service.ts";
|
|
110
112
|
import { detectDeviations, detectPlan } from "./plan/plan.detect.ts";
|
|
111
113
|
import { evaluatePlanGate, planVerdict } from "./plan/plan.service.ts";
|
|
114
|
+
import { DEFAULTS } from "./policy/policy.defaults.ts";
|
|
112
115
|
import { guardPolicySurface } from "./policy/policy.guard.ts";
|
|
113
116
|
import {
|
|
114
117
|
acceptPolicySources,
|
|
@@ -122,12 +125,14 @@ import {
|
|
|
122
125
|
import {
|
|
123
126
|
isUnderCodePaths,
|
|
124
127
|
loadPolicy,
|
|
128
|
+
resolvedWithoutProjectTier,
|
|
125
129
|
resolveProjectPosture,
|
|
126
130
|
resolveProjectSyncMode,
|
|
127
131
|
} from "./policy/policy.loader.ts";
|
|
128
132
|
import { operatorBootstrapLines } from "./policy/policy.operator.ts";
|
|
129
133
|
import { isOperatorMode, OPERATOR_MODES } from "./policy/policy.posture.ts";
|
|
130
134
|
import { activeRails } from "./policy/policy.rails.ts";
|
|
135
|
+
import { pruneShadowed, shadowedKeys } from "./policy/policy.shadow.ts";
|
|
131
136
|
import { forProvider } from "./policy/policy.types.ts";
|
|
132
137
|
import { checkCollision, heartbeat, register, release, sweepStale } from "./presence/presence.service.ts";
|
|
133
138
|
import { freshness, freshnessMessage, mayReplace, shouldRefetch } from "./pricing/pricing.freshness.ts";
|
|
@@ -139,6 +144,27 @@ import {
|
|
|
139
144
|
readDecisions,
|
|
140
145
|
} from "./release/release.decisions.ts";
|
|
141
146
|
import { readReleaseSeen, writeReleaseSeen } from "./release/release.seen.ts";
|
|
147
|
+
import {
|
|
148
|
+
actionDecision,
|
|
149
|
+
effectiveVerdict,
|
|
150
|
+
evaluateRules,
|
|
151
|
+
stopDecision,
|
|
152
|
+
strictest,
|
|
153
|
+
} from "./rules/rules.decide.ts";
|
|
154
|
+
import { gateObservation, observationFrom } from "./rules/rules.observe.ts";
|
|
155
|
+
import { buildRuleSet } from "./rules/rules.parse.ts";
|
|
156
|
+
import { unobservedKinds } from "./rules/rules.proof.ts";
|
|
157
|
+
import {
|
|
158
|
+
decideAction,
|
|
159
|
+
decideStop,
|
|
160
|
+
loadRules,
|
|
161
|
+
observe,
|
|
162
|
+
observeGate,
|
|
163
|
+
wantsGateObservation,
|
|
164
|
+
wantsObservation,
|
|
165
|
+
} from "./rules/rules.service.ts";
|
|
166
|
+
import { readObservations, readRuleSources, recordObservation } from "./rules/rules.store.ts";
|
|
167
|
+
import { firingRules } from "./rules/rules.trigger.ts";
|
|
142
168
|
import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
|
|
143
169
|
import { clearShellStall } from "./shell-policy/shell-policy.stall.ts";
|
|
144
170
|
import { coversHandler, decideShim } from "./shim/shim.precedence.ts";
|
|
@@ -295,6 +321,7 @@ export const coreFacade = {
|
|
|
295
321
|
buildAuthoredLesson,
|
|
296
322
|
authoredLessonId,
|
|
297
323
|
selectLessons,
|
|
324
|
+
previewLessonSelection,
|
|
298
325
|
touchAccessed,
|
|
299
326
|
upsertProjectLesson,
|
|
300
327
|
upsertLesson,
|
|
@@ -329,6 +356,7 @@ export const coreFacade = {
|
|
|
329
356
|
},
|
|
330
357
|
observability: {
|
|
331
358
|
DEFAULT_OBS,
|
|
359
|
+
resolveObsLevel,
|
|
332
360
|
recordObs,
|
|
333
361
|
recordFromEvent,
|
|
334
362
|
recordAudit,
|
|
@@ -374,6 +402,13 @@ export const coreFacade = {
|
|
|
374
402
|
activeRails,
|
|
375
403
|
operatorBootstrapLines,
|
|
376
404
|
loadPolicy,
|
|
405
|
+
// why on the facade and not exported from the barrel: `core/index.ts` publishes types and this object only, so
|
|
406
|
+
// a caller outside `core` reaching for the shipped defaults comes through here
|
|
407
|
+
// ([/decisions/ad-004.md](/decisions/ad-004.md), [/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
408
|
+
DEFAULTS,
|
|
409
|
+
resolvedWithoutProjectTier,
|
|
410
|
+
shadowedKeys,
|
|
411
|
+
pruneShadowed,
|
|
377
412
|
resolveProjectPosture,
|
|
378
413
|
resolveProjectSyncMode,
|
|
379
414
|
OPERATOR_MODES,
|
|
@@ -421,6 +456,32 @@ export const coreFacade = {
|
|
|
421
456
|
hasRecentEvidence,
|
|
422
457
|
newestChangeMs,
|
|
423
458
|
},
|
|
459
|
+
rules: {
|
|
460
|
+
// invariant: the single door. No entrypoint imports core/rules directly
|
|
461
|
+
// ([/decisions/ad-016.md](/decisions/ad-016.md)).
|
|
462
|
+
// what the entrypoints call
|
|
463
|
+
decideAction,
|
|
464
|
+
decideStop,
|
|
465
|
+
observe,
|
|
466
|
+
observeGate,
|
|
467
|
+
wants: wantsObservation,
|
|
468
|
+
wantsGate: wantsGateObservation,
|
|
469
|
+
load: loadRules,
|
|
470
|
+
// the pieces, for doctor and for tests
|
|
471
|
+
read: readRuleSources,
|
|
472
|
+
build: buildRuleSet,
|
|
473
|
+
firing: firingRules,
|
|
474
|
+
observations: readObservations,
|
|
475
|
+
record: recordObservation,
|
|
476
|
+
observationFrom,
|
|
477
|
+
gateObservation,
|
|
478
|
+
evaluate: evaluateRules,
|
|
479
|
+
strictest,
|
|
480
|
+
actionDecision,
|
|
481
|
+
stopDecision,
|
|
482
|
+
effectiveVerdict,
|
|
483
|
+
unobservedKinds,
|
|
484
|
+
},
|
|
424
485
|
presence: {
|
|
425
486
|
register,
|
|
426
487
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
207
|
-
//
|
|
208
|
-
return {
|
|
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
|
}
|