@dieulc/workflow 0.5.2 → 0.6.1

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/README.md CHANGED
@@ -12,6 +12,7 @@ Plan↔Build mode with an independent **Review Mode** reviewer, questionnaire, s
12
12
  - **Rewind** — shadow bare-repo checkpoints per cwd (`agent/checkpoints/checkpoint-<slug>.git`), `isDirty` + `createSafetySnapshot` + `restoreCode`
13
13
  - **Plannotator bridge** — `plannotator:request/review-result/plan-approved` events, inline markdown handoff (`ScrollView` → inline scrollback), `getUtcDatePrefix` / `normalizePlanPath` for `.pi/plans/<UTC-date>-<slug>.md`
14
14
  - **Model roles** — Planner/Explorer/Builder roles with one model + thinking level each (`roles.ts`, `~/.pi/agent/roles.json` v2). `/plan` switches to the planner model, `/build` to the builder model, `explore` subagents use the explorer model. `/role` opens a tabbed picker (scoped models first, `Ctrl+O` for all).
15
+ - **Write permission** — a user-controlled `allow`/`ask`/`never` permission for file writes in **Default** mode, the mode that is otherwise ungated. `/write-policy` sets it; see [Write permission](#write-permission) below.
15
16
  - **Model profiles** — named combinations of role models (`profiles.ts`, `~/.pi/agent/profiles.json` v1). Create a profile per provider or per budget, switch with `/profile` — activation is session-scoped, so two concurrent sessions can run different profiles. With no profile active the roles.json flow applies unchanged. See [Model Profiles](#model-profiles).
16
17
 
17
18
  ## Install
@@ -43,6 +44,7 @@ Manage it with `pi list`, `pi update --extensions`, `pi remove npm:@dieulc/workf
43
44
  | `/review` | Run one review round against the current plan file now |
44
45
  | `/commit` | Commit staged changes with an AI-summarized message (`-a`: stage tracked changes first). Confirmation-gated; the confirmation arms the commit exemption for that run |
45
46
  | `/commit-policy [never\|ask\|allow] [global\|project]` | View or set the commit policy (default `never`: the agent cannot `git add`/`commit`/`push` — you commit, or use `/commit`) |
47
+ | `/write-policy [allow\|ask\|never] [global\|project]` | View or set the write permission for Default mode (default `allow`: the agent writes freely; `ask` prompts per write; `never` blocks writes — see [Write permission](#write-permission)) |
46
48
 
47
49
  ## Model Profiles
48
50
 
@@ -308,6 +310,68 @@ The confirmation step is required: with no positive answer no authorization exis
308
310
 
309
311
  Known limits (accepted, same trade-off OpenCode/Claude Code make): a configured git alias (e.g. `ci` → `commit`) is not recognized, and exotic obfuscations (`sh -c 'git commit'`) are caught only when the plain text survives. Plan-mode and subagent read-only gating are unaffected by the policy; the shadow-checkpoint rewind commits live in a separate bare repo and never appear in your history.
310
312
 
313
+ ## Write permission
314
+
315
+ The extension has three modes — **Plan → Build → Default**. Plan mode is read-only and Build mode is unrestricted *by design* (it is the approved-execution path), but **Default mode was also completely unrestricted**: the tool gate has no role to apply there, so the agent edits your files without asking, and it does so inconsistently — sometimes it asks for clarification, sometimes it goes straight to `edit`.
316
+
317
+ `workflow.writePolicy` closes that gap. It is a **user-controlled permission**, enforced where the tools execute (not in prose — prompt rules are soft and get ignored), and it applies to **Default mode only**:
318
+
319
+ | Mode | Meaning |
320
+ | --- | --- |
321
+ | `allow` (default) | The agent writes files freely — today's behaviour, and the shipped default so existing installs are unaffected |
322
+ | `ask` | Every file write pops a dialog: **Allow once** / **Always allow this path** (or *this command*) / **Deny** / **Deny with instructions…**. A permanent grant is remembered, so the same target stops asking |
323
+ | `never` | The agent may not write files at all. You apply the changes, or the user switches to `/build` for the approved-execution path |
324
+
325
+ The status line always shows the active permission (`◦ commits:never · writes:ask`), so you can see at a glance what the agent may touch.
326
+
327
+ ### What is gated
328
+
329
+ - The `edit` and `write` tools.
330
+ - `bash` commands that may modify files. A command is treated as a write when it is **not provably read-only**: a file redirect (`>`, `>>`), a heredoc or command substitution, a command outside the read-only/verify allowlist (`rm`, `mv`, `cp`, `sed -i`, `npm install`, `git checkout`, interpreters, unknown binaries …), or a write flag on an otherwise-safe command (`eslint --fix`). Read-only commands (`ls`, `cat`, `rg`, `git status`, `npm test`, `tsc --noEmit`) and harmless redirections (`2>/dev/null`, `2>&1`) stay free.
331
+ - `powershell` is always treated as a write — it cannot be classified by the POSIX allowlist, so it fails closed.
332
+
333
+ `git add` / `git commit` / `git push` are **not** treated as file writes: they touch the index and the object store, not your working tree, and the independent [commit policy](#commit-policy) already governs them.
334
+
335
+ ### Scope and carve-outs
336
+
337
+ - **Default mode only.** Plan mode keeps its stricter read-only rules (writes only to `.pi/plans/`), and Build mode stays the deliberate "go ahead" escape hatch.
338
+ - **Subagents are unaffected.** A spawned child carries `PI_WORKFLOW_ROLE` and is already read-only through the role gate — and it has no UI to answer a prompt with.
339
+ - **`.pi/plans/` writes stay allowed** in every mode, so a session that leaves Plan mode mid-flow is not stranded.
340
+ - **Third-party write tools need opting in.** `ast_grep_replace` (pi-lens) rewrites files, and the `office_*` tools (pi-office-protocol) write documents; neither is covered by the built-in scope. List them in `mutatingTools` to gate them (see below).
341
+
342
+ ```text
343
+ /write-policy # status: mode, which scope supplied it, the allowlists
344
+ /write-policy ask # set globally (~/.pi/agent/settings.json)
345
+ /write-policy never project # this repo only (<cwd>/.pi/settings.json)
346
+ ```
347
+
348
+ **Headless sessions fail closed** under `ask`: there is nobody to answer the dialog, so the write is blocked with a reason telling the model to report the change instead. `allow` and `never` are unaffected.
349
+
350
+ ### Denying with guidance
351
+
352
+ A plain **Deny** tells the model only that the write was refused. It does not say what you actually want, so the model either stops cold or re-proposes the same kind of change. **Deny with instructions…** opens a one-line text box, and your sentence steers the model:
353
+
354
+ ```
355
+ Write denied — tell the agent how to proceed
356
+ > only explore and propose a plan, do not edit files yet
357
+ ```
358
+
359
+ The instruction reaches the model through **two channels**, on purpose:
360
+
361
+ 1. **The tool result** — the block reason quotes your sentence verbatim and ends with the disposition (*"Do not retry this write. Follow the instruction above. If it cannot be followed, say so and stop."*).
362
+ 2. **A user message** — the same sentence is delivered as a real user turn (`deliverAs: "steer"`), after the current tool batch and before the next model call.
363
+
364
+ The second channel is not redundancy for its own sake: Claude Code shipped the comment-on-rejection design and then shipped a bug where the model ignored the comment (it kept trying workarounds) — [anthropics/claude-code#89231](https://github.com/anthropics/claude-code/issues/89231), [#52824](https://github.com/anthropics/claude-code/issues/52824). A tool result is a fact the model may reinterpret; a user turn is an instruction it must follow. Cursor and Claude Code now offer the same affordance (*"Reject? Tell what to do differently"*).
365
+
366
+ Behaviour worth knowing:
367
+
368
+ - **Scope: this write only.** The instruction governs the denied call. The next write asks again — nothing is remembered, nothing is persisted (your sentence is never written to `settings.json`; it lives in the transcript and in one audit entry).
369
+ - **A plain Deny behaves exactly as it always has** — the agent's run is not cut short, and the model keeps its turn. A denial never sets pi's `terminate` flag: that flag is only consulted *after* a whole tool batch has run, so it cannot stop a sibling write — it would only take away the model's next turn.
370
+ - **For a durable "explore only, don't edit yet", use `/plan`.** Plan mode is read-only by design and keeps the agent inside that rule for the whole session; a denial sentence applies to one write.
371
+ - **Cancelling the text box is a plain Deny** — never an implicit allow. An empty or whitespace-only sentence behaves the same way.
372
+ - **Long or noisy input is sanitized**: line endings are normalised, control characters are stripped, and the text is capped at 2000 characters.
373
+ - **One prompt per gated call.** If the model asks for several writes in one turn, each is a separate dialog.
374
+
311
375
  ## Settings
312
376
 
313
377
  Role config lives in `~/.pi/agent/roles.json` v2 (user) with optional per-project override at `<cwd>/.pi/roles.json` (per-role replace). Each role holds exactly one `{provider, id, thinking}`. v1 `modelPool` files are discarded and reseeded on load. Seeded with defaults on first run. Respects `theme`, `modelRegistry`, and `plannotator` phases from `agent/plannotator.json`.
@@ -364,9 +428,32 @@ Commit Policy settings live under `workflow.commitPolicy` (same global/project p
364
428
 
365
429
  `"commitPolicy": "ask"` (a bare mode string) is accepted as shorthand for `{ "mode": "ask" }`. Booleans, numbers and unknown modes fall back to the default. `/commit-policy` writes this key read-modify-write, preserving every other settings key.
366
430
 
431
+ Write Permission settings live under `workflow.writePolicy` (same global/project precedence — `mode` is project-over-global, and the allowlists are **unioned** across scopes, because an allowlist is a grant of trust):
432
+
433
+ | Key | Default | Meaning |
434
+ | --- | --- | --- |
435
+ | `mode` | `"allow"` | `"allow"` = the agent writes freely in Default mode; `"ask"` = every write prompts; `"never"` = writes are blocked (you write, or the user switches to `/build`) |
436
+ | `allowedPaths` | `[]` | Absolute paths the agent may write without being asked again. An entry covers the file itself and everything under it on a directory boundary |
437
+ | `allowedCommands` | `[]` | Command keys the agent may run without being asked again (the mutating shell segment, e.g. `rm -rf build`) |
438
+ | `mutatingTools` | `[]` | Extra tool names to treat as file writes (e.g. `ast_grep_replace`, `office_excel_write_cells`) |
439
+
440
+ ```json
441
+ {
442
+ "workflow": {
443
+ "writePolicy": {
444
+ "mode": "ask",
445
+ "allowedPaths": ["C:/work/project/src"],
446
+ "mutatingTools": ["ast_grep_replace"]
447
+ }
448
+ }
449
+ }
450
+ ```
451
+
452
+ `"writePolicy": "ask"` (a bare mode string) is accepted as shorthand for `{ "mode": "ask" }`. Booleans, numbers and unknown modes fall back to the default. `/write-policy` writes this key read-modify-write, preserving every other settings key; the three list fields are **additive** — a write never drops the entries already stored (remove one by editing the JSON directly).
453
+
367
454
  ## How it works
368
455
 
369
- See `index.ts`, `checkpoint.ts` (shadow bare-repo), `commit-policy.ts` (commit policy config, coercion and write — no runtime imports, unit-testable), `utils.ts` (safe-command gating, todo/plan extraction, plan hashing), `roles.ts` (role registry, one model per role), `profiles.ts` (profile registry, the model overlay and the `workflow.defaultProfile` setting — no Pi runtime needed, unit-testable), and the Review Mode modules: `review.ts` (config, prompt, state, transcript, child session — no runtime imports, so it is unit-testable), `review-providers.ts` (parent→reviewer provider seeding and the provider-agnostic auth preflight), `review-tools.ts` (the reviewer's tool set), `review-runtime.ts` (round orchestration), `review-ui.ts` (the main-transcript entries: line buffer, formatters, renderers). Hot-reload via `/reload` when placed in `~/.pi/agent/extensions/workflow/` or loaded via `pi.extensions` manifest.
456
+ See `index.ts`, `checkpoint.ts` (shadow bare-repo), `commit-policy.ts` (commit policy config, coercion and write — no runtime imports, unit-testable), `edit-policy.ts` (write-policy config, coercion, path/command matching, the decision table, and the guided-denial helpers — no runtime imports, unit-testable), `permissions.ts` (shell-command mutation classification), `utils.ts` (safe-command gating, todo/plan extraction, plan hashing), `roles.ts` (role registry, one model per role), `profiles.ts` (profile registry, the model overlay and the `workflow.defaultProfile` setting — no Pi runtime needed, unit-testable), and the Review Mode modules: `review.ts` (config, prompt, state, transcript, child session — no runtime imports, so it is unit-testable), `review-providers.ts` (parent→reviewer provider seeding and the provider-agnostic auth preflight), `review-tools.ts` (the reviewer's tool set), `review-runtime.ts` (round orchestration), `review-ui.ts` (the main-transcript entries: line buffer, formatters, renderers). Hot-reload via `/reload` when placed in `~/.pi/agent/extensions/workflow/` or loaded via `pi.extensions` manifest.
370
457
 
371
458
  ### Todo protocol (industry-aligned)
372
459
 
@@ -410,9 +497,17 @@ What to do:
410
497
  cd agent/extensions/workflow
411
498
  npm install
412
499
  npm run typecheck # tsc --noEmit (covers every source file, including the review modules)
413
- npm test # node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts review.test.ts review-ui.test.ts commit-policy.test.ts
500
+ npm test # node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts review.test.ts review-ui.test.ts commit-policy.test.ts edit-policy.test.ts
414
501
  ```
415
502
 
503
+ The write policy also has an end-to-end check that drives the real `tool_call` handler against the real `settings.json` (it temporarily writes your global settings and restores them afterwards):
504
+
505
+ ```bash
506
+ node --experimental-transform-types scripts/verify-write-policy.mjs
507
+ ```
508
+
509
+ It covers the allow/ask/never modes, the "always allow" grant, and the guided denial (the instruction dialog, the two delivery channels, the audit entry, and the cancelled/empty fallbacks). What it cannot check is the part that matters most: how the real TUI renders the text box, and whether a live model actually obeys your instruction — do those two by hand once.
510
+
416
511
  Plain TypeScript, no build step — Pi loads `index.ts` directly (via jiti). From the repo root, `npm run verify` checks this package's manifest, tarball contents, tests and load path before a release. Live reload: edit the `.ts` file and run `/reload` in Pi.
417
512
 
418
513
  ## License