triss-coworker 0.22.4 → 0.24.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 CHANGED
@@ -7,6 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] — 2026-07-10
11
+
12
+ ### Added
13
+
14
+ - **OpenCode Zen provider for `triss coder`.** The default `opencode` engine
15
+ is no longer limited to Z.AI GLM — it can run any model served by
16
+ [OpenCode Zen](https://opencode.ai/docs/zen/) (`opencode/<id>`, e.g. the free
17
+ `opencode/hy3-free` / Tencent Hunyuan 3) by authenticating with a new
18
+ `OPENCODE_API_KEY`. Credentials are provider-scoped: a run forwards **only**
19
+ the key its model needs, so a Zen-only machine needs no `ZHIPU_API_KEY`. Z.AI
20
+ GLM stays the default provider; `crush` remains Z.AI-only. (#12)
21
+ - `triss coder init --provider opencode-zen` — guided Zen setup: prompts for
22
+ `OPENCODE_API_KEY`, resolves the model against the **live Zen catalogue**
23
+ (`GET /zen/v1/models`, first-available from a free-model priority list), writes
24
+ `opencode.json` (deny-first bash policy + agent templates), and pins the chosen
25
+ model into `TRISS_CODER_MODEL` so a bare `triss coder run` works. Provider is
26
+ auto-inferred from a preset/single credential when the flag is omitted. (#12)
27
+ - `OPENCODE_API_KEY` is a first-class, optional, masked secret on the coder
28
+ manifest — surfaced in `triss status`, the `config` wizard, and the MCP coder
29
+ tools (which now light up on `ZHIPU_API_KEY` **or** `OPENCODE_API_KEY`). (#12)
30
+ - `--allow-unsafe-bash` on `triss coder init` — explicit opt-in to proceed when
31
+ an existing `opencode.json` has no deny-first bash policy. (#12)
32
+ - New `docs/opencode-zen.md` deep-dive (auth, live catalogue, precedence, setup
33
+ gates, privacy, MCP, verification). (#12)
34
+
35
+ ### Changed
36
+
37
+ - `triss coder init` / `config wizard coder` now **fail (non-zero)** instead of
38
+ reporting a misleading success on an unrunnable setup: a missing provider key,
39
+ a pin shadowed by a shell export or higher-precedence `.env`, or an
40
+ existing/cross-scope `opencode.json` that is unsafe (no deny-first policy) or
41
+ unusable (a `small_model` that is cross-provider, cross-plan, or no longer in
42
+ the live catalogue). Config and templates are still written, so fixing the
43
+ cause and re-running is a clean idempotent completion. (#12)
44
+
45
+ ### Security
46
+
47
+ - Resolved 8 CodeQL code-scanning alerts (input validation and safe handling in
48
+ `completion.js`, `_contract.js`, `handlers.js`, `secrets.js`, and hardened the
49
+ `publish` / `test` GitHub workflows).
50
+ - Rewrote `stripHtml` as a state machine and removed HTML-entity decoding to
51
+ eliminate a double-unescape flagged by CodeQL.
52
+
53
+ ## [0.23.0] — 2026-07-07
54
+
55
+ ### Added
56
+
57
+ - **Second GLM coding engine: `crush`** (`@phpcraftdream/crush`, pinned
58
+ `0.1.3`), selectable per run with `triss coder run --engine crush` or
59
+ globally with `TRISS_CODER_ENGINE=crush`; set up via
60
+ `triss coder init --engine crush`. Compared to the default opencode
61
+ engine it emits one JSON envelope (no ndjson fold), uses native
62
+ get-or-create session ids (no `.triss/sessions.json` map), reports a real
63
+ per-call `delta_cost_usd`, and exposes `--role smart|fast`. Both engines
64
+ share the single `ZHIPU_API_KEY` (crush reads it natively on ≥0.1.1, with
65
+ a `ZAI_API_KEY` compatibility alias). (#10)
66
+ - `--restrict` / `--no-restrict` on `triss coder run`, plus the
67
+ `TRISS_CODER_CRUSH_RESTRICT` env var, to opt into a crush command
68
+ allowlist. When on, triss emits the read-only allowlist as CLI flags
69
+ (`--allow-bash` / `--allow-tool`); it is **opt-in** (default off) because
70
+ a denied bash command currently deadlocks crush until the timeout. (#10)
71
+ - `TRISS_CODER_CRUSH_VERSION` to pin a specific `@phpcraftdream/crush`
72
+ npm version. (#10)
73
+ - New `docs/glm-clients.md` — a single reference for how Triss talks to
74
+ GLM (both engines, key/endpoint routing, model selection, usage modes,
75
+ and the safety model), and `docs/crush-issues.md` /
76
+ `docs/crush-restrict-issues.md` capturing the crush maintainer bug
77
+ reports. (#10)
78
+
79
+ ### Changed
80
+
81
+ - `triss coder run --engine crush` isolates by default (a disposable
82
+ `.triss/wt/<slug>` worktree); pass `--no-isolate` to opt out. crush's
83
+ own `crush.json` `permissions.run` config is not yet honored upstream and
84
+ a denied command deadlocks, so the worktree is crush's reliable safety
85
+ layer. opencode is unchanged (isolate off; its `opencode.json` bash
86
+ allowlist is enforced). (#10)
87
+
10
88
  ## [0.22.4] — 2026-07-04
11
89
 
12
90
  ### Fixed
package/README.md CHANGED
@@ -237,7 +237,8 @@ The exposed tool set is **filtered by configured credentials**:
237
237
  - **`triss_github_*`** when `GITHUB_TOKEN` is set (or `gh` CLI logged in).
238
238
  - **`triss_gitlab_*`** when `GITLAB_TOKEN` is set.
239
239
  - **`triss_coder_run` + `triss_coder_status`** when `ZHIPU_API_KEY` is set
240
- (setup: `triss coder init`). `triss_coder_run`'s timeout defaults to
240
+ (setup: `triss coder init`). `triss_coder_run` takes an optional `engine`
241
+ (`opencode` default, or `crush`); its timeout defaults to
241
242
  1500s (25 min) over MCP, above the CLI's 900s, since GLM runs are
242
243
  expected to be long; override per call via the `timeout` arg. For
243
244
  runs that may exceed it, use `triss coder run` on the CLI instead.
@@ -268,8 +269,8 @@ and writing**.
268
269
  | `triss review` | Code review on current branch or a PR (diff + linked ticket) | The agent reading the whole diff |
269
270
  | `triss commit-msg` | Generates a commit message from staged diff | Hand-writing or copy-pasting from web LLMs |
270
271
  | `triss usage` | Cumulative cost / token usage with per-project breakdown | Squinting at stderr after each call |
271
- | `triss coder init` | Sets up a GLM coding agent (opencode engine): key, `opencode.json`, agent templates | Manually installing/configuring opencode |
272
- | `triss coder run` | Spawns the GLM coding agent and prints one JSON envelope (`--isolate` for a disposable worktree). **POSIX only** (macOS/Linux) — refuses to run on Windows. | Manually driving `opencode run` and parsing its ndjson stream |
272
+ | `triss coder init` | Sets up a coding agent (default `opencode` engine; `--engine crush` for crush): provider key (Z.AI GLM, or `--provider opencode-zen` for free OpenCode Zen models like `hy3`), `opencode.json` / crush models + `permissions.run` policy, agent templates. Blocks (non-zero) on an unsafe existing `opencode.json` — missing deny-first bash policy (override with `--allow-unsafe-bash`) or a stale/cross-provider `small_model` | Manually installing/configuring opencode |
273
+ | `triss coder run` | Spawns the GLM coding agent and prints one JSON envelope (`--engine opencode\|crush`; `--isolate` for a disposable worktree — opencode defaults to isolate-OFF, crush defaults to isolate-ON; crush adds opt-in `--restrict`/`--no-restrict` for its CLI allowlist). **POSIX only** (macOS/Linux) — refuses to run on Windows. | Manually driving `opencode run` and parsing its ndjson stream |
273
274
  | `triss coder clean` | Removes finished `.triss/wt` isolation worktrees (`--all` forces all) | Manually finding and deleting stale git worktrees |
274
275
  | `triss init` | Drops a tiny (~15 line) delegation block into `CLAUDE.md` / `AGENTS.md` | Hand-writing routing rules |
275
276
  | `triss agent-help` | Prints the full delegation cookbook on demand (the nano block points here) | A 200-line CLAUDE.md that always loads |
@@ -430,27 +431,80 @@ default timeout, configurable via `--timeout <ms>`.
430
431
  ### `triss coder`
431
432
 
432
433
  Delegates an implementation task to a GLM coding agent (the `opencode`
433
- engine) instead of the primary model writing the code itself.
434
+ engine by default; `crush` is an alternative — see **Engines** below)
435
+ instead of the primary model writing the code itself.
434
436
 
435
437
  ```bash
436
438
  triss coder init # once: key, opencode.json, agent templates
437
439
  triss coder run "add input validation to /signup" --isolate
440
+ triss coder run "..." --engine crush # crush engine (isolates by default; restrict is opt-in)
441
+ triss coder run "..." --engine crush --restrict # crush + CLI allowlist on top of the worktree
438
442
  triss coder clean # remove finished isolation worktrees
439
443
  ```
440
444
 
441
- `triss coder init` probes which Z.AI plan your key actually works with
442
- (subscription vs. pay-as-you-go) and writes the matching model prefix, and
443
- lets you pick the GLM model interactively instead of always defaulting to
444
- `glm-5.2`.
445
+ `triss coder init` first asks which provider to configure — **Z.AI GLM**
446
+ (default) or **OpenCode Zen** (`--provider opencode-zen`, free models incl.
447
+ Hunyuan `hy3`). For Z.AI it probes which plan your key works with (subscription
448
+ vs. pay-as-you-go) and writes the matching model prefix; for Zen it saves an
449
+ `OPENCODE_API_KEY` and writes an `opencode/<id>` model. Either way it lets you
450
+ pick the model interactively. See the **Providers** section below and
451
+ [docs/opencode-zen.md](docs/opencode-zen.md) for the OpenCode Zen deep‑dive.
445
452
 
446
453
  Prints one JSON envelope to stdout — `files_changed`, `diff_stat`, and
447
454
  `worktree` tell you what to review; `--isolate` runs the agent in a
448
455
  disposable `.triss/wt/<slug>` git worktree so it never touches your
449
456
  working tree directly. `--session <slug>` continues the same opencode
450
457
  conversation across calls. **POSIX only** (macOS/Linux) for now. See
451
- `docs/configuration.md` for the coder env vars and `docs/mcp.md` for the
458
+ `docs/glm-clients.md` for the full picture of how Triss talks to GLM
459
+ (both engines, key/endpoint routing, models, and every usage mode),
460
+ `docs/configuration.md` for the coder env vars, and `docs/mcp.md` for the
452
461
  MCP tool equivalents.
453
462
 
463
+ **Engines** — `opencode` (default) enforces a deny-first bash allowlist
464
+ (`opencode.json`) that actually works — prefer it when you want that safety
465
+ layer baked in. `crush` (`--engine crush` / `TRISS_CODER_ENGINE=crush`, npm
466
+ `@phpcraftdream/crush` ≥0.1.3) has a **weaker, interim** safety story: live
467
+ testing proved crush 0.1.3 **ignores** its `permissions.run` config block and
468
+ a denied bash command **deadlocks to the timeout**. So triss ships crush with
469
+ isolate-**ON** by default (the disposable worktree is the reliable safety
470
+ layer) and makes restrict **opt-in** (default OFF). `triss coder init` still
471
+ seeds a `permissions.run` block into crush.json as forward-compat (harmless,
472
+ correct once upstream honors it), but the working allowlist today is the CLI
473
+ flags: when you pass `--restrict` (or `TRISS_CODER_CRUSH_RESTRICT=1`), `triss
474
+ coder run` emits `--restrict-run` plus `--allow-bash`/`--allow-tool` flags for
475
+ each entry. Override per-run with `--restrict` / `--no-restrict` (resolution:
476
+ CLI flag > env > crush.json `permissions.run.restrict` > default OFF). For
477
+ Z.AI GLM, both engines share the single `ZHIPU_API_KEY` (crush ≥0.1.1 reads it
478
+ natively; triss also forwards it as `ZAI_API_KEY` for older binaries; see
479
+ **Providers** below for the opencode-only OpenCode Zen alternative). See
480
+ `docs/crush-restrict-issues.md` for the live-verified bug facts.
481
+
482
+ **Providers** — the `opencode` engine isn't limited to Z.AI. The required
483
+ API key follows the model's `<provider>/` prefix: `zai-coding-plan/*` and
484
+ `zai/*` (GLM) use `ZHIPU_API_KEY`, while `opencode/*` models — served free by
485
+ [OpenCode Zen](https://opencode.ai/docs/zen/), e.g. **`opencode/hy3-free`**
486
+ (Tencent Hunyuan 3) — use `OPENCODE_API_KEY`. To point coder at a Zen model:
487
+
488
+ ```bash
489
+ triss coder init --provider opencode-zen # guided: key + opencode.json
490
+ # …or by hand:
491
+ triss config set OPENCODE_API_KEY <key> # or add it to .triss.env
492
+ export TRISS_CODER_MODEL=opencode/hy3-free # default it for every run
493
+ export TRISS_CODER_SMALL_MODEL=opencode/hy3-free
494
+ triss coder run "add input validation to /signup"
495
+ # …or per-run, without changing the default:
496
+ triss coder run "..." --model opencode/hy3-free
497
+ ```
498
+
499
+ `triss coder run` passes the resolved model to opencode with `--model` and
500
+ forwards only the key that model needs, so a Zen run works with
501
+ `OPENCODE_API_KEY` alone — no Z.AI key required. The deny-first `opencode.json`
502
+ bash policy applies to Zen runs too. Any Zen model id works via
503
+ `TRISS_CODER_MODEL=opencode/<id>` / `--model`, not just the free ones the
504
+ picker lists. (The `crush` engine speaks Z.AI only.) Full details, the model
505
+ catalogue, and every configuration path are in
506
+ [docs/opencode-zen.md](docs/opencode-zen.md).
507
+
454
508
  If your Z.AI plan hits its usage limit, `triss coder run` fails fast with
455
509
  the reset time converted to your local timezone (Z.AI reports it in
456
510
  Beijing time), instead of hanging until `--timeout`. Under the hood
package/bin/triss.js CHANGED
@@ -223,23 +223,30 @@ config
223
223
 
224
224
  const coder = program
225
225
  .command('coder')
226
- .description('Run a GLM coding agent (opencode engine)');
226
+ .description('Run a GLM coding agent (opencode or crush engine)');
227
227
 
228
228
  coder
229
229
  .command('init')
230
- .description('Install/configure the opencode engine, Z.AI key, permission policy, and agent templates')
230
+ .description('Install/configure a coding engine (opencode default, or crush), provider key, permission policy, and agent templates')
231
231
  .option('-g, --global', 'save to the global scope (~/.config/triss/.env, ~/.config/opencode/)')
232
232
  .option('-l, --local', 'save to the project scope (./.triss.env, ./opencode.json)')
233
+ .option('--engine <name>', 'coding engine to configure: opencode (default) or crush')
234
+ .option('--provider <name>', 'opencode engine model provider: zai (Z.AI GLM, default) or opencode-zen (free models incl. hy3)')
235
+ .option('--allow-unsafe-bash', 'proceed even if an existing opencode.json has no deny-first bash policy (the agent runs with --auto)')
233
236
  .action(wrap(runCoderInit));
234
237
 
235
238
  coder
236
239
  .command('run [prompt]')
237
- .description('Spawn a GLM coding agent (opencode engine) and print a JSON envelope to stdout')
240
+ .description('Spawn a GLM coding agent (opencode default, or --engine crush) and print a JSON envelope to stdout')
241
+ .option('--engine <name>', 'coding engine: opencode (default) or crush')
238
242
  .option('--session <id>', 'triss-side session slug, mapped to a real opencode session id in .triss/sessions.json')
239
243
  .option('--continue', 'continue the most recent opencode session (maps to opencode --continue)')
240
244
  .option('--agent <name>', 'opencode agent template to use', 'coder')
241
245
  .option('--model <p/m>', 'override the model for this run only')
242
246
  .option('--isolate', 'run in a disposable git worktree under .triss/wt/<slug>')
247
+ .option('--no-isolate', 'disable worktree isolation (opencode defaults to OFF; crush defaults to ON)')
248
+ .option('--restrict', 'crush only: enforce the allowlist via CLI --allow-bash/--allow-tool flags (--restrict-run). Opt-in (default OFF)')
249
+ .option('--no-restrict', 'crush only: disable restrict (crush auto-approves every tool — the default)')
243
250
  .option('--cwd <path>', 'working directory (ignored with --isolate)')
244
251
  .option('--timeout <sec>', 'kill the engine after this many seconds', (v) => parseInt(v, 10), 900)
245
252
  .option('--stdin', 'read the prompt from piped stdin instead of the [prompt] argument')
@@ -616,3 +616,314 @@ reuse, 2 safety-layer denial checks, 2 `--dir`/global-config checks, plus
616
616
  retries) against the `zai-coding-plan` endpoint, all subscription-covered.
617
617
  One `zai` (non-coding-plan) call was attempted and failed before any
618
618
  tokens were billed (`isRetryable` error prior to a completion).
619
+
620
+ ## Phase 6 recon (crush fork — 2026-07-05)
621
+
622
+ The prereq is unblocked: the fork publishes **`@phpcraftdream/crush`** on
623
+ npm (`0.1.0`, FSL-1.1-MIT, bin `crush`, node ≥18, Go-free prebuilt binary
624
+ via per-platform optionalDependencies). Live recon of that binary
625
+ (`v0.0.0-20260704…+dirty`) in a scratch dir against the existing
626
+ `ZHIPU_API_KEY` — the fork's `crush run` **diverges substantially from
627
+ this plan's Phase 6 assumptions**. Corrections, all verified live:
628
+
629
+ - **Provider / key var mismatch.** crush ships Z.AI as a built-in Catwalk
630
+ provider `zai` (`type: openai-compat`, endpoint
631
+ `https://api.z.ai/api/coding/paas/v4` — same coding-plan endpoint as
632
+ opencode), models `glm-5.2 / glm-5.1 / glm-5-turbo / glm-4.7 / …`. But
633
+ it reads **`ZAI_API_KEY`**, NOT `ZHIPU_API_KEY`. Adapter must bridge
634
+ this: simplest is to map the value into `ZAI_API_KEY` inside crush's
635
+ spawn-env allowlist so triss keeps a single user-facing `ZHIPU_API_KEY`.
636
+ Confirmed working: `crush ping` → `zai / glm-5.2 … status: ok`; a
637
+ `crush run --role smart --json` returned a clean `end_turn` envelope.
638
+ - **Single JSON envelope (no ndjson fold).** `crush run --json` prints ONE
639
+ object to stdout at end-of-run:
640
+ `{session_id, exit_reason, final_text, assistant_notes?, tool_calls,
641
+ usage:{delta_tokens, delta_cost_usd}, duration_ms, error}`. `exit_reason`
642
+ vocabulary: `end_turn | done | canceled | timeout | max_cost |
643
+ max_tokens | error`. Tool-call heartbeat still goes to stderr as
644
+ `▶ <toolName>`. So the crush adapter's `foldOutput` is a trivial
645
+ parse-last-line, not a streaming fold. **cost is real here**
646
+ (`delta_cost_usd: 0.000048` observed) — unlike opencode's coding-plan
647
+ cost:0. Usage is a combined `delta_tokens`, NOT split prompt/completion.
648
+ - **Native sessions — no slug→id map needed.** `crush run --session <id>`
649
+ is genuine get-or-create with a **caller-supplied arbitrary id** (docs:
650
+ "an arbitrary new id to start a fresh session with that exact id — handy
651
+ for CI"). So for crush, pass triss's own slug straight through; the
652
+ `.triss/sessions.json` mapping (an opencode-only workaround) is
653
+ unnecessary. Adapter flag e.g. `needsSessionMap: false`.
654
+ - **`--role` is REQUIRED.** Every `crush run` must declare `--role
655
+ smart|large` (big model) or `--role fast|small` (cheap). `--model
656
+ <provider/model[@level]>` overrides per invocation. Default large model
657
+ resolved from `crush models use <large> <small>` (atoms `glm5_2`,
658
+ `glm5_turbo`) written to `crush.json` (global
659
+ `~/.local/share/crush/crush.json`, local `./.crush/crush.json`).
660
+ - **SAFETY MODEL IS WEAKER — the headline caveat.** This confirms Phase 6
661
+ note 3, but stronger: `crush run` is non-interactive and
662
+ **auto-approves EVERY permission request; the agent gets the full tool
663
+ set with no prompting** and there is **no bash command-pattern
664
+ allowlist** like opencode's deny-first `opencode.json`. crush's own
665
+ `run --help` warns runs are "fast but irreversible — only run in a
666
+ workspace you can afford to lose." Mitigations available, all must be
667
+ applied by triss for crush: (a) force/strongly-default `--isolate`
668
+ (disposable git worktree) for crush; (b) `CRUSH_FORBID_WRITES=<paths>`
669
+ env blocks write/edit tools from named paths; (c) `--agents single`
670
+ (default) disables sub-agent fan-out/recursion; (d) `--max-cost` /
671
+ `--max-tokens` runaway caps. There is NO way to reproduce opencode's
672
+ per-command bash allowlist — document this trade-off prominently
673
+ (agent can't be given a curated safe-command list; it's all-or-nothing
674
+ inside the sandbox).
675
+ - **stdin IS accepted** (unlike opencode): prompt = `<stdin>\n\n<args>`.
676
+ - **Extra levers worth surfacing in the adapter/docs:** `--timeout`
677
+ (accepts `900`/`900s`/`15m`; preserves partial answer in envelope),
678
+ `--on-finish "cmd"` (sets `CRUSH_SESSION_ID`/`CRUSH_EXIT_REASON`/…),
679
+ `crush sessions cancel <id>` for external cancellation, `--format json`
680
+ post-processing that strips code fences from `final_text`.
681
+
682
+ Net: the crush adapter is in most respects SIMPLER than opencode
683
+ (single-envelope parse, native sessions, no template-shape guessing) but
684
+ its **safety story is the one place it is worse** and must not be papered
685
+ over. The engine-abstraction shape in Phase 6 step 1 still holds; the
686
+ `configTemplate` member becomes a `crush.json` + `crush models use`
687
+ writer, and a new adapter member is needed for the spawn-env key bridge
688
+ (`ZHIPU_API_KEY` → `ZAI_API_KEY`) and the crush-only safety env
689
+ (`CRUSH_FORBID_WRITES`).
690
+
691
+ ## Phase 6 re-eval (crush 0.1.3 — 2026-07-06)
692
+
693
+ The fork maintainer shipped three releases (0.1.1 → 0.1.3, all 2026-07-05)
694
+ in direct response to `docs/crush-issues.md`. Re-verified every one of the
695
+ seven reported issues live on `0.1.3` (real `ZHIPU_API_KEY`, live Z.AI
696
+ coding-plan endpoint). Scorecard:
697
+
698
+ | # | Sev | Issue | Status | Evidence (0.1.3) |
699
+ |---|-----|-------|--------|------------------|
700
+ | 1 | High | `--version` = `v0.0.0…+dirty` | ✅ fixed | reports clean `v0.1.3` |
701
+ | 2 | High | `zai` reads only `ZAI_API_KEY` | ✅ fixed | with only `ZHIPU_API_KEY` set (`ZAI_API_KEY` unset), `zai` hit the network and returned 401 → key read natively |
702
+ | 3 | Med | `--role fast` (glm-5-turbo) hung to timeout | ✅ fixed | original repro (`crush run --role fast --model zai/glm-5-turbo`) → `exit_reason:end_turn`, `final_text:"PONG"` in 47s |
703
+ | 4 | Med | no allowlist, everything auto-approved | ✅ fixed | `--restrict-run` + `--allow-bash` (forms: prefix / `exact:` / `glob:` / `regex:`, chaining-guarded) + `--allow-tool` + config `permissions.run.{restrict,allow_bash,allow_tools}`; CLI merges with config |
704
+ | 5 | Med | `models list` does network + disk writes | ✅ fixed | default reads cache/embedded, no network, no writes; `--refresh` is opt-in |
705
+ | 6 | Low | `ping` rejects `--role` | ✅ fixed | `ping --role smart\|fast` + dedicated `ping-fast` |
706
+ | 7 | Low | startup WARN noise on stderr | 🟡 mostly | old git-repo / Apple-Terminal warnings gone; stderr is clean once models are configured; one WARN remains only on misconfig (large-as-small fallback for `local-cli`) |
707
+
708
+ **6/7 fully fixed, 1 (low) mostly. Every High and Medium is resolved.**
709
+ Issue #4 was closed exactly as suggested (command-pattern allowlist honored
710
+ by `crush run`), and better — CLI flags merge with `crush.json`.
711
+
712
+ Bonus levers added (not requested), worth surfacing later: `--effort
713
+ low|medium|high`, `--on-finish` hook (`CRUSH_SESSION_ID/EXIT_REASON/COST_USD/
714
+ TOKENS/DURATION_SEC`), watchdog `--timeout-extends-on-progress` +
715
+ `--timeout-hard-cap`, `--small-model`, `--format json-schema:<file>`,
716
+ `--aggregation attach|concat` for sub-agent fan-out + reduction-loss
717
+ warning, per-session budget persistence, default `--timeout` now 60m.
718
+
719
+ The `exit_reason` vocabulary is unchanged (`done | canceled | timeout |
720
+ max_cost | max_tokens | error`, plus `end_turn`) → `mapCrushExitReason`
721
+ already covers all of them; **no mapper change needed.**
722
+
723
+ ### What to do now (adapter work — implement via GLM, Claude reviews)
724
+
725
+ Assignee: GLM (`triss coder run --engine crush`). Reviewer: Claude.
726
+ All changes are in `src/coder-engines/crush.js` + tests + docs. Ship on
727
+ `feat/coder-crush-engine`.
728
+
729
+ 1. **Bump the pin + enforce the version.**
730
+ - `CRUSH_PIN_DEFAULT`: `0.1.0` → `0.1.3`.
731
+ - `detectCrush()` is presence-only today with a TODO "enforce pin once
732
+ crush --version reports clean semver." That precondition now holds
733
+ (`v0.1.3`). Parse the `vX.Y.Z` out of `crush --version` and return a
734
+ structured `{found, version, satisfiesPin}`; the caller warns (dim
735
+ stderr, non-fatal) on mismatch. Keep it non-fatal — a newer crush must
736
+ still run. Drop the two `TODO: enforce pin` comments.
737
+ - Test: feed a fake `sh` returning `crush version v0.1.3` and assert
738
+ parsed `version === '0.1.3'` / `satisfiesPin === true`; also a
739
+ `v0.2.0` (newer, still `found:true`) and a garbage string.
740
+
741
+ 2. **Retire / re-annotate the ZHIPU→ZAI env bridge.**
742
+ - crush now reads `ZHIPU_API_KEY` natively (issue #2), so the
743
+ `ZHIPU_API_KEY → ZAI_API_KEY` copy in `buildCrushSpawnEnv` is no longer
744
+ required. Decision: **keep it as belt-and-suspenders** (harmless, helps
745
+ anyone still on 0.1.0) BUT rewrite the "silently unconfigured zai
746
+ provider" comment — it is now false for 0.1.1+. New comment: crush
747
+ ≥0.1.1 reads `ZHIPU_API_KEY` directly; we still forward `ZAI_API_KEY`
748
+ as a compatibility alias for older binaries. Also forward
749
+ `ZHIPU_API_KEY` itself into the env allowlist so the native path works.
750
+ - Test: assert the spawn env contains BOTH `ZHIPU_API_KEY` and
751
+ `ZAI_API_KEY` when the base env has `ZHIPU_API_KEY`.
752
+
753
+ 3. **Adopt real restricted-run safety — DECISION: Variant A (parity with
754
+ opencode), restrict ON by default.** This closes the one gap we flagged.
755
+ The design mirrors opencode's deny-first `opencode.json`: persist the
756
+ policy in `crush.json`, do NOT inject the allowlist as per-run CLI flags.
757
+ Rationale: crush merges `permissions.run.allow_bash` from config with CLI
758
+ flags and honors `permissions.run.restrict:true` — so a config-seeded
759
+ policy travels into isolated worktrees and lets users add/remove/replace
760
+ commands by editing ONE file, exactly like `opencode.json`. Injecting via
761
+ CLI flags would let users only ADD, not remove our defaults.
762
+ - **Seed `permissions.run` into `crush.json` at init**
763
+ (`configureCrushModels` / the crush init path). `crush models use`
764
+ already writes `.crush/crush.json` (local) / `~/.local/share/crush/
765
+ crush.json` (global) with a `models` block and NO permissions — there
766
+ is no `crush config` CLI, so read-modify-write that JSON, merging the
767
+ `permissions.run` block into the existing object (never clobber the
768
+ `models` block). Write:
769
+ ```json
770
+ "permissions": { "run": {
771
+ "restrict": true,
772
+ "allow_bash": [ …shared read-only set… ],
773
+ "allow_tools": ["view"]
774
+ }}
775
+ ```
776
+ - **Shared allowlist constant.** Mirror the opencode adapter's trusted
777
+ set verbatim so both engines behave identically. opencode's is
778
+ `opencode.json` `permission.bash` (see coder.js ~L453): `git status`,
779
+ `git diff*`, `git log*`, `ls*`, `node --test*`, `npm test*`,
780
+ `npm run test*`, with `webfetch/websearch: deny`. Translate to crush
781
+ forms (`'git diff'` prefix, `'glob:ls *'`, `'glob:npm test *'`,
782
+ `'glob:node --test *'`, etc.) in ONE named constant so a bump is one
783
+ edit. crush's chaining-guard on prefix patterns is stricter than
784
+ opencode's glob — fine, keep it.
785
+ - **No-clobber, like opencode.** If the user already has a
786
+ `permissions.run` block in crush.json, do NOT overwrite it; if an
787
+ existing crush.json has no restrict policy, warn on stderr (dim), same
788
+ as the opencode "existing config lacks deny policy" warning path
789
+ (coder.js ~L503).
790
+ - **Flip crush's isolate default to OFF** to complete the parity. Today
791
+ `coder.js:1697` defaults crush isolate-ON because it had no allowlist;
792
+ under Variant A the config-seeded deny-first policy is the safety layer,
793
+ so crush should default isolate-OFF like opencode. Update that line AND
794
+ its comment. `--isolate` / `--no-isolate` tristate still wins.
795
+
796
+ **User override surface (precedence high → low) — document all four:**
797
+ 1. Per-run CLI: add `--no-restrict` / `--restrict` tristate on `coder
798
+ run` in `bin/triss.js` (mirror the existing `--isolate` tristate — no
799
+ Commander default, undefined = "use env/config default"). `--no-restrict`
800
+ drops to auto-approve for one run; `--isolate` brings back the
801
+ disposable worktree for one run.
802
+ 2. Env: `TRISS_CODER_CRUSH_RESTRICT=0` disables restrict by default
803
+ without touching config or flags. (Add to README + .env.example.)
804
+ 3. Persistent: edit `crush.json` `permissions.run` — set
805
+ `restrict:false`, or add/remove/replace `allow_bash`/`allow_tools`.
806
+ This is the recommended, opencode-equivalent override.
807
+ 4. Fallback default when none of the above is set: restrict ON.
808
+
809
+ Resolution order in code: CLI flag (if defined) > `TRISS_CODER_CRUSH_
810
+ RESTRICT` env (if set) > `crush.json` `permissions.run.restrict` (if the
811
+ user hand-set it) > built-in default `true`. When restrict resolves ON
812
+ and we control the config, `buildCrushRunArgv` passes `--restrict-run`
813
+ (belt-and-suspenders even though config also has `restrict:true`); when
814
+ it resolves OFF, pass nothing (crush's auto-approve default).
815
+ - Tests: (a) init seeds `permissions.run.restrict:true` + the shared
816
+ allow_bash into a fresh crush.json WITHOUT dropping the `models` block;
817
+ (b) init does NOT clobber a user's existing `permissions.run`;
818
+ (c) `buildCrushRunArgv` with restrict ON contains `--restrict-run`,
819
+ with restrict OFF contains neither `--restrict-run` nor allow flags;
820
+ (d) resolution order: CLI `--no-restrict` beats env=1 beats config.
821
+
822
+ 4. **Docs lockstep (per CLAUDE.md "user-visible change" rule).**
823
+ - `docs/crush-issues.md`: add a "Resolved in 0.1.3 (2026-07-06)" header
824
+ block at the top mapping each issue → fixed/mostly, so the report reads
825
+ as closed, not open. Do not delete the original findings (they are the
826
+ provenance).
827
+ - `README.md` + `.env.example`: document any new env var introduced in
828
+ step 3 (`TRISS_CODER_CRUSH_RESTRICT` / allowlist) and the
829
+ `TRISS_CODER_CRUSH_VERSION` pin now defaulting to `0.1.3`.
830
+ - `docs/mcp.md` only if a new gating env var is added.
831
+
832
+ 5. **Do NOT (out of scope for this pass, note for later):** wiring
833
+ `--effort`, `--on-finish`, or the progress-watchdog flags. They are real
834
+ upside but belong in a follow-up; keep this PR to "close the 7 issues +
835
+ safety parity."
836
+
837
+ Review focus for Claude: (a) version parse handles `+dirty`/garbage/newer
838
+ without throwing; (b) the env bridge keeps BOTH keys and the comment no
839
+ longer claims the provider is "silently unconfigured"; (c) Variant A parity
840
+ is correct — init seeds `permissions.run` WITHOUT clobbering the `models`
841
+ block or an existing user policy, the shared allowlist actually matches the
842
+ opencode set, crush isolate default is flipped to OFF, and the override
843
+ resolution order (CLI > env > config > default-ON) is exactly as specified;
844
+ (d) `spawnSync` stays argv-array (never `shell:true`); (e) all three doc
845
+ files moved in lockstep, and the `--no-restrict` flag + `TRISS_CODER_CRUSH_
846
+ RESTRICT` env are both documented in README + .env.example.
847
+
848
+ ## Phase 6 fix — restrict enforcement is CLI-only (2026-07-06, live-verified)
849
+
850
+ Post-merge review (Fable + live crush runs) found that the committed Variant A
851
+ (commit `2ad14dc`) **does not actually restrict crush**, and by flipping crush
852
+ to isolate-OFF it shipped a net safety *regression*. All facts below were
853
+ verified live against `@phpcraftdream/crush@0.1.3` with a real Z.AI key; see
854
+ `docs/crush-restrict-issues.md` for the maintainer bug report.
855
+
856
+ **What's broken (verified live):**
857
+ - **Config `permissions.run` is inert.** `crush run --restrict-run` with an
858
+ `allow_bash` policy seeded into `crush.json` (tried `./crush.json`,
859
+ `./.crush/crush.json`, and both) still ran a non-allowlisted `echo` to
860
+ completion. Our `seedCrushPermissions` writes a policy crush ignores.
861
+ - **`--restrict-run` with no CLI allow flags == unrestricted** (auto-approves
862
+ everything), not deny-all.
863
+ - **CLI `--allow-bash` / `--allow-tool` DO enforce.** Only the command-line
864
+ flags take effect. Verified tool taxonomy: file tools are `view`, `edit`,
865
+ `write`, `ls` (accepts `name` or `tool:action`, e.g. `edit:write`); a coder
866
+ successfully created a file via `write` under `--restrict-run` + those
867
+ allow-tools with no deadlock.
868
+ - **A denied *bash* command deadlocks to timeout** (`Context deadline
869
+ exceeded`, no envelope) instead of denying cleanly. File-tool denials were
870
+ not observed to deadlock; only bash.
871
+
872
+ **Consequence for the default.** A coding agent routinely runs bash outside a
873
+ read-only allowlist (`npm run build`, `tsc`, `npm run lint`, …). With the
874
+ deadlock bug, every such call dead-ends the whole run at the timeout. So
875
+ **Variant-A "restrict ON by default" is NOT viable for the coder use case
876
+ until the maintainer fixes the deadlock** — it would make crush runs
877
+ routinely dead-end. This supersedes the "restrict ON by default" decision
878
+ above: that decision assumed clean deny (as the crush `--help` text promises),
879
+ which live testing disproved.
880
+
881
+ **Revised interim stance (until the two crush bugs are fixed upstream):**
882
+ - **crush isolate default → back to ON** (revert the flip in `2ad14dc`). The
883
+ disposable worktree is the reliable, deadlock-free safety layer — the same
884
+ posture crush shipped with originally.
885
+ - **crush restrict default → OFF**, but **opt-in restrict actually works**:
886
+ when the user passes `--restrict` / `TRISS_CODER_CRUSH_RESTRICT=1`,
887
+ `buildCrushRunArgv` emits the allowlist as **CLI flags** (not config).
888
+ - Net interim posture: *worktree containment (default) + opt-in CLI allowlist
889
+ for defense-in-depth*. Once the maintainer fixes deadlock + config, revisit
890
+ flipping restrict back ON by default for true opencode parity.
891
+
892
+ ### Tasks (implement, then live-verify — enforcement can't be unit-tested)
893
+
894
+ 1. **`buildCrushRunArgv` — emit CLI allow flags when restrict is ON.**
895
+ For each pattern in `CRUSH_ALLOW_BASH_PATTERNS` push `--allow-bash <p>`,
896
+ and for each tool in a new `CRUSH_ALLOW_TOOLS = ['view','edit','write','ls']`
897
+ constant push `--allow-tool <t>`, alongside the existing `--restrict-run`.
898
+ When restrict is OFF, emit none of them (unchanged). Keep argv an array.
899
+ 2. **Revert the isolate default for crush to ON.** In `src/commands/coder.js`
900
+ change `opts.isolate === undefined ? false` back to `? engine === 'crush'`
901
+ (opencode stays OFF). Update the comment to cite the deadlock/inert-config
902
+ reason, not the old "policy is the safety layer" reason.
903
+ 3. **Change `CRUSH_RESTRICT_DEFAULT` to `false`.** `resolveCrushRestrict`
904
+ precedence is unchanged (CLI > env > crush.json > default); only the final
905
+ default flips. `--restrict` / `TRISS_CODER_CRUSH_RESTRICT=1` still turn it on.
906
+ 4. **Keep `seedCrushPermissions` but label it forward-compat.** The config is
907
+ inert today; keep seeding it (harmless, and correct once the maintainer
908
+ honors it) BUT update the comment + any user-facing message to say the
909
+ allowlist is currently enforced via CLI flags, not this config block.
910
+ 5. **Fable low fixes:**
911
+ - `seedCrushPermissions`: a valid-JSON-but-non-object crush.json (e.g. `[]`)
912
+ must route into the same warn-and-skip branch as a parse error — do NOT
913
+ silently overwrite it.
914
+ - `detectCrush`/pin: when the pin string itself doesn't parse to semver
915
+ (e.g. `TRISS_CODER_CRUSH_VERSION=latest`), skip the comparison (treat as
916
+ satisfied / emit a dim "pin unparseable" note) instead of a perpetual
917
+ `satisfiesPin:false` warning.
918
+ 6. **Docs lockstep + reconcile the now-false parity claims:** `glm-clients.md`
919
+ §2 table (Safety/Isolation rows) + §8, `crush-issues.md` row #4, README
920
+ Engines section, and `.env.example` currently say crush has working
921
+ `permissions.run` parity and isolate-OFF. Correct all to the interim stance:
922
+ config inert / CLI-flag enforcement / isolate-ON / restrict opt-in.
923
+
924
+ **Live-verify after implementing (mandatory — this is the whole point):**
925
+ `triss coder run --engine crush --restrict "<edit a file + run an allowed
926
+ test>"` actually edits and runs; a run that tries a disallowed bash command
927
+ fails safe (times out) rather than executing it; default (no `--restrict`)
928
+ runs isolated. Passing unit tests are necessary but NOT sufficient — the
929
+ `2ad14dc` regression passed 431 tests.
@@ -122,7 +122,7 @@ Removes a variable.
122
122
  | `worker` | `TRISS_WORKER_API_KEY`, `TRISS_WORKER_BASE_URL`, `TRISS_WORKER_FLASH_MODEL`, `TRISS_WORKER_PRO_MODEL` | only `TRISS_WORKER_API_KEY` is required |
123
123
  | `jira` | `ATLASSIAN_BASE_URL`, `ATLASSIAN_EMAIL`, `ATLASSIAN_API_TOKEN` | all three |
124
124
  | `linear` | `LINEAR_API_KEY`, `LINEAR_API_URL` | only `LINEAR_API_KEY` is required |
125
- | `coder` | `ZHIPU_API_KEY` (setup: `triss coder init` — engine, `opencode.json`, agent templates) | `ZHIPU_API_KEY` is required |
125
+ | `coder` | `ZHIPU_API_KEY`, `OPENCODE_API_KEY` (setup: `triss coder init` — engine, `opencode.json`, agent templates) | `ZHIPU_API_KEY` (Z.AI GLM); `OPENCODE_API_KEY` optional (OpenCode Zen, e.g. `opencode/hy3-free`) |
126
126
 
127
127
  When you add a new integration (see [extending.md](extending.md)), its
128
128
  `envVars` declaration is automatically picked up — no wizard changes needed.
@@ -241,14 +241,18 @@ self-hosted endpoints).
241
241
  | `TRISS_WORKER_PRO_MODEL` | no | `deepseek-v4-pro` | Override the `pro` preset |
242
242
  | `TRISS_DEFAULT_MODEL` | no | `flash` | Which preset is used when no `--model`|
243
243
 
244
- ### Coder (GLM coding agent, opencode engine)
244
+ ### Coder (GLM coding agent)
245
245
 
246
246
  | Variable | Required | Default | Notes |
247
247
  | -------------------------------- | -------- | ------------------ | ------------------------------------------ |
248
- | `ZHIPU_API_KEY` | yes | — | Z.AI API key for GLM models — <https://z.ai/manage-apikey/apikey-list> |
249
- | `TRISS_CODER_MODEL` | no | `zai-coding-plan/glm-5.2` | Model written to `opencode.json` |
248
+ | `ZHIPU_API_KEY` | yes¹ | — | Z.AI API key for GLM models — <https://z.ai/manage-apikey/apikey-list> |
249
+ | `OPENCODE_API_KEY` | no¹ | — | OpenCode Zen key (opencode engine only) — unlocks `opencode/*` models like `opencode/hy3-free` — <https://opencode.ai/docs/zen/> |
250
+ | `TRISS_CODER_MODEL` | no | `zai-coding-plan/glm-5.2` | Resolved model, passed to opencode via `--model` (and written to `opencode.json` by `init`). Use `opencode/hy3-free` for OpenCode Zen |
250
251
  | `TRISS_CODER_SMALL_MODEL` | no | `zai-coding-plan/glm-5-turbo` | Small/fast model written to `opencode.json`|
251
252
  | `TRISS_CODER_OPENCODE_VERSION` | no | `1.17.13` | Pin override for the `opencode-ai` npm install |
253
+ | `TRISS_CODER_ENGINE` | no | `opencode` | Coding engine: `opencode` (default) or `crush` |
254
+ | `TRISS_CODER_CRUSH_VERSION` | no | `0.1.3` | Pin override for the `@phpcraftdream/crush` npm install (crush engine) |
255
+ | `TRISS_CODER_CRUSH_RESTRICT` | no | unset (`0`) | crush only — `1` opts INTO the CLI allowlist (`--restrict-run` + `--allow-bash`/`--allow-tool`, the only enforcement path that works today — crush 0.1.3 ignores the `permissions.run` config). Unset leaves crush unrestricted; crush then defaults to isolate-ON. CLI `--restrict`/`--no-restrict` overrides; crush.json `permissions.run.restrict` is the next fallback (forward-compat — currently inert) |
252
256
 
253
257
  `triss coder init` auto-detects which Z.AI endpoint `ZHIPU_API_KEY`
254
258
  actually authenticates against — the `zai-coding-plan` (subscription)
@@ -261,6 +265,45 @@ file) when the existing `model` prefix doesn't match what the key
261
265
  verified against — that mismatch is what makes opencode retry a model
262
266
  call it can never complete.
263
267
 
268
+ ¹ **Credentials are provider-specific.** A run needs the one key its
269
+ resolved model requires: `zai-coding-plan/*` and `zai/*` (GLM) need
270
+ `ZHIPU_API_KEY`; `opencode/*` OpenCode Zen models (e.g. the free
271
+ `opencode/hy3-free`) need `OPENCODE_API_KEY`. So `ZHIPU_API_KEY` is
272
+ "required" only in the sense that it is the default provider's key — a
273
+ zen-only setup runs on `OPENCODE_API_KEY` alone. `triss coder run` forwards
274
+ whichever key the model needs to the engine subprocess and gates on it
275
+ before spawning. The `crush` engine speaks Z.AI only and always needs
276
+ `ZHIPU_API_KEY`. `triss status` / `triss_coder_status` show both keys, and
277
+ the coder MCP tools surface once **either** is set. Run
278
+ `triss coder init --provider opencode-zen` to set up a Zen model interactively
279
+ (key + `opencode.json`); full details are in
280
+ [opencode-zen.md](opencode-zen.md).
281
+
282
+ **Engines.** `opencode` (default) enforces a deny-first per-command bash
283
+ allowlist via `opencode.json` (curated safe commands only) that actually
284
+ works. `crush` (`--engine crush` or `TRISS_CODER_ENGINE=crush`; npm
285
+ `@phpcraftdream/crush` ≥0.1.3, bin `crush`) has a **weaker, interim** safety
286
+ story: live testing proved crush 0.1.3 **ignores** its `permissions.run`
287
+ config block and a denied bash command **deadlocks to timeout**, so triss
288
+ ships crush isolate-**ON** by default (the disposable worktree is the reliable
289
+ safety layer) and makes restrict **opt-in** (default OFF). `triss coder init`
290
+ still seeds a `permissions.run` block into crush.json as forward-compat
291
+ (harmless, correct once upstream honors it), but the working allowlist today
292
+ is the CLI flags: `--restrict` (or `TRISS_CODER_CRUSH_RESTRICT=1`) makes
293
+ `triss coder run` emit `--restrict-run` plus `--allow-bash`/`--allow-tool` for
294
+ each entry. Override per-run with `--restrict` / `--no-restrict` (resolution:
295
+ CLI flag > env > crush.json `permissions.run.restrict` > default OFF). Prefer
296
+ opencode when you want the bash-policy safety layer baked into the project;
297
+ crush is simpler (one JSON envelope, native session ids) but pair it with its
298
+ default isolation, or opt into `--restrict` for a CLI allowlist on top — don't
299
+ combine `--no-restrict` with `--no-isolate` in a workspace you can't afford to
300
+ lose. For Z.AI GLM, both engines share the single `ZHIPU_API_KEY` — crush
301
+ ≥0.1.1 reads it natively; triss also forwards it as `ZAI_API_KEY` for older
302
+ binaries. (opencode can alternatively run OpenCode Zen `opencode/*` models on
303
+ `OPENCODE_API_KEY` — see the coder env-var table above.)
304
+ See `docs/crush-restrict-issues.md` for the live-verified bug facts and
305
+ `docs/crush-issues.md` for the fuller list of crush caveats.
306
+
264
307
  `triss coder run` is **POSIX only** (macOS/Linux) — its engine env
265
308
  allowlist and `--timeout` kill both rely on POSIX process-group
266
309
  semantics. It refuses to run on Windows with a clear error rather than