@dieulc/workflow 0.4.0 → 0.5.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 +57 -4
- package/commit-policy.ts +170 -0
- package/index.ts +1940 -141
- package/package.json +3 -2
- package/profiles.ts +617 -0
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
|
+
- **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).
|
|
15
16
|
|
|
16
17
|
## Install
|
|
17
18
|
|
|
@@ -34,14 +35,42 @@ Manage it with `pi list`, `pi update --extensions`, `pi remove npm:@dieulc/workf
|
|
|
34
35
|
| `/todos` | Show todo list |
|
|
35
36
|
| `/btw` | Queue note for next turn |
|
|
36
37
|
| `/role` | Config screen: one model + thinking per role (planner/explorer/builder/reviewer tabs, custom roles included). CLI: `list`, `set <role> <provider/model> [thinking]`, `use <role\|auto>`, `add/remove/reset` |
|
|
38
|
+
| `/profile` | Switch or manage model profiles (named role-model combinations). Picker: activate / edit / rename / delete / New profile… / Default…. CLI: `list`, `show <name>`, `use <name\|none>`, `create <name>`, `edit <name>`, `rename <old> <new>`, `delete <name>`, `default [name\|none] [global\|project]` |
|
|
37
39
|
| `/review-mode [on\|off] [global\|project]` | Enable/disable Review Mode (default `off`). `status` prints the effective config, naming the settings file that supplied the timeout |
|
|
38
40
|
| `/review-mode timeout <seconds\|unlimited> [global\|project]` | Set or show the per-pass time cap (`unlimited`/`0` = no cap). Writes to global settings by default |
|
|
39
41
|
| `/review-prompt [global]` | Edit the reviewer's guidance (defaults to "align with the existing framework and industry best practice") |
|
|
40
42
|
| `/review-status` | Show review phase, model, plan hash, findings and any error |
|
|
41
43
|
| `/review` | Run one review round against the current plan file now |
|
|
42
|
-
| `/commit` | Commit staged changes with an AI-summarized message (`-a`: stage tracked changes first). Confirmation-gated |
|
|
44
|
+
| `/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 |
|
|
43
45
|
| `/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`) |
|
|
44
46
|
|
|
47
|
+
## Model Profiles
|
|
48
|
+
|
|
49
|
+
A profile is a **named combination of role model settings** — one `{provider, id, thinking}` each for planner, explorer, builder and reviewer. Profiles layer on top of Model Roles: role identity (tools, system prompts, custom roles) stays global in `roles.json`; a profile only overrides models while it is active.
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
/profile # picker: activate / edit / rename / delete / New profile… / Default…
|
|
53
|
+
/profile create budget # name it, then pick model + thinking per role (same flow as /role)
|
|
54
|
+
/profile use budget # activate for THIS session (none = back to the roles.json flow)
|
|
55
|
+
/profile edit budget # re-open the tabbed model editor
|
|
56
|
+
/profile rename budget cheap
|
|
57
|
+
/profile delete budget # confirm dialog; deleting the active profile falls back to base roles
|
|
58
|
+
/profile default budget # workflow.defaultProfile — new sessions start on it (global|project)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Semantics
|
|
62
|
+
|
|
63
|
+
- **What a profile overrides**: model + thinking for the four built-in roles only. Tools, `systemPromptAddendum` and custom roles are global (roles.json) and are never touched by a profile. Partial profiles are allowed in hand-edited files — roles a profile does not name fall back to the base role model.
|
|
64
|
+
- **Activation is session-scoped.** `/profile use` changes only the current session and rides the session's `workflow` entry, so `/resume` restores it and **two concurrent sessions can run different profiles** — session 1 on a cheap provider, session 2 on a premium one. There is deliberately no global "active profile" file.
|
|
65
|
+
- **Optional default**: `workflow.defaultProfile` (see [Settings](#settings)) seeds sessions that never made an explicit choice; an explicit `/profile use <name|none>` always wins for that session.
|
|
66
|
+
- **Effect**: `/plan` switches to the profile's planner, `/build` to the profile's builder, `explore` subagents run on the profile's explorer, and Review Mode's reviewer session uses the profile's reviewer (planner fallback preserved). Activating while in plan/build re-applies the mode model immediately; in Default mode nothing switches until the next plan/build/explore/review use — the manual chat model is never touched by activation.
|
|
67
|
+
- **`/role` keeps editing the base roles.** With a profile active, the `/role` picker shows a notice and `/role list` prints the profile's overrides; `/profile edit <name>` is the way to change what actually runs.
|
|
68
|
+
- **Fallback**: no profiles (or none active) → the roles.json flow, identical to before profiles existed. A profile referencing a model missing from the registry is warned about at save time and refused at switch time ("staying on current model"), same as `/role`.
|
|
69
|
+
|
|
70
|
+
**Why**: mix models from different providers in one workflow (frontier planner + cheap explorer), keep one profile per provider to control budget per provider, run concurrent sessions on different providers to work around per-provider concurrency limits, and exploit cheaper starter plans across several providers.
|
|
71
|
+
|
|
72
|
+
Profile config lives in `~/.pi/agent/profiles.json` v1 (user) with optional per-project override at `<cwd>/.pi/profiles.json` (per-profile replace, project wins). Names are case-insensitively unique, ≤ 40 chars, no whitespace (keep them CLI-addressable), and `none`/`auto` are reserved. A missing file is the normal empty state; a corrupt or wrong-version file is never rewritten (warning + in-memory empty config).
|
|
73
|
+
|
|
45
74
|
## Review Mode
|
|
46
75
|
|
|
47
76
|
An independent second agent supervises the plan before you see it. The flow is strictly **sequential**: Plan Mode finishes, then the reviewer runs, then the reviewed plan goes to Plannotator.
|
|
@@ -238,7 +267,7 @@ Settings live under `workflow.reviewMode` in `~/.pi/agent/settings.json` (global
|
|
|
238
267
|
|
|
239
268
|
## Commit policy
|
|
240
269
|
|
|
241
|
-
By default the agent **cannot touch your git history**: the tool gate deterministically blocks `git add`, `git commit` and `git push` — including compound commands (`git add -A && git commit -m x`), global-option forms (`git -C path add`) and `powershell` wrappers. You commit manually, or run `/commit` for an AI-summarized, confirmation-gated commit
|
|
270
|
+
By default the agent **cannot touch your git history**: the tool gate deterministically blocks `git add`, `git commit` and `git push` — including compound commands (`git add -A && git commit -m x`), global-option forms (`git -C path add`) and `powershell` wrappers. You commit manually, or run `/commit` for an AI-summarized, confirmation-gated commit. Read-only git (`status`, `log`, `diff`, `branch -a`, `config --get`, …) stays free, and plan mode keeps its stricter read-only rules regardless.
|
|
242
271
|
|
|
243
272
|
The default exists because prompt-only rules are soft: models habitually commit per plan step, and "never commit unless asked" instructions in CLAUDE.md-style files are famously ignored (anthropics/claude-code#58883, #68724, #7029). Claude Code, Codex CLI and OpenCode all treat agent-initiated commits as something to gate or approve — so the policy here is enforced where commands execute, not in prose:
|
|
244
273
|
|
|
@@ -259,6 +288,16 @@ The default exists because prompt-only rules are soft: models habitually commit
|
|
|
259
288
|
|
|
260
289
|
The status line always shows the active policy (`▶ build 2/5 · commits:never`), so you can see at a glance what the agent may do to history.
|
|
261
290
|
|
|
291
|
+
### `/commit` under any policy
|
|
292
|
+
|
|
293
|
+
`/commit` works under `never` and `allow`-during-a-plan because it is the user's own request. The flow is multi-turn (read the staged diff → confirm the message in a questionnaire → commit), so the exemption is **armed at the user's confirmation**, not when the flow starts:
|
|
294
|
+
|
|
295
|
+
- `/commit` records a *pending flow* and injects its agent turn. Nothing is exempted yet.
|
|
296
|
+
- When the confirmation questionnaire is answered **positively** (✅ Commit, or a custom message), the extension arms a scoped *commit authorization*. Choosing ❌ Cancel clears the flow and arms nothing.
|
|
297
|
+
- The authorization exempts **only `git commit`** — `git add` and `git push` stay blocked — and is valid until the run settles (`agent_settled`), additionally bounded by a 10-minute TTL and a 10-commit cap. It is also dropped on `session_shutdown` and on rewind (`session_tree`).
|
|
298
|
+
|
|
299
|
+
The confirmation step is required: with no positive answer no authorization exists and the policy keeps blocking. `-a` staging happens extension-side before the flow, outside the tool gate.
|
|
300
|
+
|
|
262
301
|
```text
|
|
263
302
|
/commit-policy # status: mode, which scope supplied it, both settings paths
|
|
264
303
|
/commit-policy never # set globally (~/.pi/agent/settings.json)
|
|
@@ -271,6 +310,20 @@ Known limits (accepted, same trade-off OpenCode/Claude Code make): a configured
|
|
|
271
310
|
|
|
272
311
|
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`.
|
|
273
312
|
|
|
313
|
+
Profile config lives in `~/.pi/agent/profiles.json` v1 (user) with optional per-project override at `<cwd>/.pi/profiles.json` (per-profile replace). Each profile holds `{name, description, roles}` where `roles` maps any of the four built-in role names to one `{provider, id, thinking}` — see [Model Profiles](#model-profiles).
|
|
314
|
+
|
|
315
|
+
The default profile lives under `workflow.defaultProfile` (global/project precedence like the other workflow settings):
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"workflow": {
|
|
320
|
+
"defaultProfile": "cheap"
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`/profile default <name|none> [global|project]` writes this key read-modify-write, preserving every other settings key. Sessions that never picked explicitly (fresh or resumed) re-read it at every start, so changing the default applies to them; an explicit `/profile use` in a session pins that session until changed.
|
|
326
|
+
|
|
274
327
|
Review Mode settings live under `workflow.reviewMode`:
|
|
275
328
|
|
|
276
329
|
| Key | Default | Meaning |
|
|
@@ -311,7 +364,7 @@ Commit Policy settings live under `workflow.commitPolicy` (same global/project p
|
|
|
311
364
|
|
|
312
365
|
## How it works
|
|
313
366
|
|
|
314
|
-
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), 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.
|
|
367
|
+
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.
|
|
315
368
|
|
|
316
369
|
### Todo protocol (industry-aligned)
|
|
317
370
|
|
|
@@ -355,7 +408,7 @@ What to do:
|
|
|
355
408
|
cd agent/extensions/workflow
|
|
356
409
|
npm install
|
|
357
410
|
npm run typecheck # tsc --noEmit (covers every source file, including the review modules)
|
|
358
|
-
npm test # node --test utils.todo.test.ts review.test.ts review-ui.test.ts
|
|
411
|
+
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
|
|
359
412
|
```
|
|
360
413
|
|
|
361
414
|
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.
|
package/commit-policy.ts
CHANGED
|
@@ -321,6 +321,176 @@ export function stepCommitDecision(
|
|
|
321
321
|
};
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// ── /commit authorization (scoped exemption from the gate) ───────────
|
|
325
|
+
//
|
|
326
|
+
// The /commit flow is multi-turn (read diff → confirm via questionnaire →
|
|
327
|
+
// commit), so a single-turn exemption is always disarmed before the commit
|
|
328
|
+
// runs. Instead the flow starts a PendingCommitFlow and the authorization is
|
|
329
|
+
// armed only when the user CONFIRMS in the extension-owned questionnaire
|
|
330
|
+
// tool. The authorization is valid until the run settles, bounded by a TTL and
|
|
331
|
+
// a commit cap, and exempts ONLY `git commit` (never `add`/`push`).
|
|
332
|
+
|
|
333
|
+
/** How long a /commit flow may sit awaiting the user's decision. */
|
|
334
|
+
export const COMMIT_FLOW_TTL_MS = 30 * 60 * 1000;
|
|
335
|
+
|
|
336
|
+
/** How long an armed authorization stays valid (run-settle is the primary bound). */
|
|
337
|
+
export const COMMIT_AUTH_TTL_MS = 10 * 60 * 1000;
|
|
338
|
+
|
|
339
|
+
/** Defense-in-depth cap on commits made under one /commit authorization. */
|
|
340
|
+
export const COMMIT_AUTH_MAX_COMMITS = 10;
|
|
341
|
+
|
|
342
|
+
let commitFlowCounter = 0;
|
|
343
|
+
|
|
344
|
+
/** A /commit flow awaiting the user's confirmation. */
|
|
345
|
+
export interface PendingCommitFlow {
|
|
346
|
+
id: string;
|
|
347
|
+
startedAt: number;
|
|
348
|
+
expiresAt: number;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** An armed exemption from the commit gate, created at the user's confirmation. */
|
|
352
|
+
export interface CommitAuthorizationState {
|
|
353
|
+
flowId: string;
|
|
354
|
+
armedAt: number;
|
|
355
|
+
expiresAt: number;
|
|
356
|
+
commits: number;
|
|
357
|
+
maxCommits: number;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Start a new /commit flow (id is unique within the process). */
|
|
361
|
+
export function startCommitFlow(
|
|
362
|
+
now: number,
|
|
363
|
+
ttlMs: number = COMMIT_FLOW_TTL_MS,
|
|
364
|
+
): PendingCommitFlow {
|
|
365
|
+
commitFlowCounter += 1;
|
|
366
|
+
return {
|
|
367
|
+
id: `commit-${now}-${commitFlowCounter}`,
|
|
368
|
+
startedAt: now,
|
|
369
|
+
expiresAt: now + ttlMs,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Arm the exemption for one flow. Never throws. */
|
|
374
|
+
export function armCommitAuthorization(input: {
|
|
375
|
+
flowId: string;
|
|
376
|
+
now: number;
|
|
377
|
+
ttlMs?: number;
|
|
378
|
+
maxCommits?: number;
|
|
379
|
+
}): CommitAuthorizationState {
|
|
380
|
+
const ttlMs = input.ttlMs ?? COMMIT_AUTH_TTL_MS;
|
|
381
|
+
return {
|
|
382
|
+
flowId: input.flowId,
|
|
383
|
+
armedAt: input.now,
|
|
384
|
+
expiresAt: input.now + ttlMs,
|
|
385
|
+
commits: 0,
|
|
386
|
+
maxCommits: input.maxCommits ?? COMMIT_AUTH_MAX_COMMITS,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** True only while an authorization is present, unexpired and under its cap. */
|
|
391
|
+
export function isAuthorizationActive(
|
|
392
|
+
auth: CommitAuthorizationState | null | undefined,
|
|
393
|
+
now: number,
|
|
394
|
+
): boolean {
|
|
395
|
+
if (!auth) return false;
|
|
396
|
+
if (!(now < auth.expiresAt)) return false;
|
|
397
|
+
return auth.commits < auth.maxCommits;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** A normalized questionnaire question (only the fields the detector needs). */
|
|
401
|
+
export interface CommitConfirmationQuestion {
|
|
402
|
+
question?: string;
|
|
403
|
+
header?: string;
|
|
404
|
+
options?: { label?: string; description?: string }[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Is this questionnaire the /commit confirmation? Signals, in order: the
|
|
409
|
+
* "Commit" header, the canonical question text, or the "✅ Commit" option
|
|
410
|
+
* label. Deliberately tolerant — a missed match leaves the policy blocking
|
|
411
|
+
* (fail safe), so false negatives are a UX regression, false positives a risk.
|
|
412
|
+
*/
|
|
413
|
+
export function isCommitConfirmation(
|
|
414
|
+
questions: readonly CommitConfirmationQuestion[] | undefined,
|
|
415
|
+
): boolean {
|
|
416
|
+
if (!Array.isArray(questions)) return false;
|
|
417
|
+
return questions.some((q) => {
|
|
418
|
+
if (!q || typeof q !== "object") return false;
|
|
419
|
+
if (typeof q.header === "string" && /^commit$/i.test(q.header.trim())) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
typeof q.question === "string" &&
|
|
424
|
+
/commit these staged changes/i.test(q.question)
|
|
425
|
+
) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return (q.options ?? []).some(
|
|
429
|
+
(o: { label?: string; description?: string }) =>
|
|
430
|
+
typeof o?.label === "string" && /✅\s*commit/i.test(o.label),
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One questionnaire answer, as far as the decision needs to know. */
|
|
436
|
+
export interface CommitConfirmationAnswer {
|
|
437
|
+
answer?: string;
|
|
438
|
+
wasCustom?: boolean;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Did the user authorize the commit? Cancel (`cancelled` or a ❌/cancel-only
|
|
443
|
+
* label) never arms. Freeform text always arms (it is the user's own commit
|
|
444
|
+
* message) unless it is exactly a cancel token. A canned label arms unless it
|
|
445
|
+
* reads as cancel without commit.
|
|
446
|
+
*/
|
|
447
|
+
export function isPositiveCommitDecision(input: {
|
|
448
|
+
cancelled?: boolean;
|
|
449
|
+
answer?: CommitConfirmationAnswer;
|
|
450
|
+
}): boolean {
|
|
451
|
+
if (input?.cancelled) return false;
|
|
452
|
+
const answer = input?.answer;
|
|
453
|
+
if (!answer) return false;
|
|
454
|
+
const label = String(answer.answer ?? "").trim();
|
|
455
|
+
if (!label) return false;
|
|
456
|
+
const isBareCancel = /^(❌\s*)?cancel[.!]?$/i.test(label);
|
|
457
|
+
if (isBareCancel) return false;
|
|
458
|
+
if (answer.wasCustom) return true;
|
|
459
|
+
if (/\bcancel\b/i.test(label) && !/\bcommit\b/i.test(label)) return false;
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The commit-policy gate outcome for one tool call. */
|
|
464
|
+
export type CommitGateDecision = "allow" | "block" | "ask";
|
|
465
|
+
|
|
466
|
+
/** Git subcommands the commit policy gates (mirrors permissions.ts CommitPolicyHit). */
|
|
467
|
+
export type CommitGateHit = "add" | "commit" | "push";
|
|
468
|
+
|
|
469
|
+
export interface CommitGateInput {
|
|
470
|
+
policy: CommitPolicyMode;
|
|
471
|
+
hit: CommitGateHit;
|
|
472
|
+
hasTodos: boolean;
|
|
473
|
+
auth?: CommitAuthorizationState | null;
|
|
474
|
+
now: number;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The pure commit-policy decision. Ordering mirrors the original inline gate:
|
|
479
|
+
* an `allow` policy with no active plan owns nothing → allow; `ask` keeps
|
|
480
|
+
* fresh staging free; an armed authorization exempts ONLY `git commit`; then
|
|
481
|
+
* never/allow block and anything left (`ask`) prompts. Never throws.
|
|
482
|
+
*/
|
|
483
|
+
export function decideCommitGate(input: CommitGateInput): CommitGateDecision {
|
|
484
|
+
const { policy, hit, hasTodos } = input;
|
|
485
|
+
if (policy === "allow" && !hasTodos) return "allow";
|
|
486
|
+
if (policy === "ask" && hit === "add") return "allow";
|
|
487
|
+
if (hit === "commit" && isAuthorizationActive(input.auth, input.now)) {
|
|
488
|
+
return "allow";
|
|
489
|
+
}
|
|
490
|
+
if (policy === "never" || policy === "allow") return "block";
|
|
491
|
+
return "ask";
|
|
492
|
+
}
|
|
493
|
+
|
|
324
494
|
/**
|
|
325
495
|
* Read-modify-write `workflow.commitPolicy` into a settings file, preserving
|
|
326
496
|
* every other key in that file (Pi owns settings.json — we must not clobber it).
|