@r1z4x/tezgah 1.8.3 → 1.9.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 +35 -1
- package/MANIFEST +6 -0
- package/bin/tezgah-docs +5 -4
- package/bin/tezgah-optimize +338 -0
- package/bin/tezgah-research +2 -2
- package/bin/tezgah-route +6 -2
- package/bin/tezgah-setup +15 -1
- package/bin/tezgah-task +239 -27
- package/bin/tezgah-taste +3 -1
- package/docs/architecture.md +8 -0
- package/docs/clarity-baseline.json +1 -1
- package/docs/environment.md +1 -1
- package/docs/evidence.md +23 -20
- package/docs/gate.md +19 -14
- package/docs/models.md +9 -2
- package/docs/operations.md +9 -5
- package/docs/testing.md +18 -14
- package/hooks/lang/en.json +3 -0
- package/hooks/lang/tr.json +3 -0
- package/hooks/tezgah_context.py +31 -19
- package/hooks/tezgah_gate.py +75 -36
- package/hooks/tezgah_integrity.py +90 -134
- package/hooks/tezgah_models.py +62 -26
- package/hooks/tezgah_paths.py +84 -28
- package/hooks/tezgah_policy.py +10 -4
- package/hooks/tezgah_research.py +14 -8
- package/hooks/tezgah_stopspec.py +36 -83
- package/hooks/tezgah_store.py +7 -0
- package/hooks/tezgah_task.py +104 -35
- package/hosts/opencode/plugins/tezgah.js +136 -0
- package/output-styles/tezgah.md +5 -2
- package/package.json +1 -1
- package/skills/plan-add/SKILL.md +8 -4
- package/skills/tezgah-contract/SKILL.md +14 -0
package/hooks/tezgah_task.py
CHANGED
|
@@ -471,9 +471,10 @@ def parallel(rows):
|
|
|
471
471
|
|
|
472
472
|
|
|
473
473
|
def active(cwd, base, held=False):
|
|
474
|
-
"""The active task as a dict, or None. A linked worktree on a
|
|
475
|
-
|
|
476
|
-
|
|
474
|
+
"""The active task as a dict, or None. A linked worktree on a branch that
|
|
475
|
+
binds a plan - `plan/NNN-...` or the plan's declared `branch:` - answers that
|
|
476
|
+
plan when it carries a phase (`branch_task`), so two plans can run at once,
|
|
477
|
+
one per worktree. Anywhere else - the main
|
|
477
478
|
checkout, or a worktree on another branch - it reads the main checkout's
|
|
478
479
|
`.tezgah/plans/open/*.md` sorted by name: the first plan carrying a valid
|
|
479
480
|
`phase` that no linked worktree holds (`plan_holders`). When every phased
|
|
@@ -498,8 +499,9 @@ def active(cwd, base, held=False):
|
|
|
498
499
|
main = linked_main(root) or root
|
|
499
500
|
records = _phased(main)
|
|
500
501
|
branches = _worktree_branches(main) if records else {}
|
|
502
|
+
declared = declared_branches(main) if branches else {}
|
|
501
503
|
for record in records:
|
|
502
|
-
if not any(_binds(record["id"], b) for b in branches):
|
|
504
|
+
if not any(_binds(record["id"], b, declared) for b in branches):
|
|
503
505
|
return record
|
|
504
506
|
if not held or not records:
|
|
505
507
|
return None
|
|
@@ -570,19 +572,50 @@ def worktree_branch(top):
|
|
|
570
572
|
return head[len(prefix):] if head.startswith(prefix) else None
|
|
571
573
|
|
|
572
574
|
|
|
573
|
-
def
|
|
574
|
-
"""
|
|
575
|
+
def declared_branches(main):
|
|
576
|
+
"""{branch: plan id} for every open plan under the main checkout `main` that
|
|
577
|
+
declares a `branch:` - Orca names a worktree's branch `<user>/<slug>`, and
|
|
578
|
+
the plan declaring it is the one that worktree holds (plan 105). Read from
|
|
579
|
+
the plan files' frontmatter, never from git: the gate asks on every write.
|
|
580
|
+
Two plans declaring one branch: the lowest id binds it, so the answer never
|
|
581
|
+
depends on which file a directory listing names first."""
|
|
582
|
+
out = {}
|
|
583
|
+
for name, _, text in _plan_files(main, "open"):
|
|
584
|
+
fields = frontmatter(text)
|
|
585
|
+
branch, ident = fields.get("branch", ""), _plan_id(fields, name)
|
|
586
|
+
if branch and (branch not in out or ident < out[branch]):
|
|
587
|
+
out[branch] = ident
|
|
588
|
+
return out
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def plan_of_branch(branch, main=None):
|
|
592
|
+
"""The plan id `branch` binds. Without `main` it is the bare parse: NNN for
|
|
593
|
+
a `plan/NNN-slug` branch, else None. Given the main checkout `main`, it is
|
|
594
|
+
an open plan's id only: NNN when plan NNN is open there, else the open plan
|
|
595
|
+
declaring `branch` (`declared_branches`), else None - so `plan/999-nope`
|
|
596
|
+
binds nothing (review of plan 105, F3). One pass over the open plans."""
|
|
575
597
|
found = _PLAN_BRANCH.match(branch or "")
|
|
576
|
-
|
|
598
|
+
if not main:
|
|
599
|
+
return found.group(1) if found else None
|
|
600
|
+
declared = None
|
|
601
|
+
for name, _, text in _plan_files(main, "open") if branch else ():
|
|
602
|
+
fields = frontmatter(text)
|
|
603
|
+
ident = _plan_id(fields, name)
|
|
604
|
+
if found and ident == found.group(1):
|
|
605
|
+
return ident
|
|
606
|
+
if fields.get("branch") == branch and (declared is None or ident < declared):
|
|
607
|
+
declared = ident # the lowest id, as `declared_branches` picks
|
|
608
|
+
return declared
|
|
577
609
|
|
|
578
610
|
|
|
579
|
-
def _binds(ident, branch):
|
|
580
|
-
"""Does `branch` hold plan `ident`? It has to name the plan: `plan/NNN
|
|
581
|
-
|
|
582
|
-
run on
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
611
|
+
def _binds(ident, branch, declared):
|
|
612
|
+
"""Does `branch` hold plan `ident`? It has to name the plan: `plan/NNN-...`,
|
|
613
|
+
or be the branch the plan declares (`declared`, `declared_branches`'s map).
|
|
614
|
+
The slices a plan fans out to run on other `plan/NNN-...` branches
|
|
615
|
+
(`plan/097-records` beside `plan/097-workspace-integrity`): each slice is
|
|
616
|
+
held to its plan's scope, never to another plan's, which is what a
|
|
617
|
+
first-unheld-plan reading gave it (plan 097, D6)."""
|
|
618
|
+
return bool(branch) and (plan_of_branch(branch) or declared.get(branch)) == ident
|
|
586
619
|
|
|
587
620
|
|
|
588
621
|
def _worktree_branches(main):
|
|
@@ -606,28 +639,30 @@ def plan_holders(main):
|
|
|
606
639
|
out = {}
|
|
607
640
|
if not branches:
|
|
608
641
|
return out
|
|
642
|
+
declared = declared_branches(main)
|
|
609
643
|
for name, _, text in _plan_files(main, "open"):
|
|
610
644
|
fields = frontmatter(text)
|
|
611
645
|
ident = _plan_id(fields, name)
|
|
612
|
-
held = sorted(b for b in branches if _binds(ident, b))
|
|
646
|
+
held = sorted(b for b in branches if _binds(ident, b, declared))
|
|
613
647
|
if held:
|
|
614
|
-
|
|
615
|
-
out[ident] = branches[
|
|
648
|
+
own = fields.get("branch", "")
|
|
649
|
+
out[ident] = branches[own if own in held else held[0]]
|
|
616
650
|
return out
|
|
617
651
|
|
|
618
652
|
|
|
619
653
|
def branch_task(top):
|
|
620
654
|
"""Plan NNN's record (active()'s dict, plus "worktree": `top`, the checkout
|
|
621
655
|
whose HEAD and tree its checkpoint is read against) when `top` is a linked
|
|
622
|
-
worktree on a
|
|
623
|
-
|
|
624
|
-
|
|
656
|
+
worktree on a branch that binds plan NNN (`plan_of_branch`: `plan/NNN-...`
|
|
657
|
+
or the plan's declared `branch:`) and plan NNN, open under the main
|
|
658
|
+
checkout, carries a phase. None otherwise - another branch, a plan with no
|
|
659
|
+
phase, an unknown mapping - and the caller reads the main checkout's task
|
|
625
660
|
(plan 093)."""
|
|
626
|
-
ident = plan_of_branch(worktree_branch(top))
|
|
627
|
-
if ident is None:
|
|
628
|
-
return None
|
|
629
661
|
from tezgah_paths import linked_main
|
|
630
662
|
main = linked_main(top)
|
|
663
|
+
ident = plan_of_branch(worktree_branch(top), main)
|
|
664
|
+
if ident is None:
|
|
665
|
+
return None
|
|
631
666
|
for record in _phased(main, ident) if main else ():
|
|
632
667
|
record["worktree"] = top
|
|
633
668
|
return record
|
|
@@ -636,7 +671,7 @@ def branch_task(top):
|
|
|
636
671
|
|
|
637
672
|
def worktree_task(path, cwd, base):
|
|
638
673
|
"""The record a write to `path` is held to when it lands in a sibling linked
|
|
639
|
-
worktree on a plan branch (`branch_task`), else None. A path under cwd's own
|
|
674
|
+
worktree on a plan's branch (`branch_task`), else None. A path under cwd's own
|
|
640
675
|
root is active()'s to answer, so it pays one realpath and nothing more."""
|
|
641
676
|
root = repo_root(cwd, base)
|
|
642
677
|
path = os.path.expanduser(path)
|
|
@@ -883,6 +918,35 @@ def set_box(path, n, checked):
|
|
|
883
918
|
return items[n - 1]
|
|
884
919
|
|
|
885
920
|
|
|
921
|
+
def set_unmet(path, n, reason):
|
|
922
|
+
"""Append `UNMET: <reason>` to the last line of Acceptance item `n` (1-based,
|
|
923
|
+
as `tick` counts) and return the item, or None when the plan has no item `n`.
|
|
924
|
+
|
|
925
|
+
The marker has to land inside the item's own text - `open_items` reads it
|
|
926
|
+
there, and it is the predicate `close NNN done` refuses on - and an item
|
|
927
|
+
wraps onto the lines below its box, up to the next item or a blank line, so
|
|
928
|
+
the anchor is the item's last line and not its first (plan 110). Raises
|
|
929
|
+
OSError."""
|
|
930
|
+
with plans_lock(path):
|
|
931
|
+
lines = _lines(path)
|
|
932
|
+
items = acceptance_items("\n".join(lines))
|
|
933
|
+
if not 1 <= n <= len(items):
|
|
934
|
+
return None
|
|
935
|
+
end = items[n - 1]["line"] - 1
|
|
936
|
+
for i in range(end + 1, len(lines)):
|
|
937
|
+
# the section ends at the next `## ` heading as well, which is where
|
|
938
|
+
# `acceptance_items` stops: without it an item that ended straight at
|
|
939
|
+
# a heading swallowed the line under it into the item's own text
|
|
940
|
+
# (review of plan 110, F1)
|
|
941
|
+
if (not lines[i].strip() or _ITEM.match(lines[i])
|
|
942
|
+
or lines[i].lstrip().startswith("## ")):
|
|
943
|
+
break
|
|
944
|
+
end = i
|
|
945
|
+
lines[end] = lines[end].rstrip() + " UNMET: " + reason
|
|
946
|
+
_write(path, "\n".join(lines))
|
|
947
|
+
return acceptance_items("\n".join(lines))[n - 1]
|
|
948
|
+
|
|
949
|
+
|
|
886
950
|
def set_section(path, heading, body):
|
|
887
951
|
"""Replace the body of the last `## <heading>` section - the newest, since
|
|
888
952
|
a plan appends a dated `## State (...)` - with `body` and one blank line,
|
|
@@ -915,10 +979,8 @@ def set_section(path, heading, body):
|
|
|
915
979
|
|
|
916
980
|
SLICE_KINDS = ("agent", "job")
|
|
917
981
|
SLICE_HOSTS = ("omp", "claude", "codex", "cursor", "opencode", "dsh")
|
|
918
|
-
# `stale`:
|
|
919
|
-
#
|
|
920
|
-
# event) or a child that died without writing its end. It leaves the pending
|
|
921
|
-
# list without being read as finished or as failed.
|
|
982
|
+
# `stale`: an idle background job whose result stays in its parent transcript.
|
|
983
|
+
# An agent with no observed end stays owed as stopped with no failure class.
|
|
922
984
|
SLICE_STATUSES = ("open", "done", "stopped", "resumed", "stale")
|
|
923
985
|
SLICE_CLOSED = ("done", "stopped", "resumed", "stale")
|
|
924
986
|
SLICE_IDLE_SECONDS = 6 * 3600
|
|
@@ -1372,11 +1434,12 @@ def open_slice(root, sid, *, kind="agent", host="", agent="", brief="", parent="
|
|
|
1372
1434
|
# prompt and Stop - never globs (40 ms on 1393 project dirs, measured)
|
|
1373
1435
|
rec["transcript_dir"] = _omp_session_dir(rec["parent"]) or ""
|
|
1374
1436
|
# the slice's own plan: the review guard reads its own plan's records only.
|
|
1375
|
-
# Its worktree's plan branch
|
|
1376
|
-
#
|
|
1377
|
-
# reads - with two plans running
|
|
1437
|
+
# Its worktree's plan branch (`plan/NNN-...` or a plan's declared one), else
|
|
1438
|
+
# the plan its task or shared brief names, else the tag a re-open already
|
|
1439
|
+
# carries, else the task the main checkout reads - with two plans running
|
|
1440
|
+
# that is only one of them
|
|
1378
1441
|
named = _BRIEF_PLAN.search(own) or _BRIEF_PLAN.search(brief or "")
|
|
1379
|
-
rec["plan"] = (plan_of_branch(rec.get("branch")) or (named and named.group(1))
|
|
1442
|
+
rec["plan"] = (plan_of_branch(rec.get("branch"), main) or (named and named.group(1))
|
|
1380
1443
|
or rec.get("plan") or (active(main, main) or {}).get("id") or "")
|
|
1381
1444
|
rec.update(base=_git(main, "rev-parse", "HEAD") or "", status="open",
|
|
1382
1445
|
failure="", updated=now)
|
|
@@ -1649,8 +1712,9 @@ def pending(root):
|
|
|
1649
1712
|
omp record still open whose child transcript has ended is closed first:
|
|
1650
1713
|
stopped with its failure class, or done when it ended normally. An open
|
|
1651
1714
|
record nothing has moved for SLICE_IDLE_SECONDS (an omp child's transcript
|
|
1652
|
-
included)
|
|
1653
|
-
at most one SLICE_TAIL read per
|
|
1715
|
+
included) closes stopped with no failure class; only a `job` past
|
|
1716
|
+
SLICE_JOB_SECONDS closes stale. One stat and at most one SLICE_TAIL read per
|
|
1717
|
+
open omp record."""
|
|
1654
1718
|
out = []
|
|
1655
1719
|
for rec in slices(root):
|
|
1656
1720
|
if rec.get("status") != "open":
|
|
@@ -1680,7 +1744,12 @@ def pending(root):
|
|
|
1680
1744
|
except OSError:
|
|
1681
1745
|
pass
|
|
1682
1746
|
if rec.get("status") == "open" and idle is not None and idle > limit:
|
|
1683
|
-
|
|
1747
|
+
# A job's result stays in its parent transcript; an agent with no
|
|
1748
|
+
# observed end stays stopped and offered for resume. Unknown is not
|
|
1749
|
+
# cut_off: slice_stats skips a class it cannot name, so signature
|
|
1750
|
+
# counts go quiet for hosts whose ends tezgah cannot observe.
|
|
1751
|
+
job = rec.get("kind") == "job"
|
|
1752
|
+
rec = close_slice(root, rec["id"], "stale" if job else "stopped", "") or rec
|
|
1684
1753
|
if rec.get("status") in ("open", "stopped"):
|
|
1685
1754
|
out.append(rec)
|
|
1686
1755
|
return out
|
|
@@ -3201,3 +3201,139 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3201
3201
|
// (TEZGAH_STATUS_BIN) is the safe path today.
|
|
3202
3202
|
}
|
|
3203
3203
|
}
|
|
3204
|
+
|
|
3205
|
+
// opencode 2 validates a plugin module's DEFAULT export: an object with a string
|
|
3206
|
+
// `id` and a `setup(ctx)` that registers hooks through the v2 context - it calls
|
|
3207
|
+
// nothing else, so a module with only the callable above is rejected at load
|
|
3208
|
+
// with `PluginModule.LoadError: Plugin must export a default definition with an
|
|
3209
|
+
// id and an effect or setup function` (measured on opencode v2.0.30, and the
|
|
3210
|
+
// reason the host showed both entrypoints as failed plugins). opencode 1 calls
|
|
3211
|
+
// the exported callable and uses the hook map it returns. One file serves both,
|
|
3212
|
+
// as the sibling plugin on this machine does
|
|
3213
|
+
// (~/.config/opencode/plugins/orca-opencode-status.js).
|
|
3214
|
+
//
|
|
3215
|
+
// v2 merged the v1 `input` and `output` objects into ONE mutable event
|
|
3216
|
+
// (services/www/src/docs/content/build/plugins/migrate-v1.mdx), so the adapter
|
|
3217
|
+
// passes the event as the v1 `input` and rebuilds the v1 `output` field the v1
|
|
3218
|
+
// callback reads (`args` from `output.args`). The fields below are the ones a
|
|
3219
|
+
// probe plugin measured on opencode v2.0.30: `execute.before` carries
|
|
3220
|
+
// {tool, sessionID, agent, messageID, id, input}. A refusal is a thrown Error in
|
|
3221
|
+
// both generations and the v1 callback already throws it, so the deny path is
|
|
3222
|
+
// the same object and no decision is rewritten here.
|
|
3223
|
+
//
|
|
3224
|
+
// Registered in this piece: the tool envelopes (the enforcement path), the shell
|
|
3225
|
+
// env hook, the session prompt and the event subscription. permission/evaluate,
|
|
3226
|
+
// the compaction hook and the TUI plugin still need their own probe of the v2
|
|
3227
|
+
// event before they are registered - guessing a field there would silently drop a
|
|
3228
|
+
// decision.
|
|
3229
|
+
export default {
|
|
3230
|
+
id: "tezgah",
|
|
3231
|
+
|
|
3232
|
+
async setup(ctx) {
|
|
3233
|
+
const directory = (ctx.location && ctx.location.directory) || process.cwd()
|
|
3234
|
+
const hooks = await Tezgah({ directory })
|
|
3235
|
+
// The same double-load guard the callable keeps: opencode 1 and 2 both scan
|
|
3236
|
+
// `plugin/` and `plugins/`, and the second instance returns no hook map. A
|
|
3237
|
+
// registration without a hook would raise at load and fail the whole plugin,
|
|
3238
|
+
// which is the one state this file must never reach.
|
|
3239
|
+
if (!hooks || typeof hooks !== "object") return
|
|
3240
|
+
|
|
3241
|
+
const before = hooks["tool.execute.before"]
|
|
3242
|
+
if (typeof before === "function") {
|
|
3243
|
+
await ctx.tool.hook("execute.before", (event) =>
|
|
3244
|
+
before(event, { args: event && event.input }))
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
const after = hooks["tool.execute.after"]
|
|
3248
|
+
if (typeof after === "function") {
|
|
3249
|
+
await ctx.tool.hook("execute.after", (event) => {
|
|
3250
|
+
// On the "error" status the v2 event carries a Tool.Error, not a
|
|
3251
|
+
// result object, so the v1 labels (labelResult) and rows
|
|
3252
|
+
// (recordEvidence) have no carrier for a failed call: skipped, not
|
|
3253
|
+
// faked. On "completed" it translates onto the v1 pair: the callback
|
|
3254
|
+
// reads `tool` and `sessionID` from its input, `args` from
|
|
3255
|
+
// `input.args` and the result from its second argument, which it
|
|
3256
|
+
// labels in place.
|
|
3257
|
+
if (event && event.status === "completed")
|
|
3258
|
+
after({ sessionID: event.sessionID, tool: event.tool,
|
|
3259
|
+
args: event.input }, event.result)
|
|
3260
|
+
})
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
const chatMessage = hooks["chat.message"]
|
|
3264
|
+
if (typeof chatMessage === "function" && ctx.session && ctx.session.hook) {
|
|
3265
|
+
// v2 hands the plugin `{sessionID, messageID, prompt: {text, files}}` and
|
|
3266
|
+
// gives it no parts array, while the v1 callback reaches the model by
|
|
3267
|
+
// pushing a synthetic text part. So the adapter builds the v1 `parts`
|
|
3268
|
+
// array from the prompt text, runs the same callback, and appends whatever
|
|
3269
|
+
// it pushed to the v2 prompt text: the reminder lands in the message either
|
|
3270
|
+
// way, and the decision (which rule it arms) is the callback's own.
|
|
3271
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
3272
|
+
if (!event) return
|
|
3273
|
+
const prompt = event.prompt || (event.prompt = {})
|
|
3274
|
+
const parts = [{ type: "text", text: String(prompt.text || "") }]
|
|
3275
|
+
await chatMessage({ sessionID: event.sessionID, messageID: event.messageID },
|
|
3276
|
+
{ parts, message: { id: event.messageID, sessionID: event.sessionID } })
|
|
3277
|
+
const extra = parts.slice(1).map((p) => p && p.text).filter(Boolean).join("\n")
|
|
3278
|
+
if (extra) prompt.text = (prompt.text ? prompt.text + "\n" : "") + extra
|
|
3279
|
+
})
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
const shellEnv = hooks["shell.env"]
|
|
3283
|
+
if (typeof shellEnv === "function") {
|
|
3284
|
+
// v2's shell event carries `env` in place, and v1's callback fills
|
|
3285
|
+
// `output.env`, so one object serves both roles. The session id sits under
|
|
3286
|
+
// `metadata` at runtime rather than on the event, and the v1 callback reads
|
|
3287
|
+
// `input.sessionID`, so it is lifted where it exists (review of plan 111,
|
|
3288
|
+
// F3): a shell event with neither skips the session variable, as it did
|
|
3289
|
+
// before.
|
|
3290
|
+
await ctx.shell.hook("create.before", (event) => shellEnv(
|
|
3291
|
+
Object.assign({}, event,
|
|
3292
|
+
{ sessionID: event.sessionID || (event.metadata && event.metadata.sessionID) }),
|
|
3293
|
+
event))
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// The v1 `event` hook joins a subagent's ledger to its parent's on
|
|
3297
|
+
// `session.created`, and v2 delivers host events through a subscription
|
|
3298
|
+
// rather than a callback, so this block consumes it.
|
|
3299
|
+
if (typeof hooks.event === "function" && ctx.event && ctx.event.subscribe) {
|
|
3300
|
+
// v2's subscription returns an async iterable of {type, data} and never
|
|
3301
|
+
// calls a callback - the sibling plugin consumes it with `for await` - so
|
|
3302
|
+
// this reads it the same way and hands the v1 callback the shape it reads
|
|
3303
|
+
// (`input.event`, with the v1 `properties`). The loop runs for the life of
|
|
3304
|
+
// the plugin; a failed subscription costs the v1 event hook and nothing
|
|
3305
|
+
// else. Review of plan 111, F1.
|
|
3306
|
+
const consume = async () => {
|
|
3307
|
+
// `await` because the subscription may hand back a promise of the
|
|
3308
|
+
// iterable: `for await` takes the iterable itself, and on a promise it
|
|
3309
|
+
// throws where a bare `catch` would hide it (review of plan 111, F1).
|
|
3310
|
+
for await (const input of await ctx.event.subscribe({})) {
|
|
3311
|
+
if (!input || !input.type) continue
|
|
3312
|
+
try {
|
|
3313
|
+
await hooks.event({ event: { type: input.type, properties: input.data } })
|
|
3314
|
+
} catch {}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
consume().catch(() => {})
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// Not registered on v2, each with its reason rather than a stub:
|
|
3321
|
+
//
|
|
3322
|
+
// `permission.ask` - the v1 callback decides from the call's tool and its
|
|
3323
|
+
// args (`permissionToolArgs`: metadata.tool, metadata.pattern, the pattern
|
|
3324
|
+
// list), and v2's evaluate event carries `action`, `resources`, `metadata`
|
|
3325
|
+
// and `source` instead; whether the host fills `metadata` with the tool and
|
|
3326
|
+
// the pattern is unverified, and a wrong reading there would deny a call the
|
|
3327
|
+
// before-hook allows. The same decisions are already enforced by
|
|
3328
|
+
// `tool.execute.before`, which is registered above and runs on every call,
|
|
3329
|
+
// so nothing is left unenforced - only the redundancy is dropped.
|
|
3330
|
+
// `experimental.session.compacting` - v1 reaches the model by mutating
|
|
3331
|
+
// `output.parts`; v2's compaction event carries `messages` and a
|
|
3332
|
+
// `result = {summary}` to set, so this is a different hook, not a rename.
|
|
3333
|
+
// `config` - v1 mutates one global config object to register the repository's
|
|
3334
|
+
// generated subagents; v2 has no global config and registers them through
|
|
3335
|
+
// `ctx.agent.transform`, which needs the agents' own shape.
|
|
3336
|
+
// The TUI plugin (`hosts/opencode/tui/`) has its own v2 definition to port.
|
|
3337
|
+
},
|
|
3338
|
+
}
|
|
3339
|
+
|
package/output-styles/tezgah.md
CHANGED
|
@@ -66,7 +66,9 @@ examples. Off: `tezgah-adhd off`, or the repo's `.no-adhd`.
|
|
|
66
66
|
|
|
67
67
|
**Deliver the whole ask; never the shortcut.** The request defines the
|
|
68
68
|
deliverable: every named item is in scope until the user says otherwise, and the
|
|
69
|
-
ask is a floor, not a ceiling
|
|
69
|
+
ask is a floor, not a ceiling - its kind is one: a research, analysis, review or
|
|
70
|
+
plan ask ends at the written plan, and building it waits for the user's answer.
|
|
71
|
+
Ponytail shrinks the solution, never the request.
|
|
70
72
|
FORBIDDEN: swapping in a cheaper, deferred or partial stand-in for what was
|
|
71
73
|
asked; silently narrowing scope; deciding a requested item is
|
|
72
74
|
"unnecessary"/"YAGNI" and dropping it; a token gesture reported as done; stopping
|
|
@@ -110,7 +112,8 @@ lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
|
|
|
110
112
|
Work and process - the order, "shall I go on", a check, a plan, a consult
|
|
111
113
|
member, a spend within `spend_cap`, a local merge of the session's branch,
|
|
112
114
|
removing its worktree - are taken and stated in one line, never asked; a taste
|
|
113
|
-
or intent fork goes in the batch with a default taken this turn
|
|
115
|
+
or intent fork goes in the batch with a default taken this turn, but building a
|
|
116
|
+
plan this turn wrote waits for the answer. An outward step
|
|
114
117
|
on the user's own remote (push, PR, release, their package) runs on a grant
|
|
115
118
|
("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"), else is asked once. Never on a
|
|
116
119
|
default - ask and wait for an answer naming action and target: a force push,
|
package/package.json
CHANGED
package/skills/plan-add/SKILL.md
CHANGED
|
@@ -96,14 +96,18 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
|
|
|
96
96
|
the scope with `tezgah-task allow <glob> ... --why "<reason>"`, which records the reason. Never retype the record:
|
|
97
97
|
the gate refuses a write to it, because the CLI's checks would be skipped.
|
|
98
98
|
Tick an Acceptance box with `tezgah-task tick NNN <n>` (`--undo` unticks),
|
|
99
|
-
rewrite State or Next with `tezgah-task note NNN state|next "<text>"`,
|
|
99
|
+
rewrite State or Next with `tezgah-task note NNN state|next "<text>"`,
|
|
100
100
|
record the spike's answer with `tezgah-task spike NNN "<answer>"`, which
|
|
101
|
-
writes `spike_recorded:` on the plan, active or not
|
|
102
|
-
|
|
101
|
+
writes `spike_recorded:` on the plan, active or not, and annotate an item the
|
|
102
|
+
owner accepted as not met with `tezgah-task unmet NNN <n> "<reason, who
|
|
103
|
+
accepted it>"`, which writes `UNMET:` and is the form `close done` takes in
|
|
104
|
+
place of a tick. These four work from the main checkout or from any of its
|
|
105
|
+
worktrees.
|
|
103
106
|
After the merge, `tezgah-task close NNN done` deactivates the plan,
|
|
104
107
|
moves it to done and removes every merged worktree. It refuses while an
|
|
105
108
|
Acceptance box is open: tick it, or, only when the owner accepted the item
|
|
106
|
-
as not met, end it with `
|
|
109
|
+
as not met, end it with `tezgah-task unmet NNN <n> "<reason, who accepted
|
|
110
|
+
it>"`, which writes the `UNMET:` line and leaves the box open.
|
|
107
111
|
|
|
108
112
|
10. If the work settles a decision that is hard to reverse, spans components, or
|
|
109
113
|
keeps being re-explained, record it as an ADR under `.tezgah/decisions/` with
|
|
@@ -266,6 +266,20 @@ code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
|
266
266
|
the graph, grep only for literal text"; never a grep-only explorer (observed).
|
|
267
267
|
Subagents never orchestrate: no nested harnesses, no sub-subagents.
|
|
268
268
|
|
|
269
|
+
### The verification batch: `tezgah-optimize`
|
|
270
|
+
`~/.config/tezgah/bin/tezgah-optimize next [--files PATH ...]` answers, per command
|
|
271
|
+
the active plan's Acceptance names, one of `run now`, `defer` or `already green` -
|
|
272
|
+
the first from no measurement or a median below the in-turn bar, the second from a
|
|
273
|
+
median at or above it, the third from a green row recorded for the same sha. The
|
|
274
|
+
router calls it at a phase move and before it spends a check the same change
|
|
275
|
+
already passed, and calls `push` once at the end of a step instead of pushing
|
|
276
|
+
every iteration: a build or a CI run paid once per attempt is the cost this
|
|
277
|
+
decides. `record <command> --seconds N --exit N` is how a measurement gets in, so
|
|
278
|
+
the session that runs a check hands the number back. The tiers are measured, not
|
|
279
|
+
declared, so a project that never adopted this repository's `AGENTS.md` still gets
|
|
280
|
+
a batch. See `docs/testing.md` and, for the decision behind it, the research line
|
|
281
|
+
`tezgah-optimization`.
|
|
282
|
+
|
|
269
283
|
### Tier 2 - codegen (cheap model via OpenRouter)
|
|
270
284
|
`~/.config/tezgah/bin/codegen "task" --files a.py b.py` drafts a bounded edit on a cheap model
|
|
271
285
|
(override with --model or CODEGEN_MODEL) for a fraction of the cost. On Claude
|