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