@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 +97 -2
- package/edit-policy.ts +955 -0
- package/index.ts +429 -20
- package/package.json +3 -2
- package/permissions.ts +25 -5
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
|