@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.
- package/README.md +44 -8
- package/bin/tlc-cli.ts +40 -1
- 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 +87 -81
- package/dist/obs-cli.mjs +83 -77
- 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 +84 -78
- 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 +54 -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 +97 -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 +58 -4
- package/src/platform/pricing.ts +3 -3
- package/src/platform/screen.ts +62 -3
- package/tools/doctor.ts +162 -2
- package/tools/help-topic.ts +39 -23
- package/tools/init-project.ts +51 -6
- package/tools/install-runtime.ts +23 -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 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};
|
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";
|
|
@@ -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(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
42
|
+
machineConfigPath(),
|
|
43
43
|
join(projectStateDir(root), MODE_FILE),
|
|
44
44
|
...FLAG_FILES.map((flag) => join(flagsDir(root), flag)),
|
|
45
45
|
];
|