@r1z4x/tezgah 1.11.0 → 1.14.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 +507 -1
- package/MANIFEST +40 -0
- package/NOTICE +152 -0
- package/README.de.md +41 -20
- package/README.es.md +39 -19
- package/README.fr.md +41 -20
- package/README.md +18 -10
- package/README.tr.md +39 -21
- package/bin/codegen +9 -2
- package/bin/consult +6 -0
- package/bin/tezgah-adhd +3 -0
- package/bin/tezgah-agents +3 -0
- package/bin/tezgah-capture +3 -0
- package/bin/tezgah-design +329 -54
- package/bin/tezgah-docs +158 -42
- package/bin/tezgah-dsh +1 -1
- package/bin/tezgah-gate +7 -2
- package/bin/tezgah-import-ai-research +16 -4
- package/bin/tezgah-index +3 -0
- package/bin/tezgah-lessons +13 -4
- package/bin/tezgah-mcp +4 -0
- package/bin/tezgah-migrate +3 -0
- package/bin/tezgah-ocr-review +2 -1
- package/bin/tezgah-optimize +4 -1
- package/bin/tezgah-pony +3 -0
- package/bin/tezgah-research +51 -10
- package/bin/tezgah-rollback +3 -0
- package/bin/tezgah-route +1 -1
- package/bin/tezgah-setup +1741 -95
- package/bin/tezgah-status +11 -1
- package/bin/tezgah-task +58 -17
- package/bin/tezgah.js +10 -3
- package/commands/adhd.md +1 -1
- package/docs/README.md +19 -8
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +11 -17
- package/docs/clarity-baseline.json +12 -6
- package/docs/contract.md +83 -34
- package/docs/environment.md +59 -24
- package/docs/evidence.md +128 -25
- package/docs/gate.md +77 -23
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +343 -48
- package/docs/layers.md +2 -2
- package/docs/models.md +6 -0
- package/docs/operations.md +83 -29
- package/docs/research.md +37 -11
- package/docs/skills.md +31 -8
- package/docs/status-line.md +39 -23
- package/docs/testing.md +50 -17
- package/docs/vision.md +6 -2
- package/hooks/hooks.json +1 -1
- package/hooks/lang/en.json +16 -2
- package/hooks/lang/tr.json +14 -2
- package/hooks/projects-posttooluse.py +11 -3
- package/hooks/projects-stop.py +3 -2
- package/hooks/tezgah_agents.py +27 -3
- package/hooks/tezgah_attest.py +79 -10
- package/hooks/tezgah_context.py +61 -31
- package/hooks/tezgah_gate.py +480 -126
- package/hooks/tezgah_integrity.py +577 -133
- package/hooks/tezgah_lessons.py +5 -2
- package/hooks/tezgah_paths.py +60 -8
- package/hooks/tezgah_policy.py +48 -18
- package/hooks/tezgah_replay.py +8 -2
- package/hooks/tezgah_research.py +167 -24
- package/hooks/tezgah_skill_pick.py +1 -1
- package/hooks/tezgah_snapshot.py +6 -3
- package/hooks/tezgah_stack.py +236 -1
- package/hooks/tezgah_stopspec.py +2 -1
- package/hooks/tezgah_task.py +2 -1
- package/hosts/amp/hook.py +217 -0
- package/hosts/amp/tezgah-plugin.ts.in +396 -0
- package/hosts/antigravity/hook.py +255 -0
- package/hosts/codex/hook.py +3 -2
- package/hosts/commandcode/hook.py +239 -0
- package/hosts/copilot/hook.py +284 -0
- package/hosts/cursor/hook.py +4 -2
- package/hosts/droid/hook.py +259 -0
- package/hosts/dsh/hooks.json +1 -1
- package/hosts/gemini/hook.py +213 -0
- package/hosts/goose/hook.py +145 -0
- package/hosts/omp/hook.py +5 -2
- package/hosts/omp/tezgah-hook.ts.in +157 -42
- package/hosts/opencode/plugins/tezgah.js +209 -63
- package/hosts/qwen/hook.py +206 -0
- package/output-styles/tezgah.md +11 -6
- package/package.json +6 -3
- package/packaging/build.sh +4 -4
- package/packaging/install.sh +11 -0
- package/packaging/upgrade.sh +3 -0
- package/skills/ai-research/LICENSE +21 -0
- package/skills/design-contract/SKILL.md +27 -6
- package/skills/design-library/LICENSE +21 -0
- package/skills/harness/SKILL.md +2 -0
- package/skills/i-have-adhd/SKILL.md +10 -1
- package/skills/plan-add/SKILL.md +5 -1
- package/skills/plan-add/render_table.py +7 -0
- package/skills/pm-frameworks/LICENSE +21 -0
- package/skills/research/SKILL.md +22 -9
- package/skills/rl-env/LICENSE +201 -0
- package/skills/tezgah-contract/SKILL.md +21 -8
- package/statusline.py +20 -9
- package/third_party/open-code-review/LICENSE +201 -0
- package/third_party/open-code-review/README.md +15 -0
- package/third_party/open-code-review/prompts/main_task_system.md +26 -0
- package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
- package/workflows/graph-map.js +14 -5
- package/workflows/graph-review.js +14 -2
|
@@ -301,6 +301,68 @@ function labelled(content: any, label: string): any {
|
|
|
301
301
|
return undefined;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// The calls one omp tool call carries, as the core names them (B-010). omp
|
|
305
|
+
// routes an MCP call through its device tool - `write` to `xd://mcp__<name>`
|
|
306
|
+
// with the call's arguments as JSON in `content` - and the core's MCP rules,
|
|
307
|
+
// the MCP_EFFECT spawn filter and the Stop rule's `external` row are keyed on
|
|
308
|
+
// the real name, so the device form is read as `mcp__<name>` with the parsed
|
|
309
|
+
// arguments (an unparsable body stays readable as `content`). The URL is
|
|
310
|
+
// matched the way omp resolves it: trimmed, the scheme in any case. `ast_edit`
|
|
311
|
+
// (direct or as `xd://ast_edit`) only stages its rewrite and omp applies it
|
|
312
|
+
// later through `write xd://resolve`, so the gate reads its targets here: one
|
|
313
|
+
// `edit` per `paths` item, the ops' replacement text as the new content, marked
|
|
314
|
+
// `via: "ast_edit"` - only such an item is read as a glob or a directory.
|
|
315
|
+
// `outer` keeps the device `write` itself in front, for the gate only: its
|
|
316
|
+
// content rules (a credential, a skip) read the whole body whatever class the
|
|
317
|
+
// carried call is, as they did before the route (review 153 F1). The gate
|
|
318
|
+
// writes no `began` for it (tezgah_gate.ROUTED_DEVICE): its result is recorded
|
|
319
|
+
// as the call it carries.
|
|
320
|
+
const DEVICE = /^xd:\/\/(mcp__[^/?#]+|ast_edit)$/i;
|
|
321
|
+
function deviceOf(tool: string, args: any): RegExpExecArray | null {
|
|
322
|
+
return tool.toLowerCase() === "write" ? DEVICE.exec(String(args?.path ?? "").trim()) : null;
|
|
323
|
+
}
|
|
324
|
+
function routed(tool: string, args: any, outer = false): Array<[string, any]> {
|
|
325
|
+
let name = tool;
|
|
326
|
+
let input = args ?? {};
|
|
327
|
+
const device = deviceOf(name, input);
|
|
328
|
+
const calls: Array<[string, any]> = device && outer ? [[name, input]] : [];
|
|
329
|
+
if (device) {
|
|
330
|
+
name = device[1].toLowerCase() === "ast_edit" ? "ast_edit"
|
|
331
|
+
: "mcp__" + device[1].slice("mcp__".length);
|
|
332
|
+
const raw = String(input?.content ?? "");
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(raw);
|
|
335
|
+
input = parsed && typeof parsed === "object" ? parsed : { content: raw };
|
|
336
|
+
} catch {
|
|
337
|
+
input = { content: raw };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (name.toLowerCase() !== "ast_edit") return [...calls, [name, input]];
|
|
341
|
+
const ops = Array.isArray(input?.ops) ? input.ops : [];
|
|
342
|
+
const out = ops.map((op: any) => String(op?.out ?? "")).join("\n");
|
|
343
|
+
const paths = Array.isArray(input?.paths) ? input.paths : [];
|
|
344
|
+
return [...calls, ...paths.map((p: any): [string, any] =>
|
|
345
|
+
["edit", { path: String(p), new_string: out, via: "ast_edit" }])];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// omp emits a device call and then, under the same id, the native event of the
|
|
349
|
+
// call it carries: the device event leaves one key per carried call it passed
|
|
350
|
+
// and that native event consumes it, so a row, a label and a retry count are
|
|
351
|
+
// written once (review 153 F4). Nothing else is skipped: a direct call leaves
|
|
352
|
+
// no key, and a reused id, an empty one or a refused call reaches the gate and
|
|
353
|
+
// the ledger again (review 153 round 2 #1). ponytail: the newest 512 keys.
|
|
354
|
+
const CARRIED = { call: new Set<string>(), result: new Set<string>() };
|
|
355
|
+
function carriedKey(id: any, tool: string, input: any): string | null {
|
|
356
|
+
if (id == null || id === "") return null;
|
|
357
|
+
return `${id}:${tool.toLowerCase()}:${String(input?.path ?? "")}`;
|
|
358
|
+
}
|
|
359
|
+
function carry(seen: Set<string>, keys: string[]): void {
|
|
360
|
+
for (const key of keys) {
|
|
361
|
+
seen.add(key);
|
|
362
|
+
if (seen.size > 512) seen.delete(seen.values().next().value as string);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
304
366
|
// A check's own output saying it ran nothing: the same literal as
|
|
305
367
|
// hooks/tezgah_integrity.py `EMPTY_RUN` (tests/test_omp_extension.py pins the
|
|
306
368
|
// two equal), read here because the bridge sends the hook a size and never the
|
|
@@ -315,6 +377,22 @@ function emptyRun(content: any): boolean {
|
|
|
315
377
|
return EMPTY_RUN.test(text.slice(-EMPTY_RUN_TAIL));
|
|
316
378
|
}
|
|
317
379
|
|
|
380
|
+
// The text of one assistant message, whatever shape it arrives in: omp hands
|
|
381
|
+
// `session_stop` the message OBJECT its `getLastAssistantMessage()` returns,
|
|
382
|
+
// and the core must judge the prose, never its `str()` (plan 143). A thinking
|
|
383
|
+
// or tool block is not the reply: only `type: "text"` is.
|
|
384
|
+
function textOf(message: any): string | undefined {
|
|
385
|
+
if (typeof message === "string") return message;
|
|
386
|
+
if (typeof message?.content === "string") return message.content;
|
|
387
|
+
if (Array.isArray(message?.content)) {
|
|
388
|
+
// joined and trimmed exactly as the core does (`reply_prose`), so the two
|
|
389
|
+
// never hand the rule a different text for one reply
|
|
390
|
+
return message.content.map((p: any) => (p?.type === "text" && typeof p.text === "string"
|
|
391
|
+
? p.text : "")).join("\n").trim();
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
318
396
|
// The text of the newest assistant message in an agent_end `messages` list:
|
|
319
397
|
// the subagent's final report, which the hook reads like a Stop reply.
|
|
320
398
|
function lastReply(messages: any): string | undefined {
|
|
@@ -322,11 +400,8 @@ function lastReply(messages: any): string | undefined {
|
|
|
322
400
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
323
401
|
const m = messages[i];
|
|
324
402
|
if (m?.role !== "assistant") continue;
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
return m.content.map((p: any) => (p?.type === "text" && typeof p.text === "string"
|
|
328
|
-
? p.text : "")).join("\n");
|
|
329
|
-
}
|
|
403
|
+
const text = textOf(m);
|
|
404
|
+
if (text !== undefined) return text;
|
|
330
405
|
}
|
|
331
406
|
return undefined;
|
|
332
407
|
}
|
|
@@ -460,6 +535,13 @@ function isSubagent(ctx: any): boolean {
|
|
|
460
535
|
}
|
|
461
536
|
}
|
|
462
537
|
|
|
538
|
+
// A subagent's events name the main session as `parent`, as its session_start
|
|
539
|
+
// does: the switch latch counts from the earlier of the two sessions' first
|
|
540
|
+
// rows, so a switch the parent made mid-session holds no sway here (B-036).
|
|
541
|
+
function parentOf(ctx: any): string | undefined {
|
|
542
|
+
return isSubagent(ctx) ? process.env.TEZGAH_SESSION || undefined : undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
463
545
|
// The session an omp fork was forked from: a main session whose header still
|
|
464
546
|
// names a `parentSession` (an id, or a `<stamp>_<id>.jsonl` path). The hook
|
|
465
547
|
// writes it as a `forked` row, so the race rule names a fork as one instead of
|
|
@@ -575,7 +657,8 @@ export default function tezgahOmp(pi: any): void {
|
|
|
575
657
|
// A failed hook answers nothing, so the last good line would keep claiming a
|
|
576
658
|
// gate that is not running: the widget is repainted with the failure instead.
|
|
577
659
|
const refresh = (ctx: any): void => {
|
|
578
|
-
const res = ask({ event: "status", cwd: cwdOf(ctx), session_id: sessionOf(ctx)
|
|
660
|
+
const res = ask({ event: "status", cwd: cwdOf(ctx), session_id: sessionOf(ctx),
|
|
661
|
+
parent: parentOf(ctx) });
|
|
579
662
|
if (hookError) return markBroken(ctx);
|
|
580
663
|
show(ctx, res);
|
|
581
664
|
};
|
|
@@ -706,7 +789,7 @@ export default function tezgahOmp(pi: any): void {
|
|
|
706
789
|
// `subagent`: a task prompt is the parent's brief, never the user's grant
|
|
707
790
|
const res = ask({ event: "user_prompt", cwd: cwdOf(ctx), prompt,
|
|
708
791
|
session_id: session, transcript_path: sessionFileOf(ctx),
|
|
709
|
-
subagent: isSubagent(ctx) });
|
|
792
|
+
subagent: isSubagent(ctx), parent: parentOf(ctx) });
|
|
710
793
|
if (hookError) return noticeMessage(ctx, session);
|
|
711
794
|
if (!res.context) return undefined;
|
|
712
795
|
// A hidden message, not a system-prompt rewrite: omp keeps per-request
|
|
@@ -717,38 +800,44 @@ export default function tezgahOmp(pi: any): void {
|
|
|
717
800
|
});
|
|
718
801
|
|
|
719
802
|
pi.on("tool_call", async (event: any, ctx: any) => {
|
|
720
|
-
const tool = String(event?.toolName ?? event?.name ?? "");
|
|
721
803
|
// An MCP tool is named per server (`mcp__<server>_<tool>`), so no exact
|
|
722
804
|
// list can hold it: an effect verb in the name reaches the gate, whose
|
|
723
|
-
// content rules read its payload (tezgah_integrity.mcp_class).
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
805
|
+
// content rules read its payload (tezgah_integrity.mcp_class). A device
|
|
806
|
+
// call is judged as itself and as the call it carries (`routed`), and the
|
|
807
|
+
// native event of a carried call it passed is not asked twice (`CARRIED`).
|
|
808
|
+
const callId = event?.toolCallId;
|
|
809
|
+
const name = String(event?.toolName ?? event?.name ?? "");
|
|
810
|
+
const args = event?.input ?? event?.args ?? {};
|
|
811
|
+
const device = deviceOf(name, args) != null;
|
|
812
|
+
const carried: string[] = [];
|
|
813
|
+
for (const [n, [tool, input]] of routed(name, args, true).entries()) {
|
|
814
|
+
if (!GATED.includes(tool.toLowerCase()) && !MCP_EFFECT.test(tool)) continue;
|
|
815
|
+
const key = carriedKey(callId, tool, input);
|
|
816
|
+
if (device) {
|
|
817
|
+
if (n > 0 && key) carried.push(key); // the device `write` itself is first
|
|
818
|
+
} else if (key && CARRIED.call.delete(key)) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const res = ask({ event: "pre_tool_use", cwd: cwdOf(ctx), tool, input,
|
|
822
|
+
session_id: sessionOf(ctx), parent: parentOf(ctx) });
|
|
823
|
+
if (hookError) {
|
|
824
|
+
markBroken(ctx);
|
|
825
|
+
return undefined; // a hook that cannot answer cannot refuse either
|
|
826
|
+
}
|
|
827
|
+
if (res.deny) return { block: true, reason: res.deny };
|
|
828
|
+
if (res.advice && event?.toolCallId != null) {
|
|
829
|
+
ADVICE.set(String(event.toolCallId), String(res.advice));
|
|
830
|
+
}
|
|
737
831
|
}
|
|
832
|
+
carry(CARRIED.call, carried);
|
|
738
833
|
return undefined;
|
|
739
834
|
});
|
|
740
835
|
|
|
741
836
|
pi.on("tool_result", async (event: any, ctx: any) => {
|
|
742
|
-
const tool = String(event?.toolName ?? "");
|
|
743
|
-
const args = event?.input ?? {};
|
|
744
837
|
const id = event?.toolCallId != null ? String(event.toolCallId) : "";
|
|
745
838
|
const advice = ADVICE.get(id);
|
|
746
839
|
ADVICE.delete(id);
|
|
747
840
|
const graph = advice ? { additionalContext: advice } : undefined;
|
|
748
|
-
if (!WATCHED.includes(tool.toLowerCase()) && !tool.startsWith("mcp__")
|
|
749
|
-
&& !skillRead(tool, args)) {
|
|
750
|
-
return graph;
|
|
751
|
-
}
|
|
752
841
|
// Tri-state: an unread outcome must never be written as a passed check,
|
|
753
842
|
// because the Stop rule below reads this ledger as evidence.
|
|
754
843
|
const failed = typeof event?.isError === "boolean" ? event.isError : null;
|
|
@@ -762,18 +851,34 @@ export default function tezgahOmp(pi: any): void {
|
|
|
762
851
|
const content = event?.content;
|
|
763
852
|
const resultLen = typeof content === "string" ? content.length
|
|
764
853
|
: Array.isArray(content) ? content.length : undefined;
|
|
765
|
-
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
const
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
854
|
+
let res: any;
|
|
855
|
+
// a device call is recorded as the call it carries (`routed`): an MCP
|
|
856
|
+
// query's answer is an `external` row, never an edit of `xd://...`
|
|
857
|
+
const name = String(event?.toolName ?? "");
|
|
858
|
+
const input = event?.input ?? {};
|
|
859
|
+
const device = deviceOf(name, input) != null;
|
|
860
|
+
for (const [tool, args] of routed(name, input)) {
|
|
861
|
+
if (!WATCHED.includes(tool.toLowerCase()) && !tool.startsWith("mcp__")
|
|
862
|
+
&& !skillRead(tool, args)) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const key = carriedKey(event?.toolCallId, tool, args);
|
|
866
|
+
if (!device && key && CARRIED.result.delete(key)) continue;
|
|
867
|
+
// omp's todo call holds one op and its result's `details.phases` the whole
|
|
868
|
+
// list, so that list crosses for this one tool: the queue the Stop rule and
|
|
869
|
+
// the idle-wait warning read (tezgah_integrity.note_todo).
|
|
870
|
+
const phases = tool.toLowerCase() === "todo" ? event?.details?.phases : undefined;
|
|
871
|
+
const todo = Array.isArray(phases)
|
|
872
|
+
? phases.flatMap((p: any) => (Array.isArray(p?.tasks) ? p.tasks : []))
|
|
873
|
+
: undefined;
|
|
874
|
+
res = ask({ event: "post_tool_use", cwd: cwdOf(ctx), tool,
|
|
875
|
+
input: args, failed, idx: idxGlyph,
|
|
876
|
+
session_id: sessionOf(ctx), parent: parentOf(ctx), result_len: resultLen,
|
|
877
|
+
empty_run: emptyRun(content) || undefined, todo });
|
|
878
|
+
if (hookError) return markBroken(ctx);
|
|
879
|
+
if (device && key) carry(CARRIED.result, [key]);
|
|
880
|
+
}
|
|
881
|
+
if (!res) return graph;
|
|
777
882
|
show(ctx, res);
|
|
778
883
|
// A result that came from outside the user and the workspace is labelled in
|
|
779
884
|
// place; every other result is left exactly as the host produced it.
|
|
@@ -785,9 +890,19 @@ export default function tezgahOmp(pi: any): void {
|
|
|
785
890
|
});
|
|
786
891
|
|
|
787
892
|
pi.on("session_stop", async (event: any, ctx: any) => {
|
|
893
|
+
// `sessionModes` exists only in an interactive TUI session; its `plan`
|
|
894
|
+
// state is what every other host sends as `permission_mode`, so the core
|
|
895
|
+
// reads one field (tezgah_integrity.read_only_turn). A session without it
|
|
896
|
+
// (print, RPC, a subagent) sends nothing and is judged as `default`.
|
|
897
|
+
let mode: string | undefined;
|
|
898
|
+
try {
|
|
899
|
+
mode = ctx?.sessionModes?.getState?.()?.plan === "active" ? "plan" : undefined;
|
|
900
|
+
} catch { mode = undefined; }
|
|
788
901
|
const res = ask({ event: "stop", cwd: cwdOf(ctx), session_id: sessionOf(ctx),
|
|
789
|
-
last_assistant_message: event?.last_assistant_message
|
|
790
|
-
|
|
902
|
+
last_assistant_message: textOf(event?.last_assistant_message)
|
|
903
|
+
?? lastReply(event?.messages),
|
|
904
|
+
stop_hook_active: event?.stop_hook_active === true,
|
|
905
|
+
permission_mode: mode });
|
|
791
906
|
if (hookError) {
|
|
792
907
|
markBroken(ctx);
|
|
793
908
|
return undefined; // an unanswerable Stop rule must not block the turn
|