@r1z4x/tezgah 1.5.1 → 1.6.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/CHANGELOG.md +47 -1
- package/MANIFEST +6 -0
- package/bin/consult +13 -10
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-research +53 -3
- package/bin/tezgah-setup +1 -1
- package/bin/tezgah-task +192 -39
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +3 -3
- package/docs/contract.md +4 -1
- package/docs/feature-audit.md +24 -3
- package/docs/gate.md +10 -11
- package/docs/index.json +21 -7
- package/docs/research.md +46 -0
- package/docs/skills.md +23 -5
- package/hooks/tezgah_agents.py +3 -2
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +65 -14
- package/hooks/tezgah_gate.py +62 -76
- package/hooks/tezgah_policy.py +38 -14
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_research.py +3 -3
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_task.py +2 -2
- package/hosts/opencode/plugins/tezgah.js +17 -37
- package/package.json +1 -1
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +20 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +11 -7
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +10 -1
- package/skills/tezgah-contract/SKILL.md +63 -39
package/hooks/tezgah_gate.py
CHANGED
|
@@ -38,28 +38,26 @@ Rules, all only inside a tezgah root:
|
|
|
38
38
|
8. a write while an active task is set - a plan file under .tezgah/plans/open whose
|
|
39
39
|
frontmatter carries a valid `phase` - is refused when that phase is one
|
|
40
40
|
that only reads, or when the file is outside the task's `allowed_paths`
|
|
41
|
-
refused (`task`). The record is
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
refused (`task`). The record is written by bin/tezgah-task, which the
|
|
42
|
+
orchestrating session runs itself as the work moves, so the refusal leans
|
|
43
|
+
on a state the CLI vetted (an Acceptance item per command, an answered
|
|
44
|
+
spike, a checkpoint) rather than one this gate inferred: no active task
|
|
45
|
+
means no requirement, and the rule invents nothing. The record also
|
|
46
|
+
carries where the phase returns to - a
|
|
45
47
|
`checkpoint:` sha, or `pending` while the tree still holds work no commit
|
|
46
48
|
names - and a write in `implementation` is refused while it reads `pending`
|
|
47
49
|
and the tree is dirty, so the phase's work starts from a named state rather
|
|
48
50
|
than on top of uncommitted changes; the per-file snapshots are not one
|
|
49
|
-
state to return to.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
measured open before they were closed - with the refusal naming the command
|
|
53
|
-
that lifts it, an armed arm removed or disabled the gate in 25 of 25 runs
|
|
54
|
-
and obeyed it in none (an internal benchmark, block E7) - and neither
|
|
55
|
-
refusal names a command now. The phase covers the shell too, because the
|
|
51
|
+
state to return to. A write whose target IS that record is refused too:
|
|
52
|
+
retyping the frontmatter would skip the preconditions the CLI checks. The
|
|
53
|
+
phase covers the shell too, because the
|
|
56
54
|
write tools are not the only way to change a file: with them refused, the
|
|
57
55
|
armed arm wrote the target with a heredoc redirect in 3 of 25 runs, so a
|
|
58
56
|
reading phase refuses a shell command that writes. A code-execution tool is
|
|
59
57
|
a third route the phase does not cover: the same block measured one row in
|
|
60
58
|
25 writing the target through `eval` after the write tools and the shell
|
|
61
59
|
were both refused, and that one is named rather than closed. The `task-off`
|
|
62
|
-
kill switch removes all
|
|
60
|
+
kill switch removes all three.
|
|
63
61
|
9. a `git commit` (or `--amend`) is refused while the newest check recorded in
|
|
64
62
|
this session failed (`order`): a commit is a claim that the tree passed, and
|
|
65
63
|
the state it would freeze is the one a check just rejected. The state is the
|
|
@@ -681,9 +679,10 @@ CLASS_NOTE = {
|
|
|
681
679
|
"transient": "it names a failure the host's own client may retry - a "
|
|
682
680
|
"timeout, a connection error, a rate limit, a 5xx - and an "
|
|
683
681
|
"identical repeat by the agent adds nothing to that",
|
|
684
|
-
"user": "it names something only the user can
|
|
685
|
-
"login, an access grant - so
|
|
686
|
-
"
|
|
682
|
+
"user": "it names something only the user can supply - a credential, a "
|
|
683
|
+
"login, an access grant - so take a route that needs none first "
|
|
684
|
+
"(another provider or member, a local run, a public source) and "
|
|
685
|
+
"ask the user once, naming what it unlocks, only when none is left",
|
|
687
686
|
"permanent": "an assertion or a bad argument does not change by re-running "
|
|
688
687
|
"it",
|
|
689
688
|
}
|
|
@@ -980,52 +979,47 @@ def race_reason(inp, session_id, cwd=None):
|
|
|
980
979
|
return None
|
|
981
980
|
|
|
982
981
|
|
|
983
|
-
# --- task scope: the
|
|
984
|
-
# What this closes: a session that has
|
|
985
|
-
#
|
|
986
|
-
# files it may touch - and then writes anyway. Two shapes cost
|
|
987
|
-
#
|
|
988
|
-
#
|
|
989
|
-
# bounded task quietly becomes an unbounded one.
|
|
990
|
-
#
|
|
991
|
-
#
|
|
992
|
-
#
|
|
982
|
+
# --- task scope: the plan's own phase and path allowlist --------------------
|
|
983
|
+
# What this closes: a session that has declared what it is working on - a plan
|
|
984
|
+
# file under .tezgah/plans/open is active, so the work has a phase and the
|
|
985
|
+
# files it may touch - and then writes anyway. Two shapes cost real work: a
|
|
986
|
+
# write in `discovery`, where the whole point of the phase is to read before
|
|
987
|
+
# deciding, and a write outside the plan's own allowlist, which is how a
|
|
988
|
+
# bounded task quietly becomes an unbounded one. The record is written by
|
|
989
|
+
# bin/tezgah-task, which the orchestrating session runs itself: the plan's
|
|
990
|
+
# lifecycle is the orchestrator's job, never a hand-off to the user. What keeps
|
|
991
|
+
# the boundary is the CLI, not who runs it - a writing phase needs every
|
|
992
|
+
# Acceptance item to name its proving command, `implementation` an answered
|
|
993
|
+
# spike and a checkpoint, `close done` an approving review - so the record's own
|
|
994
|
+
# file is refused to a write (TASK_RECORD_DENY): retyping the frontmatter is the
|
|
995
|
+
# direct route that skips those checks. A shell line's positional target (`sed
|
|
996
|
+
# -i`, `cp`) is not read here (see write_paths), the residual this rule names.
|
|
993
997
|
#
|
|
994
998
|
# The direction is fail-open wherever the record is missing, unreadable or out
|
|
995
|
-
# of phase vocabulary (tezgah_task.active returns None for those)
|
|
996
|
-
#
|
|
997
|
-
#
|
|
998
|
-
#
|
|
999
|
-
#
|
|
1000
|
-
#
|
|
999
|
+
# of phase vocabulary (tezgah_task.active returns None for those): a rule that
|
|
1000
|
+
# refused every write until a record appeared would be the fail-closed trap the
|
|
1001
|
+
# design report names, with the whole session as its blast radius. An empty
|
|
1002
|
+
# allowlist is no scope declared rather than "nothing is allowed", so the phase
|
|
1003
|
+
# is the whole requirement there.
|
|
1004
|
+
#
|
|
1005
|
+
# The refusals name the CLI move, because running it is the work the rule asks
|
|
1006
|
+
# for and the CLI vets it. E7 measured a refusal that named a way to take the
|
|
1007
|
+
# rule off (the armed arm disabled the gate in 25 of 25 runs); the switches that
|
|
1008
|
+
# do that stay behind the control-plane rule below.
|
|
1001
1009
|
TASK_PHASE_DENY = (
|
|
1002
1010
|
"Task gate: the active task %s is in phase `%s`, and this call writes. "
|
|
1003
|
-
"Writes belong to %s.
|
|
1004
|
-
"
|
|
1011
|
+
"Writes belong to %s. Finish the reading this phase is for, then move the "
|
|
1012
|
+
"phase yourself with `tezgah-task phase implementation` - it refuses until "
|
|
1013
|
+
"the plan's Acceptance, spike and checkpoint are in order.")
|
|
1005
1014
|
TASK_SCOPE_DENY = (
|
|
1006
|
-
"Task gate: %s is outside the paths the active task %s allows (%s).
|
|
1007
|
-
"
|
|
1008
|
-
"why
|
|
1009
|
-
|
|
1010
|
-
# one that lifts them, and the E7 block measured what that costs: with the unlock
|
|
1011
|
-
# printed in the refusal, the armed arm removed or disabled the gate in 25 of 25
|
|
1012
|
-
# runs and obeyed it in none (an internal benchmark, block E7). A refusal
|
|
1013
|
-
# is a boundary or it is an instruction; it cannot be both.
|
|
1015
|
+
"Task gate: %s is outside the paths the active task %s allows (%s). If the "
|
|
1016
|
+
"plan's work genuinely reaches it, widen the scope yourself with `tezgah-task "
|
|
1017
|
+
"allow <glob> ... --why \"<reason>\"` (listing the current globs too); "
|
|
1018
|
+
"otherwise leave the file alone.")
|
|
1014
1019
|
TASK_RECORD_DENY = (
|
|
1015
1020
|
"Task gate: %s is the active task's own record. The phase and the allowlist "
|
|
1016
|
-
"live in that file
|
|
1017
|
-
"
|
|
1018
|
-
TASK_UNLOCK_DENY = (
|
|
1019
|
-
"Task gate: this command changes the active task's own record. The phase and "
|
|
1020
|
-
"the allowlist are the user's, and a session that can move them is held to "
|
|
1021
|
-
"nothing. Ask the user to change the task instead.")
|
|
1022
|
-
# A shell command that changes the record. Matched on the masked text, so a
|
|
1023
|
-
# command that merely names the CLI - a grep over the docs, a commit message -
|
|
1024
|
-
# is not refused. The token is the CLI's own basename, with or without `.py`
|
|
1025
|
-
# and run through any interpreter, so any path to it counts.
|
|
1026
|
-
TASK_CHANGE = re.compile(
|
|
1027
|
-
r"(?:^|[|;&(]\s*|\s)(?:python3?\s+)?\S*tezgah-task(?:\.py)?\s+"
|
|
1028
|
-
r"(?:start|phase|allow|stop)\b")
|
|
1021
|
+
"live in that file and only `tezgah-task` writes them, because it checks the "
|
|
1022
|
+
"plan's Acceptance, spike and review on the way; change the task through it.")
|
|
1029
1023
|
# The shapes that make a shell command a write, for the phase rule below. The
|
|
1030
1024
|
# tools are not the only way to change a file, and E7b measured the other one:
|
|
1031
1025
|
# with `edit` and `write` refused twelve and six times, the armed arm wrote the
|
|
@@ -1053,8 +1047,8 @@ SHELL_WRITE = re.compile(
|
|
|
1053
1047
|
TASK_SHELL_DENY = (
|
|
1054
1048
|
"Task gate: the active task %s is in phase `%s`, and this command writes a "
|
|
1055
1049
|
"file. The shell is a write route like any other, so the phase covers it. "
|
|
1056
|
-
"
|
|
1057
|
-
"
|
|
1050
|
+
"Finish the reading this phase is for, then move the phase yourself with "
|
|
1051
|
+
"`tezgah-task phase implementation`.")
|
|
1058
1052
|
|
|
1059
1053
|
|
|
1060
1054
|
# The marker a caller passes when it has NOT read the record, so a rule reads it
|
|
@@ -1100,11 +1094,10 @@ def task_shell_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
|
1100
1094
|
def task_record_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
1101
1095
|
"""A deny reason when this write's target IS the active task's record.
|
|
1102
1096
|
|
|
1103
|
-
The
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
scope, because a scope that can widen itself is not one."""
|
|
1097
|
+
The record is a file in the repo, so an agent that may write files could
|
|
1098
|
+
retype the phase instead of running the CLI, and skip the Acceptance, spike
|
|
1099
|
+
and review checks the CLI makes on the way. Refused whatever the phase and
|
|
1100
|
+
whatever the allowlist says - the record is not inside its own scope."""
|
|
1108
1101
|
task = _task(cwd, base, task)
|
|
1109
1102
|
if not task:
|
|
1110
1103
|
return None
|
|
@@ -1182,11 +1175,9 @@ def task_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
|
1182
1175
|
# be read, and `task-off` above. A refusal has to rest on something a reader can
|
|
1183
1176
|
# check, and a rule that cannot answer must not refuse.
|
|
1184
1177
|
#
|
|
1185
|
-
# The refusal names the one command that makes the boundary real
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
# command *satisfies* the requirement - running it is the work the rule asks for,
|
|
1189
|
-
# so naming it is naming what the rule wants rather than a way around it.
|
|
1178
|
+
# The refusal names the one command that makes the boundary real: running it is
|
|
1179
|
+
# the work the rule asks for, so naming it is naming what the rule wants rather
|
|
1180
|
+
# than a way around it.
|
|
1190
1181
|
CHECKPOINT_DENY = (
|
|
1191
1182
|
"Checkpoint gate: the active task %s is in phase `implementation` and its "
|
|
1192
1183
|
"plan records a pending checkpoint (`checkpoint: pending <sha>`), so this "
|
|
@@ -1823,8 +1814,8 @@ def _refusing(label):
|
|
|
1823
1814
|
# `tezgah-gate decide` writes a genuine `began` row with `check=1`; the capture
|
|
1824
1815
|
# CLI writes snapshot rows; pony and adhd write CONFIG_DIR; `tezgah-context
|
|
1825
1816
|
# attest` re-attests a session, and a clean result clears its drift mark. The
|
|
1826
|
-
# task CLI is not here:
|
|
1827
|
-
#
|
|
1817
|
+
# task CLI is not here: the orchestrating session runs it, and the checks that
|
|
1818
|
+
# matter live in the CLI itself (see the task rule above).
|
|
1828
1819
|
# `tezgah-context user_prompt` is a prompt event, and a prompt naming a switch
|
|
1829
1820
|
# writes the `authorized` row the switch latch honors (tezgah_paths.off).
|
|
1830
1821
|
CONTROL_CLIS = {"tezgah-gate": ("decide",), "tezgah-capture": "",
|
|
@@ -2651,11 +2642,9 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
2651
2642
|
reason = race_reason(inp, session_id, cwd)
|
|
2652
2643
|
if reason:
|
|
2653
2644
|
return _deny(session_id, "race", reason, tool, inp, base)
|
|
2654
|
-
# Task scope: the
|
|
2655
|
-
#
|
|
2656
|
-
#
|
|
2657
|
-
# through the CLI is refused with it: a boundary the agent can move is not a
|
|
2658
|
-
# boundary, and both routes to moving this one were measured open (E7).
|
|
2645
|
+
# Task scope: the active plan's phase and the files its allowlist names (see
|
|
2646
|
+
# task_reason). The record itself is refused first of all: only
|
|
2647
|
+
# bin/tezgah-task writes it, through the checks it makes on the way.
|
|
2659
2648
|
if not off("task-off"):
|
|
2660
2649
|
# The record is read once and handed to every rule below: each read lists
|
|
2661
2650
|
# `.tezgah/plans/open/` and parses a plan, and this path is every write of
|
|
@@ -2667,9 +2656,6 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
2667
2656
|
reason = task_record_reason(inp, cwd, base, task)
|
|
2668
2657
|
if reason:
|
|
2669
2658
|
return _deny(session_id, "task", reason, tool, inp, base)
|
|
2670
|
-
if t in BASH_TOOLS and TASK_CHANGE.search(
|
|
2671
|
-
mask(str(inp.get("command") or ""))):
|
|
2672
|
-
return _deny(session_id, "task", TASK_UNLOCK_DENY, tool, inp, base)
|
|
2673
2659
|
if t in BASH_TOOLS:
|
|
2674
2660
|
reason = task_shell_reason(inp, cwd, base, task)
|
|
2675
2661
|
if reason:
|
package/hooks/tezgah_policy.py
CHANGED
|
@@ -236,17 +236,32 @@ rewrite it plainly.
|
|
|
236
236
|
**Merge authority: standing, not per-PR.** The user is not the approval step
|
|
237
237
|
on a merge. Review first with a FRESH reviewer (never my own read of my own
|
|
238
238
|
code), run the full suite, and if the review is clean and every test passes,
|
|
239
|
-
merge
|
|
240
|
-
|
|
241
|
-
failing test
|
|
239
|
+
merge and close the plan yourself (`tezgah-task close NNN done`, which also
|
|
240
|
+
deactivates it and removes every merged worktree), then report the outcome. Nothing
|
|
241
|
+
goes back to the user: a failing test you fix, and a critical or major finding
|
|
242
|
+
left after round two you fix yourself, prove with a test and close with
|
|
243
|
+
`tezgah-task close NNN done --resolved "<fix and test>"`. One review per plan, over its whole diff in the verification
|
|
242
244
|
phase; at most two rounds - only a confirmed critical or major finding opens
|
|
243
245
|
round two, a minor or suggestion one is recorded fix-later, and after round two
|
|
244
|
-
what is left
|
|
246
|
+
what is left is fixed by the session and closed with `--resolved`, or recorded fix-later, never a third round.
|
|
245
247
|
A round spawns its reviewers in ONE message, one per dimension (correctness
|
|
246
248
|
and contract, security, tests and performance) or file group; a diff under 300
|
|
247
249
|
changed lines gets one reviewer. Per-task work gets tests, not a reviewer, and
|
|
248
250
|
`tezgah-task review` refuses a third round.
|
|
249
251
|
|
|
252
|
+
**The process is yours: never hand a step back.** The orchestrator runs the
|
|
253
|
+
plan lifecycle itself - `tezgah-task start`, every `phase` move, `review`,
|
|
254
|
+
`close`, the merge, and removing a slice's worktree once its branch is merged -
|
|
255
|
+
and never ends a reply with "run this command" or "shall I close it?" for a
|
|
256
|
+
step it can take. A process parameter the user did not set (a spend ceiling, a
|
|
257
|
+
sample size, a retry budget, a member to consult) is picked by you: choose the
|
|
258
|
+
conservative default, state it in one line, proceed. A credential, a login or an
|
|
259
|
+
access grant is the one thing no tool supplies, and even then take every route
|
|
260
|
+
that needs none first - another consult member or provider, local compute
|
|
261
|
+
instead of managed, a public source - and ask once, naming what it unlocks, only
|
|
262
|
+
when no route is left. A requested item you would drop is still asked about
|
|
263
|
+
(see "Deliver the whole ask").
|
|
264
|
+
|
|
250
265
|
**Attribution: none, anywhere, ever.** Nothing you persist or publish may name
|
|
251
266
|
the assistant, model, vendor or "AI" as author, co-author, generator or helper -
|
|
252
267
|
on any host (Claude, opencode, Codex, Cursor, dsh, omp), including subagents and cheap
|
|
@@ -402,10 +417,11 @@ question needs facts newer or wider than the codebase - current versions, CVEs,
|
|
|
402
417
|
vendor status, breaking-change news; skip it for pure code/design reasoning.
|
|
403
418
|
|
|
404
419
|
Exit 4 means no choice is recorded and exit 5 means every recorded member
|
|
405
|
-
failed: nothing answered, and the output lists `offer:`/`reoffer:` members.
|
|
406
|
-
the user
|
|
407
|
-
|
|
408
|
-
|
|
420
|
+
failed: nothing answered, and the output lists `offer:`/`reoffer:` members. Do
|
|
421
|
+
not ask the user: record the first listed member from a model family other than
|
|
422
|
+
your own (two make a panel) with `{CONSULT_BIN} --use <member>[,<member>]`, ask
|
|
423
|
+
again, and say which member you chose. After two such rounds with nothing
|
|
424
|
+
answering, report the second opinion as skipped and carry on.
|
|
409
425
|
|
|
410
426
|
Treat every answer as advisory evidence, never as truth: verify each claim
|
|
411
427
|
against the actual code before adopting it, and tell the user which models were
|
|
@@ -453,8 +469,9 @@ are what keeps results comparable, and breaking one silently invalidates the run
|
|
|
453
469
|
never edit a node once a run has answered it (branch a child instead); the run
|
|
454
470
|
command and environment are a fixed contract identical on every node; vary the
|
|
455
471
|
committed code/config, never CLI args or env knobs; grow the experiment tree
|
|
456
|
-
downward, not sideways. Local research needs no login; managed compute does
|
|
457
|
-
|
|
472
|
+
downward, not sideways. Local research needs no login; managed compute does -
|
|
473
|
+
run locally at a scale that fits, and ask for `{ORX_BIN} login` only when the
|
|
474
|
+
question cannot be answered without managed compute.
|
|
458
475
|
|
|
459
476
|
A result row and every claim built on it say what they were measured on: `real`
|
|
460
477
|
for the running system, `fixture` for an input you generated, `derived` for a
|
|
@@ -806,8 +823,10 @@ never your own summary, conclusion or self-assessment - a reviewer handed the
|
|
|
806
823
|
author's framing finds measurably fewer defects. It answers with independent
|
|
807
824
|
models, then one referee: read back the referee's named fields (recommendation,
|
|
808
825
|
key disagreements, unchecked assumptions, what would change its mind, requested
|
|
809
|
-
evidence), not a paraphrase. On exit 4/5
|
|
810
|
-
member
|
|
826
|
+
evidence), not a paraphrase. On exit 4/5 record the first listed
|
|
827
|
+
`offer:`/`reoffer:` member of another model family yourself with `--use` and ask
|
|
828
|
+
again - never ask the user to choose; after two failed rounds say it was
|
|
829
|
+
skipped. Relay the failure
|
|
811
830
|
class and retry line it prints, verify every claim against the code, report
|
|
812
831
|
which models disagreed, and if nothing answered say the second opinion was
|
|
813
832
|
skipped. Skip trivial local edits.
|
|
@@ -871,7 +890,9 @@ evidence class - `user-verbatim`, `behaviour`, `ui-observed`, `code`, `external`
|
|
|
871
890
|
and a finding without one is a question, not a finding. Run it as a research line
|
|
872
891
|
(`{RESEARCH_BIN}`), say what it did not look at, and read the `product-analysis`
|
|
873
892
|
skill for the artifact shape and the scorecard, and `feature-audit` for the
|
|
874
|
-
layer-coherence matrices when the ask is one feature.
|
|
893
|
+
layer-coherence matrices when the ask is one feature. A new capability built from
|
|
894
|
+
data or an ask runs the `product-flow` skill: `{RESEARCH_BIN} next --topic <t>` is
|
|
895
|
+
the step source, and its first missing or due step is the next one. Off: `research-off`.
|
|
875
896
|
|
|
876
897
|
**No AI attribution, ever, on any host.** Nothing persisted or published may
|
|
877
898
|
name the assistant, model, vendor or "AI" as author/co-author/generator/helper:
|
|
@@ -922,7 +943,10 @@ its own branch and checkout - inside Orca `orca worktree create --name <slug>
|
|
|
922
943
|
--parent-worktree active --json`, else `git worktree add -b <branch> <absolute
|
|
923
944
|
path>`; its brief names every read, edit and shell `cwd` as an absolute path
|
|
924
945
|
under that checkout, never the main checkout, whose `git status --short` must
|
|
925
|
-
not grow. Read-only slices need no worktree.
|
|
946
|
+
not grow. Read-only slices need no worktree. Once a slice's branch is merged,
|
|
947
|
+
its worktree goes in the same turn - `tezgah-task close` removes every merged
|
|
948
|
+
one, and outside a plan `orca worktree rm --worktree path:<p>` (else `git
|
|
949
|
+
worktree remove <p>`) does it; never leave one for the user to find. Pick each agent by task class from
|
|
926
950
|
the specialists line; `{ROUTE_BIN}` names the tier for code work. Serialize
|
|
927
951
|
only a real dependency - one slice's output is another's input, or two slices
|
|
928
952
|
edit one file - and say which. Never wait idle on a spawned slice while
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The product-flow playbook (plan 074, ADR 025): the one place its steps are
|
|
2
|
+
written. `next_steps` computes each step's status from the topic's files under
|
|
3
|
+
`.tezgah/analysis/<topic>/` - `flow.md`, one `## <step>` heading per step, and
|
|
4
|
+
`spec.md` - and nothing stores it."""
|
|
5
|
+
import datetime
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
import tezgah_paths as tp
|
|
10
|
+
|
|
11
|
+
# A topic becomes a path below, so anything outside kebab-case reads no file.
|
|
12
|
+
TOPIC = re.compile(r"(?=.{1,64}\Z)[a-z0-9]+(?:-[a-z0-9]+)*")
|
|
13
|
+
OBSERVE = re.compile(r"observe after:\s*(\d{4}-\d{2}-\d{2})", re.I)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _step(sid, skill, first=""):
|
|
17
|
+
"""A step whose artifact is its `## <sid>` note in flow.md; `first` is what
|
|
18
|
+
the session does before writing it."""
|
|
19
|
+
return {"id": sid, "skill": skill,
|
|
20
|
+
"leaves": "## %s in .tezgah/analysis/<topic>/flow.md" % sid,
|
|
21
|
+
"command": "%swrite ## %s in .tezgah/analysis/{topic}/flow.md"
|
|
22
|
+
% (first, sid)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# `{topic}` in a command is filled per topic. The design check and the Stop UI
|
|
26
|
+
# rule judge design and build; the flow only asks for their note.
|
|
27
|
+
_STEPS = (
|
|
28
|
+
_step("research", "research"),
|
|
29
|
+
_step("metrics", "product-analysis"),
|
|
30
|
+
_step("assumptions", "product-analysis"),
|
|
31
|
+
_step("decision", "product-analysis"),
|
|
32
|
+
_step("instrumentation", "product-analysis"),
|
|
33
|
+
{"id": "forward-spec", "skill": "feature-audit",
|
|
34
|
+
"leaves": ".tezgah/analysis/<topic>/spec.md",
|
|
35
|
+
"command": "feature-audit forward mode: write .tezgah/analysis/{topic}/spec.md "
|
|
36
|
+
"naming the decision"},
|
|
37
|
+
_step("design", "design-contract", "tezgah-design check, then "),
|
|
38
|
+
_step("build", "design-contract", "tezgah-design check, then "),
|
|
39
|
+
_step("re-measure", "analyze-app", "measure the decision's metric again, then "),
|
|
40
|
+
)
|
|
41
|
+
FLOWS = {
|
|
42
|
+
"metric-dashboard": _STEPS,
|
|
43
|
+
# A feature may reuse a metric another topic owns: its own metric steps join
|
|
44
|
+
# the flow only when its flow.md holds `## metrics` (`next_steps`).
|
|
45
|
+
"feature": tuple(s for s in _STEPS if s["id"] not in ("metrics", "instrumentation")),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sections(text):
|
|
50
|
+
"""{step: body} of the `## <step>` headings in `text`, lower-cased."""
|
|
51
|
+
parts = re.split(r"^##[ \t]+(.+?)[ \t]*$", text, flags=re.M)
|
|
52
|
+
return {parts[i].lower(): parts[i + 1] for i in range(1, len(parts) - 1, 2)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read(path):
|
|
56
|
+
try:
|
|
57
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
58
|
+
return fh.read()
|
|
59
|
+
except OSError:
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def next_steps(repo, topic, flow="metric-dashboard", today=""):
|
|
64
|
+
"""[{"id", "skill", "status", "detail", "command"}] for `topic` under `flow`,
|
|
65
|
+
status one of done | missing | due, read from `.tezgah/analysis/<topic>/` of
|
|
66
|
+
`repo`. A topic outside TOPIC or with no folder lists every step missing."""
|
|
67
|
+
if flow not in FLOWS:
|
|
68
|
+
raise ValueError("unknown flow %r (%s)" % (flow, ", ".join(FLOWS)))
|
|
69
|
+
try:
|
|
70
|
+
day = datetime.date.fromisoformat(today) if today else datetime.date.today()
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise ValueError("today %r is not YYYY-MM-DD" % (today,)) from None
|
|
73
|
+
safe = isinstance(topic, str) and TOPIC.fullmatch(topic)
|
|
74
|
+
rel = os.path.join(".tezgah", "analysis", topic if safe else "<topic>")
|
|
75
|
+
folder = os.path.join(tp.workspace(repo), "analysis", topic) if safe else ""
|
|
76
|
+
notes = _sections(_read(os.path.join(folder, "flow.md"))) if folder else {}
|
|
77
|
+
flow_rel = os.path.join(rel, "flow.md")
|
|
78
|
+
spec_rel = os.path.join(rel, "spec.md")
|
|
79
|
+
steps = FLOWS["metric-dashboard"] if flow == "feature" and "metrics" in notes \
|
|
80
|
+
else FLOWS[flow]
|
|
81
|
+
|
|
82
|
+
def status(sid):
|
|
83
|
+
if sid == "forward-spec":
|
|
84
|
+
return ("done", "%s exists" % spec_rel) \
|
|
85
|
+
if folder and os.path.isfile(os.path.join(folder, "spec.md")) \
|
|
86
|
+
else ("missing", "%s does not exist" % spec_rel)
|
|
87
|
+
if sid in notes:
|
|
88
|
+
return "done", "%s ## %s" % (flow_rel, sid)
|
|
89
|
+
if sid == "re-measure":
|
|
90
|
+
after = OBSERVE.search(notes.get("decision", ""))
|
|
91
|
+
try:
|
|
92
|
+
when = datetime.date.fromisoformat(after.group(1)) if after else None
|
|
93
|
+
except ValueError:
|
|
94
|
+
when = None
|
|
95
|
+
if when is not None:
|
|
96
|
+
return ("due", "observe after %s has passed" % when) if day > when \
|
|
97
|
+
else ("missing", "observe after %s, not yet" % when)
|
|
98
|
+
return "missing", "no observe after: in %s ## decision" % flow_rel
|
|
99
|
+
return "missing", "no ## %s in %s" % (sid, flow_rel)
|
|
100
|
+
|
|
101
|
+
out = []
|
|
102
|
+
for s in steps:
|
|
103
|
+
state, detail = status(s["id"])
|
|
104
|
+
out.append({"id": s["id"], "skill": s["skill"], "status": state,
|
|
105
|
+
"detail": detail, "command": s["command"].format(
|
|
106
|
+
topic=topic if safe else "<topic>")})
|
|
107
|
+
return out
|
package/hooks/tezgah_research.py
CHANGED
|
@@ -2842,9 +2842,9 @@ def _check_append_only(repo, label, path, errors, warnings, hard):
|
|
|
2842
2842
|
old = [line for line in older.splitlines() if line.strip()]
|
|
2843
2843
|
new = [line for line in newer.splitlines() if line.strip()]
|
|
2844
2844
|
if new[:len(old)] != old:
|
|
2845
|
-
_soft(errors, warnings, hard, "%s was rewritten after the run: a
|
|
2846
|
-
"row changed or went missing, and results are
|
|
2847
|
-
"correction is a new row" % label)
|
|
2845
|
+
_soft(errors, warnings, hard, "%s was rewritten after the run: a "
|
|
2846
|
+
"committed row changed or went missing, and results are "
|
|
2847
|
+
"append-only - a correction is a new row" % label)
|
|
2848
2848
|
return
|
|
2849
2849
|
|
|
2850
2850
|
|
|
@@ -347,10 +347,40 @@ def skill_roots(extra=()):
|
|
|
347
347
|
return out
|
|
348
348
|
|
|
349
349
|
|
|
350
|
+
# One routing line of a skill's own INDEX.md: `name - what it makes - path`.
|
|
351
|
+
_INDEX_ROW = re.compile(r"^\S+ - .+ - (\S+/SKILL\.md)\s*$")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _entries(root, name):
|
|
355
|
+
"""[(skill, path)] for one skill directory: its SKILL.md, then every entry its
|
|
356
|
+
INDEX.md routes to. design-library keeps 39 entries under
|
|
357
|
+
`<plugin>/skills/<entry>/SKILL.md` and reaches them only through that index,
|
|
358
|
+
so a search over top-level files alone never saw a word of them. A nested
|
|
359
|
+
entry is addressed `<skill>/<path>`, the form `skill://` resolves."""
|
|
360
|
+
base = os.path.join(root, name)
|
|
361
|
+
out = [(name, os.path.join(base, "SKILL.md"))]
|
|
362
|
+
try:
|
|
363
|
+
with open(os.path.join(base, "INDEX.md"), encoding="utf-8") as fh:
|
|
364
|
+
lines = fh.read().splitlines()
|
|
365
|
+
except (OSError, UnicodeDecodeError):
|
|
366
|
+
return out
|
|
367
|
+
# A row is followed only inside the skill's own directory: an absolute path,
|
|
368
|
+
# a `..` or a symlink out would index a file the skill does not ship.
|
|
369
|
+
inside = os.path.realpath(base) + os.sep
|
|
370
|
+
for line in lines:
|
|
371
|
+
hit = _INDEX_ROW.match(line)
|
|
372
|
+
if not hit or os.path.isabs(hit.group(1)):
|
|
373
|
+
continue
|
|
374
|
+
path = os.path.join(base, hit.group(1))
|
|
375
|
+
if os.path.realpath(path).startswith(inside):
|
|
376
|
+
out.append(("%s/%s" % (name, hit.group(1)), path))
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
350
380
|
def _rows(roots=None):
|
|
351
381
|
"""[(skill, path, title, start, end, text)] over every installed skill's
|
|
352
|
-
sections,
|
|
353
|
-
is one entry, not five."""
|
|
382
|
+
sections, and the entries a skill's INDEX.md routes to, deduplicated on the
|
|
383
|
+
file's realpath so a skill five hosts link is one entry, not five."""
|
|
354
384
|
out, seen = [], set()
|
|
355
385
|
for root in (skill_roots() if roots is None else roots):
|
|
356
386
|
try:
|
|
@@ -358,14 +388,23 @@ def _rows(roots=None):
|
|
|
358
388
|
except OSError:
|
|
359
389
|
continue
|
|
360
390
|
for name in names:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for title, start, end, text in sections(path):
|
|
365
|
-
if (path, start) in seen:
|
|
391
|
+
for skill, raw in _entries(root, name):
|
|
392
|
+
path = os.path.realpath(raw)
|
|
393
|
+
if not os.path.isfile(path):
|
|
366
394
|
continue
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
secs = sections(path)
|
|
396
|
+
if "/" in skill and secs:
|
|
397
|
+
# A routed entry is one row, the whole file: its short
|
|
398
|
+
# sections out-scored whole skills on two common words
|
|
399
|
+
# ("give", "thing") and silenced the right hint, while the
|
|
400
|
+
# entry as a whole still carries its topic's words.
|
|
401
|
+
secs = [(secs[0][0], secs[0][1], secs[-1][2],
|
|
402
|
+
"\n".join(s[3] for s in secs))]
|
|
403
|
+
for title, start, end, text in secs:
|
|
404
|
+
if (path, start) in seen:
|
|
405
|
+
continue
|
|
406
|
+
seen.add((path, start))
|
|
407
|
+
out.append((skill, path, title, start, end, text))
|
|
369
408
|
return out
|
|
370
409
|
|
|
371
410
|
|
|
@@ -490,9 +529,10 @@ def rank_postings(query, posts, lengths, k):
|
|
|
490
529
|
|
|
491
530
|
|
|
492
531
|
def _signature():
|
|
493
|
-
"""[[root, skill, mtime_ns, size]] per installed SKILL.md
|
|
494
|
-
keys on. The file's own stat, not its
|
|
495
|
-
removing one skill is exactly the
|
|
532
|
+
"""[[root, skill, mtime_ns, size]] per installed SKILL.md and per entry an
|
|
533
|
+
INDEX.md routes to: what a rebuild keys on. The file's own stat, not its
|
|
534
|
+
directory's - installing, editing or removing one skill is exactly the
|
|
535
|
+
change the cache must notice."""
|
|
496
536
|
sig = []
|
|
497
537
|
for root in skill_roots():
|
|
498
538
|
try:
|
|
@@ -500,11 +540,12 @@ def _signature():
|
|
|
500
540
|
except OSError:
|
|
501
541
|
continue
|
|
502
542
|
for name in names:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
543
|
+
for skill, path in _entries(root, name):
|
|
544
|
+
try:
|
|
545
|
+
st = os.stat(path)
|
|
546
|
+
except OSError:
|
|
547
|
+
continue
|
|
548
|
+
sig.append([root, skill, st.st_mtime_ns, st.st_size])
|
|
508
549
|
return sig
|
|
509
550
|
|
|
510
551
|
|
package/hooks/tezgah_task.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""The active task: one plan file's phase and its path allowlist.
|
|
3
3
|
|
|
4
4
|
A task is not a second record beside the plan - it IS the plan file under
|
|
5
|
-
`.tezgah/plans/open/`, with two optional frontmatter keys that
|
|
6
|
-
(bin/tezgah-task
|
|
5
|
+
`.tezgah/plans/open/`, with two optional frontmatter keys that only its CLI
|
|
6
|
+
(bin/tezgah-task, run by the orchestrating session itself) writes:
|
|
7
7
|
|
|
8
8
|
phase: implementation # discovery | implementation | verification
|
|
9
9
|
allowed_paths: # globs, relative to the repo root
|