triss-coworker 0.22.3 → 0.23.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,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] — 2026-07-07
11
+
12
+ ### Added
13
+
14
+ - **Second GLM coding engine: `crush`** (`@phpcraftdream/crush`, pinned
15
+ `0.1.3`), selectable per run with `triss coder run --engine crush` or
16
+ globally with `TRISS_CODER_ENGINE=crush`; set up via
17
+ `triss coder init --engine crush`. Compared to the default opencode
18
+ engine it emits one JSON envelope (no ndjson fold), uses native
19
+ get-or-create session ids (no `.triss/sessions.json` map), reports a real
20
+ per-call `delta_cost_usd`, and exposes `--role smart|fast`. Both engines
21
+ share the single `ZHIPU_API_KEY` (crush reads it natively on ≥0.1.1, with
22
+ a `ZAI_API_KEY` compatibility alias). (#10)
23
+ - `--restrict` / `--no-restrict` on `triss coder run`, plus the
24
+ `TRISS_CODER_CRUSH_RESTRICT` env var, to opt into a crush command
25
+ allowlist. When on, triss emits the read-only allowlist as CLI flags
26
+ (`--allow-bash` / `--allow-tool`); it is **opt-in** (default off) because
27
+ a denied bash command currently deadlocks crush until the timeout. (#10)
28
+ - `TRISS_CODER_CRUSH_VERSION` to pin a specific `@phpcraftdream/crush`
29
+ npm version. (#10)
30
+ - New `docs/glm-clients.md` — a single reference for how Triss talks to
31
+ GLM (both engines, key/endpoint routing, model selection, usage modes,
32
+ and the safety model), and `docs/crush-issues.md` /
33
+ `docs/crush-restrict-issues.md` capturing the crush maintainer bug
34
+ reports. (#10)
35
+
36
+ ### Changed
37
+
38
+ - `triss coder run --engine crush` isolates by default (a disposable
39
+ `.triss/wt/<slug>` worktree); pass `--no-isolate` to opt out. crush's
40
+ own `crush.json` `permissions.run` config is not yet honored upstream and
41
+ a denied command deadlocks, so the worktree is crush's reliable safety
42
+ layer. opencode is unchanged (isolate off; its `opencode.json` bash
43
+ allowlist is enforced). (#10)
44
+
45
+ ## [0.22.4] — 2026-07-04
46
+
47
+ ### Fixed
48
+
49
+ - `triss coder run` / `triss_coder_run` no longer hang until `--timeout`
50
+ when the Z.AI plan hits its usage limit. opencode retries the throttled
51
+ provider call indefinitely and emits nothing parseable on stdout, so a
52
+ limited run used to run out the full timeout (900s CLI / 1500s MCP) and
53
+ then throw a generic "opencode produced no parseable output". A watchdog
54
+ now polls the engine log, kills the run within a few seconds of the
55
+ limit, and fails with the reset time converted from Z.AI's Beijing clock
56
+ (UTC+8) to the host's local timezone, e.g. `GLM usage limit reached —
57
+ quota resets at Jul 4, 2026, 3:39:04 PM GMT+4 (local time)`. The poll is
58
+ a local log read only — no API calls, no token cost — and by cutting the
59
+ retry loop short it reduces wasted provider attempts. (#9)
60
+
10
61
  ## [0.22.3] — 2026-07-04
11
62
 
12
63
  ### 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 GLM coding agent (default `opencode` engine; `--engine crush` for crush): key, `opencode.json` / crush models + `permissions.run` policy, agent templates | 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,11 +431,14 @@ 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
 
@@ -448,9 +452,36 @@ Prints one JSON envelope to stdout — `files_changed`, `diff_stat`, and
448
452
  disposable `.triss/wt/<slug>` git worktree so it never touches your
449
453
  working tree directly. `--session <slug>` continues the same opencode
450
454
  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
455
+ `docs/glm-clients.md` for the full picture of how Triss talks to GLM
456
+ (both engines, key/endpoint routing, models, and every usage mode),
457
+ `docs/configuration.md` for the coder env vars, and `docs/mcp.md` for the
452
458
  MCP tool equivalents.
453
459
 
460
+ **Engines** — `opencode` (default) enforces a deny-first bash allowlist
461
+ (`opencode.json`) that actually works — prefer it when you want that safety
462
+ layer baked in. `crush` (`--engine crush` / `TRISS_CODER_ENGINE=crush`, npm
463
+ `@phpcraftdream/crush` ≥0.1.3) has a **weaker, interim** safety story: live
464
+ testing proved crush 0.1.3 **ignores** its `permissions.run` config block and
465
+ a denied bash command **deadlocks to the timeout**. So triss ships crush with
466
+ isolate-**ON** by default (the disposable worktree is the reliable safety
467
+ layer) and makes restrict **opt-in** (default OFF). `triss coder init` still
468
+ seeds a `permissions.run` block into crush.json as forward-compat (harmless,
469
+ correct once upstream honors it), but the working allowlist today is the CLI
470
+ flags: when you pass `--restrict` (or `TRISS_CODER_CRUSH_RESTRICT=1`), `triss
471
+ coder run` emits `--restrict-run` plus `--allow-bash`/`--allow-tool` flags for
472
+ each entry. Override per-run with `--restrict` / `--no-restrict` (resolution:
473
+ CLI flag > env > crush.json `permissions.run.restrict` > default OFF). Both
474
+ engines share the single `ZHIPU_API_KEY` (crush ≥0.1.1 reads it natively; triss
475
+ also forwards it as `ZAI_API_KEY` for older binaries). See
476
+ `docs/crush-restrict-issues.md` for the live-verified bug facts.
477
+
478
+ If your Z.AI plan hits its usage limit, `triss coder run` fails fast with
479
+ the reset time converted to your local timezone (Z.AI reports it in
480
+ Beijing time), instead of hanging until `--timeout`. Under the hood
481
+ opencode retries the throttled call silently, so the reset time is read
482
+ from the engine log and the run is killed within a few seconds of the
483
+ limit.
484
+
454
485
  ## Integrations
455
486
 
456
487
  External-service plugins live under `src/integrations/<name>/`. They are
package/bin/triss.js CHANGED
@@ -223,23 +223,28 @@ 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 GLM coding engine (opencode default, or crush), Z.AI 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')
233
234
  .action(wrap(runCoderInit));
234
235
 
235
236
  coder
236
237
  .command('run [prompt]')
237
- .description('Spawn a GLM coding agent (opencode engine) and print a JSON envelope to stdout')
238
+ .description('Spawn a GLM coding agent (opencode default, or --engine crush) and print a JSON envelope to stdout')
239
+ .option('--engine <name>', 'coding engine: opencode (default) or crush')
238
240
  .option('--session <id>', 'triss-side session slug, mapped to a real opencode session id in .triss/sessions.json')
239
241
  .option('--continue', 'continue the most recent opencode session (maps to opencode --continue)')
240
242
  .option('--agent <name>', 'opencode agent template to use', 'coder')
241
243
  .option('--model <p/m>', 'override the model for this run only')
242
244
  .option('--isolate', 'run in a disposable git worktree under .triss/wt/<slug>')
245
+ .option('--no-isolate', 'disable worktree isolation (opencode defaults to OFF; crush defaults to ON)')
246
+ .option('--restrict', 'crush only: enforce the allowlist via CLI --allow-bash/--allow-tool flags (--restrict-run). Opt-in (default OFF)')
247
+ .option('--no-restrict', 'crush only: disable restrict (crush auto-approves every tool — the default)')
243
248
  .option('--cwd <path>', 'working directory (ignored with --isolate)')
244
249
  .option('--timeout <sec>', 'kill the engine after this many seconds', (v) => parseInt(v, 10), 900)
245
250
  .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.
@@ -241,7 +241,7 @@ 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
  | -------------------------------- | -------- | ------------------ | ------------------------------------------ |
@@ -249,6 +249,9 @@ self-hosted endpoints).
249
249
  | `TRISS_CODER_MODEL` | no | `zai-coding-plan/glm-5.2` | Model written to `opencode.json` |
250
250
  | `TRISS_CODER_SMALL_MODEL` | no | `zai-coding-plan/glm-5-turbo` | Small/fast model written to `opencode.json`|
251
251
  | `TRISS_CODER_OPENCODE_VERSION` | no | `1.17.13` | Pin override for the `opencode-ai` npm install |
252
+ | `TRISS_CODER_ENGINE` | no | `opencode` | Coding engine: `opencode` (default) or `crush` |
253
+ | `TRISS_CODER_CRUSH_VERSION` | no | `0.1.3` | Pin override for the `@phpcraftdream/crush` npm install (crush engine) |
254
+ | `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
255
 
253
256
  `triss coder init` auto-detects which Z.AI endpoint `ZHIPU_API_KEY`
254
257
  actually authenticates against — the `zai-coding-plan` (subscription)
@@ -261,6 +264,29 @@ file) when the existing `model` prefix doesn't match what the key
261
264
  verified against — that mismatch is what makes opencode retry a model
262
265
  call it can never complete.
263
266
 
267
+ **Engines.** `opencode` (default) enforces a deny-first per-command bash
268
+ allowlist via `opencode.json` (curated safe commands only) that actually
269
+ works. `crush` (`--engine crush` or `TRISS_CODER_ENGINE=crush`; npm
270
+ `@phpcraftdream/crush` ≥0.1.3, bin `crush`) has a **weaker, interim** safety
271
+ story: live testing proved crush 0.1.3 **ignores** its `permissions.run`
272
+ config block and a denied bash command **deadlocks to timeout**, so triss
273
+ ships crush isolate-**ON** by default (the disposable worktree is the reliable
274
+ safety layer) and makes restrict **opt-in** (default OFF). `triss coder init`
275
+ still seeds a `permissions.run` block into crush.json as forward-compat
276
+ (harmless, correct once upstream honors it), but the working allowlist today
277
+ is the CLI flags: `--restrict` (or `TRISS_CODER_CRUSH_RESTRICT=1`) makes
278
+ `triss coder run` emit `--restrict-run` plus `--allow-bash`/`--allow-tool` for
279
+ each entry. Override per-run with `--restrict` / `--no-restrict` (resolution:
280
+ CLI flag > env > crush.json `permissions.run.restrict` > default OFF). Prefer
281
+ opencode when you want the bash-policy safety layer baked into the project;
282
+ crush is simpler (one JSON envelope, native session ids) but pair it with its
283
+ default isolation, or opt into `--restrict` for a CLI allowlist on top — don't
284
+ combine `--no-restrict` with `--no-isolate` in a workspace you can't afford to
285
+ lose. Both engines share the single `ZHIPU_API_KEY` — crush ≥0.1.1 reads it
286
+ natively; triss also forwards it as `ZAI_API_KEY` for older binaries.
287
+ See `docs/crush-restrict-issues.md` for the live-verified bug facts and
288
+ `docs/crush-issues.md` for the fuller list of crush caveats.
289
+
264
290
  `triss coder run` is **POSIX only** (macOS/Linux) — its engine env
265
291
  allowlist and `--timeout` kill both rely on POSIX process-group
266
292
  semantics. It refuses to run on Windows with a clear error rather than