@r1z4x/tezgah 1.7.0 → 1.8.2
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/CHANGELOG.md +61 -1
- package/MANIFEST +2 -0
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +15 -7
- package/bin/tezgah-route +22 -7
- package/bin/tezgah-task +320 -75
- package/bin/tezgah-taste +8 -5
- package/docs/architecture.md +21 -7
- package/docs/environment.md +3 -1
- package/docs/evidence.md +9 -1
- package/docs/gate.md +24 -7
- package/docs/models.md +40 -12
- package/docs/operations.md +31 -1
- package/docs/research.md +3 -2
- package/docs/testing.md +18 -11
- package/hooks/projects-auto-init.py +29 -1
- package/hooks/tezgah_agents.py +24 -7
- package/hooks/tezgah_context.py +67 -35
- package/hooks/tezgah_gate.py +430 -64
- package/hooks/tezgah_integrity.py +108 -41
- package/hooks/tezgah_models.py +90 -30
- package/hooks/tezgah_paths.py +44 -6
- package/hooks/tezgah_policy.py +3 -3
- package/hooks/tezgah_research.py +9 -5
- package/hooks/tezgah_task.py +319 -67
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +23 -3
- package/hosts/cursor/hook.py +11 -0
- package/hosts/opencode/plugins/tezgah.js +171 -4
- package/package.json +1 -1
- package/skills/plan-add/SKILL.md +23 -9
- package/skills/plan-add/render_table.py +8 -3
- package/skills/tezgah-contract/SKILL.md +3 -3
|
@@ -228,6 +228,48 @@ const SECRET_SINK =
|
|
|
228
228
|
// message about the rule rather than a credential in a write. `|` is NOT a
|
|
229
229
|
// boundary here but a carrier, and the split is read off the masked text.
|
|
230
230
|
const SEGMENT = /\|\||&&|[;&\n]/g
|
|
231
|
+
// Plan 096, as hooks/tezgah_gate reads it (SECRET_PREFIX, SECRET_RUNNER, ...,
|
|
232
|
+
// _secret_runner): a token held only by a program's leading `NAME=value` words
|
|
233
|
+
// is in that program's env, so its redirected output carries no key - but only
|
|
234
|
+
// for a script runner named by itself, given a script file, with no option that
|
|
235
|
+
// runs its own text, stdin or a preload, or traces. Any other program holds.
|
|
236
|
+
const SECRET_PREFIX =
|
|
237
|
+
/^[\s;&|]*(?:env\s+)?(?:[A-Za-z_]\w*=(?:'[^']*'|"(?:\\.|[^"\\])*"|\$\([^()]*\)|`[^`]*`|\\.|[^\s;&|()<>'"`\\])*\s+)+/
|
|
238
|
+
// the sinks that carry a program's output and nothing but it (SECRET_OUTPUT)
|
|
239
|
+
const SECRET_OUTPUT = />>?(?![&=])|\|\s*tee\b/g
|
|
240
|
+
const SECRET_RUNNER = /^(?:python[\d.]*|node|bun|deno|bash|sh|zsh)$/
|
|
241
|
+
const SECRET_SCRIPT = /^[^$`\s]+\.(?:py|js|mjs|ts|sh)$/
|
|
242
|
+
const SECRET_INLINE = {python: /^-[A-Za-z]*[cmi]/,
|
|
243
|
+
sh: /^(?:-[A-Za-z]*[csxv]|-o$|--(?:verbose|debugger))/,
|
|
244
|
+
js: /^(?:-[A-Za-z]*[epri]|--(?:eval|print|require|import|loader|experimental-loader|preload|interactive)(?:=|$))/}
|
|
245
|
+
// the line holds (SECRET_ENVIRON, SECRET_STARTUP, SECRET_TRACE, SECRET_FUNCTION)
|
|
246
|
+
const SECRET_ENVIRON = /\/proc\/[^/\s]*\/environ/
|
|
247
|
+
const SECRET_STARTUP =
|
|
248
|
+
/(?<![\w$])(?:SHELLOPTS|BASHOPTS|BASH_ENV|ENV|PS4|ZDOTDIR|NODE_OPTIONS|PYTHONSTARTUP|PYTHONINSPECT|PERL5OPT|PERL5DB|RUBYOPT|LD_PRELOAD|DYLD_\w*)=/
|
|
249
|
+
const SECRET_TRACE = /(?:^|[\s;&|(])set\s+(?:-[A-Za-z]*[xv]|-o\s+(?:xtrace|verbose))/
|
|
250
|
+
const SECRET_FUNCTION =
|
|
251
|
+
/(?:^|[\s;&|(])(?:function\s+[^\s(){};&|]+|[^\s(){};&|$=]+\s*\(\s*\))/
|
|
252
|
+
const SECRET_UNEXPANDED = /\$[A-Za-z_{(]|`/
|
|
253
|
+
// the programs a carried name is printed by when they own a sink
|
|
254
|
+
const SECRET_PRINTERS = new Set(["echo", "printf", "cat", "eval", "awk", "perl",
|
|
255
|
+
"ruby"])
|
|
256
|
+
// a versioned python is read by /^python[\d.]+$/: only pythonBin() spells the
|
|
257
|
+
// interpreter's name (tests/test_interpreter.py)
|
|
258
|
+
const SECRET_INTERPRETERS = new Set(["python", "sh", "bash", "zsh", "dash"])
|
|
259
|
+
const SECRET_KEYWORDS = new Set(["if", "then", "elif", "else", "do", "while",
|
|
260
|
+
"until", "!", "{", "time"])
|
|
261
|
+
// the wrappers `_program` reads past, with the options that take a value
|
|
262
|
+
const SECRET_WRAPPERS = new Map([
|
|
263
|
+
["sudo", ["-u", "-g", "-h", "-p", "-C", "-D", "-r", "-t", "-U"]],
|
|
264
|
+
["env", ["-u", "--unset", "-C", "--chdir"]], ["nice", ["-n", "--adjustment"]],
|
|
265
|
+
["nohup", []], ["exec", ["-a"]], ["time", ["-f", "-o"]], ["command", []],
|
|
266
|
+
["timeout", ["-s", "--signal", "-k", "--kill-after"]], ["stdbuf", ["-i", "-o", "-e"]]])
|
|
267
|
+
const SECRET_INTERPRETER_OPTS = ["-X", "-W", "-o", "+o", "-O"]
|
|
268
|
+
// `NAME=value` at a word start, and the `$NAME`/`${NAME}` a later word reads
|
|
269
|
+
// (hooks/tezgah_gate.SHELL_ASSIGN, SHELL_VAR)
|
|
270
|
+
const SECRET_ASSIGN =
|
|
271
|
+
/(?:^|(?<=[\s;&|(]))([A-Za-z_][A-Za-z0-9_]*)=((?:'[^']*'|"[^"]*"|[^\s;&|)'"])*)/g
|
|
272
|
+
const SECRET_VAR = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g
|
|
231
273
|
const SECRET_DENY =
|
|
232
274
|
"Credential write denied: this command would land a credential in a file " +
|
|
233
275
|
"(`>`/`>>`, `tee`, `git add` or a curl trace). Record the credential's " +
|
|
@@ -1941,23 +1983,148 @@ function realPath(p) {
|
|
|
1941
1983
|
// The refusal when this command would write a credential into a file, or null.
|
|
1942
1984
|
// Reading an env var or running a tool with a key in its env is the normal work
|
|
1943
1985
|
// this must not touch, so a token only counts next to a write sink, and a sink
|
|
1944
|
-
// only carries the text of its own simple command.
|
|
1986
|
+
// only carries the text of its own simple command. A token held only by the
|
|
1987
|
+
// leading `NAME=value` words of an allowlisted program is in that program's env
|
|
1988
|
+
// (envPrefixOnly), unless the line reads /proc's environ or defines a function.
|
|
1989
|
+
// A name an earlier command assigned a token is carried, and a printer owning a
|
|
1990
|
+
// sink that reads it still writes it (printerOwnsSink; plan 096).
|
|
1945
1991
|
function secretCommand(command) {
|
|
1946
1992
|
const c = String(command || "")
|
|
1947
1993
|
if (!c || !SECRET_TOKEN.test(c)) return null
|
|
1948
1994
|
const masked = maskText(c)
|
|
1995
|
+
const relax = !(SECRET_ENVIRON.test(c) || SECRET_STARTUP.test(c)
|
|
1996
|
+
|| SECRET_FUNCTION.test(masked) || SECRET_TRACE.test(masked))
|
|
1949
1997
|
let start = 0
|
|
1998
|
+
const carried = new Set()
|
|
1950
1999
|
const ends = [...masked.matchAll(SEGMENT)].map((m) => m.index)
|
|
1951
2000
|
for (const end of ends.concat(c.length)) {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
return SECRET_DENY
|
|
2001
|
+
const text = c.slice(start, end)
|
|
2002
|
+
if (SECRET_SINK.test(masked.slice(start, end))) {
|
|
2003
|
+
if (SECRET_TOKEN.test(text) && !(relax && envPrefixOnly(text))) return SECRET_DENY
|
|
2004
|
+
if ([...text.matchAll(SECRET_VAR)].some((v) => carried.has(v[1] || v[2]))
|
|
2005
|
+
&& printerOwnsSink(text, masked.slice(start, end))) {
|
|
2006
|
+
return SECRET_DENY
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
for (const a of text.matchAll(SECRET_ASSIGN)) {
|
|
2010
|
+
if (SECRET_TOKEN.test(a[0])) carried.add(a[1])
|
|
1955
2011
|
}
|
|
1956
2012
|
start = end
|
|
1957
2013
|
}
|
|
1958
2014
|
return null
|
|
1959
2015
|
}
|
|
1960
2016
|
|
|
2017
|
+
// True when every token in this simple command's text sits in its leading
|
|
2018
|
+
// `NAME=value` words, no sink runs inside them, the sinks after them carry only
|
|
2019
|
+
// the program's output (a `git add` or a curl trace holds), and the program is
|
|
2020
|
+
// on the allowlist (hooks/tezgah_gate._env_prefix_only).
|
|
2021
|
+
function envPrefixOnly(text) {
|
|
2022
|
+
const m = text.match(SECRET_PREFIX)
|
|
2023
|
+
if (!m) return false
|
|
2024
|
+
const rest = text.slice(m[0].length)
|
|
2025
|
+
return !(SECRET_TOKEN.test(rest) || SECRET_SINK.test(m[0])
|
|
2026
|
+
|| SECRET_SINK.test(rest.replace(SECRET_OUTPUT, " "))
|
|
2027
|
+
|| !secretRunner(rest))
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// the words of the first command `text` runs, a `$( )` body skipped
|
|
2031
|
+
function firstCommand(text) {
|
|
2032
|
+
const first = shellCommands(text.replace(/^[ \t\n;&|]+/, ""))
|
|
2033
|
+
.find((cmd) => cmd[1] !== "$)")
|
|
2034
|
+
return first ? first[0] : []
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// True when the first program `text` runs, past the wrappers, is a script
|
|
2038
|
+
// runner named by itself (a path could be any program) given a script file,
|
|
2039
|
+
// with no `-` or inline, stdin, preload, interactive or trace option before it
|
|
2040
|
+
// (hooks/tezgah_gate._secret_runner).
|
|
2041
|
+
function secretRunner(text) {
|
|
2042
|
+
const words = secretPastWrappers(firstCommand(text))
|
|
2043
|
+
if (!words.length || !SECRET_RUNNER.test(words[0])) return false
|
|
2044
|
+
const name = words[0]
|
|
2045
|
+
let args = words.slice(1)
|
|
2046
|
+
if (name === "deno") {
|
|
2047
|
+
if (args[0] !== "run") return false
|
|
2048
|
+
args = args.slice(1)
|
|
2049
|
+
}
|
|
2050
|
+
const inline = SECRET_INLINE[name.startsWith("python") ? "python"
|
|
2051
|
+
: ["node", "bun", "deno"].includes(name) ? "js" : "sh"]
|
|
2052
|
+
for (let i = 0; i < args.length;) {
|
|
2053
|
+
const arg = args[i]
|
|
2054
|
+
if (arg === "-" || inline.test(arg)) return false
|
|
2055
|
+
if (!/^[-+]/.test(arg)) return SECRET_SCRIPT.test(arg)
|
|
2056
|
+
i += SECRET_INTERPRETER_OPTS.includes(arg) ? 2 : 1
|
|
2057
|
+
}
|
|
2058
|
+
return false
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// True when a pipeline stage that owns a file sink - a `>` to a file other than
|
|
2062
|
+
// /dev/null, or a pipe into `tee` - is a printer
|
|
2063
|
+
// (hooks/tezgah_gate._printer_owns_sink).
|
|
2064
|
+
function printerOwnsSink(text, masked) {
|
|
2065
|
+
const cuts = [...masked.matchAll(/(?<!>)\|/g)].map((m) => m.index)
|
|
2066
|
+
const starts = [0, ...cuts.map((k) => k + 1)]
|
|
2067
|
+
const stops = [...cuts, text.length]
|
|
2068
|
+
for (let k = 0; k < starts.length; k++) {
|
|
2069
|
+
const a = starts[k], b = stops[k]
|
|
2070
|
+
let owns = [...masked.slice(a, b).matchAll(/>>?(?![&=])/g)].some((m) =>
|
|
2071
|
+
text.slice(a + m.index + m[0].length, b).trim().split(/\s+/)[0] !== "/dev/null")
|
|
2072
|
+
if (k + 1 < starts.length && /^\s*tee\b/.test(masked.slice(starts[k + 1]))) owns = true
|
|
2073
|
+
if (owns && secretPrinter(text.slice(a, b))) return true
|
|
2074
|
+
}
|
|
2075
|
+
return false
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// True when the first program `text` runs prints a carried name
|
|
2079
|
+
// (hooks/tezgah_gate._secret_printer).
|
|
2080
|
+
function secretPrinter(text) {
|
|
2081
|
+
const program = secretProgram(firstCommand(text))[0]
|
|
2082
|
+
return SECRET_PRINTERS.has(program) || SECRET_INTERPRETERS.has(program)
|
|
2083
|
+
|| /^python[\d.]+$/.test(program) || SECRET_UNEXPANDED.test(program)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// `words` past keywords, env assignments and the wrappers
|
|
2087
|
+
// (hooks/tezgah_gate._past_wrappers)
|
|
2088
|
+
function secretPastWrappers(words) {
|
|
2089
|
+
let i = 0
|
|
2090
|
+
while (i < words.length) {
|
|
2091
|
+
const word = words[i]
|
|
2092
|
+
if (/^[A-Za-z_]\w*=/.test(word) || SECRET_KEYWORDS.has(word)) { i += 1; continue }
|
|
2093
|
+
const wrapper = basename(word)
|
|
2094
|
+
const opts = SECRET_WRAPPERS.get(wrapper)
|
|
2095
|
+
if (!opts) break
|
|
2096
|
+
i += 1
|
|
2097
|
+
while (i < words.length && words[i].startsWith("-")) i += opts.includes(words[i]) ? 2 : 1
|
|
2098
|
+
if (wrapper === "timeout" && i < words.length) i += 1
|
|
2099
|
+
}
|
|
2100
|
+
return words.slice(i)
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// [program basename without `.py`, its arguments] for one simple command, past
|
|
2104
|
+
// the wrappers and an interpreter's options before its script
|
|
2105
|
+
// (hooks/tezgah_gate._program).
|
|
2106
|
+
function secretProgram(words) {
|
|
2107
|
+
words = secretPastWrappers(words)
|
|
2108
|
+
if (words[0] === "[") {
|
|
2109
|
+
const close = words.indexOf("]")
|
|
2110
|
+
words = words.slice(close < 0 ? words.length : close + 1)
|
|
2111
|
+
}
|
|
2112
|
+
if (!words.length) return ["", []]
|
|
2113
|
+
let name = basename(words[0])
|
|
2114
|
+
if (SECRET_INTERPRETERS.has(name) || /^python[\d.]+$/.test(name)) {
|
|
2115
|
+
const own = name.startsWith("python") ? /^-[A-Za-z]*[cm]$/ : /^-[A-Za-z]*c[A-Za-z]*$/
|
|
2116
|
+
let j = 1
|
|
2117
|
+
while (j < words.length && /^[-+]/.test(words[j]) && words[j] !== "-") {
|
|
2118
|
+
if (own.test(words[j])) { j = words.length + 1; break }
|
|
2119
|
+
j += SECRET_INTERPRETER_OPTS.includes(words[j]) ? 2 : 1
|
|
2120
|
+
}
|
|
2121
|
+
if (j < words.length) words = words.slice(j)
|
|
2122
|
+
}
|
|
2123
|
+
if (!words.length) return ["", []]
|
|
2124
|
+
name = basename(words[0])
|
|
2125
|
+
return [name.endsWith(".py") ? name.slice(0, -3) : name, words.slice(1)]
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1961
2128
|
// --- untrusted content: the channel and the label ---------------------------
|
|
1962
2129
|
// The two halves the Python hosts get from
|
|
1963
2130
|
// hooks/tezgah_integrity.untrusted_source and hooks/tezgah_untrusted.marks,
|
package/package.json
CHANGED
package/skills/plan-add/SKILL.md
CHANGED
|
@@ -72,9 +72,15 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
|
|
|
72
72
|
9. Start it yourself with `tezgah-task` - the lifecycle is the session's, never
|
|
73
73
|
a command handed to the user:
|
|
74
74
|
`~/.config/tezgah/bin/tezgah-task start NNN --phase discovery` writes `phase:`
|
|
75
|
-
into this file and clears the phase from every other open plan
|
|
76
|
-
|
|
77
|
-
`
|
|
75
|
+
into this file and clears the phase from every other open plan. To run two
|
|
76
|
+
plans at once, give each its own worktree on its own branch (`branch:`, or
|
|
77
|
+
`plan/NNN-slug`) before `start`: a plan held by its worktree keeps its phase
|
|
78
|
+
when the plan being started is held by one too and their `allowed_paths:`
|
|
79
|
+
share no prefix, and the gate holds a write in each worktree to that plan's
|
|
80
|
+
record. The gate then allows a write only from `implementation` on and only
|
|
81
|
+
inside the plan's `allowed_paths:` (`--allow '<glob>' ...` replaces them);
|
|
82
|
+
`phase`, `allow` and `stop` run inside a plan's worktree act on that plan,
|
|
83
|
+
and `allow` refuses a scope another running plan's worktree holds. Entering
|
|
78
84
|
`implementation` or `verification` with no allowlist is refused unless
|
|
79
85
|
`--any-path` is passed, so keep the plan's scope and never drop it to get
|
|
80
86
|
through. Three more refusals land at the same moment, and you fix them in the
|
|
@@ -90,9 +96,14 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
|
|
|
90
96
|
the scope with `tezgah-task allow <glob> ... --why "<reason>"`, which records the reason. Never retype the record:
|
|
91
97
|
the gate refuses a write to it, because the CLI's checks would be skipped.
|
|
92
98
|
Tick an Acceptance box with `tezgah-task tick NNN <n>` (`--undo` unticks),
|
|
93
|
-
|
|
99
|
+
rewrite State or Next with `tezgah-task note NNN state|next "<text>"`, and
|
|
100
|
+
record the spike's answer with `tezgah-task spike NNN "<answer>"`, which
|
|
101
|
+
writes `spike_recorded:` on the plan, active or not. These three work from
|
|
102
|
+
the main checkout or from any of its worktrees.
|
|
94
103
|
After the merge, `tezgah-task close NNN done` deactivates the plan,
|
|
95
|
-
moves it to done and removes every merged worktree.
|
|
104
|
+
moves it to done and removes every merged worktree. It refuses while an
|
|
105
|
+
Acceptance box is open: tick it, or, only when the owner accepted the item
|
|
106
|
+
as not met, end it with `UNMET: <reason, who accepted it>`.
|
|
96
107
|
|
|
97
108
|
10. If the work settles a decision that is hard to reverse, spans components, or
|
|
98
109
|
keeps being re-explained, record it as an ADR under `.tezgah/decisions/` with
|
|
@@ -133,14 +144,16 @@ after:
|
|
|
133
144
|
|
|
134
145
|
`allowed_paths:` is written here, at creation (step 5): the scope the task's
|
|
135
146
|
writes have to stay inside, one glob per `- ` line. `tezgah-task` adds the rest:
|
|
136
|
-
`phase:` - `discovery`, `implementation` or `verification`,
|
|
137
|
-
|
|
147
|
+
`phase:` - `discovery`, `implementation` or `verification`, on one open plan, or
|
|
148
|
+
on one plan per worktree when their scopes share no prefix (step 9) - is the
|
|
149
|
+
activation key the gate reads, set when the session starts
|
|
138
150
|
the task itself (step 9); `allowed_from:` records where a scope the CLI replaced came from.
|
|
139
151
|
`tezgah-task phase verification` lists what the plan owes before it is done: its
|
|
140
152
|
acceptance commands run unpiped, and a fresh reviewer (the host's
|
|
141
153
|
`tezgah-reviewer` subagent) reading the diff, whose verdict
|
|
142
154
|
`tezgah-task review NNN <reviewer> approve|changes` records as `review:`.
|
|
143
|
-
`tezgah-task close NNN done` (plan-sync) refuses without an `approve
|
|
155
|
+
`tezgah-task close NNN done` (plan-sync) refuses without an `approve`, and on an
|
|
156
|
+
unticked Acceptance item with no `UNMET: <reason>`; `discarded` needs neither.
|
|
144
157
|
|
|
145
158
|
`after:` names the plans that must be done before this one starts - a `- NNN`
|
|
146
159
|
block or `after: 012, 015`. `tezgah-task status` reads it: each open plan's
|
|
@@ -160,7 +173,8 @@ return to is what a large refactor has instead of 200 per-file snapshots
|
|
|
160
173
|
The four `spike:` keys are optional and go together, on a plan that must answer
|
|
161
174
|
one question before it can be built: `spike:` is that question, `spike_box:` the
|
|
162
175
|
time box (minutes, or one focused session), `spike_recorded:` where the answer
|
|
163
|
-
is recorded -
|
|
176
|
+
is recorded (`tezgah-task spike NNN "<answer>"` writes it) - empty until it is,
|
|
177
|
+
which is what refuses `implementation` - and
|
|
164
178
|
`spike_throwaway:` whether the code is thrown away once it has answered. Nothing
|
|
165
179
|
enforces the throwaway half, because `allowed_paths:` already does: a spike pins
|
|
166
180
|
its scope to a scratch path and is never merged, and the knowledge, not the
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Rewrite the status table in <
|
|
2
|
+
"""Rewrite the status table in <main checkout>/.tezgah/plans/README.md from .tezgah/plans/open/*.md.
|
|
3
3
|
Usage: python3 render_table.py [--pr-info 'NNN=text' ...] (extra text appended to the pr cell)
|
|
4
4
|
python3 render_table.py --acceptance (the acceptance items that name no
|
|
5
5
|
command, with the counts it read; a report, not a gate - always exit 0)
|
|
6
6
|
python3 render_table.py --acceptance --strict (the same report, and exit 1
|
|
7
7
|
when an item still under .tezgah/plans/open names no command either)
|
|
8
8
|
Creates .tezgah/plans/README.md, or appends the marker block to it, when either is missing."""
|
|
9
|
-
import glob, os, re,
|
|
9
|
+
import glob, os, re, sys
|
|
10
10
|
# The one reader of a plan file lives in the module that owns the record, so the
|
|
11
11
|
# table and the acceptance report cannot disagree about what a plan says. Three
|
|
12
12
|
# dirnames up from skills/plan-add/render_table.py is the tree that holds hooks/,
|
|
@@ -14,7 +14,12 @@ import glob, os, re, subprocess, sys
|
|
|
14
14
|
HERE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
|
15
15
|
sys.path.insert(0, os.path.join(HERE, "hooks"))
|
|
16
16
|
import tezgah_task as tt # noqa: E402
|
|
17
|
-
|
|
17
|
+
# The project's main checkout, the one that keeps `.tezgah/plans`: git's own
|
|
18
|
+
# top-level answered `.tezgah` from inside it (its private repository) and a
|
|
19
|
+
# worktree from a linked checkout, and the table was written there instead.
|
|
20
|
+
root = tt.main_repo(os.getcwd())
|
|
21
|
+
if not root:
|
|
22
|
+
sys.exit("render_table: %s is not inside a git checkout" % os.getcwd())
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
def acceptance(strict):
|
|
@@ -258,9 +258,9 @@ Routing: `~/.config/tezgah/bin/tezgah-route "<brief>"` names the worker - tezgah
|
|
|
258
258
|
-frontier, or the specialist whose whole job the brief is (tezgah-tester,
|
|
259
259
|
-docs, -security, -ui, -researcher, -reviewer) - and the spawn uses that name,
|
|
260
260
|
never the host's generic task agent (overrides for stored data, credentials,
|
|
261
|
-
security first; then a Jev judgement; the static phase table with no key). A
|
|
262
|
-
`ESCALATE
|
|
263
|
-
continued; the main thread never switches model (the cache is per model). A
|
|
261
|
+
security first; then a Jev judgement; the static phase table with no key). A report missing an output field, or an
|
|
262
|
+
`ESCALATE:`, gets one narrower re-brief of the same role naming that field or judgement; only a second miss is
|
|
263
|
+
restarted on tezgah-frontier with the original brief, never continued; the main thread never switches model (the cache is per model). A
|
|
264
264
|
code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
265
265
|
`impact`, `node`, `files`, `codegraph_explore`) and says "answer from
|
|
266
266
|
the graph, grep only for literal text"; never a grep-only explorer (observed).
|